Guides
Advanced Patterns

Advanced Patterns

Deep-dive into cross-chain security and session key lifecycle management with production-ready patterns.


VAA Verification

Wormhole VAAs (Verifiable Action Approvals) are signed attestations from the Guardian network that prove a cross-chain message is authentic. Verifying VAAs is critical for any application that processes cross-chain data.

npm run advanced:vaa

What's in a VAA

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Header                                    β”‚
β”‚   Version (1 byte)                        β”‚
β”‚   Guardian Set Index (4 bytes)            β”‚
β”‚   Number of Signatures (1 byte)           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Signatures (repeated)                     β”‚
β”‚   Guardian Index (1 byte)                 β”‚
β”‚   ECDSA Signature (65 bytes)              β”‚
β”‚   Minimum 13/19 Guardians required        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Body                                      β”‚
β”‚   Timestamp (4 bytes)                     β”‚
β”‚   Nonce (4 bytes)                         β”‚
β”‚   Emitter Chain ID (2 bytes)              β”‚
β”‚   Emitter Address (32 bytes)              β”‚
β”‚   Sequence Number (8 bytes)               β”‚
β”‚   Consistency Level (1 byte)              β”‚
β”‚   Payload (variable)                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Parse and verify a VAA

import { createSDK, parseVAA, hasQuorum, normalizeEmitterAddress } from '@veridex/sdk';
import { Wallet, JsonRpcProvider } from 'ethers';
 
const sdk = createSDK('base');
const chainConfig = sdk.getChainConfig();
 
// 1. Execute a cross-chain action to get a VAA
const prepared = await sdk.prepareTransfer({
  token: 'native',
  recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f5b0e7',
  amount: parseEther('0.0001'),
  targetChain: 10005, // Optimism Sepolia
});
const result = await sdk.executeTransfer(prepared, signer);
 
// 2. Fetch the VAA (wait ~15s for Guardian signatures)
const vaaBytes = await sdk.fetchVAAForTransaction(result.transactionHash);
 
// 3. Parse the VAA
const vaa = parseVAA(vaaBytes);
console.log('Version:', vaa.version);
console.log('Signatures:', vaa.signatures.length, '/ 19');
console.log('Emitter Chain:', vaa.emitterChain);
console.log('Sequence:', vaa.sequence);
 
// 4. Verify Guardian quorum (13/19 required)
const valid = hasQuorum(vaa, true); // true = testnet
 
// 5. Verify emitter matches your Hub contract
const hubContract = chainConfig.contracts?.hub || '';
const emitterMatch =
  normalizeEmitterAddress(vaa.emitterAddress) ===
  normalizeEmitterAddress(hubContract);

Verification checklist

Before accepting any VAA, verify all of the following:

CheckWhat to verify
Signature quorumAt least 13/19 Guardian signatures present and valid
Emitter chainEmitter chain ID matches expected source chain
Emitter addressEmitter address matches expected contract (normalized to 32 bytes)
SequenceSequence number hasn't been processed before (replay protection)
PayloadPayload format matches expected schema, parameters in range
Target chainTarget chain matches the current chain

Common errors

ErrorCauseFix
"VAA not found"VAA not finalized yetWait 15–30 seconds after transaction
"Invalid signatures"Tampered VAA or wrong Guardian setRe-fetch from Wormhole API
"Emitter mismatch"VAA from wrong contractVerify chain config and Hub address
"Sequence already processed"Replay attemptCheck sequence tracking before processing

Session Key Lifecycle

Session keys let users authenticate once with a passkey and then execute multiple transactions without repeated biometric prompts. This example walks through the full create β†’ use β†’ refresh β†’ revoke lifecycle.

npm run advanced:session

Lifecycle overview

CREATE ──→ USE ──→ REFRESH (optional) ──→ REVOKE
  β”‚          β”‚           β”‚                    β”‚
  β”‚ Passkey  β”‚ No promptsβ”‚ Passkey re-auth    β”‚ Invalidate
  β”‚ auth     β”‚ needed    β”‚ extends expiry     β”‚ session key

