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:vaaWhat'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:
| Check | What to verify |
|---|---|
| Signature quorum | At least 13/19 Guardian signatures present and valid |
| Emitter chain | Emitter chain ID matches expected source chain |
| Emitter address | Emitter address matches expected contract (normalized to 32 bytes) |
| Sequence | Sequence number hasn't been processed before (replay protection) |
| Payload | Payload format matches expected schema, parameters in range |
| Target chain | Target chain matches the current chain |
Common errors
| Error | Cause | Fix |
|---|---|---|
| "VAA not found" | VAA not finalized yet | Wait 15β30 seconds after transaction |
| "Invalid signatures" | Tampered VAA or wrong Guardian set | Re-fetch from Wormhole API |
| "Emitter mismatch" | VAA from wrong contract | Verify chain config and Hub address |
| "Sequence already processed" | Replay attempt | Check 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:sessionLifecycle overview
CREATE βββ USE βββ REFRESH (optional) βββ REVOKE
β β β β
β Passkey β No promptsβ Passkey re-auth β Invalidate
β auth β needed β extends expiry β session keyStep 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
| Practice | Recommendation |
|---|---|
| Duration | Keep sessions short (1β24 hours). Longer = higher risk if compromised |
| Value limits | Set maxValue appropriate to the use case. Lower = safer |
| Revocation | Always revoke on logout. Implement automatic revocation |
| Storage | Store session keys in encrypted storage. Never expose private keys |
| Refresh | Refresh before expiry for seamless UX. Require periodic re-auth |
| Monitoring | Track session usage. Alert on unusual patterns. Log all operations |
Use cases
| Use case | Duration | Max value | Notes |
|---|---|---|---|
| Gaming | 2β4 hours | $10β50 | In-game purchases, fast actions |
| DeFi trading | 1β2 hours | $100β1000 | Multiple swaps in a session |
| Social/tipping | 24 hours | $5β20 | Micro-transactions, content purchases |
| Mobile apps | 4β8 hours | $50 | Reduced biometric prompts |
| Automation | 1 hour | Varies | Scheduled transactions, bots |
Next Steps
- Examples Hub β Browse all available examples
- Session Keys Guide β Session key concepts and theory
- Cross-Chain Guide β Wormhole bridging fundamentals
- Multisig Wallet β Build a production multisig app