Skip to Content
Integration

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

VariantFires whenPolicy string
Policy::RequireAlwaystrana.require
Policy::Limit { param_offset, limit }u64 at offset ≥ limittrana.limit
Policy::NotBefore { slot }current slot < slottrana.not_before
Policy::NotAfter { slot }current slot > slottrana.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 signatureparam_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 CPI

Notes:

  • A ComputeBudget instruction 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

ErrorCodeMeaning
MissingProof0x1770No record_proof at ix[N-1] or no secp256r1 at ix[N-2]
ProofExpired0x1771proof.expiry < clock.unix_timestamp
PayloadMismatch0x1772Intent hash mismatch: accounts, params, cluster, or nonce changed
WrongSigner0x1773Pubkey in secp256r1 doesn’t match the registry
PolicyMismatch0x1777Policy string in record_proof doesn’t match enforced policy
ClusterMismatch0x177aproof.cluster doesn’t match the cluster baked into this binary
RegistryDisabledtrana_registry.enabled == false, fails at account validation
PasskeyNotRegisteredNo registry PDA found. Your constraint, not the guard’s.
Last updated on