Compose with Cadence Transactions
In this tutorial, you will compose with someone else's contracts on Flow testnet. You'll write a Cadence transaction that reads public state from a contract named Counter and only increments the counter when it is odd. Then you will extend the transaction to mint NFTs when the counter is odd, demonstrating how to compose multiple contracts in a single transaction. Everything runs against testnet using the Flow CLI and the dependency manager.
You can use transactions developed and tested this way from the frontend of your app.
Objectives
After completing this guide, you will be able to:
- Configure the Flow CLI dependency manager to import named contracts from testnet
- Write a Cadence transaction that reads and writes to a public contract you did not deploy
- Run the transaction on testnet with a funded account using the Flow CLI
- Extend the transaction to compose multiple public contracts (Counter+ExampleNFT+NonFungibleToken) without redeploying anything
- Set up NFT collections and mint NFTs conditionally based on on-chain state
- View transaction results and NFT transfers using Flowscan
Prerequisites
- Flow CLI installed
- A funded testnet account to sign transactions
 See Create accounts and Fund accounts in the Flow CLI commands:
Getting Started
Create a new project with the Flow CLI:
_10flow init
Follow the prompts and create a Basic Cadence project (no dependencies).
Install dependencies
We will resolve imports using string format (import "Counter") using the dependency manager.
This is the recommended way of working with imports of already-deployed contracts. You should also use the CLI to create new files and add existing ones to flow.json.
For this exercise, you need to delete the existing contract entry for Counter from your flow.json. You could also use an alias here, but this is simpler since you won't be deploying the Counter contract.
You can install dependencies for already deployed contracts, whether yours or those deployed by others:
_10# Add a deployed instance of the Counter contract_10flow dependencies install testnet://0x8a4dce54554b225d.Counter
Pick none for the deployment account as you won't need to redeploy these contracts.
Once installed with the dependency manager, Cadence imports like import "Counter" will resolve to the testnet address when sending transactions on testnet.
In Cadence, contracts are deployed to the account storage of the deploying address. Due to security reasons, the same private key produces different address on Cadence testnet and mainnet. One of the features of the dependency manager is to automatically select the right address for imports based on the network you're working on.
Compose with the public Counter contract
Review the Counter contract that's created as an example by flow init:
_31access(all) contract Counter {_31_31    access(all) var count: Int_31_31    // Event to be emitted when the counter is incremented_31    access(all) event CounterIncremented(newCount: Int)_31_31    // Event to be emitted when the counter is decremented_31    access(all) event CounterDecremented(newCount: Int)_31_31    init() {_31        self.count = 0_31    }_31_31    // Public function to increment the counter_31    access(all) fun increment() {_31        self.count = self.count + 1_31        emit CounterIncremented(newCount: self.count)_31    }_31_31    // Public function to decrement the counter_31    access(all) fun decrement() {_31        self.count = self.count - 1_31        emit CounterDecremented(newCount: self.count)_31    }_31_31    // Public function to get the current count_31    view access(all) fun getCount(): Int {_31        return self.count_31    }_31}
It's an example of a simple contract.
Unlike in Solidity, apps aren't limited to the functionality deployed in a smart contract. One of the ways you can expand your app is to write new transactions that call multiple functions in multiple contracts, with branching based on conditions and state, using a single call and a single signature. You don't need to deploy a new contract, use a proxy, or switch to V2.
In this simple example, imagine that you've already deployed a product that has thousands of users and is dependent on the Counter smart contract. After a time, you realize that a significant portion of your users only wish to use the increment feature if the current count is odd, to try and make the number be even.
In Cadence, this sort of upgrade is easy, even if you didn't anticipate the need at contract deployment.
All you need to do is to write a new transaction that reads the current count from Counter and only increments it if the value is odd.
Create a new transaction called IncrementIfOdd using the Flow CLI:
_10flow generate transaction IncrementIfOdd
Start by adding the code from the existing IncrementCounter transaction:
_17import "Counter"_17_17transaction {_17_17    prepare(acct: &Account) {_17        // Authorizes the transaction_17    }_17_17    execute {_17        // Increment the counter_17        Counter.increment()_17_17        // Retrieve the new count and log it_17        let newCount = Counter.getCount()_17        log("New count after incrementing: ".concat(newCount.toString()))_17    }_17}
Then, modify it to handle the new feature:
_21import "Counter"_21_21transaction() {_21  prepare(account: &Account) {}_21_21  execute {_21    // Get the current count from the Counter contract (public read)_21    let currentCount = Counter.getCount()_21_21    // Print the current count_21    log("Current count: ".concat(currentCount.toString()))_21_21    // If odd (remainder when divided by 2 is not 0), increment_21    if currentCount % 2 != 0 {_21      Counter.increment()_21      log("Counter was odd, incremented to: ".concat(Counter.getCount().toString()))_21    } else {_21      log("Counter was even, no increment performed")_21    }_21  }_21}
As with most blockchains, logs are not exposed or returned when transactions are run on testnet or mainnet, but they are visible in the console when you use the emulator.
Run on testnet
You need a funded testnet account to sign the transaction. For development tasks, the CLI has account commands that you can use to create and manage your accounts.
Create and fund an account called testnet-account:
_10# If needed, create a testnet account (one-time)_10flow accounts create --network testnet_10_10# If needed, fund it (one-time)_10flow accounts fund testnet-account
As with other blockchain accounts, once the private key for an account is compromised, anyone with that key has complete control over an account and it's assets. Never put private keys directly in flow.json.
Creating an account using the CLI automatically puts the private key in a .pkey file, which is already in .gitignore.
Send the transaction to testnet, signed with testnet-account:
_10flow transactions send cadence/transactions/IncrementIfOdd.cdc --signer testnet-account --network testnet
You should see logs that show the prior value and whether the increment occurred.
This same transaction could be triggered from an app and signed by a wallet with a single user click. Your dApp would assemble and submit this exact Cadence transaction using your preferred client library, and the user's wallet would authorize it.
Extend with NFT Minting
Now let's take our composition to the next level by adding NFT minting functionality when the counter is odd. We'll use an example NFT contract that's already deployed on testnet.
This is a silly use case, but it demonstrates the complex use cases you can add to your apps, after contract deployment, and even if you aren't the author of any of the contracts!
Install the NFT Contract
First, let's install the ExampleNFT contract dependency:
_10flow dependencies install testnet://012e4d204a60ac6f.ExampleNFT
This repository uses different deployments for core contracts than those installed by the Flow CLI. If you previously installed core contract dependencies (like NonFungibleToken, MetadataViews, etc.) using the CLI, you should manually delete all dependencies except Counter from your flow.json file to avoid conflicts.
Understanding NFT Minting
Let's look at how NFT minting works with this contract. The MintExampleNFT transaction shows the pattern:
_31import "ExampleNFT"_31import "NonFungibleToken"_31_31transaction(_31    recipient: Address,_31    name: String,_31    description: String,_31    thumbnail: String,_31    creator: String,_31    rarity: String_31) {_31    let recipientCollectionRef: &{NonFungibleToken.Receiver}_31_31    prepare(signer: &Account) {_31        self.recipientCollectionRef = getAccount(recipient)_31            .capabilities.get<&{NonFungibleToken.Receiver}>(ExampleNFT.CollectionPublicPath)_31            .borrow()_31            ?? panic("Could not get receiver reference to the NFT Collection")_31    }_31_31    execute {_31        ExampleNFT.mintNFT(_31            recipient: self.recipientCollectionRef,_31            name: name,_31            description: description,_31            thumbnail: thumbnail,_31            creator: creator,_31            rarity: rarity_31        )_31    }_31}
You can copy this functionality and adapt it for our use case.
Update the IncrementIfOdd Transaction
Now let's update our IncrementIfOdd transaction to mint an NFT when the counter is odd. You can either modify the existing transaction or create a new one:
_43import "Counter"_43import "ExampleNFT"_43import "NonFungibleToken"_43_43transaction() {_43    let recipientCollectionRef: &{NonFungibleToken.Receiver}_43_43    prepare(acct: &Account) {_43        // Get the recipient's NFT collection reference_43        self.recipientCollectionRef = getAccount(acct.address)_43            .capabilities.get<&{NonFungibleToken.Receiver}>(ExampleNFT.CollectionPublicPath)_43            .borrow()_43            ?? panic("Could not get receiver reference to the NFT Collection")_43    }_43_43    execute {_43        // Get the current count from the Counter contract (public read)_43        let currentCount = Counter.getCount()_43_43        // Print the current count_43        log("Current count: ".concat(currentCount.toString()))_43_43        // If odd (remainder when divided by 2 is not 0), increment and mint NFT_43        if currentCount % 2 != 0 {_43            Counter.increment()_43            let newCount = Counter.getCount()_43            log("Counter was odd, incremented to: ".concat(newCount.toString()))_43_43            // Mint an NFT to celebrate the odd number_43            ExampleNFT.mintNFT(_43                recipient: self.recipientCollectionRef,_43                name: "Odd Counter NFT #".concat(newCount.toString()),_43                description: "This NFT was minted when the counter was odd!",_43                thumbnail: "https://example.com/odd-counter.png",_43                creator: "Counter Composer",_43                rarity: "Rare"_43            )_43            log("Minted NFT for odd counter!")_43        } else {_43            log("Counter was even, no increment performed")_43        }_43    }_43}
Setup NFT Collection
Before you can mint NFTs, you need to set up an NFT collection in your account. Create a transaction to do this:
_10flow generate transaction SetupCollection
Add this content to the new transaction:
_18import "ExampleNFT"_18import "NonFungibleToken"_18import "MetadataViews"_18_18transaction {_18    prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {_18        if signer.storage.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) != nil {_18            return_18        }_18_18        let collection <- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>())_18_18        signer.storage.save(<-collection, to: ExampleNFT.CollectionStoragePath)_18_18        let cap = signer.capabilities.storage.issue<&ExampleNFT.Collection>(ExampleNFT.CollectionStoragePath)_18        signer.capabilities.publish(cap, at: ExampleNFT.CollectionPublicPath)_18    }_18}
Run the setup transaction:
_10flow transactions send cadence/transactions/SetupCollection.cdc --signer testnet-account --network testnet
Test the Enhanced Transaction
Now run the enhanced transaction:
_10flow transactions send cadence/transactions/IncrementIfOdd.cdc --signer testnet-account --network testnet
You may need to run the regular IncrementCounter transaction first to get an odd number:
_10flow transactions send cadence/transactions/IncrementCounter.cdc --signer testnet-account --network testnet
View Your NFT
Click the transaction link in the console to view the transaction in testnet Flowscan. After running the transaction while the counter is odd, you'll see an NFT in the Asset Transfers tab.
The broken image is expected. We didn't use a real URL in the example nft metadata.

Why this matters
- No redeploys, no forks: You composed your app logic with on-chain public contracts you do not control.
- Cadence-first composition: Transactions can include arbitrary logic that calls into multiple contracts in one atomic operation with a single signature.
- Production-ready path: The same code path works from a CLI or a dApp frontend, authorized by a wallet.
Conclusion
In this tutorial, you learned how to compose with multiple on-chain contracts using Cadence transactions. You built a transaction that conditionally interacts with a Counter contract based on its current state, and then extended it to mint NFTs when the counter is odd, demonstrating the power and flexibility of Cadence's composition model.
Now that you have completed the tutorial, you should be able to:
- Configure the Flow CLI dependency manager to import named contracts from testnet
- Write a Cadence transaction that reads and writes to a public contract you did not deploy
- Run the transaction on testnet with a funded account using the Flow CLI
- Extend the transaction to compose multiple public contracts (Counter+ExampleNFT+NonFungibleToken) without redeploying anything
- Set up NFT collections and mint NFTs conditionally based on on-chain state
- View transaction results and NFT transfers using Flowscan
This approach gives you the freedom to build complex application logic that composes with any public contracts on Flow, making Cadence's composition model a powerful tool for developers building on Flow.