Step 1: Initialize SessionManager

import { createSDK, SessionManager, EVMHubClientAdapter } from '@veridex/sdk';
import { parseEther, formatEther, Wallet, JsonRpcProvider, getBytes } from 'ethers';
 
const sdk = createSDK('base');
const provider = new JsonRpcProvider('https://sepolia.base.org');
const signer = new Wallet(PRIVATE_KEY, provider);
 
const credential = sdk.getCredential();
if (!credential) {
  throw new Error('No credential set. Run create-wallet first.');
}
 
const hubClient = new EVMHubClientAdapter(
  sdk.getChainClient() as any,
  signer as any,
);
 
const sessionManager = new SessionManager(
  credential,
  hubClient,
  (challenge) => sdk.passkey.sign(challenge),
  { duration: 3600, maxValue: parseEther('0.1') },
);

Step 2: Create a session

One passkey authentication creates the session. All subsequent actions within the session's bounds require no prompts.

const session = await sessionManager.createSession();
 
console.log('Key Hash:', session.keyHash);
console.log('Expires:', new Date(session.expiry).toISOString());
console.log('Max Value:', formatEther(session.maxValue), 'ETH');
console.log('Active:', sessionManager.isActive());

Step 3: Sign actions without prompts

const chainConfig = sdk.getChainConfig();
 
async function signTransfer(amount: bigint) {
  const actionPayload = await sdk.buildTransferPayload({
    targetChain: chainConfig.wormholeChainId,
    token: 'native',
    recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f5b0e7',
    amount,
  });
 
  return sessionManager.signAction({
    action: 'transfer',
    targetChain: chainConfig.wormholeChainId,
    payload: getBytes(actionPayload),
    nonce: Number(await sdk.getNonce()),
    value: amount,
  });
}
 
// All of these execute instantly β€” no biometric prompts
await signTransfer(parseEther('0.0001'));
await signTransfer(parseEther('0.0001'));
await signTransfer(parseEther('0.0001'));

Step 4: Check session status

const isActive = sessionManager.isActive();
const timeRemaining = sessionManager.getTimeRemaining();
 
console.log('Active:', isActive);
console.log('Time left:', Math.floor(timeRemaining / 60), 'minutes');

Step 5: Test session limits

Sessions enforce value limits. Transactions exceeding maxValue are rejected:

try {
  await signTransfer(parseEther('0.2')); // Exceeds 0.1 ETH limit
} catch (e) {
  console.log('Correctly rejected:', e.message);
}

Step 6: Refresh session

Extend a session before it expires (requires passkey re-authentication):

const refreshed = await sessionManager.refreshSession();
console.log('New expiry:', new Date(refreshed.expiry).toISOString());

Step 7: Revoke session

Always revoke sessions when done. Revoked sessions cannot be used:

await sessionManager.revokeSession();
 
// Verify
console.log('Active:', sessionManager.isActive()); // false
 
try {
  await signTransfer(parseEther('0.0001'));
} catch (e) {
  console.log('Correctly rejected:', e.message); // Session revoked
}

Security best practices

PracticeRecommendation
DurationKeep sessions short (1–24 hours). Longer = higher risk if compromised
Value limitsSet maxValue appropriate to the use case. Lower = safer
RevocationAlways revoke on logout. Implement automatic revocation
StorageStore session keys in encrypted storage. Never expose private keys
RefreshRefresh before expiry for seamless UX. Require periodic re-auth
MonitoringTrack session usage. Alert on unusual patterns. Log all operations

Use cases

Use caseDurationMax valueNotes
Gaming2–4 hours$10–50In-game purchases, fast actions
DeFi trading1–2 hours$100–1000Multiple swaps in a session
Social/tipping24 hours$5–20Micro-transactions, content purchases
Mobile apps4–8 hours$50Reduced biometric prompts
Automation1 hourVariesScheduled transactions, bots

Next Steps