Intro
Now deploying contracts is all fun and games, but it becomes boring pretty fast, unless you can interact with them :)
There are two ways how you can interact with contracts and accounts on Flow - you can read
data (via scripts
) and you can write
data (via transactions).
The biggest difference is โ you can't mutate the state of the chain when executing scripts. Even if you called the contract's method, which implementation modifies the state, it won't be preserved when a script returns.
Let's start with non-intrusive interactions - scripts.
Prerequisites
Let's assume we are using the same setup from our last part, and we already have our cadence
folder. Later we will plant some scripts into scripts
folder.
Create a new test suit file
npx @onflow/flow-js-testing make interaction
Calculator
Since Cadence is able to do basic math operations, let's try to sum two numbers. Import executeScript
and add new test inside describe
block:
test("calculator", async () => {
const [result] = await executeScript({
code: `
pub fun main(a: Int, b: Int): Int{
return a + b
}
`,
args: ["10", "32"],
});
expect(result).toBe("42");
});
Notice that we are passing numbers as strings here and the final result returned from the script will also be a string. You can add necessary conversions at your leisure, but this is how network and libraries process numbers on Flow
executeScript
will return you a tuple (an array with 2 values) [result, error]
, which you can
use in your assertions.
Account Management
The Framework provides you a clean and easy way to operate accounts - getAccountAddress
function.
This function will return an address, which you can pass into any kind of interaction and framework will do the rest for you. For example, you can use it as a transaction signer or one of the
arguments in a function:
const Alice = await getAccountAddress("Alice");
What is cool about this method is that this specific account is available anywhere inside your tests. So you can create a helper function, get Alice's account address one more time, and it will
be resolved to exactly the same value! โจ
Read Balance
Calculator is fun and games, but let's do something more practical. For example, we can read the FLOW balance of any account. It is pretty easy if you know you way around Cadence:
test("read balance", async () => {
const Alice = await getAccountAddress("Alice");
const [balance] = await executeScript({
code: `
pub fun main(address: Address): UFix64 {
let account = getAccount(address)
return account.balance
}
`,
args: [Alice],
});
expect(balance).toBe("0.00100000");
});
All newly created accounts start with
0.001
amount of Flow, so we can assert this value and use it as baseline for future computations
Contract Access
How about reading something from imported contract? Easy enough! Let's redeploy our Message
contract and try to read message
field from it:
test("read message", async () => {
const message = "noice";
await shallResolve(deployContractByName({ name: "Message", args: [message] }));
const [contractValue] = await executeScript({
code: `
import Message from 0x1
pub fun main():String{
return Message.message
}
`,
});
expect(contractValue).toBe(message);
});
We can import contracts from any address - framework will resolve them and assign proper ones, before sending the script to the emulator.
Short Notation
Let's shorten that script notation, by moving Cadence code into read-message.cdc
file and place it into scripts
folder. executeScript
have a shorthand notion, where the first argument is a name of the file containing the script.
test("read message - short", async () => {
const message = "noice";
await shallResolve(deployContractByName({ name: "Message", args: [message] }));
const [contractValue] = await executeScript("read-message");
expect(contractValue).toBe(message);
});
Arguments can be passed as second argument. Let's add read-balance.cdc
file with code from above and rewrite our balance reading script:
test("balance - short", async () => {
const Alice = await getAccountAddress("Alice");
const [balance] = await executeScript("read-balance", [Alice]);
expect(balance).toBe("0.00100000");
});
Noice!
Cadence Ninja Mutants ๐ข
Let's bring some mutations to the mix! We will make two accounts, mint some tokens for one of them and transfer them to another account. This should be fun!
In order to mint FLOW, we will utilize mintFlow
function, which expects the address and amount as arguments. Import mintFlow
and sendTransaction
at the top of the file
test("FLOW transfer", async () => {
const Alice = await getAccountAddress("Alice");
const Bob = await getAccountAddress("Bob");
await mintFlow(Alice, "42");
const [aliceBalance] = await executeScript("read-balance", [Alice]);
expect(aliceBalance).toBe("42.00100000");
});
Similar to how we've used shallPass
to deploy contract, we can utilize it, when sending transactions.
test("FLOW transfer", async () => {
const Alice = await getAccountAddress("Alice");
const Bob = await getAccountAddress("Bob");
await mintFlow(Alice, "42");
const [aliceBalance] = await executeScript("read-balance", [Alice]);
expect(aliceBalance).toBe("42.00100000");
await shallPass(
sendTransaction({
code: `
import FungibleToken from 0x1
transaction(receiverAddress: Address, amount: UFix64){
prepare(sender: AuthAccount){
let receiver = getAccount(receiverAddress)
// Withdraw necessary amount into separate vault
let senderVault <- sender
.borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!
.withdraw(amount: amount)
// Send to receiver
getAccount(receiverAddress)
.getCapability(/public/flowTokenReceiver)!
.borrow<&{FungibleToken.Receiver}>()!
.deposit(from: <- senderVault)
}
}
`,
args: [Bob, "1"],
signers: [Alice],
})
);
// Let's read updated balances and compare to expected values
const [newAliceBalance] = await executeScript("read-balance", [Alice]);
const [bobBalance] = await executeScript("read-balance", [Bob]);
expect(newAliceBalance).toBe("41.00100000");
expect(bobBalance).toBe("1.00100000");
});
Send via Short Notation
Similar to how we've done it with scripts, sendTransaction
function also supports short notation:
sendTransaction(
fileName, // file name in "transactions folder"
[signers], // {optional) list of signers, even if that's single one
[arguments] // (optional) list of arguments
);
Let's move all the Cadence code from example above into send-flow.cdc
file under cadence/transactions
folder and rewrite our test:
test("FLOW transfer - short", async () => {
const Alice = await getAccountAddress("Alice");
const Bob = await getAccountAddress("Bob");
await mintFlow(Alice, "42");
const [aliceBalance] = await executeScript("read-balance", [Alice]);
expect(aliceBalance).toBe("42.00100000");
const signers = [Alice];
const recipient = Bob;
const amount = "1";
const args = [recipient, amount];
await shallPass(sendTransaction("send-flow", signers, args));
// Let's read updated balances and compare to expected values
const [newAliceBalance] = await executeScript("read-balance", [Alice]);
const [bobBalance] = await executeScript("read-balance", [Bob]);
expect(newAliceBalance).toBe("41.00100000");
expect(bobBalance).toBe("1.00100000");
});
"Multisig" is supported out of the box, simply specify the expected number of signers in your transaction code and pass the corresponding number of addresses via signers
parameter ๐
That's all, folks! Next time, we will take a look at how we can inspect account storage and ensure resources were correctly minted or transferred. Until next time! ๐
Top comments (0)