Skip to main content

Actions and Reducers

Actions are similar to Events, in terms of implementation. Both are Field arrays that are emitted by ZkApp methods and stored on an archive node. The added feature of actions is that the hash of all emitted actions is also stored on chain, so it is possible to process every action with a reducer and prove that every past transaction has been rolled up.

So events are useful for off-chain indexing and user interfaces. You can track app state changes, or store a dashboard of user activity. Actions are meant to be used on-chain to update state in batches.

Actions

Actions are an array of Fields under the hood, but o1js allows you to define a TypeScript interface for better structure in your business logic. A SmartContract may have only one type of action. Unlike events, all actions must be exactly the same shape, because that shape is what will be used in the reducer constraint system.

// Simple action would be just a single Field
class SimpleAction extends Field {}

// You can also use a more complex Struct for actions
class ComplexAction extends Struct({
id: UInt32,
flag: Bool,
}) {}

// To represent different types of actions, use a key to discriminate
class DiscriminatedAction extends Struct({
type: UInt32, // 1 = typeA, 2 = typeB
payload: Field,
aSpecificField: Field, // only for typeA
bSpecificField: Field, // only for typeB
}) {}

// Dispatch actions to a reducer within a Zkapp
class ActionContract extends SmartContract {
@state(Field) actionState = State<Field>();

reducer = Reducer({ actionType: SimpleAction });

@method async dispatchAction(value: Field) {
value.assertGreaterThan(Field(0));
this.reducer.dispatch(value);
}
}

Reducers

The reducer is a special function that can process several actions in a row and set the new state in a single proof/transaction. You need to write your own reducer function based on your business logic. For instance, you may have a voting app where the number of processed voted always goes up, or you could have a marketplace where the price can go up or down.

class ActionReducerContract extends SmartContract {
@state(Field) sum = State<Field>();
@state(Field) actionState = State<Field>();

reducer = Reducer({ actionType: SimpleAction });

@method async dispatchAction(value: Field) {
value.assertGreaterThan(Field(0));
this.reducer.dispatch(value);
}

// Write a method to reduce actions in your SmartContract
@method async reduce() {
const actions = this.reducer.getActions();
const initial = this.sum.getAndRequireEquals();
let newState = this.reducer.reduce(
actions,
Field,
(state: Field, action: Field) => state.add(action),
initial
);
this.actionState.set(newState);
}
}

Fetching Actions

Use fetchActions to retrieve previously emitted actions from an archive node. The difference between Mina.fetchActions and reducer.fetchActions is that the former returns raw Field arrays, while the latter automatically converts the Fields into your defined action type.

Using Mina.fetchActions

Fetch actions for any public key with optional filtering:

import { Mina, PublicKey } from "o1js";

let contractKey: PublicKey; // Address of deployed contract

// Fetch all actions for a contract
const actions = await Mina.fetchActions(contractKey);

// Fetch actions with block range filtering
const filteredActions = await Mina.fetchActions(
contractKey,
undefined, // actionStates, e.g. { fromActionState: X, toActionState: Y }
undefined, // tokenId, default to Mina token, but can specify another custom token instead
100, // from block
200 // to block
);

console.log("Actions:", actions);

Using reducer.fetchActions

Fetch actions for a specific contract with automatic type conversion:


let contractAddress: PublicKey; // Address of deployed contract

class ActionStruct extends Struct({
candidate: Field,
}) {}

class VotingContract extends SmartContract {
@state(Field) totalVotes = State<Field>();

reducer = Reducer({ actionType: ActionStruct });

@method
async vote(candidate: Field) {
this.reducer.dispatch({ candidate });
}
}

// Deploy and use the contract...
const contract = new VotingContract(contractAddress);

// Fetch typed actions from the reducer
const voteActions = await contract.reducer.fetchActions();
console.log("Vote actions:", voteActions);

let startState: Field; // The starting action state to query from
let endState: Field; // The ending action state to query to

// Fetch actions within a specific action state range
const rangedActions = await contract.reducer.fetchActions({
fromActionState: startState,
endActionState: endState,
});

console.log("Ranged vote actions:", rangedActions);

Further Reading

See ZkNoid's excellent article series on Actions and Reducers for guide motivated by real use cases.

Also, see the API reference for Reducers.