Docs
App-chain's runtime

App-chain's runtime

The starter-kit project contains an example application chain, that consists of a runtime created from a single runtime module called Balances. This runtime module stores a ledger of balances for public keys, and the current circulating supply.

It also defines a single runtime method called addBalance, which allows tokens to be minted for a certain address. It looks like this:

packages/chain/src/balances.ts
import { runtimeModule, state, runtimeMethod } from "@proto-kit/module";
import { State, assert } from "@proto-kit/protocol";
import { Balance, Balances as BaseBalances, TokenId } from "@proto-kit/library";
import { PublicKey } from "o1js";
 
interface BalancesConfig {
  totalSupply: Balance;
}
 
@runtimeModule()
export class Balances extends BaseBalances<BalancesConfig> {
  @state() public circulatingSupply = State.from(Balance);
 
  // implicitly inherited from `BaseBalances`
  // @state() public balances = StateMap.from(..);
 
  @runtimeMethod()
  public addBalance(tokenId: TokenId, address: PublicKey, amount: Balance) {
    const circulatingSupply = this.circulatingSupply.get();
    const newCirculatingSupply = Balance.from(circulatingSupply.value).add(
      amount
    );
    assert(
      newCirculatingSupply.lessThanOrEqual(this.config.totalSupply),
      "Circulating supply would be higher than total supply"
    );
    this.circulatingSupply.set(newCirculatingSupply);
    this.mint(tokenId, address, amount);
  }
}
 

Testing the runtime

In general it is considered a good practice to test your runtime modules in isolation. The starter kit comes with a simple test suite that demonstrates how to do this. The test suite below does the following:

  1. Setup a TestingAppChain.fromRuntime, providing only the Balances module.
  2. Provides non-mutable configuration for the Balances module, including the totalSupply.
  3. Starts the application chain, generates keypairs for testing and sets the transaction signer.
  4. Forges a transaction to the Balances module, calling the addBalance method.
  5. Signs and sends the transaction and produces a block for the application chain.
  6. Queries the Balances module for the balance of the test account (keypair generated earlier).
  7. Asserts that the transaction was included in the block, while the execution of the transaction was successful.
  8. Asserts that the balance of the test account has updated accordingly.

The TestingAppChain is a special type of application chain that is designed to be used in tests. It runs an in-memory version of the sequencer as well!

packages/chain/test/balances.test.ts
import { TestingAppChain } from "@proto-kit/sdk";
import { PrivateKey } from "o1js";
import { Balances } from "../src/balances";
import { log } from "@proto-kit/common";
import { BalancesKey, TokenId, UInt64 } from "@proto-kit/library";
 
log.setLevel("ERROR");
 
describe("balances", () => {
  it("should demonstrate how balances work", async () => {
    const appChain = TestingAppChain.fromRuntime({
      Balances,
    });
 
    appChain.configurePartial({
      Runtime: {
        Balances: {
          totalSupply: UInt64.from(10000),
        },
      },
    });
 
    await appChain.start();
 
    const alicePrivateKey = PrivateKey.random();
    const alice = alicePrivateKey.toPublicKey();
    const tokenId = TokenId.from(0);
 
    appChain.setSigner(alicePrivateKey);
 
    const balances = appChain.runtime.resolve("Balances");
 
    const tx1 = await appChain.transaction(alice, () => {
      balances.addBalance(tokenId, alice, UInt64.from(1000));
    });
 
    await tx1.sign();
    await tx1.send();
 
    const block = await appChain.produceBlock();
 
    const key = new BalancesKey({ tokenId, address: alice });
    const balance = await appChain.query.runtime.Balances.balances.get(key);
 
    expect(block?.transactions[0].status.toBoolean()).toBe(true);
    expect(balance?.toBigInt()).toBe(1000n);
  }, 1_000_000);
});

Running the test(s)

The test above can be executed with the following command:

Note: the --watchAll flag is optional, and will re-run the test suite when files change.

pnpm run test --filter=chain --watchAll