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
| Field | Type | Bytes | Meaning |
|---|---|---|---|
| (discriminator) | - | 8 | Anchor |
owner | Pubkey | 32 | The wallet this log belongs to |
authority | Pubkey | 32 | May call commit_root on the owner's behalf |
merkle_root | [u8; 32] | 32 | Rolling root over leaves 0..memory_count-1 |
memory_count | u64 | 8 | Leaves the root covers. Monotonic |
nonce | u64 | 8 | Incremented per successful commit |
last_updated | i64 | 8 | Unix timestamp of the last commit |
bump | u8 | 1 | PDA bump |
| 129 | Fixed. 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.
| Account | Constraint |
|---|---|
user_commit | init, seeds = ["user", owner], space = 129 |
owner | UncheckedAccount - only its key is stored, no signature required |
payer | Signer, 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.
| Account | Constraint |
|---|---|
user_commit | mut, seeds = ["user", user_commit.owner], bump = user_commit.bump |
signer | Signer |
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
| Error | Cause |
|---|---|
Unauthorized | Signer is neither owner nor authority |
NonceMismatch | expected_nonce != nonce. A concurrent commit landed first |
CountRegressed | new_count < memory_count |
NonceOverflow | nonce 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.
| Account | Constraint |
|---|---|
owner | UncheckedAccount - not a signer. Only its key seeds the PDA |
stake_authority | UncheckedAccount, seeds = ["stake", owner] |
stake_vault | mut, ATA of stake_authority for mint |
depositor | Signer - funds and authorizes the transfer |
depositor_token | mut, token account of mint |
mint | InterfaceAccount<Mint> |
token_program | Interface<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.
| Account | Constraint |
|---|---|
owner | Signer |
stake_authority | UncheckedAccount, seeds = ["stake", owner], bump |
stake_vault | mut, ATA of stake_authority for mint |
destination | mut, token account of mint |
mint | InterfaceAccount<Mint> |
token_program | Interface<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
| Error | Cause |
|---|---|
AmountZero | amount == 0 |
InsufficientStake | amount > 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/.