Skip to Content
Quickstart

Quickstart

Three instructions. One CPI call. Five minutes.


1. Add the dependency

crates.io npm

[dependencies]
anchor-lang = "0.32.1"
trana_guard = { version = "0.1.0", features = ["cpi", "devnet"] }
# localnet: features = ["cpi", "localnet"]

2. Add Trana accounts

Add a trana_cpi_ctx() helper on your accounts struct so the enforce call is one line everywhere.

use trana_guard::{cpi::accounts::Enforce, program::TranaGuard, Policy};
 
#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut, has_one = owner)]
    pub vault:       Account<'info, VaultState>,
    #[account(mut)]
    pub owner:       Signer<'info>,
    /// CHECK: withdrawal destination
    #[account(mut)]
    pub destination: UncheckedAccount<'info>,
 
    // --- trana_guard ---
    pub trana_guard_program: Program<'info, TranaGuard>,
    /// CHECK: guard validates ownership and proof internally
    #[account(mut)]
    pub trana_registry: UncheckedAccount<'info>,
    /// CHECK: instructions sysvar
    #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
    pub instructions: UncheckedAccount<'info>,
}
 
impl<'info> Withdraw<'info> {
    fn trana_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.instructions.to_account_info(),
            },
        )
    }
}

The registry PDA is derived client-side with seeds ["passkey", owner] and passed in as UncheckedAccount. The guard validates it.

3. 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 from
    // instruction data — the caller cannot fake it.
    trana_guard::cpi::enforce(
        ctx.accounts.trana_ctx(),
        Policy::Limit { param_offset: 0, limit: 1_000_000_000 },
    )?;
 
    **ctx.accounts.vault.to_account_info().try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.destination.to_account_info().try_borrow_mut_lamports()? += amount;
    ctx.accounts.vault.balance -= amount;
    Ok(())
}

Below 1 SOL the guard returns Ok(()) immediately — no proof needed. Above the limit it verifies the secp256r1 proof from ix[N-2].

param_offset is the byte position of your u64 after the 8-byte Anchor discriminator:

Instructionparam_offset
fn withdraw(ctx, amount: u64)0
fn transfer(ctx, recipient: Pubkey, amount: u64)32
fn action(ctx, flag: bool, amount: u64)1

4. Client — derive registry PDA

import { PublicKey } from "@solana/web3.js"
 
const TRANA_GUARD_ID = new PublicKey("TRAqChewX8boPDuBbVXjS7iCQAnh9gDThfBRwXauwsG")
 
function registryPda(owner: PublicKey): PublicKey {
  const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("passkey"), owner.toBuffer()],
    TRANA_GUARD_ID,
  )
  return pda
}

5. Client — build and send a protected transaction

import { TranaGuardClient, Policy } from "@tranaprotocol/sdk"
 
const client = new TranaGuardClient({ connection, cluster: "devnet" })
 
// Build your protected instruction as usual
const withdrawIx = await program.methods
  .withdraw(new BN(amount))
  .accounts({
    vault,
    owner:              wallet.publicKey,
    destination:        wallet.publicKey,
    tranaGuardProgram:  TRANA_GUARD_ID,
    tranaRegistry:      registryPda(wallet.publicKey),
    instructions:       SYSVAR_INSTRUCTIONS_PUBKEY,
  })
  .instruction()
 
// Build the secp256r1 + record_proof proof pair
const { secp256r1Ix, recordProofIx } = await client.buildProof({
  protectedIx:  withdrawIx,
  owner:        wallet.publicKey,
  credentialId: storedHandle.credentialId,  // saved from registerPasskey()
  policy:       Policy.Limit(0, 1_000_000_000n),
  rpId:         window.location.hostname,
})
 
// Assemble the triplet and send
const tx = new Transaction().add(secp256r1Ix, recordProofIx, withdrawIx)
await wallet.sendTransaction(tx, connection)

Policies

VariantRequires proof when
Policy::RequireAlways
Policy::Limit { param_offset, limit }u64 at offset >= limit
Policy::NotBefore { slot }current slot < slot
Policy::NotAfter { slot }current slot > slot

Authenticators

Touch ID · Face ID · Android biometric · YubiKey 5 · Google Titan · Windows Hello

Last updated on