Integration
Complete Rust/Anchor and TypeScript SDK reference.
Onchain: Rust / Anchor
1. Dependency
[dependencies]
anchor-lang = "0.32.0"
trana = { version = "0.1", features = ["cpi"] }2. Imports
use trana::cpi::accounts::Enforce;
use trana::program::Trana;
use trana::Policy;3. Three Trana accounts
Add a trana_cpi_ctx() helper on the accounts struct; the enforce call becomes a single line at every use site.
#[derive(Accounts)]
pub struct Withdraw<'info> {
// --- your accounts ---
#[account(mut, has_one = owner, seeds = [b"vault", owner.key().as_ref()], bump)]
pub vault: Account<'info, VaultState>,
#[account(mut)]
pub owner: Signer<'info>,
/// CHECK: withdrawal destination
#[account(mut)]
pub destination: UncheckedAccount<'info>,
// --- Trana ---
pub trana_guard_program: Program<'info, Trana>,
#[account(
mut,
seeds = [b"2fa", owner.key().as_ref()],
bump,
seeds::program = trana_guard_program.key(),
constraint = trana_registry.enabled @ VaultError::PasskeyNotRegistered,
)]
pub trana_registry: Account<'info, trana::TwoFactorRegistry>,
/// CHECK: Instructions sysvar
#[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
pub trana_instructions: UncheckedAccount<'info>,
}
impl<'info> Withdraw<'info> {
pub fn trana_cpi_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Enforce<'info>> {
CpiContext::new(self.trana_guard_program.to_account_info(), Enforce {
registry: self.trana_registry.to_account_info(),
owner: self.owner.to_account_info(),
instructions: self.trana_instructions.to_account_info(),
})
}
}4. Call enforce
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
require!(amount > 0, VaultError::ZeroAmount);
require!(amount <= ctx.accounts.vault.balance, VaultError::InsufficientFunds);
// Passkey required when amount >= 1 SOL. Guard reads amount directly; the caller can't fake it.
trana::cpi::enforce(ctx.accounts.trana_cpi_ctx(), Policy::Limit { param_offset: 0, limit: 1_000_000_000 })?;
let vault_info = ctx.accounts.vault.to_account_info();
let dest_info = ctx.accounts.destination.to_account_info();
**vault_info.try_borrow_mut_lamports()? -= amount;
**dest_info.try_borrow_mut_lamports()? += amount;
ctx.accounts.vault.balance = ctx.accounts.vault.balance
.checked_sub(amount)
.ok_or(VaultError::Overflow)?;
Ok(())
}Policies
| Variant | Fires when | Policy string |
|---|---|---|
Policy::Require | Always | trana.require |
Policy::Limit { param_offset, limit } | u64 at offset ≥ limit | trana.limit |
Policy::NotBefore { slot } | current slot < slot | trana.not_before |
Policy::NotAfter { slot } | current slot > slot | trana.not_after |
The guard reads u64 directly from your instruction data at param_offset bytes after the 8-byte Anchor discriminator. Conditional policies are no-ops when their condition is false.
param_offset layout: byte position of your u64 after the discriminator:
| Instruction signature | param_offset |
|---|---|
fn action(ctx, amount: u64) | 0 |
fn action(ctx, recipient: Pubkey, amount: u64) | 32 |
fn action(ctx, flag: bool, amount: u64) | 1 |
fn action(ctx, a: u32, b: u32, amount: u64) | 8 |
Each type occupies its byte size: u64 = 8, u32 = 4, Pubkey = 32, bool/u8 = 1. Sum the sizes of all parameters that come before the target u64.
Transaction shape
The triplet must appear in this exact order, contiguously:
ix[N-2]: secp256r1 precompile native P-256 sig verify (SIMD-0075)
ix[N-1]: trana::record_proof WebAuthn data carrier
ix[N]: your instruction calls trana::cpi::enforce() via CPINotes:
- A
ComputeBudgetinstruction can be prepended; it shifts all indices up and the guard adjusts automatically. - Multiple protected instructions work in one transaction. Each needs its own
(secp256r1, record_proof, protected)triplet, with proofs signed at consecutive nonces. - V0 transactions with lookup tables work. The Instructions sysvar exposes fully-resolved pubkeys, so account hashes computed client-side match on-chain.
- The three instructions in each triplet must be contiguous with nothing between them.
authorizeAndSend handles triplet construction automatically.
Client: TypeScript
Setup
import { TranaProvider, TranaModal, useTrana } from "@tranaprotocol/guard-sdk"
import { PublicKey } from "@solana/web3.js"
const TRANA_GUARD_PROGRAM_ID = new PublicKey(process.env.NEXT_PUBLIC_TRANA_GUARD_PROGRAM_ID!)
export function TranaRoot({ children }: { children: React.ReactNode }) {
return (
<TranaProvider config={{
tranaGuardProgramId: TRANA_GUARD_PROGRAM_ID,
policy: "trana.limit",
expiryTtlSec: 120, // default: 120
}}>
{children}
<TranaModal />
</TranaProvider>
)
}authorizeAndSend
Pass instruction to tell the SDK what you’re authorizing. It computes the intent hash from the instruction, inserts the (secp256r1, record_proof) pair immediately before it, and submits the complete transaction.
const { authorizeAndSend } = useTrana()
// Simple: SDK builds the transaction automatically:
await authorizeAndSend({
instruction: withdrawIx,
label: "Withdraw 1.5 SOL",
})
// Custom transaction (priority fees, extra instructions):
await authorizeAndSend({
instruction: withdrawIx,
buildTransaction: async ({ recentBlockhash }) => {
const tx = new Transaction({ recentBlockhash, feePayer: wallet.publicKey })
tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 5000 }))
tx.add(withdrawIx)
return tx
},
})Low-level helpers
import {
buildSecp256r1Ix,
buildWebAuthnMessage,
buildRecordProofIx,
intentFromInstruction,
buildIntent,
hashIntent,
} from "@tranaprotocol/guard-sdk"Error reference
| Error | Code | Meaning |
|---|---|---|
MissingProof | 0x1770 | No record_proof at ix[N-1] or no secp256r1 at ix[N-2] |
ProofExpired | 0x1771 | proof.expiry < clock.unix_timestamp |
PayloadMismatch | 0x1772 | Intent hash mismatch: accounts, params, cluster, or nonce changed |
WrongSigner | 0x1773 | Pubkey in secp256r1 doesn’t match the registry |
PolicyMismatch | 0x1777 | Policy string in record_proof doesn’t match enforced policy |
ClusterMismatch | 0x177a | proof.cluster doesn’t match the cluster baked into this binary |
RegistryDisabled | — | trana_registry.enabled == false, fails at account validation |
PasskeyNotRegistered | — | No registry PDA found. Your constraint, not the guard’s. |