Skip to main content

Program Reference

toda uses two Anchor programs. toda-commit is deployed on devnet. toda-staking is not deployed.


toda-commit

Program ID: 5SwaBH3pXzEKhmL6pbqZ1JgeDiGaf6i5oVj8951RX9sm
Cluster: devnet
PDA seeds: ["user", owner_pubkey]

One account per user stores the rolling Merkle root. Root computation is defined by @toda/crypto and runs offchain.

Account: UserCommit

FieldTypeBytesMeaning
(discriminator)-8Anchor
ownerPubkey32The wallet this log belongs to
authorityPubkey32May call commit_root on the owner's behalf
merkle_root[u8; 32]32Rolling root over leaves 0..memory_count-1
memory_countu648Leaves the root covers. Monotonic
nonceu648Incremented per successful commit
last_updatedi648Unix timestamp of the last commit
bumpu81PDA bump
129Fixed. Does not grow with the log

Integers are little-endian (Borsh).

init_user(authority: Pubkey)

Creates the commitment PDA. The PDA can exist only once per owner.

AccountConstraint
user_commitinit, seeds = ["user", owner], space = 129
ownerUncheckedAccount - only its key is stored, no signature required
payerSigner, mut - funds rent
system_program

Initializes merkle_root to 32 zero bytes, memory_count and nonce to 0. The authority argument names who may commit; pass the owner's own key for a self-anchoring user.

The owner does not sign init_user. Any payer can create the account, and the first caller sets authority. Self-managed deployments should initialize the account with the intended authority.

commit_root(new_root: [u8; 32], new_count: u64, expected_nonce: u64)

Pushes a new rolling root.

AccountConstraint
user_commitmut, seeds = ["user", user_commit.owner], bump = user_commit.bump
signerSigner

Checks, in order:

require!(signer == uc.owner || signer == uc.authority, Unauthorized);
require!(expected_nonce == uc.nonce, NonceMismatch);
require!(new_count >= uc.memory_count, CountRegressed);

Then writes merkle_root, memory_count, last_updated, increments nonce (checked), and emits RootCommitted.

The checks provide:

  • Authorization: only the owner or authority can write.
  • Ordering: concurrent commits with stale nonces fail.
  • Monotonicity: committed memory count cannot decrease. See monotonicity.

Event: RootCommitted

pub struct RootCommitted {
pub owner: Pubkey,
pub merkle_root: [u8; 32],
pub memory_count: u64,
pub nonce: u64, // the nonce AFTER incrementing
}

Errors

ErrorCause
UnauthorizedSigner is neither owner nor authority
NonceMismatchexpected_nonce != nonce. A concurrent commit landed first
CountRegressednew_count < memory_count
NonceOverflownonce would exceed u64::MAX

toda-staking

Program ID: DPV1ytT8jGMwUx4FKZJRXs6MTCsxmTA8nQfkEtAM2kae
Cluster: not deployed
PDA seeds: ["stake", owner_pubkey]

The staking program provides self-custody vaults. It has no admin account, config account, or privileged signer. See Staking.

Transfers use transfer_checked with the mint's decimals. Classic SPL and Token-2022 mints are supported. The client creates the vault ATA.

stake(amount: u64)

Moves amount base units from the depositor's token account into the owner's vault.

AccountConstraint
ownerUncheckedAccount - not a signer. Only its key seeds the PDA
stake_authorityUncheckedAccount, seeds = ["stake", owner]
stake_vaultmut, ATA of stake_authority for mint
depositorSigner - funds and authorizes the transfer
depositor_tokenmut, token account of mint
mintInterfaceAccount<Mint>
token_programInterface<TokenInterface>

Requires amount > 0 (AmountZero). Emits Staked { owner, mint, amount }.

Any depositor can add tokens to any owner's vault. Only the owner can withdraw.

unstake(amount: u64)

Moves amount base units from the owner's vault to any token account of the same mint.

AccountConstraint
ownerSigner
stake_authorityUncheckedAccount, seeds = ["stake", owner], bump
stake_vaultmut, ATA of stake_authority for mint
destinationmut, token account of mint
mintInterfaceAccount<Mint>
token_programInterface<TokenInterface>

Requires amount > 0 (AmountZero) and stake_vault.amount >= amount (InsufficientStake). Signs the CPI with ["stake", owner, bump]. Emits Unstaked { owner, mint, amount }.

The vault authority PDA is derived from owner.key(). unstake requires owner to sign.

Errors

ErrorCause
AmountZeroamount == 0
InsufficientStakeamount > stake_vault.amount

Typed clients

@toda/solana wraps both programs.

// Commitment
deriveUserCommitPda(owner, programId): [PublicKey, number]
initUser(connection, authority, owner, commitAuthority?): Promise<string>
commitRoot({ connection, authority, owner, newRoot, newCount, expectedNonce }): Promise<string>
readUserCommit(connection, owner): Promise<OnchainCommit | null>

// Staking transaction builders. They do not sign.
deriveStakeAuthorityPda(owner, programId): [PublicKey, number]
getStakeMintInfo(connection, mint): Promise<StakeMintInfo> // reads decimals + token program
stakeVaultAddress(owner, mintInfo, programId): PublicKey
buildStakeTx({ connection, owner, mintInfo, amount, programId }): Promise<{ tx, blockhash, lastValidBlockHeight }>
buildUnstakeTx({}): Promise<{ tx, blockhash, lastValidBlockHeight }>
readStakedBalance(connection, owner, mintInfo, programId): Promise<bigint>
readStakeChunks(connection, vault): Promise<StakeChunk[] | null> // null means unknown

commitRoot rejects a newRoot that is not exactly 32 bytes before it reaches the chain.

readTokenBalance returns 0n for a missing token account. Other failures throw. See reconciliation.

IDLs

anchor build emits target/idl/*.json and target/types/*.ts. @toda/solana consumes committed copies in packages/solana/src/idl/.

Refresh the committed IDLs after every program change. The typed client reads the committed copy, not target/.