Docs
Methods

Methods

Besides state, runtime modules can also contain methods. In general there are two kinds of methods available: runtime methods and non-runtime methods. Non-runtime methods are just regular typescript methods, however the runtime methods are in addition decorated by the @runtimeMethod() decorator, making them callable by users via transactions.

Non-runtime methods

Non-runtime methods are especially useful for internal logic that should not be exposed to the user. In order to maintain good testability of the runtime module, it is recommended to keep the general method implementation without side-effects as well. Methods may freely interact with the state API or other methods, and can include a return value too.

balances.ts
import {
  RuntimeModule,
  runtimeModule,
  state,
} from "@proto-kit/module";
import { State, StateMap, Option } from "@proto-kit/protocol";
import { PublicKey } from "o1js";
import { UInt64 } from "@proto-kit/library";
 
interface BalancesConfig {
  totalSupply: UInt64;
}
 
@runtimeModule()
export class Balances extends RuntimeModule<BalancesConfig> {
  @state() public balances = StateMap.from(PublicKey, UInt64);
 
  public getBalance(address: PublicKey): UInt64 {
    const balance = this.balances.get(address);
    return UInt64.from(balance.value);
  }
}
⚠️

Non-runtime methods that read the state are only executable by the sequencer. You won't be able to call these methods on the client side (e.g. in the browser or tests), since they would not have access to the required state behind the scenes.

Method visibility

You can make use of public, private, protected keywords to control the visibility of your methods (or state properties). This becomes useful when you want to control access to methods in the runtime module inheritance chain.

Runtime methods

To elevate a method into a runtime method, we must decorate it with the @runtimeMethod() decorator. Runtime methods are callable by users via transactions. Where as non-runtime methods are only callable by other methods in the runtime.

Keep in mind that runtime methods will be only as secure, as you implement them. The only guarantees that exist within runtime methods are the ones you implement yourself. Method visibility has no effect on the @runtimeMethod() decorator, these methods will be callable via transactions regardless of visibility.

The example below allows anyone to set a balance for anyone. This is obviously not a good idea, do not do this.

balances.ts
import {
  RuntimeModule,
  runtimeModule,
  state,
  runtimeMethod,
} from "@proto-kit/module";
import { State, StateMap, Option } from "@proto-kit/protocol";
import { PublicKey } from "o1js";
import { UInt64 } from "@proto-kit/library"
 
interface BalancesConfig {
  totalSupply: UInt64;
}
 
@runtimeModule()
export class Balances extends RuntimeModule<BalancesConfig> {
  @state() public balances = StateMap.from(PublicKey, UInt64);
 
  @runtimeMethod()
  public mint(address: PublicKey, amount: UInt64) {
    this.balances.set(address, amount);
  }
}

Valid argument types

Runtime methods can only accept arguments of a valid type.

✅ You can use the following data types as runtime method arguments:

  • UInt64, UInt32
  • Field
  • Signature
  • Merkle witness
  • PublicKey and other o1js primitives
  • Structs
  • 🤯 Proofs (0-2 proof arguments allowed)

❌ The following types are not supported as runtime method arguments:

  • number
  • string
  • object
  • array
  • and other non-o1js/native primitives

Proofs as arguments

Using proofs as arguments is a powerful feature of the runtime, which results in the provided proof being recursively included in the proof of the runtime method execution. You can generate client-side proofs using o1js's zk-program, and pass them as arguments to your runtime methods.

⚠️

The zk program below is a dummy program, it always returns true, effectively proving nothing. For a real use case, make sure to implement a secure and sound zk program that correctly verifies eligibility of the user to mint.

Here's how an example proof as argument would look like:

balances.ts
import {
  RuntimeModule,
  runtimeModule,
  state,
  runtimeMethod,
} from "@proto-kit/module";
import { State, StateMap, Option } from "@proto-kit/protocol";
import { PublicKey, Bool, Experimental } from "o1js";
import { dummyBase64Proof } from "o1js/dist/node/lib/proof_system";
import { Pickles } from "o1js/dist/node/snarky";
import { UInt64 } from "@proto-kit/library";
 
// your zk-circuit proving the user is eligible to mint
const canMint = () => {
  return Bool(true);
};
 
// your zk program goes here
const canMintProgram = Experimental.ZkProgram({
  publicOutput: Field,
  publicInput: Bool,
 
  methods: {
    canMint: {
      privateInputs: [],
      // eslint-disable-next-line putout/putout
      method: canMint,
    },
  },
});
 
// define the type of the proof
class CanMintProof extends Experimental.ZkProgram.Proof(canMintProgram) {}
 
// generate a dummy proof, to be used when testing the runtime method
const [, dummy] = Pickles.proofOfBase64(await dummyBase64Proof(), 2);
const publicInput = Field(0);
const proof = new CanMintProof({
  proof: dummy,
  publicOutput: canMint(publicInput),
  publicInput,
  maxProofsVerified: 2,
});
 
@runtimeModule()
export class Balances extends RuntimeModule<Record<string, never>> {
  @state() public balances = StateMap.from(PublicKey, UInt64);
 
  @runtimeMethod()
  public mint(canMintProof: CanMintProof, amount: UInt64) {
    canMintProof.verify();
    this.balances.set(address, amount);
  }
}

Assertions

Both regular and runtime methods within the runtime cannot forcefully fail. This means that to handle various logical cases in our runtime logic, we must use soft-failing assertions. The framework exposes an assert(...) method to address this issue.

We can easily check for various conditions which result in Bool values. And then assert these conditions to be truthy, and record an error for the transaction if they're not.

You can use as many assertions within your runtime method as you'd like, the overall execution status of the runtime is a subject to all the assert calls within the method. If one assertion fails, the entire transaction will fail.

️🚫

You cannot use the o1js built in assertion methods, such as assertEquals within your runtime methods. This unfortunately rules out usage of primitives that include range checks, or call the built in assertions in any way - such as UInt64 or UInt32.

You can use our built in math primitives instead - they are shipped
as @proto-kit/library.

balances.ts
import {
  RuntimeModule,
  runtimeModule,
  state,
  runtimeMethod,
} from "@proto-kit/module";
import { State, StateMap, Option, assert } from "@proto-kit/protocol";
import { PublicKey } from "o1js";
import { UInt64 } from "@proto-kit/library";
 
interface BalancesConfig {
  totalSupply: UInt64;
}
 
@runtimeModule()
export class Balances extends RuntimeModule<BalancesConfig> {
  @state() public balances = StateMap.from(PublicKey, UInt64);
 
  @runtimeMethod()
  public transfer(from: PublicKey, to: PublicKey, amount: UInt64) {
    const fromBalance = this.balances.get(from);
    const isFromBalanceSufficient = fromBalance.moreThanOrEqual(amount);
    assert(isFromBalanceSufficient, "From balance insufficient");
    // ... additional transfer logic
  }
}