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:
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:
- Setup a
TestingAppChain.fromRuntime
, providing only the Balances module. - Provides non-mutable configuration for the Balances module, including the totalSupply.
- Starts the application chain, generates keypairs for testing and sets the transaction signer.
- Forges a transaction to the
Balances
module, calling theaddBalance
method. - Signs and sends the transaction and produces a block for the application chain.
- Queries the Balances module for the balance of the test account (keypair generated earlier).
- Asserts that the transaction was included in the block, while the execution of the transaction was successful.
- 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!
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