Error Handling
This guide covers error handling patterns in the SIP SDK, including error types, recovery strategies, debugging tips, and solutions to common issues.
Error Hierarchy
Section titled “Error Hierarchy”All SIP errors extend the base SIPError class, which provides machine-readable error codes, debugging context, and cause preservation.
Base Error Class
Section titled “Base Error Class”import { SIPError, ErrorCode, isSIPError } from '@sip-protocol/sdk'
try { await sip.execute(intent, quote)} catch (e) { if (isSIPError(e)) { console.log(`Error ${e.code}: ${e.message}`) console.log('Context:', e.context) if (e.cause) console.log('Caused by:', e.cause) }}All SIP errors include:
code: Machine-readable error code (e.g.,SIP_2001)message: Human-readable descriptioncontext: Additional debugging informationcause: Original error if wrappedtimestamp: When the error occurred
Error Types
Section titled “Error Types”| Error Class | Description | Use Case |
|---|---|---|
ValidationError | Input validation failures | Invalid amounts, chains, addresses |
CryptoError | Cryptographic operation failures | Encryption, decryption, commitments |
ProofError | ZK proof operation failures | Proof generation, verification |
IntentError | Intent lifecycle errors | Expired intents, invalid state |
NetworkError | External service failures | RPC calls, API requests |
WalletError | Wallet interaction failures | Connection, signing, transactions |
Error Codes Reference
Section titled “Error Codes Reference”General (1xxx)
Section titled “General (1xxx)”| Code | Description | When It Occurs |
|---|---|---|
SIP_1000 | Unknown error | Unexpected failures |
SIP_1001 | Internal error | SDK internal failures |
SIP_1002 | Not implemented | Feature not available |
Validation (2xxx)
Section titled “Validation (2xxx)”| Code | Description | Recovery |
|---|---|---|
SIP_2000 | Validation failed | Check input parameters |
SIP_2001 | Invalid input | Verify data format and type |
SIP_2002 | Invalid chain | Use supported chain ID |
SIP_2003 | Invalid privacy level | Use: transparent, shielded, or compliant |
SIP_2004 | Invalid amount | Ensure positive bigint value |
SIP_2005 | Invalid hex string | Check hex format (0x prefix) |
SIP_2006 | Invalid key | Verify key length and format |
SIP_2007 | Invalid address | Check address format for chain |
SIP_2008 | Missing required field | Provide all required parameters |
SIP_2009 | Value out of range | Check min/max constraints |
Example:
try { const intent = sip.intent() .input('invalid_chain', 'SOL', 100n) // ❌ Invalid chain} catch (e) { if (hasErrorCode(e, ErrorCode.INVALID_CHAIN)) { // Retry with valid chain const intent = sip.intent() .input('solana', 'SOL', 100n) // ✅ Valid }}Cryptographic (3xxx)
Section titled “Cryptographic (3xxx)”| Code | Description | Recovery |
|---|---|---|
SIP_3000 | Crypto operation failed | Retry or check inputs |
SIP_3001 | Encryption failed | Verify key and data |
SIP_3002 | Decryption failed | Check key matches encryption key |
SIP_3003 | Key derivation failed | Verify seed/path parameters |
SIP_3004 | Commitment failed | Check value and blinding |
SIP_3005 | Signature failed | Verify signing key |
SIP_3006 | Invalid curve point | Check point coordinates |
SIP_3007 | Invalid scalar | Verify scalar is in valid range |
SIP_3008 | Invalid key size | Check key is 32 bytes |
SIP_3009 | Invalid encrypted data | Verify ciphertext integrity |
SIP_3010 | Invalid commitment | Check commitment format |
Example:
import { generateViewingKey, encryptForViewing, decryptWithViewing, CryptoError } from '@sip-protocol/sdk'
const viewingKey = generateViewingKey('/compliance/auditor')const data = { sender: '0x...', amount: '100', timestamp: Date.now() }
try { const encrypted = encryptForViewing(data, viewingKey) const decrypted = decryptWithViewing(encrypted, viewingKey)} catch (e) { if (e instanceof CryptoError) { console.error('Crypto operation failed:', e.operation, e.code) // Check if ciphertext was corrupted // Verify viewing key is correct }}Proof (4xxx)
Section titled “Proof (4xxx)”| Code | Description | Recovery |
|---|---|---|
SIP_4000 | Proof operation failed | Check inputs and retry |
SIP_4001 | Proof generation failed | Verify proof parameters |
SIP_4002 | Proof verification failed | Check proof and public inputs |
SIP_4003 | Proof not implemented | Use mock provider or wait for implementation |
SIP_4004 | Proof provider not ready | Call provider.initialize() first |
SIP_4005 | Invalid proof params | Verify all required fields |
Example:
import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'
const provider = new NoirProofProvider({ wasmPath: '/noir' })
try { // ❌ Provider not initialized await provider.generateFundingProof(params)} catch (e) { if (hasErrorCode(e, ErrorCode.PROOF_PROVIDER_NOT_READY)) { // ✅ Initialize first await provider.initialize() const result = await provider.generateFundingProof(params) }}Intent (5xxx)
Section titled “Intent (5xxx)”| Code | Description | Recovery |
|---|---|---|
SIP_5000 | Intent operation failed | Check intent state and retry |
SIP_5001 | Intent expired | Create new intent with longer TTL |
SIP_5002 | Intent cancelled | Cannot recover, create new intent |
SIP_5003 | Intent not found | Verify intent ID |
SIP_5004 | Invalid intent state | Check intent lifecycle |
SIP_5005 | Proofs required | Generate required proofs |
SIP_5006 | Quote expired | Request new quote |
Example:
import { isExpired } from '@sip-protocol/sdk'
const intent = await sip.intent() .input('solana', 'SOL', 1_000_000_000n) .output('zcash', 'ZEC', 50_000_000n) .ttl(300) // 5 minutes .build()
// Before execution, check expiryif (isExpired(intent)) { throw new IntentError('Intent expired', ErrorCode.INTENT_EXPIRED, { context: { expiry: intent.expiry, now: Math.floor(Date.now() / 1000) } })}Network (6xxx)
Section titled “Network (6xxx)”| Code | Description | Recovery |
|---|---|---|
SIP_6000 | Network operation failed | Check connectivity and retry |
SIP_6001 | Network timeout | Increase timeout or check connection |
SIP_6002 | Network unavailable | Wait and retry with backoff |
SIP_6003 | RPC error | Check RPC endpoint and params |
SIP_6004 | API error | Check API credentials and quota |
SIP_6005 | Rate limited | Wait before retrying |
Example:
async function executeWithRetry(intent: ShieldedIntent, quote: Quote, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await sip.execute(intent, quote) } catch (e) { if (e instanceof NetworkError) { if (hasErrorCode(e, ErrorCode.RATE_LIMITED)) { // Exponential backoff await delay(1000 * Math.pow(2, i)) continue } if (hasErrorCode(e, ErrorCode.NETWORK_TIMEOUT)) { // Just retry continue } } throw e } } throw new Error('Max retries exceeded')}Wallet (7xxx)
Section titled “Wallet (7xxx)”| Code | Value | Description | Recovery |
|---|---|---|---|
SIP_7000 | WALLET_UNKNOWN_ERROR | Generic wallet error | Check wallet state |
SIP_7001 | WALLET_NOT_CONNECTED | Wallet not connected | Call wallet.connect() |
SIP_7002 | WALLET_CONNECTION_FAILED | Connection failed | Check wallet installation |
SIP_7003 | WALLET_SIGNING_FAILED | Signing failed | Retry signing |
SIP_7004 | WALLET_TRANSACTION_FAILED | Transaction failed | Check gas and balance |
Wallet-Specific Error Codes
Section titled “Wallet-Specific Error Codes”The WalletError class includes additional wallet-specific codes:
| Code | Description | Recovery |
|---|---|---|
WALLET_NOT_INSTALLED | Wallet extension not found | Install wallet browser extension |
WALLET_CONNECTION_REJECTED | User rejected connection | Ask user to approve |
WALLET_SIGNING_REJECTED | User rejected signing | Ask user to approve |
WALLET_TRANSACTION_REJECTED | User rejected transaction | Ask user to approve |
WALLET_INSUFFICIENT_FUNDS | Not enough balance | Add funds to wallet |
WALLET_UNSUPPORTED_CHAIN | Chain not supported | Switch to supported chain |
WALLET_CHAIN_SWITCH_REJECTED | User rejected chain switch | Ask user to approve |
WALLET_STEALTH_NOT_SUPPORTED | Stealth addresses not supported | Use different wallet |
WALLET_VIEWING_KEY_NOT_SUPPORTED | Viewing keys not supported | Use different wallet |
Example:
import { WalletError } from '@sip-protocol/sdk'
try { await wallet.connect() const signature = await wallet.signMessage(message)} catch (e) { if (e instanceof WalletError) { if (e.isConnectionError()) { // Handle connection errors console.error('Connection failed:', e.walletCode) } if (e.isUserRejection()) { // User rejected - don't retry automatically console.log('User rejected the request') } if (e.isPrivacyError()) { // Privacy features not supported console.log('Wallet does not support privacy features') } }}Recovery Strategies
Section titled “Recovery Strategies”Retry with Exponential Backoff
Section titled “Retry with Exponential Backoff”For transient network errors:
async function withExponentialBackoff<T>( fn: () => Promise<T>, maxRetries = 5, baseDelay = 1000): Promise<T> { for (let i = 0; i < maxRetries; i++) { try { return await fn() } catch (e) { if (i === maxRetries - 1) throw e
if (e instanceof NetworkError) { const delay = baseDelay * Math.pow(2, i) const jitter = Math.random() * 1000 await sleep(delay + jitter) continue }
throw e // Non-retryable error } } throw new Error('Unreachable')}
// Usageconst quotes = await withExponentialBackoff(() => sip.getQuotes(intent))Fallback to Alternative Provider
Section titled “Fallback to Alternative Provider”For proof generation failures:
import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'import { MockProofProvider } from '@sip-protocol/sdk'
const providers = [ new NoirProofProvider({ wasmPath: '/noir' }), new MockProofProvider(), // Fallback for testing]
async function generateProofWithFallback(params: FundingProofParams) { for (const provider of providers) { try { await provider.initialize() return await provider.generateFundingProof(params) } catch (e) { console.warn(`Provider ${provider.constructor.name} failed:`, e) continue } } throw new ProofError('All proof providers failed', ErrorCode.PROOF_GENERATION_FAILED)}Circuit Breaker Pattern
Section titled “Circuit Breaker Pattern”Prevent cascading failures:
class CircuitBreaker { private failures = 0 private lastFailTime = 0 private readonly threshold = 5 private readonly timeout = 60000 // 1 minute
async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.isOpen()) { throw new NetworkError('Circuit breaker open', ErrorCode.NETWORK_UNAVAILABLE) }
try { const result = await fn() this.reset() return result } catch (e) { this.recordFailure() throw e } }
private isOpen(): boolean { if (this.failures >= this.threshold) { if (Date.now() - this.lastFailTime < this.timeout) { return true } this.reset() } return false }
private recordFailure() { this.failures++ this.lastFailTime = Date.now() }
private reset() { this.failures = 0 }}
const breaker = new CircuitBreaker()const result = await breaker.execute(() => sip.execute(intent, quote))Graceful Degradation
Section titled “Graceful Degradation”For optional features:
import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'
async function createIntent(withProofs = true) { const builder = sip.intent() .input('solana', 'SOL', 1_000_000_000n) .output('zcash', 'ZEC', 50_000_000n) .privacy(PrivacyLevel.SHIELDED)
if (withProofs) { try { // Try to generate proofs const proofProvider = new NoirProofProvider({ wasmPath: '/noir' }) await proofProvider.initialize() builder.withProvider(proofProvider) } catch (e) { console.warn('Proof generation not available, continuing without proofs:', e) // Continue without proofs (for development) } }
return builder.build()}Debugging
Section titled “Debugging”Enable Debug Logging
Section titled “Enable Debug Logging”The SDK doesn’t include built-in debug logging by default, but you can wrap operations:
function withLogging<T>( fn: () => Promise<T>, operation: string): Promise<T> { console.log(`[DEBUG] Starting: ${operation}`) const start = Date.now()
return fn() .then(result => { console.log(`[DEBUG] Success: ${operation} (${Date.now() - start}ms)`) return result }) .catch(e => { console.error(`[DEBUG] Failed: ${operation} (${Date.now() - start}ms)`, e) throw e })}
// Usageconst intent = await withLogging( () => sip.intent() .input('solana', 'SOL', 1_000_000_000n) .output('zcash', 'ZEC', 50_000_000n) .build(), 'intent creation')Inspect Error Context
Section titled “Inspect Error Context”All SIP errors include context for debugging:
try { await sip.execute(intent, quote)} catch (e) { if (isSIPError(e)) { console.log('Error Details:', { code: e.code, message: e.message, context: e.context, timestamp: e.timestamp, stack: e.stack, })
// Serialize for logging const serialized = e.toJSON() await logToService(serialized) }}Viewing Key Debugging
Section titled “Viewing Key Debugging”Use viewing keys to debug privacy transactions:
import { generateViewingKey, encryptForViewing, decryptWithViewing } from '@sip-protocol/sdk'
// Create auditor viewing keyconst auditorKey = generateViewingKey('/compliance/auditor')
// Encrypt transaction detailsconst txDetails = { sender: stealthAddress.spendingKey, recipient: recipientAddress, amount: '1000000000', timestamp: Date.now(),}
const encrypted = encryptForViewing(txDetails, auditorKey)
// Later, auditor can decrypttry { const decrypted = decryptWithViewing(encrypted, auditorKey) console.log('Transaction details:', decrypted)} catch (e) { console.error('Failed to decrypt with viewing key:', e) // Wrong viewing key or corrupted data}Test Error Scenarios
Section titled “Test Error Scenarios”Use mock components to test error handling:
import { createMockSolanaAdapter } from '@sip-protocol/sdk'
// Test connection rejectionconst rejectingWallet = createMockSolanaAdapter({ shouldFailConnect: true,})
try { await rejectingWallet.connect()} catch (e) { expect(e).toBeInstanceOf(WalletError) expect(e.walletCode).toBe('WALLET_CONNECTION_FAILED')}
// Test signing rejectionconst rejectingWallet2 = createMockSolanaAdapter({ shouldFailSign: true,})await rejectingWallet2.connect()
try { await rejectingWallet2.signMessage(new Uint8Array([1, 2, 3]))} catch (e) { expect(e).toBeInstanceOf(WalletError) expect(e.walletCode).toBe('WALLET_SIGNING_FAILED')}Common Issues
Section titled “Common Issues”Issue: Transaction Failed
Section titled “Issue: Transaction Failed”Symptoms:
- Error code:
SIP_5000orSIP_7004 - Message: “Transaction failed” or “Intent execution failed”
Causes:
- Insufficient funds in sender wallet
- Slippage tolerance too low
- Quote expired before execution
- Network congestion causing timeout
- Invalid intent state
Solutions:
// 1. Check wallet balance before executionconst balance = await wallet.getBalance()if (balance < intent.inputAmount) { throw new ValidationError('Insufficient funds', 'input.amount', { required: intent.inputAmount, available: balance, })}
// 2. Increase slippage tolerance on the intent itself (1% = .slippage(1))const intentWithSlippage = await sip.intent() .input('solana', 'SOL', 1_000_000_000n) .output('zcash', 'ZEC', 50_000_000n) .slippage(1) // 1% .build()const quote = await sip.getQuotes(intentWithSlippage)
// 3. Check quote expiryconst now = Math.floor(Date.now() / 1000)if (quote.expiry < now) { // Get new quote const newQuote = await sip.getQuotes(intent)}
// 4. Set longer timeoutconst result = await Promise.race([ sip.execute(intent, quote), new Promise((_, reject) => setTimeout(() => reject(new NetworkError('Timeout', ErrorCode.NETWORK_TIMEOUT)), 60000) )])
// 5. Verify intent stateif (isExpired(intent)) { throw new IntentError('Intent expired', ErrorCode.INTENT_EXPIRED)}Issue: Invalid Stealth Address
Section titled “Issue: Invalid Stealth Address”Symptoms:
- Error code:
SIP_2007 - Message: “Validation failed for ‘stealthAddress’: Invalid format”
Causes:
- Incorrect address format (missing prefix or invalid encoding)
- Wrong chain specified
- Corrupted keys during generation
- Using regular address instead of stealth address
Solutions:
import { generateStealthMetaAddress, encodeStealthMetaAddress, decodeStealthMetaAddress,} from '@sip-protocol/sdk'
// ✅ Generate valid stealth meta-address (chain is required)const keys = generateStealthMetaAddress('solana')const encoded = encodeStealthMetaAddress(keys.metaAddress)
// Validate format: sip:solana:<spendingKey>:<viewingKey>if (!encoded.startsWith('sip:solana:')) { throw new ValidationError('Invalid stealth address format')}
// ✅ Decode safely — decodeStealthMetaAddress validates the keys and// throws a ValidationError if the format or curve is wrongtry { const decoded = decodeStealthMetaAddress(encoded) console.log('Valid stealth address:', decoded)} catch (e) { throw new ValidationError('Invalid stealth address for chain', 'stealthAddress')}Issue: Proof Verification Failed
Section titled “Issue: Proof Verification Failed”Symptoms:
- Error code:
SIP_4002 - Message: “Proof verification failed”
Causes:
- Proof generated with wrong parameters
- Public inputs don’t match private inputs
- Proof provider not initialized
- Circuit mismatch (wrong version)
Troubleshooting:
import { NoirProofProvider } from '@sip-protocol/sdk/proofs/noir'import { commit, generateRandomBytes } from '@sip-protocol/sdk'
const provider = new NoirProofProvider({ wasmPath: '/noir' })
// 1. Always initialize firstawait provider.initialize()
// 2. Generate proof with correct parametersconst balance = 1000nconst blindingFactor = generateRandomBytes(32) // 32-byte Uint8Arrayconst proofResult = await provider.generateFundingProof({ balance, minimumRequired: 100n, blindingFactor, // Use same blinding as commitment assetId: 'SOL', userAddress: wallet.address, ownershipSignature, // Signature over userAddress})
// 3. Verify proof with matching public inputsconst publicInputs = { commitment: commit(balance, blindingFactor).commitment, // Must match! minimumRequired: 100n, assetId: 'SOL',}
const isValid = await provider.verifyFundingProof( proofResult.proof, publicInputs)
if (!isValid) { // Debug: Check each public input console.log('Public inputs:', publicInputs) console.log('Proof data:', proofResult) // Likely cause: Commitment doesn't match balance+blinding}Issue: Wallet Not Connected
Section titled “Issue: Wallet Not Connected”Symptoms:
- Error code:
SIP_7001 - Message: “Wallet not connected. Call connect() first.”
Causes:
- Forgot to call
wallet.connect() - User rejected connection
- Wallet extension not installed
- Wallet disconnected after initial connection
Solutions:
import { WalletError } from '@sip-protocol/sdk'
async function ensureConnected(wallet: WalletAdapter) { if (wallet.isConnected()) { return // Already connected }
try { await wallet.connect() } catch (e) { if (e instanceof WalletError) { switch (e.walletCode) { case 'WALLET_NOT_INSTALLED': throw new Error('Please install a compatible wallet extension')
case 'WALLET_CONNECTION_REJECTED': throw new Error('Please approve the wallet connection request')
case 'WALLET_CONNECTION_FAILED': // Retry once await wallet.connect() break
default: throw e } } throw e }}
// Usageawait ensureConnected(wallet)const signature = await wallet.signMessage(message)Issue: Commitment Verification Failed
Section titled “Issue: Commitment Verification Failed”Symptoms:
- Error code:
SIP_3010 - Message: “Invalid commitment” or “Commitment verification failed”
Causes:
- Using different blinding factor for verification
- Homomorphic addition done incorrectly
- Amount overflow (exceeds curve order)
- Corrupted commitment data
Solutions:
import { commit, verifyOpening, addCommitments, addBlindings } from '@sip-protocol/sdk'
// ✅ Store blinding factor with commitmentconst amount = 1000nconst { commitment, blinding } = commit(amount)
// Save both for later verificationconst stored = { commitment, blinding: blinding, // Must save this! amount,}
// ✅ Verify with saved blindingconst isValid = verifyOpening( stored.commitment, stored.amount, stored.blinding)
console.assert(isValid, 'Verification should succeed')
// ✅ Homomorphic additionconst c1 = commit(100n)const c2 = commit(200n)const sum = addCommitments(c1.commitment, c2.commitment)
// Verify sum (must use sum of blindings — addBlindings sums them mod the curve order)const totalBlinding = addBlindings(c1.blinding, c2.blinding)verifyOpening(sum.commitment, 300n, totalBlinding) // ✅Error Utilities
Section titled “Error Utilities”Wrapping Errors
Section titled “Wrapping Errors”Convert unknown errors to SIPError:
import { wrapError, ErrorCode } from '@sip-protocol/sdk'
async function riskyOperation() { try { await externalLibrary.doSomething() } catch (e) { throw wrapError( e, 'External operation failed', ErrorCode.INTERNAL, { operation: 'doSomething' } ) }}Type Guards
Section titled “Type Guards”Check error types safely:
import { isSIPError, hasErrorCode, ErrorCode } from '@sip-protocol/sdk'
try { await sip.execute(intent, quote)} catch (e) { if (isSIPError(e)) { // TypeScript knows e is SIPError console.log(e.code, e.context) }
if (hasErrorCode(e, ErrorCode.INTENT_EXPIRED)) { // Handle specific error code console.log('Intent expired, creating new one') }}Extract Error Message
Section titled “Extract Error Message”Safely extract message from unknown errors:
import { getErrorMessage } from '@sip-protocol/sdk'
try { await operation()} catch (e) { // Works for Error, SIPError, or any unknown type const message = getErrorMessage(e) console.error('Operation failed:', message)}Best Practices
Section titled “Best Practices”- Always check error types before handling - use
instanceoforhasErrorCode() - Preserve error causes - use the
causeoption when wrapping errors - Add context - include relevant debugging information in
contextfield - Don’t retry user rejections - only retry transient errors (network, timeout)
- Use circuit breakers - prevent cascading failures to external services
- Log serialized errors - use
error.toJSON()for structured logging - Test error paths - use mock adapters to test error handling
- Provide recovery steps - give users actionable error messages
- Validate early - check inputs before expensive operations
- Use graceful degradation - fall back to safe defaults when possible
Next Steps
Section titled “Next Steps”- API Reference - Complete API documentation
- Integration Guide - Building solvers with error handling
- Testing Guide - Testing error scenarios
- Deployment Guide - Production error monitoring