DEV Community

Max Daunarovich for Flow Blockchain

Posted on • Edited on

Build on Flow: JS Testing - 3. Execute Scripts and Send Transactions

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
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

"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)