From 41d590ec571a9d2ba68a0ad742059574427f84e3 Mon Sep 17 00:00:00 2001 From: mkothm Date: Sun, 29 Mar 2026 00:26:57 +0100 Subject: [PATCH] feat(utils): add standardized SDK error codes system (#125) - Create unified SDKErrorCode enum with 60+ error codes across 9 categories - Implement SDKError class with full metadata (code, category, severity, httpStatus, recoverable, retryable) - Add SDKErrors factory functions for common error patterns - Add type guards: isSDKError, isSDKErrorCode, isSDKErrorCategory, isRetryableError - Add toSDKError conversion utility for any error - Add legacy error code mappings for backward compatibility - Add comprehensive test suite - Add documentation for error codes standardization BREAKING CHANGE: New standardized error system. Legacy codes mapped for compatibility. --- docs/ERROR_CODES_STANDARDIZATION.md | 342 ++++ .../utils/src/__tests__/sdk-errors.test.ts | 548 +++++++ packages/utils/src/index.ts | 24 +- packages/utils/src/sdk-errors.ts | 1376 +++++++++++++++++ 4 files changed, 2289 insertions(+), 1 deletion(-) create mode 100644 docs/ERROR_CODES_STANDARDIZATION.md create mode 100644 packages/utils/src/__tests__/sdk-errors.test.ts create mode 100644 packages/utils/src/sdk-errors.ts diff --git a/docs/ERROR_CODES_STANDARDIZATION.md b/docs/ERROR_CODES_STANDARDIZATION.md new file mode 100644 index 0000000..ca6fa2c --- /dev/null +++ b/docs/ERROR_CODES_STANDARDIZATION.md @@ -0,0 +1,342 @@ +# SDK Error Codes Standardization + +> **Issue:** #125 - Implement Error Codes Standardization +> **Status:** Implemented +> **Category:** SDK, Reliability + +## Overview + +This module provides a unified error code system used consistently across all BridgeWise SDK modules including adapters, validators, executors, and UI components. + +## Problem Solved + +- ❌ **Before:** Inconsistent error messages across modules (`BridgeErrorCode`, `AdapterErrorCode`, `TokenPairErrorCode`) +- ✅ **After:** Single `SDKErrorCode` enum with consistent error handling + +## Quick Start + +```typescript +import { + SDKError, + SDKErrorCode, + SDKErrors, + isSDKError, + isRetryableError, + toSDKError, +} from '@bridgewise/bridge-core'; + +// Create errors using factory functions +throw SDKErrors.invalidAmount('100', 'Amount must be positive'); + +// Or use the SDKError class directly +throw new SDKError( + SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY, + 'Not enough liquidity for this transfer', + { token: 'USDC', amount: '1000000' } +); +``` + +## Error Code Categories + +| Category | Code Range | Description | +|----------|------------|-------------| +| `NETWORK` | 1000-1999 | Network and connectivity errors | +| `VALIDATION` | 2000-2999 | Input validation errors | +| `TRANSACTION` | 3000-3999 | Transaction execution errors | +| `BRIDGE` | 4000-4999 | Bridge and routing errors | +| `CONTRACT` | 5000-5999 | Smart contract errors | +| `ACCOUNT` | 6000-6999 | Account-related errors | +| `AUTH` | 7000-7999 | Authentication errors | +| `RATE_LIMIT` | 8000-8999 | Rate limiting errors | +| `CONFIG` | 9000-9999 | Configuration errors | +| `INTERNAL` | 10000+ | Internal SDK errors | + +## Error Codes Reference + +### Network Errors + +| Code | Description | Retryable | +|------|-------------|-----------| +| `NETWORK_ERROR` | Generic network error | ✅ | +| `NETWORK_TIMEOUT` | Request timed out | ✅ | +| `NETWORK_CONNECTION_REFUSED` | Connection refused | ✅ | +| `NETWORK_DNS_FAILED` | DNS resolution failed | ✅ | +| `NETWORK_SSL_ERROR` | SSL certificate error | ❌ | +| `NETWORK_RPC_UNAVAILABLE` | RPC endpoint unavailable | ✅ | +| `NETWORK_WEBSOCKET_FAILED` | WebSocket connection failed | ✅ | + +### Validation Errors + +| Code | Description | HTTP Status | +|------|-------------|-------------| +| `VALIDATION_FAILED` | Generic validation error | 400 | +| `VALIDATION_INVALID_ADDRESS` | Invalid address format | 400 | +| `VALIDATION_INVALID_AMOUNT` | Invalid amount | 400 | +| `VALIDATION_AMOUNT_TOO_LOW` | Below minimum | 400 | +| `VALIDATION_AMOUNT_TOO_HIGH` | Above maximum | 400 | +| `VALIDATION_INVALID_CHAIN` | Invalid chain ID | 400 | +| `VALIDATION_INVALID_TOKEN` | Invalid token ID | 400 | +| `VALIDATION_MISSING_FIELD` | Required field missing | 400 | +| `VALIDATION_INVALID_REQUEST` | Invalid request format | 400 | +| `VALIDATION_INVALID_SLIPPAGE` | Slippage out of range | 400 | +| `VALIDATION_INVALID_DEADLINE` | Invalid deadline | 400 | + +### Transaction Errors + +| Code | Description | Retryable | +|------|-------------|-----------| +| `TRANSACTION_FAILED` | Transaction failed | ✅ | +| `TRANSACTION_REJECTED` | Rejected by user | ❌ | +| `TRANSACTION_REVERTED` | Transaction reverted | ❌ | +| `TRANSACTION_INSUFFICIENT_GAS` | Not enough gas | ✅ | +| `TRANSACTION_GAS_ESTIMATION_FAILED` | Gas estimation failed | ✅ | +| `TRANSACTION_NONCE_TOO_LOW` | Nonce too low | ✅ | +| `TRANSACTION_NONCE_TOO_HIGH` | Nonce too high | ✅ | +| `TRANSACTION_UNDERPRICED` | Gas price too low | ✅ | +| `TRANSACTION_ALREADY_KNOWN` | Already submitted | ❌ | +| `TRANSACTION_PENDING_TIMEOUT` | Pending timeout | ✅ | +| `TRANSACTION_INVALID_SIGNATURE` | Invalid signature | ❌ | +| `TRANSACTION_SEQUENCE_MISMATCH` | Stellar sequence mismatch | ✅ | + +### Bridge Errors + +| Code | Description | Retryable | +|------|-------------|-----------| +| `BRIDGE_UNSUPPORTED_CHAIN_PAIR` | Chain pair not supported | ❌ | +| `BRIDGE_UNSUPPORTED_TOKEN` | Token not supported | ❌ | +| `BRIDGE_UNSUPPORTED_TOKEN_PAIR` | Token pair not supported | ❌ | +| `BRIDGE_NOT_AVAILABLE` | Bridge unavailable | ✅ | +| `BRIDGE_PAUSED` | Bridge paused | ✅ | +| `BRIDGE_ROUTE_NOT_FOUND` | No route found | ✅ | +| `BRIDGE_INSUFFICIENT_LIQUIDITY` | Not enough liquidity | ✅ | +| `BRIDGE_QUOTE_EXPIRED` | Quote expired | ✅ | +| `BRIDGE_SLIPPAGE_EXCEEDED` | Slippage exceeded | ✅ | +| `BRIDGE_ALL_ROUTES_FAILED` | All routes failed | ✅ | +| `BRIDGE_DUPLICATE_EXECUTION` | Already executing | ❌ | +| `BRIDGE_TOKEN_MAPPING_NOT_FOUND` | Token mapping missing | ❌ | +| `BRIDGE_FEE_ESTIMATION_FAILED` | Fee estimation failed | ✅ | + +### Rate Limit Errors + +| Code | Description | Retryable | +|------|-------------|-----------| +| `RATE_LIMIT_EXCEEDED` | Rate limit exceeded | ✅ | +| `RATE_LIMIT_QUOTA_EXCEEDED` | Quota exceeded | ✅ | +| `RATE_LIMIT_TOO_MANY_REQUESTS` | Too many requests | ✅ | + +## SDKError Class + +```typescript +class SDKError extends Error { + code: SDKErrorCode; // Error code + category: SDKErrorCategory; // Error category + severity: SDKErrorSeverity; // warning | error | critical + httpStatus: number; // HTTP status equivalent + recoverable: boolean; // User can recover + retryable: boolean; // Can be retried + details?: SDKErrorDetails; // Additional context + timestamp: number; // When error occurred +} +``` + +### Methods + +```typescript +// Serialize to JSON +error.toJSON(): Record + +// Get user-friendly message +error.toUserMessage(): string + +// Check error code +error.is(SDKErrorCode.NETWORK_TIMEOUT): boolean + +// Check category +error.isCategory(SDKErrorCategory.NETWORK): boolean +``` + +## Factory Functions + +`SDKErrors` provides convenient factory functions: + +```typescript +// Network +SDKErrors.networkError(message?, details?) +SDKErrors.timeout(operation?, timeoutMs?) + +// Validation +SDKErrors.invalidAddress(address?) +SDKErrors.invalidAmount(amount?, reason?) +SDKErrors.amountTooLow(amount, minimum) +SDKErrors.amountTooHigh(amount, maximum) +SDKErrors.missingField(field) + +// Bridge +SDKErrors.unsupportedChainPair(source, destination) +SDKErrors.unsupportedToken(token, chain?) +SDKErrors.insufficientLiquidity(token?, amount?) +SDKErrors.routeNotFound(source?, destination?) +SDKErrors.quoteExpired() +SDKErrors.slippageExceeded(expected?, actual?) +SDKErrors.allRoutesFailed(attemptCount?) +SDKErrors.duplicateExecution(routeId?) + +// Transaction +SDKErrors.transactionFailed(reason?, txHash?) +SDKErrors.transactionRejected(reason?) +SDKErrors.insufficientGas() + +// Account +SDKErrors.insufficientBalance(required?, available?) +SDKErrors.accountNotFound(address?) + +// Auth +SDKErrors.walletNotConnected() + +// Rate Limit +SDKErrors.rateLimited(retryAfter?) + +// Config +SDKErrors.notInitialized(component?) +SDKErrors.invalidConfig(field?, reason?) + +// Internal +SDKErrors.internal(message?, cause?) +SDKErrors.notImplemented(feature?) +``` + +## Type Guards + +```typescript +import { + isSDKError, + isSDKErrorCode, + isSDKErrorCategory, + isRetryableError, +} from '@bridgewise/bridge-core'; + +try { + await bridge.execute(route); +} catch (error) { + if (isSDKError(error)) { + console.log(`SDK Error: ${error.code}`); + + if (error.isCategory(SDKErrorCategory.NETWORK)) { + // Handle network errors + } + + if (isRetryableError(error)) { + // Retry logic + await retry(() => bridge.execute(route)); + } + } +} +``` + +## Error Conversion + +Convert any error to `SDKError`: + +```typescript +import { toSDKError } from '@bridgewise/bridge-core'; + +try { + await someOperation(); +} catch (error) { + const sdkError = toSDKError(error); + // Now you have a standardized SDKError + console.log(sdkError.code, sdkError.message); +} +``` + +## Legacy Error Migration + +For backward compatibility, legacy error codes are mapped: + +```typescript +import { fromLegacyErrorCode } from '@bridgewise/bridge-core'; + +// Convert legacy BridgeErrorCode or AdapterErrorCode +const newCode = fromLegacyErrorCode('RPC_TIMEOUT'); +// Returns: SDKErrorCode.NETWORK_TIMEOUT +``` + +### Legacy Mapping Tables + +| Legacy `BridgeErrorCode` | New `SDKErrorCode` | +|--------------------------|-------------------| +| `NETWORK_ERROR` | `NETWORK_ERROR` | +| `RPC_TIMEOUT` | `NETWORK_TIMEOUT` | +| `INVALID_ADDRESS` | `VALIDATION_INVALID_ADDRESS` | +| `INSUFFICIENT_BALANCE` | `ACCOUNT_INSUFFICIENT_BALANCE` | +| `TRANSACTION_FAILED` | `TRANSACTION_FAILED` | +| `RATE_LIMIT_EXCEEDED` | `RATE_LIMIT_EXCEEDED` | + +| Legacy `AdapterErrorCode` | New `SDKErrorCode` | +|---------------------------|-------------------| +| `INVALID_CONFIG` | `CONFIG_INVALID` | +| `UNSUPPORTED_CHAIN_PAIR` | `BRIDGE_UNSUPPORTED_CHAIN_PAIR` | +| `INSUFFICIENT_LIQUIDITY` | `BRIDGE_INSUFFICIENT_LIQUIDITY` | +| `TIMEOUT` | `NETWORK_TIMEOUT` | +| `RATE_LIMITED` | `RATE_LIMIT_EXCEEDED` | + +## Error Handling Best Practices + +### 1. Use Specific Error Codes + +```typescript +// ❌ Bad +throw new Error('Invalid input'); + +// ✅ Good +throw SDKErrors.invalidAmount(amount, 'Amount must be positive'); +``` + +### 2. Include Context in Details + +```typescript +throw new SDKError( + SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY, + 'Not enough liquidity', + { + token: 'USDC', + amount: '1000000', + available: '500000', + suggestion: 'Try a smaller amount or different route', + } +); +``` + +### 3. Check Retryable Before Retrying + +```typescript +if (isRetryableError(error)) { + const delay = error.details?.retryAfter || 1000; + await sleep(delay); + return retry(); +} +``` + +### 4. Use Categories for Broad Handling + +```typescript +if (isSDKErrorCategory(error, SDKErrorCategory.NETWORK)) { + showNetworkErrorUI(); +} else if (isSDKErrorCategory(error, SDKErrorCategory.VALIDATION)) { + showValidationErrorUI(error.details?.field); +} +``` + +## Files + +| File | Description | +|------|-------------| +| `packages/utils/src/sdk-errors.ts` | Error codes, SDKError class, factories | +| `packages/utils/src/__tests__/sdk-errors.test.ts` | Test suite | +| `packages/utils/src/error-codes.ts` | Legacy error codes (preserved) | + +## Related Documentation + +- [API Errors](./API_ERRORS.md) +- [Fallback Bridge Routing](./FALLBACK_BRIDGE_ROUTING.md) +- [Network Timeout Handling](./NETWORK_TIMEOUT_HANDLING.md) diff --git a/packages/utils/src/__tests__/sdk-errors.test.ts b/packages/utils/src/__tests__/sdk-errors.test.ts new file mode 100644 index 0000000..d42165a --- /dev/null +++ b/packages/utils/src/__tests__/sdk-errors.test.ts @@ -0,0 +1,548 @@ +/** + * Tests for SDK Error Codes Standardization + * + * @module sdk-errors.test + */ + +import { describe, it, expect } from 'vitest'; +import { + SDKError, + SDKErrorCode, + SDKErrorCategory, + SDKErrorSeverity, + SDKErrors, + SDK_ERROR_METADATA, + isSDKError, + isSDKErrorCode, + isSDKErrorCategory, + isRetryableError, + toSDKError, + fromLegacyErrorCode, + LEGACY_BRIDGE_ERROR_MAP, + LEGACY_ADAPTER_ERROR_MAP, +} from '../sdk-errors'; + +describe('SDKError', () => { + describe('constructor', () => { + it('creates error with code and default message', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + + expect(error.code).toBe(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.message).toBe('The request timed out. Please try again.'); + expect(error.category).toBe(SDKErrorCategory.NETWORK); + expect(error.severity).toBe(SDKErrorSeverity.WARNING); + expect(error.httpStatus).toBe(504); + expect(error.recoverable).toBe(true); + expect(error.retryable).toBe(true); + }); + + it('creates error with custom message', () => { + const error = new SDKError( + SDKErrorCode.VALIDATION_INVALID_AMOUNT, + 'Amount must be positive' + ); + + expect(error.code).toBe(SDKErrorCode.VALIDATION_INVALID_AMOUNT); + expect(error.message).toBe('Amount must be positive'); + }); + + it('creates error with details', () => { + const error = new SDKError( + SDKErrorCode.VALIDATION_INVALID_ADDRESS, + 'Invalid address', + { field: 'recipient', value: '0xinvalid' } + ); + + expect(error.details).toEqual({ + field: 'recipient', + value: '0xinvalid', + }); + }); + + it('sets timestamp', () => { + const before = Date.now(); + const error = new SDKError(SDKErrorCode.INTERNAL_ERROR); + const after = Date.now(); + + expect(error.timestamp).toBeGreaterThanOrEqual(before); + expect(error.timestamp).toBeLessThanOrEqual(after); + }); + + it('is instance of Error', () => { + const error = new SDKError(SDKErrorCode.INTERNAL_ERROR); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SDKError); + expect(error.name).toBe('SDKError'); + }); + }); + + describe('toJSON', () => { + it('serializes error to JSON', () => { + const error = new SDKError( + SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY, + 'Not enough liquidity', + { token: 'USDC', amount: '1000' } + ); + + const json = error.toJSON(); + + expect(json).toEqual({ + name: 'SDKError', + code: SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + message: 'Not enough liquidity', + httpStatus: 400, + recoverable: true, + retryable: true, + details: { token: 'USDC', amount: '1000' }, + timestamp: expect.any(Number), + }); + }); + }); + + describe('toUserMessage', () => { + it('returns message', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.toUserMessage()).toBe('The request timed out. Please try again.'); + }); + + it('appends suggestion if present', () => { + const error = new SDKError( + SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE, + 'Insufficient balance', + { suggestion: 'Please add more funds.' } + ); + + expect(error.toUserMessage()).toBe('Insufficient balance Please add more funds.'); + }); + }); + + describe('is', () => { + it('returns true for matching code', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.is(SDKErrorCode.NETWORK_TIMEOUT)).toBe(true); + }); + + it('returns false for non-matching code', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.is(SDKErrorCode.NETWORK_ERROR)).toBe(false); + }); + }); + + describe('isCategory', () => { + it('returns true for matching category', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.isCategory(SDKErrorCategory.NETWORK)).toBe(true); + }); + + it('returns false for non-matching category', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.isCategory(SDKErrorCategory.VALIDATION)).toBe(false); + }); + }); +}); + +describe('SDKErrors factory', () => { + it('creates network error', () => { + const error = SDKErrors.networkError('Connection failed'); + expect(error.code).toBe(SDKErrorCode.NETWORK_ERROR); + expect(error.message).toBe('Connection failed'); + }); + + it('creates timeout error', () => { + const error = SDKErrors.timeout('fetchRoutes', 5000); + expect(error.code).toBe(SDKErrorCode.NETWORK_TIMEOUT); + expect(error.message).toBe('fetchRoutes timed out after 5000ms'); + expect(error.details?.operation).toBe('fetchRoutes'); + expect(error.details?.timeoutMs).toBe(5000); + }); + + it('creates invalid address error', () => { + const error = SDKErrors.invalidAddress('0xinvalid'); + expect(error.code).toBe(SDKErrorCode.VALIDATION_INVALID_ADDRESS); + expect(error.message).toBe('Invalid address: 0xinvalid'); + expect(error.details?.field).toBe('address'); + }); + + it('creates invalid amount error', () => { + const error = SDKErrors.invalidAmount('-100', 'Amount must be positive'); + expect(error.code).toBe(SDKErrorCode.VALIDATION_INVALID_AMOUNT); + expect(error.message).toBe('Amount must be positive'); + }); + + it('creates amount too low error', () => { + const error = SDKErrors.amountTooLow('0.01', '1.00'); + expect(error.code).toBe(SDKErrorCode.VALIDATION_AMOUNT_TOO_LOW); + expect(error.message).toBe('Amount 0.01 is below minimum 1.00'); + }); + + it('creates amount too high error', () => { + const error = SDKErrors.amountTooHigh('1000000', '100000'); + expect(error.code).toBe(SDKErrorCode.VALIDATION_AMOUNT_TOO_HIGH); + expect(error.message).toBe('Amount 1000000 exceeds maximum 100000'); + }); + + it('creates missing field error', () => { + const error = SDKErrors.missingField('sourceChain'); + expect(error.code).toBe(SDKErrorCode.VALIDATION_MISSING_FIELD); + expect(error.message).toBe('Missing required field: sourceChain'); + }); + + it('creates unsupported chain pair error', () => { + const error = SDKErrors.unsupportedChainPair('ethereum', 'solana'); + expect(error.code).toBe(SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR); + expect(error.message).toBe('Chain pair ethereum -> solana is not supported'); + expect(error.details?.sourceChain).toBe('ethereum'); + expect(error.details?.destinationChain).toBe('solana'); + }); + + it('creates unsupported token error', () => { + const error = SDKErrors.unsupportedToken('SHIB', 'stellar'); + expect(error.code).toBe(SDKErrorCode.BRIDGE_UNSUPPORTED_TOKEN); + expect(error.message).toBe('Token SHIB is not supported on stellar'); + }); + + it('creates insufficient liquidity error', () => { + const error = SDKErrors.insufficientLiquidity('USDC', '1000000'); + expect(error.code).toBe(SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY); + expect(error.message).toBe('Insufficient liquidity for 1000000 USDC'); + }); + + it('creates route not found error', () => { + const error = SDKErrors.routeNotFound('ethereum', 'stellar'); + expect(error.code).toBe(SDKErrorCode.BRIDGE_ROUTE_NOT_FOUND); + expect(error.message).toBe('No route found for ethereum -> stellar'); + }); + + it('creates quote expired error', () => { + const error = SDKErrors.quoteExpired(); + expect(error.code).toBe(SDKErrorCode.BRIDGE_QUOTE_EXPIRED); + }); + + it('creates slippage exceeded error', () => { + const error = SDKErrors.slippageExceeded('0.5%', '2.5%'); + expect(error.code).toBe(SDKErrorCode.BRIDGE_SLIPPAGE_EXCEEDED); + expect(error.message).toBe('Slippage exceeded: expected 0.5%, got 2.5%'); + }); + + it('creates all routes failed error', () => { + const error = SDKErrors.allRoutesFailed(3); + expect(error.code).toBe(SDKErrorCode.BRIDGE_ALL_ROUTES_FAILED); + expect(error.message).toBe('All 3 routes failed'); + }); + + it('creates duplicate execution error', () => { + const error = SDKErrors.duplicateExecution('route-123'); + expect(error.code).toBe(SDKErrorCode.BRIDGE_DUPLICATE_EXECUTION); + expect(error.message).toBe('Route route-123 is already being executed'); + }); + + it('creates transaction failed error', () => { + const error = SDKErrors.transactionFailed('Out of gas', '0xabc'); + expect(error.code).toBe(SDKErrorCode.TRANSACTION_FAILED); + expect(error.message).toBe('Out of gas'); + expect(error.details?.txHash).toBe('0xabc'); + }); + + it('creates insufficient balance error', () => { + const error = SDKErrors.insufficientBalance('100', '50'); + expect(error.code).toBe(SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE); + expect(error.message).toBe('Insufficient balance: need 100, have 50'); + }); + + it('creates wallet not connected error', () => { + const error = SDKErrors.walletNotConnected(); + expect(error.code).toBe(SDKErrorCode.AUTH_WALLET_NOT_CONNECTED); + }); + + it('creates rate limited error', () => { + const error = SDKErrors.rateLimited(60000); + expect(error.code).toBe(SDKErrorCode.RATE_LIMIT_EXCEEDED); + expect(error.message).toBe('Rate limit exceeded. Retry after 60000ms'); + expect(error.details?.retryAfter).toBe(60000); + }); + + it('creates not initialized error', () => { + const error = SDKErrors.notInitialized('BridgeAggregator'); + expect(error.code).toBe(SDKErrorCode.CONFIG_NOT_INITIALIZED); + expect(error.message).toBe('BridgeAggregator is not initialized'); + }); + + it('creates internal error', () => { + const cause = new Error('original'); + const error = SDKErrors.internal('Something went wrong', cause); + expect(error.code).toBe(SDKErrorCode.INTERNAL_ERROR); + expect(error.details?.cause).toBe(cause); + }); + + it('creates not implemented error', () => { + const error = SDKErrors.notImplemented('multiHopRouting'); + expect(error.code).toBe(SDKErrorCode.INTERNAL_NOT_IMPLEMENTED); + expect(error.message).toBe('multiHopRouting is not implemented'); + }); +}); + +describe('Type Guards', () => { + describe('isSDKError', () => { + it('returns true for SDKError', () => { + const error = new SDKError(SDKErrorCode.NETWORK_ERROR); + expect(isSDKError(error)).toBe(true); + }); + + it('returns false for regular Error', () => { + const error = new Error('regular error'); + expect(isSDKError(error)).toBe(false); + }); + + it('returns false for non-error', () => { + expect(isSDKError('string')).toBe(false); + expect(isSDKError(null)).toBe(false); + expect(isSDKError(undefined)).toBe(false); + expect(isSDKError({})).toBe(false); + }); + }); + + describe('isSDKErrorCode', () => { + it('returns true for matching code', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(isSDKErrorCode(error, SDKErrorCode.NETWORK_TIMEOUT)).toBe(true); + }); + + it('returns false for non-matching code', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(isSDKErrorCode(error, SDKErrorCode.NETWORK_ERROR)).toBe(false); + }); + + it('returns false for non-SDKError', () => { + const error = new Error('regular error'); + expect(isSDKErrorCode(error, SDKErrorCode.NETWORK_ERROR)).toBe(false); + }); + }); + + describe('isSDKErrorCategory', () => { + it('returns true for matching category', () => { + const error = new SDKError(SDKErrorCode.VALIDATION_INVALID_AMOUNT); + expect(isSDKErrorCategory(error, SDKErrorCategory.VALIDATION)).toBe(true); + }); + + it('returns false for non-matching category', () => { + const error = new SDKError(SDKErrorCode.VALIDATION_INVALID_AMOUNT); + expect(isSDKErrorCategory(error, SDKErrorCategory.NETWORK)).toBe(false); + }); + }); + + describe('isRetryableError', () => { + it('returns true for retryable SDKError', () => { + const error = new SDKError(SDKErrorCode.NETWORK_TIMEOUT); + expect(isRetryableError(error)).toBe(true); + }); + + it('returns false for non-retryable SDKError', () => { + const error = new SDKError(SDKErrorCode.VALIDATION_INVALID_ADDRESS); + expect(isRetryableError(error)).toBe(false); + }); + + it('detects retryable patterns in regular errors', () => { + expect(isRetryableError(new Error('timeout'))).toBe(true); + expect(isRetryableError(new Error('network error'))).toBe(true); + expect(isRetryableError(new Error('ECONNREFUSED'))).toBe(true); + expect(isRetryableError(new Error('429 Too Many Requests'))).toBe(true); + expect(isRetryableError(new Error('rate limit exceeded'))).toBe(true); + }); + + it('returns false for non-retryable regular errors', () => { + expect(isRetryableError(new Error('invalid input'))).toBe(false); + }); + }); +}); + +describe('toSDKError', () => { + it('returns same error if already SDKError', () => { + const original = new SDKError(SDKErrorCode.NETWORK_ERROR); + const converted = toSDKError(original); + expect(converted).toBe(original); + }); + + it('converts Error to SDKError', () => { + const original = new Error('Request timeout'); + const converted = toSDKError(original); + + expect(converted).toBeInstanceOf(SDKError); + expect(converted.code).toBe(SDKErrorCode.NETWORK_TIMEOUT); + expect(converted.message).toBe('Request timeout'); + expect(converted.details?.cause).toBe(original); + }); + + it('converts string to SDKError', () => { + const converted = toSDKError('Invalid address provided'); + + expect(converted).toBeInstanceOf(SDKError); + expect(converted.code).toBe(SDKErrorCode.VALIDATION_INVALID_ADDRESS); + expect(converted.message).toBe('Invalid address provided'); + }); + + it('converts unknown to SDKError', () => { + const converted = toSDKError({ foo: 'bar' }); + + expect(converted).toBeInstanceOf(SDKError); + expect(converted.code).toBe(SDKErrorCode.INTERNAL_UNKNOWN); + }); + + it('matches error patterns correctly', () => { + expect(toSDKError(new Error('ECONNREFUSED')).code).toBe( + SDKErrorCode.NETWORK_CONNECTION_REFUSED + ); + expect(toSDKError(new Error('rate limit exceeded')).code).toBe( + SDKErrorCode.RATE_LIMIT_EXCEEDED + ); + expect(toSDKError(new Error('insufficient balance')).code).toBe( + SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE + ); + expect(toSDKError(new Error('transaction failed')).code).toBe( + SDKErrorCode.TRANSACTION_FAILED + ); + expect(toSDKError(new Error('unsupported chain')).code).toBe( + SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR + ); + expect(toSDKError(new Error('contract not found')).code).toBe( + SDKErrorCode.CONTRACT_NOT_FOUND + ); + }); +}); + +describe('Legacy Error Mappings', () => { + describe('fromLegacyErrorCode', () => { + it('maps legacy BridgeErrorCode', () => { + expect(fromLegacyErrorCode('NETWORK_ERROR')).toBe(SDKErrorCode.NETWORK_ERROR); + expect(fromLegacyErrorCode('RPC_TIMEOUT')).toBe(SDKErrorCode.NETWORK_TIMEOUT); + expect(fromLegacyErrorCode('INVALID_ADDRESS')).toBe(SDKErrorCode.VALIDATION_INVALID_ADDRESS); + expect(fromLegacyErrorCode('INSUFFICIENT_BALANCE')).toBe(SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE); + expect(fromLegacyErrorCode('TRANSACTION_FAILED')).toBe(SDKErrorCode.TRANSACTION_FAILED); + expect(fromLegacyErrorCode('RATE_LIMIT_EXCEEDED')).toBe(SDKErrorCode.RATE_LIMIT_EXCEEDED); + }); + + it('maps legacy AdapterErrorCode', () => { + expect(fromLegacyErrorCode('INVALID_CONFIG')).toBe(SDKErrorCode.CONFIG_INVALID); + expect(fromLegacyErrorCode('UNSUPPORTED_CHAIN_PAIR')).toBe(SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR); + expect(fromLegacyErrorCode('INSUFFICIENT_LIQUIDITY')).toBe(SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY); + expect(fromLegacyErrorCode('TIMEOUT')).toBe(SDKErrorCode.NETWORK_TIMEOUT); + expect(fromLegacyErrorCode('NOT_INITIALIZED')).toBe(SDKErrorCode.CONFIG_NOT_INITIALIZED); + }); + + it('returns INTERNAL_UNKNOWN for unknown codes', () => { + expect(fromLegacyErrorCode('DOES_NOT_EXIST')).toBe(SDKErrorCode.INTERNAL_UNKNOWN); + }); + }); + + it('all legacy bridge codes are mapped', () => { + const expectedCodes = [ + 'NETWORK_ERROR', + 'RPC_TIMEOUT', + 'RPC_CONNECTION_FAILED', + 'INVALID_CHAIN_PAIR', + 'INVALID_AMOUNT', + 'INVALID_ADDRESS', + 'INVALID_TOKEN', + 'INSUFFICIENT_BALANCE', + 'ACCOUNT_NOT_FOUND', + 'ACCOUNT_SEQUENCE_MISMATCH', + 'TRANSACTION_FAILED', + 'TRANSACTION_REJECTED', + 'INSUFFICIENT_GAS', + 'DUST_AMOUNT', + 'CONTRACT_ERROR', + 'CONTRACT_NOT_FOUND', + 'CONTRACT_INVOCATION_FAILED', + 'RATE_LIMIT_EXCEEDED', + 'QUOTA_EXCEEDED', + 'UNKNOWN_ERROR', + ]; + + for (const code of expectedCodes) { + expect(LEGACY_BRIDGE_ERROR_MAP[code]).toBeDefined(); + } + }); + + it('all legacy adapter codes are mapped', () => { + const expectedCodes = [ + 'INVALID_CONFIG', + 'MISSING_ENDPOINT', + 'INVALID_AUTH', + 'UNSUPPORTED_CHAIN_PAIR', + 'UNSUPPORTED_TOKEN', + 'INVALID_CHAIN', + 'INVALID_TOKEN', + 'INVALID_REQUEST', + 'INVALID_AMOUNT', + 'INSUFFICIENT_LIQUIDITY', + 'AMOUNT_OUT_OF_RANGE', + 'API_ERROR', + 'NETWORK_ERROR', + 'TIMEOUT', + 'RATE_LIMITED', + 'TOKEN_MAPPING_NOT_FOUND', + 'INVALID_TOKEN_MAPPING', + 'FEE_ESTIMATION_FAILED', + 'NOT_INITIALIZED', + 'NOT_READY', + 'INTERNAL_ERROR', + ]; + + for (const code of expectedCodes) { + expect(LEGACY_ADAPTER_ERROR_MAP[code]).toBeDefined(); + } + }); +}); + +describe('SDK_ERROR_METADATA', () => { + it('has metadata for all error codes', () => { + const allCodes = Object.values(SDKErrorCode); + + for (const code of allCodes) { + expect(SDK_ERROR_METADATA[code]).toBeDefined(); + expect(SDK_ERROR_METADATA[code].httpStatus).toBeGreaterThanOrEqual(200); + expect(SDK_ERROR_METADATA[code].httpStatus).toBeLessThan(600); + expect(SDK_ERROR_METADATA[code].category).toBeDefined(); + expect(SDK_ERROR_METADATA[code].severity).toBeDefined(); + expect(typeof SDK_ERROR_METADATA[code].recoverable).toBe('boolean'); + expect(typeof SDK_ERROR_METADATA[code].retryable).toBe('boolean'); + expect(SDK_ERROR_METADATA[code].defaultMessage).toBeTruthy(); + } + }); + + it('validation errors have 400 status', () => { + const validationCodes = Object.values(SDKErrorCode).filter( + (code) => code.startsWith('VALIDATION_') + ); + + for (const code of validationCodes) { + expect(SDK_ERROR_METADATA[code].httpStatus).toBe(400); + expect(SDK_ERROR_METADATA[code].category).toBe(SDKErrorCategory.VALIDATION); + } + }); + + it('rate limit errors have 429 status', () => { + const rateLimitCodes = Object.values(SDKErrorCode).filter( + (code) => code.startsWith('RATE_LIMIT_') + ); + + for (const code of rateLimitCodes) { + expect(SDK_ERROR_METADATA[code].httpStatus).toBe(429); + expect(SDK_ERROR_METADATA[code].category).toBe(SDKErrorCategory.RATE_LIMIT); + expect(SDK_ERROR_METADATA[code].retryable).toBe(true); + } + }); + + it('network errors are retryable', () => { + const networkCodes = Object.values(SDKErrorCode).filter( + (code) => code.startsWith('NETWORK_') && code !== 'NETWORK_SSL_ERROR' + ); + + for (const code of networkCodes) { + expect(SDK_ERROR_METADATA[code].retryable).toBe(true); + expect(SDK_ERROR_METADATA[code].category).toBe(SDKErrorCategory.NETWORK); + } + }); +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 42d804a..32c3106 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -28,9 +28,31 @@ export * from './fee-estimation'; // Benchmarking export * from './benchmark'; -// Error Codes and Mapping +// Error Codes and Mapping (Legacy) export * from './error-codes'; +// SDK Error Codes (Standardized) +export { + SDKError, + SDKErrorCode, + SDKErrorCategory, + SDKErrorSeverity, + SDKErrors, + SDK_ERROR_METADATA, + isSDKError, + isSDKErrorCode, + isSDKErrorCategory, + isRetryableError, + toSDKError, + fromLegacyErrorCode, + LEGACY_BRIDGE_ERROR_MAP, + LEGACY_ADAPTER_ERROR_MAP, +} from './sdk-errors'; +export type { + SDKErrorMetadata, + SDKErrorDetails, +} from './sdk-errors'; + // Aggregator export { BridgeAggregator } from './aggregator'; export type { AggregatorConfig } from './aggregator'; diff --git a/packages/utils/src/sdk-errors.ts b/packages/utils/src/sdk-errors.ts new file mode 100644 index 0000000..bfec147 --- /dev/null +++ b/packages/utils/src/sdk-errors.ts @@ -0,0 +1,1376 @@ +/** + * BridgeWise SDK - Standardized Error Codes + * + * This module provides a unified error code system used consistently + * across all SDK modules including adapters, validators, and executors. + * + * @module sdk-errors + * @version 1.0.0 + */ + +// ============================================================================ +// Error Code Enums +// ============================================================================ + +/** + * SDK Error Categories + * Top-level categorization for error codes + */ +export enum SDKErrorCategory { + /** Network and connectivity errors */ + NETWORK = 'NETWORK', + /** Validation and input errors */ + VALIDATION = 'VALIDATION', + /** Transaction execution errors */ + TRANSACTION = 'TRANSACTION', + /** Bridge and routing errors */ + BRIDGE = 'BRIDGE', + /** Contract and blockchain errors */ + CONTRACT = 'CONTRACT', + /** Authentication and authorization errors */ + AUTH = 'AUTH', + /** Rate limiting and quota errors */ + RATE_LIMIT = 'RATE_LIMIT', + /** Configuration errors */ + CONFIG = 'CONFIG', + /** Internal SDK errors */ + INTERNAL = 'INTERNAL', +} + +/** + * Unified SDK Error Codes + * + * All error codes follow the pattern: CATEGORY_SPECIFIC_ERROR + * This ensures unique, descriptive, and consistent error identification. + */ +export enum SDKErrorCode { + // ═══════════════════════════════════════════════════════════════════════════ + // Network Errors (1000-1999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Generic network error */ + NETWORK_ERROR = 'NETWORK_ERROR', + /** Request timed out */ + NETWORK_TIMEOUT = 'NETWORK_TIMEOUT', + /** Connection refused */ + NETWORK_CONNECTION_REFUSED = 'NETWORK_CONNECTION_REFUSED', + /** DNS resolution failed */ + NETWORK_DNS_FAILED = 'NETWORK_DNS_FAILED', + /** SSL/TLS error */ + NETWORK_SSL_ERROR = 'NETWORK_SSL_ERROR', + /** RPC endpoint unavailable */ + NETWORK_RPC_UNAVAILABLE = 'NETWORK_RPC_UNAVAILABLE', + /** WebSocket connection failed */ + NETWORK_WEBSOCKET_FAILED = 'NETWORK_WEBSOCKET_FAILED', + + // ═══════════════════════════════════════════════════════════════════════════ + // Validation Errors (2000-2999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Generic validation error */ + VALIDATION_FAILED = 'VALIDATION_FAILED', + /** Invalid address format */ + VALIDATION_INVALID_ADDRESS = 'VALIDATION_INVALID_ADDRESS', + /** Invalid amount */ + VALIDATION_INVALID_AMOUNT = 'VALIDATION_INVALID_AMOUNT', + /** Amount below minimum */ + VALIDATION_AMOUNT_TOO_LOW = 'VALIDATION_AMOUNT_TOO_LOW', + /** Amount above maximum */ + VALIDATION_AMOUNT_TOO_HIGH = 'VALIDATION_AMOUNT_TOO_HIGH', + /** Invalid chain identifier */ + VALIDATION_INVALID_CHAIN = 'VALIDATION_INVALID_CHAIN', + /** Invalid token identifier */ + VALIDATION_INVALID_TOKEN = 'VALIDATION_INVALID_TOKEN', + /** Missing required field */ + VALIDATION_MISSING_FIELD = 'VALIDATION_MISSING_FIELD', + /** Invalid request format */ + VALIDATION_INVALID_REQUEST = 'VALIDATION_INVALID_REQUEST', + /** Slippage out of range */ + VALIDATION_INVALID_SLIPPAGE = 'VALIDATION_INVALID_SLIPPAGE', + /** Invalid deadline */ + VALIDATION_INVALID_DEADLINE = 'VALIDATION_INVALID_DEADLINE', + + // ═══════════════════════════════════════════════════════════════════════════ + // Transaction Errors (3000-3999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Transaction failed */ + TRANSACTION_FAILED = 'TRANSACTION_FAILED', + /** Transaction rejected by user */ + TRANSACTION_REJECTED = 'TRANSACTION_REJECTED', + /** Transaction reverted */ + TRANSACTION_REVERTED = 'TRANSACTION_REVERTED', + /** Insufficient gas */ + TRANSACTION_INSUFFICIENT_GAS = 'TRANSACTION_INSUFFICIENT_GAS', + /** Gas estimation failed */ + TRANSACTION_GAS_ESTIMATION_FAILED = 'TRANSACTION_GAS_ESTIMATION_FAILED', + /** Nonce too low */ + TRANSACTION_NONCE_TOO_LOW = 'TRANSACTION_NONCE_TOO_LOW', + /** Nonce too high */ + TRANSACTION_NONCE_TOO_HIGH = 'TRANSACTION_NONCE_TOO_HIGH', + /** Transaction underpriced */ + TRANSACTION_UNDERPRICED = 'TRANSACTION_UNDERPRICED', + /** Transaction already known */ + TRANSACTION_ALREADY_KNOWN = 'TRANSACTION_ALREADY_KNOWN', + /** Replacement transaction underpriced */ + TRANSACTION_REPLACEMENT_UNDERPRICED = 'TRANSACTION_REPLACEMENT_UNDERPRICED', + /** Transaction pending timeout */ + TRANSACTION_PENDING_TIMEOUT = 'TRANSACTION_PENDING_TIMEOUT', + /** Signature invalid */ + TRANSACTION_INVALID_SIGNATURE = 'TRANSACTION_INVALID_SIGNATURE', + /** Sequence number mismatch (Stellar) */ + TRANSACTION_SEQUENCE_MISMATCH = 'TRANSACTION_SEQUENCE_MISMATCH', + + // ═══════════════════════════════════════════════════════════════════════════ + // Bridge Errors (4000-4999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Unsupported chain pair */ + BRIDGE_UNSUPPORTED_CHAIN_PAIR = 'BRIDGE_UNSUPPORTED_CHAIN_PAIR', + /** Unsupported token */ + BRIDGE_UNSUPPORTED_TOKEN = 'BRIDGE_UNSUPPORTED_TOKEN', + /** Unsupported token pair */ + BRIDGE_UNSUPPORTED_TOKEN_PAIR = 'BRIDGE_UNSUPPORTED_TOKEN_PAIR', + /** Bridge not available */ + BRIDGE_NOT_AVAILABLE = 'BRIDGE_NOT_AVAILABLE', + /** Bridge paused */ + BRIDGE_PAUSED = 'BRIDGE_PAUSED', + /** Route not found */ + BRIDGE_ROUTE_NOT_FOUND = 'BRIDGE_ROUTE_NOT_FOUND', + /** Insufficient liquidity */ + BRIDGE_INSUFFICIENT_LIQUIDITY = 'BRIDGE_INSUFFICIENT_LIQUIDITY', + /** Quote expired */ + BRIDGE_QUOTE_EXPIRED = 'BRIDGE_QUOTE_EXPIRED', + /** Slippage exceeded */ + BRIDGE_SLIPPAGE_EXCEEDED = 'BRIDGE_SLIPPAGE_EXCEEDED', + /** All routes failed */ + BRIDGE_ALL_ROUTES_FAILED = 'BRIDGE_ALL_ROUTES_FAILED', + /** Duplicate execution */ + BRIDGE_DUPLICATE_EXECUTION = 'BRIDGE_DUPLICATE_EXECUTION', + /** Token mapping not found */ + BRIDGE_TOKEN_MAPPING_NOT_FOUND = 'BRIDGE_TOKEN_MAPPING_NOT_FOUND', + /** Invalid token mapping */ + BRIDGE_INVALID_TOKEN_MAPPING = 'BRIDGE_INVALID_TOKEN_MAPPING', + /** Fee estimation failed */ + BRIDGE_FEE_ESTIMATION_FAILED = 'BRIDGE_FEE_ESTIMATION_FAILED', + + // ═══════════════════════════════════════════════════════════════════════════ + // Contract Errors (5000-5999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Contract not found */ + CONTRACT_NOT_FOUND = 'CONTRACT_NOT_FOUND', + /** Contract call failed */ + CONTRACT_CALL_FAILED = 'CONTRACT_CALL_FAILED', + /** Contract invocation failed */ + CONTRACT_INVOCATION_FAILED = 'CONTRACT_INVOCATION_FAILED', + /** Contract execution error */ + CONTRACT_EXECUTION_ERROR = 'CONTRACT_EXECUTION_ERROR', + /** Insufficient allowance */ + CONTRACT_INSUFFICIENT_ALLOWANCE = 'CONTRACT_INSUFFICIENT_ALLOWANCE', + /** Transfer failed */ + CONTRACT_TRANSFER_FAILED = 'CONTRACT_TRANSFER_FAILED', + + // ═══════════════════════════════════════════════════════════════════════════ + // Account Errors (6000-6999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Account not found */ + ACCOUNT_NOT_FOUND = 'ACCOUNT_NOT_FOUND', + /** Insufficient balance */ + ACCOUNT_INSUFFICIENT_BALANCE = 'ACCOUNT_INSUFFICIENT_BALANCE', + /** Account not activated */ + ACCOUNT_NOT_ACTIVATED = 'ACCOUNT_NOT_ACTIVATED', + + // ═══════════════════════════════════════════════════════════════════════════ + // Authentication Errors (7000-7999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Authentication required */ + AUTH_REQUIRED = 'AUTH_REQUIRED', + /** Invalid credentials */ + AUTH_INVALID_CREDENTIALS = 'AUTH_INVALID_CREDENTIALS', + /** Token expired */ + AUTH_TOKEN_EXPIRED = 'AUTH_TOKEN_EXPIRED', + /** Unauthorized */ + AUTH_UNAUTHORIZED = 'AUTH_UNAUTHORIZED', + /** Wallet not connected */ + AUTH_WALLET_NOT_CONNECTED = 'AUTH_WALLET_NOT_CONNECTED', + /** Signature required */ + AUTH_SIGNATURE_REQUIRED = 'AUTH_SIGNATURE_REQUIRED', + + // ═══════════════════════════════════════════════════════════════════════════ + // Rate Limit Errors (8000-8999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Rate limit exceeded */ + RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', + /** Quota exceeded */ + RATE_LIMIT_QUOTA_EXCEEDED = 'RATE_LIMIT_QUOTA_EXCEEDED', + /** Too many requests */ + RATE_LIMIT_TOO_MANY_REQUESTS = 'RATE_LIMIT_TOO_MANY_REQUESTS', + + // ═══════════════════════════════════════════════════════════════════════════ + // Configuration Errors (9000-9999) + // ═══════════════════════════════════════════════════════════════════════════ + /** Invalid configuration */ + CONFIG_INVALID = 'CONFIG_INVALID', + /** Missing configuration */ + CONFIG_MISSING = 'CONFIG_MISSING', + /** Invalid endpoint */ + CONFIG_INVALID_ENDPOINT = 'CONFIG_INVALID_ENDPOINT', + /** Missing API key */ + CONFIG_MISSING_API_KEY = 'CONFIG_MISSING_API_KEY', + /** Adapter not initialized */ + CONFIG_NOT_INITIALIZED = 'CONFIG_NOT_INITIALIZED', + + // ═══════════════════════════════════════════════════════════════════════════ + // Internal Errors (10000+) + // ═══════════════════════════════════════════════════════════════════════════ + /** Unknown error */ + INTERNAL_UNKNOWN = 'INTERNAL_UNKNOWN', + /** Internal error */ + INTERNAL_ERROR = 'INTERNAL_ERROR', + /** Not implemented */ + INTERNAL_NOT_IMPLEMENTED = 'INTERNAL_NOT_IMPLEMENTED', + /** Assertion failed */ + INTERNAL_ASSERTION_FAILED = 'INTERNAL_ASSERTION_FAILED', +} + +// ============================================================================ +// Error Metadata +// ============================================================================ + +/** + * Error severity levels + */ +export enum SDKErrorSeverity { + /** User-recoverable error */ + WARNING = 'warning', + /** Non-recoverable but expected error */ + ERROR = 'error', + /** Critical system error */ + CRITICAL = 'critical', +} + +/** + * Error metadata for each error code + */ +export interface SDKErrorMetadata { + /** HTTP status code equivalent */ + httpStatus: number; + /** Error category */ + category: SDKErrorCategory; + /** Severity level */ + severity: SDKErrorSeverity; + /** Whether this error is typically recoverable */ + recoverable: boolean; + /** Suggested retry strategy */ + retryable: boolean; + /** Default user-facing message */ + defaultMessage: string; +} + +/** + * Error metadata registry + */ +export const SDK_ERROR_METADATA: Record = { + // Network errors + [SDKErrorCode.NETWORK_ERROR]: { + httpStatus: 503, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'A network error occurred. Please check your connection.', + }, + [SDKErrorCode.NETWORK_TIMEOUT]: { + httpStatus: 504, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'The request timed out. Please try again.', + }, + [SDKErrorCode.NETWORK_CONNECTION_REFUSED]: { + httpStatus: 503, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Unable to connect to the server.', + }, + [SDKErrorCode.NETWORK_DNS_FAILED]: { + httpStatus: 503, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'DNS resolution failed.', + }, + [SDKErrorCode.NETWORK_SSL_ERROR]: { + httpStatus: 495, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.CRITICAL, + recoverable: false, + retryable: false, + defaultMessage: 'SSL certificate error.', + }, + [SDKErrorCode.NETWORK_RPC_UNAVAILABLE]: { + httpStatus: 503, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'RPC endpoint is unavailable.', + }, + [SDKErrorCode.NETWORK_WEBSOCKET_FAILED]: { + httpStatus: 503, + category: SDKErrorCategory.NETWORK, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'WebSocket connection failed.', + }, + + // Validation errors + [SDKErrorCode.VALIDATION_FAILED]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Validation failed. Please check your input.', + }, + [SDKErrorCode.VALIDATION_INVALID_ADDRESS]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid address format.', + }, + [SDKErrorCode.VALIDATION_INVALID_AMOUNT]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid amount.', + }, + [SDKErrorCode.VALIDATION_AMOUNT_TOO_LOW]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Amount is below the minimum required.', + }, + [SDKErrorCode.VALIDATION_AMOUNT_TOO_HIGH]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Amount exceeds the maximum allowed.', + }, + [SDKErrorCode.VALIDATION_INVALID_CHAIN]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid chain identifier.', + }, + [SDKErrorCode.VALIDATION_INVALID_TOKEN]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid token identifier.', + }, + [SDKErrorCode.VALIDATION_MISSING_FIELD]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Required field is missing.', + }, + [SDKErrorCode.VALIDATION_INVALID_REQUEST]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid request format.', + }, + [SDKErrorCode.VALIDATION_INVALID_SLIPPAGE]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Slippage value is out of range.', + }, + [SDKErrorCode.VALIDATION_INVALID_DEADLINE]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid deadline.', + }, + + // Transaction errors + [SDKErrorCode.TRANSACTION_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Transaction failed.', + }, + [SDKErrorCode.TRANSACTION_REJECTED]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Transaction was rejected.', + }, + [SDKErrorCode.TRANSACTION_REVERTED]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'Transaction reverted.', + }, + [SDKErrorCode.TRANSACTION_INSUFFICIENT_GAS]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Insufficient gas for transaction.', + }, + [SDKErrorCode.TRANSACTION_GAS_ESTIMATION_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Gas estimation failed.', + }, + [SDKErrorCode.TRANSACTION_NONCE_TOO_LOW]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Nonce too low.', + }, + [SDKErrorCode.TRANSACTION_NONCE_TOO_HIGH]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Nonce too high.', + }, + [SDKErrorCode.TRANSACTION_UNDERPRICED]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Transaction is underpriced.', + }, + [SDKErrorCode.TRANSACTION_ALREADY_KNOWN]: { + httpStatus: 409, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: false, + retryable: false, + defaultMessage: 'Transaction already submitted.', + }, + [SDKErrorCode.TRANSACTION_REPLACEMENT_UNDERPRICED]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Replacement transaction is underpriced.', + }, + [SDKErrorCode.TRANSACTION_PENDING_TIMEOUT]: { + httpStatus: 504, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Transaction pending timeout.', + }, + [SDKErrorCode.TRANSACTION_INVALID_SIGNATURE]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid transaction signature.', + }, + [SDKErrorCode.TRANSACTION_SEQUENCE_MISMATCH]: { + httpStatus: 400, + category: SDKErrorCategory.TRANSACTION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Transaction sequence number mismatch.', + }, + + // Bridge errors + [SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'This chain pair is not supported.', + }, + [SDKErrorCode.BRIDGE_UNSUPPORTED_TOKEN]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'This token is not supported.', + }, + [SDKErrorCode.BRIDGE_UNSUPPORTED_TOKEN_PAIR]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'This token pair is not supported.', + }, + [SDKErrorCode.BRIDGE_NOT_AVAILABLE]: { + httpStatus: 503, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Bridge is currently unavailable.', + }, + [SDKErrorCode.BRIDGE_PAUSED]: { + httpStatus: 503, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Bridge is temporarily paused.', + }, + [SDKErrorCode.BRIDGE_ROUTE_NOT_FOUND]: { + httpStatus: 404, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'No route found for this transfer.', + }, + [SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Insufficient liquidity for this transfer.', + }, + [SDKErrorCode.BRIDGE_QUOTE_EXPIRED]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Quote has expired. Please refresh.', + }, + [SDKErrorCode.BRIDGE_SLIPPAGE_EXCEEDED]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Slippage tolerance exceeded.', + }, + [SDKErrorCode.BRIDGE_ALL_ROUTES_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'All bridge routes failed.', + }, + [SDKErrorCode.BRIDGE_DUPLICATE_EXECUTION]: { + httpStatus: 409, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: false, + retryable: false, + defaultMessage: 'This route is already being executed.', + }, + [SDKErrorCode.BRIDGE_TOKEN_MAPPING_NOT_FOUND]: { + httpStatus: 404, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Token mapping not found.', + }, + [SDKErrorCode.BRIDGE_INVALID_TOKEN_MAPPING]: { + httpStatus: 400, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid token mapping configuration.', + }, + [SDKErrorCode.BRIDGE_FEE_ESTIMATION_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.BRIDGE, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Fee estimation failed.', + }, + + // Contract errors + [SDKErrorCode.CONTRACT_NOT_FOUND]: { + httpStatus: 404, + category: SDKErrorCategory.CONTRACT, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'Contract not found.', + }, + [SDKErrorCode.CONTRACT_CALL_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.CONTRACT, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Contract call failed.', + }, + [SDKErrorCode.CONTRACT_INVOCATION_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.CONTRACT, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Contract invocation failed.', + }, + [SDKErrorCode.CONTRACT_EXECUTION_ERROR]: { + httpStatus: 500, + category: SDKErrorCategory.CONTRACT, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Contract execution error.', + }, + [SDKErrorCode.CONTRACT_INSUFFICIENT_ALLOWANCE]: { + httpStatus: 400, + category: SDKErrorCategory.CONTRACT, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Insufficient token allowance.', + }, + [SDKErrorCode.CONTRACT_TRANSFER_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.CONTRACT, + severity: SDKErrorSeverity.ERROR, + recoverable: true, + retryable: true, + defaultMessage: 'Token transfer failed.', + }, + + // Account errors + [SDKErrorCode.ACCOUNT_NOT_FOUND]: { + httpStatus: 404, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'Account not found.', + }, + [SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Insufficient balance.', + }, + [SDKErrorCode.ACCOUNT_NOT_ACTIVATED]: { + httpStatus: 400, + category: SDKErrorCategory.VALIDATION, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Account is not activated.', + }, + + // Auth errors + [SDKErrorCode.AUTH_REQUIRED]: { + httpStatus: 401, + category: SDKErrorCategory.AUTH, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Authentication required.', + }, + [SDKErrorCode.AUTH_INVALID_CREDENTIALS]: { + httpStatus: 401, + category: SDKErrorCategory.AUTH, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Invalid credentials.', + }, + [SDKErrorCode.AUTH_TOKEN_EXPIRED]: { + httpStatus: 401, + category: SDKErrorCategory.AUTH, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Authentication token has expired.', + }, + [SDKErrorCode.AUTH_UNAUTHORIZED]: { + httpStatus: 403, + category: SDKErrorCategory.AUTH, + severity: SDKErrorSeverity.WARNING, + recoverable: false, + retryable: false, + defaultMessage: 'Unauthorized.', + }, + [SDKErrorCode.AUTH_WALLET_NOT_CONNECTED]: { + httpStatus: 401, + category: SDKErrorCategory.AUTH, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Wallet is not connected.', + }, + [SDKErrorCode.AUTH_SIGNATURE_REQUIRED]: { + httpStatus: 401, + category: SDKErrorCategory.AUTH, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: false, + defaultMessage: 'Signature is required.', + }, + + // Rate limit errors + [SDKErrorCode.RATE_LIMIT_EXCEEDED]: { + httpStatus: 429, + category: SDKErrorCategory.RATE_LIMIT, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Rate limit exceeded. Please wait before retrying.', + }, + [SDKErrorCode.RATE_LIMIT_QUOTA_EXCEEDED]: { + httpStatus: 429, + category: SDKErrorCategory.RATE_LIMIT, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Quota exceeded.', + }, + [SDKErrorCode.RATE_LIMIT_TOO_MANY_REQUESTS]: { + httpStatus: 429, + category: SDKErrorCategory.RATE_LIMIT, + severity: SDKErrorSeverity.WARNING, + recoverable: true, + retryable: true, + defaultMessage: 'Too many requests. Please slow down.', + }, + + // Config errors + [SDKErrorCode.CONFIG_INVALID]: { + httpStatus: 500, + category: SDKErrorCategory.CONFIG, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'Invalid configuration.', + }, + [SDKErrorCode.CONFIG_MISSING]: { + httpStatus: 500, + category: SDKErrorCategory.CONFIG, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'Missing required configuration.', + }, + [SDKErrorCode.CONFIG_INVALID_ENDPOINT]: { + httpStatus: 500, + category: SDKErrorCategory.CONFIG, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'Invalid endpoint configuration.', + }, + [SDKErrorCode.CONFIG_MISSING_API_KEY]: { + httpStatus: 500, + category: SDKErrorCategory.CONFIG, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'API key is missing.', + }, + [SDKErrorCode.CONFIG_NOT_INITIALIZED]: { + httpStatus: 500, + category: SDKErrorCategory.CONFIG, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'SDK is not initialized.', + }, + + // Internal errors + [SDKErrorCode.INTERNAL_UNKNOWN]: { + httpStatus: 500, + category: SDKErrorCategory.INTERNAL, + severity: SDKErrorSeverity.CRITICAL, + recoverable: false, + retryable: false, + defaultMessage: 'An unknown error occurred.', + }, + [SDKErrorCode.INTERNAL_ERROR]: { + httpStatus: 500, + category: SDKErrorCategory.INTERNAL, + severity: SDKErrorSeverity.CRITICAL, + recoverable: false, + retryable: false, + defaultMessage: 'An internal error occurred.', + }, + [SDKErrorCode.INTERNAL_NOT_IMPLEMENTED]: { + httpStatus: 501, + category: SDKErrorCategory.INTERNAL, + severity: SDKErrorSeverity.ERROR, + recoverable: false, + retryable: false, + defaultMessage: 'This feature is not implemented.', + }, + [SDKErrorCode.INTERNAL_ASSERTION_FAILED]: { + httpStatus: 500, + category: SDKErrorCategory.INTERNAL, + severity: SDKErrorSeverity.CRITICAL, + recoverable: false, + retryable: false, + defaultMessage: 'Internal assertion failed.', + }, +}; + +// ============================================================================ +// SDK Error Class +// ============================================================================ + +/** + * Additional details for SDK errors + */ +export interface SDKErrorDetails { + /** Original error that caused this error */ + cause?: unknown; + /** Field that caused the error (for validation errors) */ + field?: string; + /** Suggested action to resolve the error */ + suggestion?: string; + /** Retry delay in milliseconds (for retryable errors) */ + retryAfter?: number; + /** Additional context */ + [key: string]: unknown; +} + +/** + * Standard SDK Error class + * + * Use this class for all errors thrown by the SDK to ensure + * consistent error handling across all modules. + * + * @example + * ```typescript + * throw new SDKError( + * SDKErrorCode.VALIDATION_INVALID_AMOUNT, + * 'Amount must be greater than zero', + * { field: 'amount', value: -1 } + * ); + * ``` + */ +export class SDKError extends Error { + /** Error code */ + readonly code: SDKErrorCode; + /** Error category */ + readonly category: SDKErrorCategory; + /** Error severity */ + readonly severity: SDKErrorSeverity; + /** HTTP status code equivalent */ + readonly httpStatus: number; + /** Whether this error is recoverable */ + readonly recoverable: boolean; + /** Whether this error is retryable */ + readonly retryable: boolean; + /** Additional error details */ + readonly details?: SDKErrorDetails; + /** Error timestamp */ + readonly timestamp: number; + + constructor( + code: SDKErrorCode, + message?: string, + details?: SDKErrorDetails + ) { + const metadata = SDK_ERROR_METADATA[code]; + super(message || metadata.defaultMessage); + + this.name = 'SDKError'; + this.code = code; + this.category = metadata.category; + this.severity = metadata.severity; + this.httpStatus = metadata.httpStatus; + this.recoverable = metadata.recoverable; + this.retryable = metadata.retryable; + this.details = details; + this.timestamp = Date.now(); + + // Maintain proper prototype chain + Object.setPrototypeOf(this, SDKError.prototype); + + // Capture stack trace + if (Error.captureStackTrace) { + Error.captureStackTrace(this, SDKError); + } + } + + /** + * Convert to plain object for serialization + */ + toJSON(): Record { + return { + name: this.name, + code: this.code, + category: this.category, + severity: this.severity, + message: this.message, + httpStatus: this.httpStatus, + recoverable: this.recoverable, + retryable: this.retryable, + details: this.details, + timestamp: this.timestamp, + }; + } + + /** + * Create user-friendly error message + */ + toUserMessage(): string { + const metadata = SDK_ERROR_METADATA[this.code]; + let message = this.message || metadata.defaultMessage; + + if (this.details?.suggestion) { + message += ` ${this.details.suggestion}`; + } + + return message; + } + + /** + * Check if error is of a specific code + */ + is(code: SDKErrorCode): boolean { + return this.code === code; + } + + /** + * Check if error is in a specific category + */ + isCategory(category: SDKErrorCategory): boolean { + return this.category === category; + } +} + +// ============================================================================ +// Error Factory Functions +// ============================================================================ + +/** + * Factory functions for creating common errors + */ +export const SDKErrors = { + // Network errors + networkError: (message?: string, details?: SDKErrorDetails) => + new SDKError(SDKErrorCode.NETWORK_ERROR, message, details), + + timeout: (operation?: string, timeoutMs?: number) => + new SDKError( + SDKErrorCode.NETWORK_TIMEOUT, + operation ? `${operation} timed out after ${timeoutMs}ms` : undefined, + { operation, timeoutMs } + ), + + // Validation errors + invalidAddress: (address?: string) => + new SDKError( + SDKErrorCode.VALIDATION_INVALID_ADDRESS, + address ? `Invalid address: ${address}` : undefined, + { field: 'address', value: address } + ), + + invalidAmount: (amount?: string, reason?: string) => + new SDKError( + SDKErrorCode.VALIDATION_INVALID_AMOUNT, + reason || (amount ? `Invalid amount: ${amount}` : undefined), + { field: 'amount', value: amount } + ), + + amountTooLow: (amount: string, minimum: string) => + new SDKError( + SDKErrorCode.VALIDATION_AMOUNT_TOO_LOW, + `Amount ${amount} is below minimum ${minimum}`, + { field: 'amount', value: amount, minimum } + ), + + amountTooHigh: (amount: string, maximum: string) => + new SDKError( + SDKErrorCode.VALIDATION_AMOUNT_TOO_HIGH, + `Amount ${amount} exceeds maximum ${maximum}`, + { field: 'amount', value: amount, maximum } + ), + + missingField: (field: string) => + new SDKError( + SDKErrorCode.VALIDATION_MISSING_FIELD, + `Missing required field: ${field}`, + { field } + ), + + // Bridge errors + unsupportedChainPair: (source: string, destination: string) => + new SDKError( + SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR, + `Chain pair ${source} -> ${destination} is not supported`, + { sourceChain: source, destinationChain: destination } + ), + + unsupportedToken: (token: string, chain?: string) => + new SDKError( + SDKErrorCode.BRIDGE_UNSUPPORTED_TOKEN, + chain + ? `Token ${token} is not supported on ${chain}` + : `Token ${token} is not supported`, + { token, chain } + ), + + insufficientLiquidity: (token?: string, amount?: string) => + new SDKError( + SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY, + token && amount + ? `Insufficient liquidity for ${amount} ${token}` + : undefined, + { token, amount } + ), + + routeNotFound: (source?: string, destination?: string) => + new SDKError( + SDKErrorCode.BRIDGE_ROUTE_NOT_FOUND, + source && destination + ? `No route found for ${source} -> ${destination}` + : undefined, + { sourceChain: source, destinationChain: destination } + ), + + quoteExpired: () => + new SDKError(SDKErrorCode.BRIDGE_QUOTE_EXPIRED), + + slippageExceeded: (expected?: string, actual?: string) => + new SDKError( + SDKErrorCode.BRIDGE_SLIPPAGE_EXCEEDED, + expected && actual + ? `Slippage exceeded: expected ${expected}, got ${actual}` + : undefined, + { expected, actual } + ), + + allRoutesFailed: (attemptCount?: number) => + new SDKError( + SDKErrorCode.BRIDGE_ALL_ROUTES_FAILED, + attemptCount + ? `All ${attemptCount} routes failed` + : undefined, + { attemptCount } + ), + + duplicateExecution: (routeId?: string) => + new SDKError( + SDKErrorCode.BRIDGE_DUPLICATE_EXECUTION, + routeId ? `Route ${routeId} is already being executed` : undefined, + { routeId } + ), + + // Transaction errors + transactionFailed: (reason?: string, txHash?: string) => + new SDKError( + SDKErrorCode.TRANSACTION_FAILED, + reason, + { txHash } + ), + + transactionRejected: (reason?: string) => + new SDKError(SDKErrorCode.TRANSACTION_REJECTED, reason), + + insufficientGas: () => + new SDKError(SDKErrorCode.TRANSACTION_INSUFFICIENT_GAS), + + // Account errors + insufficientBalance: (required?: string, available?: string) => + new SDKError( + SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE, + required && available + ? `Insufficient balance: need ${required}, have ${available}` + : undefined, + { required, available } + ), + + accountNotFound: (address?: string) => + new SDKError( + SDKErrorCode.ACCOUNT_NOT_FOUND, + address ? `Account not found: ${address}` : undefined, + { address } + ), + + // Auth errors + walletNotConnected: () => + new SDKError(SDKErrorCode.AUTH_WALLET_NOT_CONNECTED), + + // Rate limit errors + rateLimited: (retryAfter?: number) => + new SDKError( + SDKErrorCode.RATE_LIMIT_EXCEEDED, + retryAfter + ? `Rate limit exceeded. Retry after ${retryAfter}ms` + : undefined, + { retryAfter } + ), + + // Config errors + notInitialized: (component?: string) => + new SDKError( + SDKErrorCode.CONFIG_NOT_INITIALIZED, + component ? `${component} is not initialized` : undefined, + { component } + ), + + invalidConfig: (field?: string, reason?: string) => + new SDKError( + SDKErrorCode.CONFIG_INVALID, + field ? `Invalid configuration for ${field}: ${reason}` : reason, + { field } + ), + + // Internal errors + internal: (message?: string, cause?: unknown) => + new SDKError( + SDKErrorCode.INTERNAL_ERROR, + message, + { cause } + ), + + notImplemented: (feature?: string) => + new SDKError( + SDKErrorCode.INTERNAL_NOT_IMPLEMENTED, + feature ? `${feature} is not implemented` : undefined, + { feature } + ), +}; + +// ============================================================================ +// Error Type Guards +// ============================================================================ + +/** + * Check if an error is an SDKError + */ +export function isSDKError(error: unknown): error is SDKError { + return error instanceof SDKError; +} + +/** + * Check if an error is a specific SDKError code + */ +export function isSDKErrorCode( + error: unknown, + code: SDKErrorCode +): error is SDKError { + return isSDKError(error) && error.code === code; +} + +/** + * Check if an error is in a specific category + */ +export function isSDKErrorCategory( + error: unknown, + category: SDKErrorCategory +): error is SDKError { + return isSDKError(error) && error.category === category; +} + +/** + * Check if an error is retryable + */ +export function isRetryableError(error: unknown): boolean { + if (isSDKError(error)) { + return error.retryable; + } + // Check for common retryable patterns in generic errors + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return ( + message.includes('timeout') || + message.includes('network') || + message.includes('econnrefused') || + message.includes('429') || + message.includes('rate limit') + ); + } + return false; +} + +// ============================================================================ +// Error Conversion Utilities +// ============================================================================ + +/** + * Convert any error to SDKError + */ +export function toSDKError(error: unknown): SDKError { + if (isSDKError(error)) { + return error; + } + + if (error instanceof Error) { + // Try to match error message to known patterns + const code = matchErrorToCode(error.message); + return new SDKError(code, error.message, { cause: error }); + } + + if (typeof error === 'string') { + const code = matchErrorToCode(error); + return new SDKError(code, error); + } + + return new SDKError( + SDKErrorCode.INTERNAL_UNKNOWN, + 'An unknown error occurred', + { cause: error } + ); +} + +/** + * Match error message to error code + */ +function matchErrorToCode(message: string): SDKErrorCode { + const lowerMessage = message.toLowerCase(); + + // Network errors + if (lowerMessage.includes('timeout')) return SDKErrorCode.NETWORK_TIMEOUT; + if (lowerMessage.includes('econnrefused') || lowerMessage.includes('connection refused')) + return SDKErrorCode.NETWORK_CONNECTION_REFUSED; + if (lowerMessage.includes('network')) return SDKErrorCode.NETWORK_ERROR; + + // Rate limit + if (lowerMessage.includes('rate limit') || lowerMessage.includes('429') || lowerMessage.includes('too many')) + return SDKErrorCode.RATE_LIMIT_EXCEEDED; + + // Validation + if (lowerMessage.includes('invalid address')) return SDKErrorCode.VALIDATION_INVALID_ADDRESS; + if (lowerMessage.includes('invalid amount')) return SDKErrorCode.VALIDATION_INVALID_AMOUNT; + if (lowerMessage.includes('insufficient balance')) return SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE; + + // Transaction + if (lowerMessage.includes('transaction failed')) return SDKErrorCode.TRANSACTION_FAILED; + if (lowerMessage.includes('rejected')) return SDKErrorCode.TRANSACTION_REJECTED; + if (lowerMessage.includes('insufficient gas')) return SDKErrorCode.TRANSACTION_INSUFFICIENT_GAS; + + // Bridge + if (lowerMessage.includes('unsupported chain')) return SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR; + if (lowerMessage.includes('insufficient liquidity')) return SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY; + if (lowerMessage.includes('route not found') || lowerMessage.includes('no route')) + return SDKErrorCode.BRIDGE_ROUTE_NOT_FOUND; + + // Contract + if (lowerMessage.includes('contract not found')) return SDKErrorCode.CONTRACT_NOT_FOUND; + if (lowerMessage.includes('contract') && lowerMessage.includes('failed')) + return SDKErrorCode.CONTRACT_CALL_FAILED; + + return SDKErrorCode.INTERNAL_UNKNOWN; +} + +// ============================================================================ +// Legacy Error Code Mappings (for backward compatibility) +// ============================================================================ + +/** + * Map legacy BridgeErrorCode to SDKErrorCode + */ +export const LEGACY_BRIDGE_ERROR_MAP: Record = { + NETWORK_ERROR: SDKErrorCode.NETWORK_ERROR, + RPC_TIMEOUT: SDKErrorCode.NETWORK_TIMEOUT, + RPC_CONNECTION_FAILED: SDKErrorCode.NETWORK_CONNECTION_REFUSED, + INVALID_CHAIN_PAIR: SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR, + INVALID_AMOUNT: SDKErrorCode.VALIDATION_INVALID_AMOUNT, + INVALID_ADDRESS: SDKErrorCode.VALIDATION_INVALID_ADDRESS, + INVALID_TOKEN: SDKErrorCode.VALIDATION_INVALID_TOKEN, + INSUFFICIENT_BALANCE: SDKErrorCode.ACCOUNT_INSUFFICIENT_BALANCE, + ACCOUNT_NOT_FOUND: SDKErrorCode.ACCOUNT_NOT_FOUND, + ACCOUNT_SEQUENCE_MISMATCH: SDKErrorCode.TRANSACTION_SEQUENCE_MISMATCH, + TRANSACTION_FAILED: SDKErrorCode.TRANSACTION_FAILED, + TRANSACTION_REJECTED: SDKErrorCode.TRANSACTION_REJECTED, + INSUFFICIENT_GAS: SDKErrorCode.TRANSACTION_INSUFFICIENT_GAS, + DUST_AMOUNT: SDKErrorCode.VALIDATION_AMOUNT_TOO_LOW, + CONTRACT_ERROR: SDKErrorCode.CONTRACT_EXECUTION_ERROR, + CONTRACT_NOT_FOUND: SDKErrorCode.CONTRACT_NOT_FOUND, + CONTRACT_INVOCATION_FAILED: SDKErrorCode.CONTRACT_INVOCATION_FAILED, + RATE_LIMIT_EXCEEDED: SDKErrorCode.RATE_LIMIT_EXCEEDED, + QUOTA_EXCEEDED: SDKErrorCode.RATE_LIMIT_QUOTA_EXCEEDED, + UNKNOWN_ERROR: SDKErrorCode.INTERNAL_UNKNOWN, +}; + +/** + * Map legacy AdapterErrorCode to SDKErrorCode + */ +export const LEGACY_ADAPTER_ERROR_MAP: Record = { + INVALID_CONFIG: SDKErrorCode.CONFIG_INVALID, + MISSING_ENDPOINT: SDKErrorCode.CONFIG_INVALID_ENDPOINT, + INVALID_AUTH: SDKErrorCode.AUTH_INVALID_CREDENTIALS, + UNSUPPORTED_CHAIN_PAIR: SDKErrorCode.BRIDGE_UNSUPPORTED_CHAIN_PAIR, + UNSUPPORTED_TOKEN: SDKErrorCode.BRIDGE_UNSUPPORTED_TOKEN, + INVALID_CHAIN: SDKErrorCode.VALIDATION_INVALID_CHAIN, + INVALID_TOKEN: SDKErrorCode.VALIDATION_INVALID_TOKEN, + INVALID_REQUEST: SDKErrorCode.VALIDATION_INVALID_REQUEST, + INVALID_AMOUNT: SDKErrorCode.VALIDATION_INVALID_AMOUNT, + INSUFFICIENT_LIQUIDITY: SDKErrorCode.BRIDGE_INSUFFICIENT_LIQUIDITY, + AMOUNT_OUT_OF_RANGE: SDKErrorCode.VALIDATION_AMOUNT_TOO_HIGH, + API_ERROR: SDKErrorCode.NETWORK_ERROR, + NETWORK_ERROR: SDKErrorCode.NETWORK_ERROR, + TIMEOUT: SDKErrorCode.NETWORK_TIMEOUT, + RATE_LIMITED: SDKErrorCode.RATE_LIMIT_EXCEEDED, + TOKEN_MAPPING_NOT_FOUND: SDKErrorCode.BRIDGE_TOKEN_MAPPING_NOT_FOUND, + INVALID_TOKEN_MAPPING: SDKErrorCode.BRIDGE_INVALID_TOKEN_MAPPING, + FEE_ESTIMATION_FAILED: SDKErrorCode.BRIDGE_FEE_ESTIMATION_FAILED, + NOT_INITIALIZED: SDKErrorCode.CONFIG_NOT_INITIALIZED, + NOT_READY: SDKErrorCode.CONFIG_NOT_INITIALIZED, + INTERNAL_ERROR: SDKErrorCode.INTERNAL_ERROR, +}; + +/** + * Convert legacy error code to SDKErrorCode + */ +export function fromLegacyErrorCode(legacyCode: string): SDKErrorCode { + return ( + LEGACY_BRIDGE_ERROR_MAP[legacyCode] || + LEGACY_ADAPTER_ERROR_MAP[legacyCode] || + SDKErrorCode.INTERNAL_UNKNOWN + ); +}