Docs

Quickstart

Three accounts. One CPI call. That's the entire integration.

Trana works with any Anchor program and any FIDO2 device: passkeys (Touch ID, Face ID, iCloud Keychain), YubiKey, Google Titan, Windows Hello.

Guard program ID (devnet): 572t8Ctxx1nrHgxJZ1EHSNZTLcMH4oxV1R6g2pRAqba6


Step 1 — Add the Guard as a CPI Dependency

In your program's Cargo.toml:

[dependencies]
anchor-lang = "0.30"
trana-guard = { version = "0.1", features = ["cpi"] }

Step 2 — Add the Enforce CPI to Your Instruction

Import the guard and call enforce() at the start of any instruction you want to protect:

use trana_guard::cpi::accounts::Enforce;
use trana_guard::program::TranaGuard;
use trana_guard::Policy;

#[derive(Accounts)]
pub struct Withdraw<'info> {
    // ... your existing accounts ...

    // Three Trana accounts — add these:
    pub guard_program: Program<'info, TranaGuard>,

    #[account(
        mut,
        seeds = [b"2fa", owner.key().as_ref()],
        bump,
        seeds::program = guard_program.key(),
    )]
    pub two_factor_registry: Account<'info, trana_guard::TwoFactorRegistry>,

    /// CHECK: Instructions sysvar — required by the guard
    #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
    pub instructions_sysvar: UncheckedAccount<'info>,
}

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // Enforce passkey approval before any logic runs
    trana_guard::cpi::enforce(
        CpiContext::new(
            ctx.accounts.guard_program.to_account_info(),
            Enforce {
                registry:           ctx.accounts.two_factor_registry.to_account_info(),
                instructions_sysvar: ctx.accounts.instructions_sysvar.to_account_info(),
            },
        ),
        Policy::Threshold { param_offset: 0, threshold: 1_000_000_000 }, // require 2FA for >= 1 SOL
    )?;

    // Your withdrawal logic here
    // ...
    Ok(())
}

That's the entire program-side integration. No changes to your account structure beyond the three Trana accounts.

Step 3 — Install the SDK

npm install @trana/sdk

Step 4 — Wrap Your App with TranaProvider

import { TranaProvider, TranaModal } from "@trana/sdk"
import { PublicKey } from "@solana/web3.js"

const GUARD_PROGRAM_ID = new PublicKey("572t8Ctxx1nrHgxJZ1EHSNZTLcMH4oxV1R6g2pRAqba6")

export default function App({ children }: { children: React.ReactNode }) {
  return (
    <TranaProvider config={{ guardProgramId: GUARD_PROGRAM_ID, cluster: "devnet" }}>
      {children}
      <TranaModal />  {/* handles passkey registration + approval prompts */}
    </TranaProvider>
  )
}

Step 5 — Call authorizeAndSend

Replace your existing sendTransaction call with authorizeAndSend:

import { useTrana } from "@trana/sdk"

export function WithdrawButton({ vaultPda, amount }: Props) {
  const { authorizeAndSend } = useTrana()
  const { publicKey } = useWallet()

  async function handleWithdraw() {
    const lamports = BigInt(Math.floor(amount * 1e9))

    await authorizeAndSend({
      // Describe the action — shown in the confirmation modal
      buildIntent: () => ({
        targetProgramId:          MY_VAULT_PROGRAM_ID,
        instructionDiscriminator: WITHDRAW_DISCRIMINATOR, // first 8 bytes of ix data
        accounts:                 [vaultPda, publicKey!],
        params:                   u64LE(lamports),         // raw instruction params
        label:                    `Withdraw ${amount} SOL`,
      }),

      // Build the actual transaction — called after passkey approval
      buildTransaction: async ({ recentBlockhash }) => {
        const tx = new Transaction({ recentBlockhash, feePayer: publicKey! })
        tx.add(buildWithdrawIx(vaultPda, publicKey!, lamports))
        return tx
      },
    })
  }

  return <button onClick={handleWithdraw}>Withdraw</button>
}

The SDK handles the rest:

  • Simulates the transaction to detect which policy fires
  • Lazy-registers the passkey if this is the user's first time
  • Shows a confirmation modal with the decoded amount (from transaction)
  • Prompts Touch ID / YubiKey for approval
  • Constructs the secp256r1 + record_proof instructions
  • Retries on blockhash expiry without a second biometric prompt

How It Works

The transaction that lands onchain has this shape:

ix[0]: secp256r1 precompile    ← Solana validates this P-256 signature natively
ix[1]: guard::record_proof     ← carries WebAuthn authenticator data + intent binding
ix[2]: your_program::withdraw  → calls guard::cpi::enforce() internally

The guard program reads ix[0] and ix[1] via the Instructions sysvar at the moment ix[2] executes. If the proof is missing, expired, or tampered — execution fails inside your program, before any state changes.

Supported FIDO2 Devices

Any WebAuthn authenticator using P-256 (COSE algorithm ES256 / -7):

DeviceNotes
Touch ID (Mac, iPhone)Synced via iCloud Keychain
Face ID (iPhone, iPad)Synced via iCloud Keychain
Android biometricSynced via Google Password Manager
YubiKey 5 seriesHardware-bound, not synced — best for enterprise
Google TitanHardware-bound
Windows HelloPlatform authenticator

Next Steps