Integration
Complete Rust/Anchor and TypeScript SDK reference.
Onchain: Rust / Anchor
1. Dependency
[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
| 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 |
param_offset layout — byte position of your u64 after the 8-byte Anchor 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 |
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
ComputeBudgetinstruction 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
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
| Code | Name | Meaning |
|---|---|---|
| 6000 | MissingProof | No record_proof at ix[N-1] or no secp256r1 at ix[N-2] |
| 6001 | ProofExpired | proof.expiry < clock.unix_timestamp |
| 6002 | PayloadMismatch | Intent hash mismatch — accounts, params, or nonce changed |
| 6003 | WrongSigner | Pubkey in secp256r1 not in registry |
| 6004 | InvalidProof | Malformed proof data |
| 6005 | NonceOverflow | Nonce wrapped (extremely unlikely) |
| 6006 | PolicyMismatch | Policy string in record_proof doesn’t match enforced policy |
| 6007 | Unauthorized | Caller is not the config authority |
| 6012 | RegistryRequired | Registry account missing for a proof-required policy |
Last updated on