Skip to Content
Integration

Integration

Complete Rust/Anchor and TypeScript SDK reference.


Onchain: Rust / Anchor

1. Dependency

crates.io

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

2. Imports

use trana_guard::{cpi::accounts::Enforce, program::TranaGuard, Policy};

3. Accounts

Add the three trana accounts to your instruction context. The registry is UncheckedAccount — the guard validates ownership and proof internally so you don’t need to constrain it.

#[derive(Accounts)]
pub struct Withdraw<'info> {
    // --- your accounts ---
    #[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(),
            },
        )
    }
}

Registry PDA seeds: [b"passkey", owner_pubkey] at trana_guard program ID.

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
    // from instruction data — the caller cannot tamper with 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(())
}

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

param_offset layout — byte position of your u64 after the 8-byte Anchor 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

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_guard::record_proof WebAuthn data carrier
ix[N]    your instruction           calls trana_guard::cpi::enforce() via CPI
  • A ComputeBudget instruction can be prepended; indices shift and the guard adjusts automatically.
  • Multiple protected instructions in one transaction each need their own triplet at consecutive nonces.
  • V0 transactions with lookup tables work. The sysvar exposes resolved pubkeys so account hashes match.

Client: TypeScript SDK

npm

Setup

import { TranaGuardClient, Policy } from "@tranaprotocol/sdk"
import { Connection } from "@solana/web3.js"
 
const connection = new Connection("https://api.devnet.solana.com")
const client     = new TranaGuardClient({ connection, cluster: "devnet" })

Register a passkey (first time)

// Browser only — triggers native OS passkey creation dialog (Touch ID / Face ID)
const { instruction, handle } = await client.registerPasskey({
  owner:           wallet.publicKey,
  rpId:            window.location.hostname,
  userDisplayName: wallet.publicKey.toBase58().slice(0, 8),
})
 
// Include instruction in a transaction and send
const tx = new Transaction().add(instruction)
await wallet.sendTransaction(tx, connection)
 
// Persist handle.credentialId — needed for every future buildProof() call
localStorage.setItem("trana_credential", JSON.stringify({
  credentialId: Array.from(handle.credentialId),
  pubkeyBytes:  Array.from(handle.pubkeyBytes),
}))

Derive registry PDA

import { PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from "@solana/web3.js"
 
// Seeds: ["passkey", owner]
const registryPda = client.registryPda(wallet.publicKey)

Build and send a protected transaction

// 1. Build your 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:     client.registryPda(wallet.publicKey),
    instructions:      SYSVAR_INSTRUCTIONS_PUBKEY,
  })
  .instruction()
 
// 2. Load stored credential
const stored       = JSON.parse(localStorage.getItem("trana_credential")!)
const credentialId = new Uint8Array(stored.credentialId)
 
// 3. Build proof — triggers biometric prompt
const { secp256r1Ix, recordProofIx } = await client.buildProof({
  protectedIx:  withdrawIx,
  owner:        wallet.publicKey,
  credentialId,
  policy:       Policy.Limit(0, 1_000_000_000n),
  rpId:         window.location.hostname,
})
 
// 4. Assemble [secp256r1, record_proof, protected] and send
const tx = new Transaction().add(secp256r1Ix, recordProofIx, withdrawIx)
await wallet.sendTransaction(tx, connection)

Error reference

CodeNameMeaning
6000MissingProofNo record_proof at ix[N-1] or no secp256r1 at ix[N-2]
6001ProofExpiredproof.expiry < clock.unix_timestamp
6002PayloadMismatchIntent hash mismatch — accounts, params, or nonce changed
6003WrongSignerPubkey in secp256r1 not in registry
6004InvalidProofMalformed proof data
6005NonceOverflowNonce wrapped (extremely unlikely)
6006PolicyMismatchPolicy string in record_proof doesn’t match enforced policy
6007UnauthorizedCaller is not the config authority
6012RegistryRequiredRegistry account missing for a proof-required policy
Last updated on