Skip to main content

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

Getting Started

Create a new project with the Flow CLI:


_10
flow 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.

warning

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
_10
flow 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.

info

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:


_31
access(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:


_10
flow generate transaction IncrementIfOdd

Start by adding the code from the existing IncrementCounter transaction:


_17
import "Counter"
_17
_17
transaction {
_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:


_21
import "Counter"
_21
_21
transaction() {
_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
}

info

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)
_10
flow accounts create --network testnet
_10
_10
# If needed, fund it (one-time)
_10
flow accounts fund testnet-account

danger

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:


_10
flow 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.

tip

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:


_10
flow dependencies install testnet://012e4d204a60ac6f.ExampleNFT

warning

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:


_31
import "ExampleNFT"
_31
import "NonFungibleToken"
_31
_31
transaction(
_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:


_43
import "Counter"
_43
import "ExampleNFT"
_43
import "NonFungibleToken"
_43
_43
transaction() {
_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:


_10
flow generate transaction SetupCollection

Add this content to the new transaction:


_18
import "ExampleNFT"
_18
import "NonFungibleToken"
_18
import "MetadataViews"
_18
_18
transaction {
_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:


_10
flow transactions send cadence/transactions/SetupCollection.cdc --signer testnet-account --network testnet

Test the Enhanced Transaction

Now run the enhanced transaction:


_10
flow 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:


_10
flow 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.

info

The broken image is expected. We didn't use a real URL in the example nft metadata.

NFT


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.