Smart Contract Interaction
Building on your local development setup from the previous tutorial, you'll now master advanced Flow development skills that every professional developer needs. This tutorial focuses on working with external dependencies, building sophisticated transactions, and establishing robust testing practices.
Flow's composability is one of its greatest strengths—contracts can easily import and use functionality from other contracts. You'll learn to leverage this power while building reliable, well-tested applications that interact seamlessly with the broader Flow ecosystem.
What You'll Learn
After completing this tutorial, you'll be able to:
- Manage external dependencies using Flow's dependency manager and integrate third-party contracts
- Build sophisticated transactions that interact with multiple contracts and handle complex state changes
- Master transaction anatomy and understand how Cadence transactions work under the hood
- Implement comprehensive testing strategies including edge cases and error conditions
- Apply test-driven development workflows to ensure code quality and reliability
- Handle transaction monitoring and error management in production scenarios
What You'll Build
Building on your Counter contract, you'll enhance it with external dependencies and create a comprehensive testing suite. By the end of this tutorial, you'll have:
- Enhanced Counter app that uses the NumberFormatter contract for better display
- Complex transactions that demonstrate advanced interaction patterns
- Comprehensive test suite covering normal operations, edge cases, and error conditions
- Professional workflow for developing, testing, and deploying contract interactions
Prerequisites:
- Completed Environment Setup tutorial
- Flow CLI, emulator running, and Counter contract deployed
- Basic understanding of Cadence syntax
Managing Dependencies
In addition to creating your own contracts, you can also install contracts that have already been deployed to the network by using the Dependency Manager. This is useful for interacting with contracts that are part of the Flow ecosystem or that have been deployed by other developers.
Flow's dependency manager allows you to:
- Install contracts deployed on any Flow network (mainnet, testnet, emulator)
- Automatically manage contract addresses across different environments
- Keep your code portable and environment-independent
For example, let's say we want to format the result of our GetCounter
script so that we display the number with commas if it's greater than 999. To do that we can install a contract called NumberFormatter
from testnet
that has a function to format numbers.
Install NumberFormatter Contract
The NumberFormatter
contract provides utilities for formatting numbers with commas, making large numbers more readable. Let's install it from testnet:
_10flow dependencies install testnet://8a4dce54554b225d.NumberFormatter
When prompted:
- Account to deploy to: Select
emulator-account
(this will deploy it locally for development) - Alias for mainnet: You can skip this by pressing Enter
This command:
- Downloads the NumberFormatter contract from testnet and any of its dependencies
- Adds it to your
imports/
directory - Configures deployment settings in
flow.json
- Sets up automatic address resolution
Configure Dependencies in flow.json
Open your flow.json
file and notice the new sections:
_24{_24 ._24 ._24 ._24 "dependencies": {_24 "NumberFormatter": {_24 "source": "testnet://8a4dce54554b225d.NumberFormatter",_24 "hash": "dc7043832da46dbcc8242a53fa95b37f020bc374df42586a62703b2651979fb9",_24 "aliases": {_24 "testnet": "8a4dce54554b225d"_24 }_24 }_24 },_24 ._24 ._24 ._24 "deployments": {_24 "emulator": {_24 "emulator-account": [_24 "NumberFormatter"_24 ]_24 }_24 }_24}
This configuration:
- Maps the
NumberFormatter
dependency to its testnet source - Sets up deployment to your emulator account
- Enables automatic address resolution in your code
Deploy External Dependencies
Now we can deploy the NumberFormatter
contract to the emulator by running:
_10flow project deploy
You should see output like:
_10Deploying 1 contracts for accounts: emulator-account_10_10NumberFormatter -> 0xf8d6e0586b0a20c7 (66e6c4210ae8263370fc3661f148f750175bb4cf2e80637fb42eafe2d6c5b385)_10_10🎉 All contracts deployed successfully
Integrate External Contract
Now let's update your GetCounter.cdc
script to use the NumberFormatter. Open cadence/scripts/GetCounter.cdc
and update it:
_14import "Counter"_14import "NumberFormatter"_14_14access(all)_14fun main(): String {_14 // Retrieve the count from the Counter contract_14 let count: Int = Counter.getCount()_14_14 // Format the count using NumberFormatter_14 let formattedCount = NumberFormatter.formatWithCommas(number: count)_14_14 // Return the formatted count_14 return formattedCount_14}
Key Points:
- Import syntax:
import "Counter"
andimport "NumberFormatter"
don't require addresses - Contract interaction: We call
NumberFormatter.formatWithCommas()
just like any other function - Return type change: The script now returns a
String
instead of anInt
Test the Integration
Run your updated script:
_10flow scripts execute cadence/scripts/GetCounter.cdc
You should see:
_10Result: "1"
The number is now formatted as a string. Let's create a more impressive example by adding a transaction that increments by 1000.
Create a Bulk Increment Transaction
Generate a new transaction to demonstrate the NumberFormatter's power:
_10flow generate transaction IncrementBy1000
Open cadence/transactions/IncrementBy1000.cdc
and replace the content with:
_20import "Counter"_20_20transaction {_20 prepare(acct: &Account) {_20 // Authorization handled automatically_20 }_20_20 execute {_20 // Increment the counter 1000 times_20 var i = 0_20 while i < 1000 {_20 Counter.increment()_20 i = i + 1_20 }_20_20 // Retrieve the new count and log it_20 let newCount = Counter.getCount()_20 log("New count after incrementing by 1000: ".concat(newCount.toString()))_20 }_20}
Execute the transaction:
_10flow transactions send cadence/transactions/IncrementBy1000.cdc --signer test-account
Now run your formatted script to see the NumberFormatter in action:
_10flow scripts execute cadence/scripts/GetCounter.cdc
Result:
_10Result: "1,001"
Perfect! The NumberFormatter automatically adds commas to make large numbers readable.
The Power of Composability: Notice what just happened—you enhanced your Counter contract's functionality without modifying the original contract. This is the power of Flow's composability: you can extend functionality by combining contracts, enabling rapid development and code reuse. Even more importantly, we did this without needing access or permission.
Building Transactions
Transactions are the foundation of blockchain state changes. Unlike scripts (which only read data), transactions can modify contract state, transfer tokens, and emit events. Let's master advanced transaction patterns.
Understanding Transaction Anatomy
Every Cadence transaction has the same basic structure:
_21import "OtherContract"_21_21transaction {_21 // Optional: Declare variables available throughout the transaction_21 let initialCount: Int_21_21 // This phase has access to account storage and capabilities_21 // Used for authorization and accessing private data_21 prepare(acct: &Account) {_21 }_21_21 // This phase contains the main transaction logic_21 // No access to account storage, only to data from prepare phase_21 execute {_21 }_21_21 // Optional: Conditions that must be true after execution_21 // Used for verification and ensuring transaction success_21 post {_21 }_21}
Transaction Phases Explained
- Import Phase: Declare contract dependencies
- Parameter Declaration: Define inputs the transaction accepts
- Variable Declaration: Declare transaction-scoped variables
- Prepare Phase: Access account storage and capabilities (authorized)
- Execute Phase: Main logic execution (no storage access)
- Post Phase: Verify transaction success conditions
Transaction with Parameters
Create a transaction that accepts a custom increment value:
_10flow generate transaction IncrementByAmount
Open cadence/transactions/IncrementByAmount.cdc
:
_39import "Counter"_39_39transaction(amount: Int) {_39_39 // Store initial value_39 let initialCount: Int_39_39 prepare(acct: &Account) {_39 // Verify the account is authorized to make this change_39 log("Account ".concat(acct.address.toString()).concat(" is incrementing by ").concat(amount.toString()))_39_39 prepare(acct: &Account) {_39 self.initialCount = Counter.getCount() // Capture initial state_39 log("Account".concat(acct.address.toString()).concat(" is incrementing by").concat(amount.toString()))_39 }_39_39 execute {_39 // Validate input_39 if amount <= 0 {_39 panic("Amount must be positive")_39 }_39_39 // Increment the specified number of times_39 var i = 0_39 while i < amount {_39 Counter.increment()_39 i = i + 1_39 }_39_39 let newCount = Counter.getCount()_39 log("Counter incremented by ".concat(amount.toString()).concat(", new value: ").concat(newCount.toString()))_39 }_39_39 post {_39 // Verify the counter increased correctly_39 Counter.getCount() == (self.initialCount + amount): "Counter must equal initial count plus increment amount"_39 }_39 }_39}
Execute with a parameter:
_10flow transactions send cadence/transactions/IncrementByAmount.cdc <amount> --network emulator --signer test-account
Testing Your Code
Testing is crucial for smart contract development. Flow provides powerful testing capabilities built into the CLI that enable comprehensive test coverage and test-driven development workflows.
Execute the test suite:
_10flow test
You should see output confirming the tests pass:
_10Test results: "Counter_test.cdc"_10- PASS: testContract_10_10All tests passed
Understanding Existing Tests
Open cadence/tests/Counter_test.cdc
to see the existing test:
_13import Test_13_13access(all) let account = Test.createAccount()_13_13access(all) fun testContract() {_13 let err = Test.deployContract(_13 name: "Counter",_13 path: "../contracts/Counter.cdc",_13 arguments: []_13 )_13_13 Test.expect(err, Test.beNil())_13}
This basic test:
- Creates a test account using
Test.createAccount()
- Deploys the Counter contract to the test environment
- Verifies deployment succeeded by checking that no error occurred
Test Integration with Dependencies
Test the NumberFormatter integration:
_24import Test_24_24access(all) let account = Test.createAccount()_24_24access(all) fun testNumberFormatterLogic() {_24 // Test NumberFormatter logic inline without contract deployment_24 // Test small number (under 1000) - should have no comma_24 let smallNumberScript = Test.executeScript(_24 "access(all) fun formatWithCommas(number: Int): String { let isNegative = number < 0; let absNumber = number < 0 ? -number : number; let numberString = absNumber.toString(); var formatted = \"\"; var count = 0; let numberLength = numberString.length; var i = numberLength - 1; while i >= 0 { let digit = numberString.slice(from: i, upTo: i + 1); formatted = digit.concat(formatted); count = count + 1; if count % 3 == 0 && i != 0 { formatted = \",\".concat(formatted) }; i = i - 1 }; if isNegative { formatted = \"-\".concat(formatted) }; return formatted }; access(all) fun main(): String { return formatWithCommas(number: 123) }",_24 []_24 )_24 Test.expect(smallNumberScript, Test.beSucceeded())_24 let smallResult = smallNumberScript.returnValue! as! String_24 Test.expect(smallResult, Test.equal("123"))_24_24 // Test large number (over 999) - should have comma_24 let largeNumberScript = Test.executeScript(_24 "access(all) fun formatWithCommas(number: Int): String { let isNegative = number < 0; let absNumber = number < 0 ? -number : number; let numberString = absNumber.toString(); var formatted = \"\"; var count = 0; let numberLength = numberString.length; var i = numberLength - 1; while i >= 0 { let digit = numberString.slice(from: i, upTo: i + 1); formatted = digit.concat(formatted); count = count + 1; if count % 3 == 0 && i != 0 { formatted = \",\".concat(formatted) }; i = i - 1 }; if isNegative { formatted = \"-\".concat(formatted) }; return formatted }; access(all) fun main(): String { return formatWithCommas(number: 1234) }",_24 []_24 )_24 Test.expect(largeNumberScript, Test.beSucceeded())_24 let largeResult = largeNumberScript.returnValue! as! String_24 Test.expect(largeResult, Test.equal("1,234"))_24}
The Formatter_test.cdc test validates that number formatting with commas works correctly by testing two scenarios: numbers under 1,000 (which should have no commas) and numbers over 999 (which should have commas). The test is constructed with two main assertions - first testing that 123 formats as "123" without commas, and second testing that 1234 formats as "1,234" with a comma.
Run Your Enhanced Test Suite
Execute the complete test suite with your new comprehensive tests:
_10flow test
You should see output like:
_10Running tests..._10_10Test results: "cadence/tests/Formatter_test.cdc"_10- PASS: testNumberFormatterLogic_10Test results: "cadence/tests/Counter_test.cdc"_10- PASS: testContract_10_10All tests passed
For a more detailed guide on Cadence testing patterns and advanced techniques, check out the tests documentation.
Conclusion
Through this tutorial, you've accomplished:
✅ Dependency Management
- Successfully integrated the NumberFormatter contract from testnet
- Learned about Flow's dependency management system and automatic address resolution
- Demonstrated contract composability by enhancing functionality without modifying source code
- Configured multi-contract deployments across different environments
✅ Transaction Development
- Understood transaction anatomy including prepare, execute, and post phases
- Implemented proper input validation and error handling patterns
✅ Testing
- Implemented test coverage for contract functionality
- Created integration tests that verify multi-contract interactions
What You've Learned
You have learned how to use Flow's dependency management system to install and integrate external contracts (like NumberFormatter), understand the structure of Cadence transactions including their prepare, execute, and post phases, and implement basic testing for contract functionality. You can now work with multi-contract applications and understand how contracts can be composed together to extend functionality.
Next Steps
With these skills, you're ready to:
- Build frontend applications that interact with your smart contracts
- Deploy contracts to live networks (testnet and mainnet)
- Explore advanced Flow patterns and ecosystem contracts
- Contribute to the growing Flow developer community
You've made significant progress in becoming a proficient Flow developer!
Resources for Continued Learning
Continue your Flow mastery with these advanced resources:
- Flow Discord Community: Connect with other developers building sophisticated Flow applications
- Cadence Language Reference: Master advanced language features including resources, capabilities, and access control
- Flow GitHub: Explore production contract examples and contribute to the ecosystem