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 your project. You do not need to install any 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, log
s 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.