@circle-fin/adapter-ethers-v6 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -19,10 +19,10 @@
19
19
  import { JsonRpcProvider, Wallet, BrowserProvider, Interface, Contract } from 'ethers';
20
20
  export { JsonRpcProvider } from 'ethers';
21
21
  import { z } from 'zod';
22
+ import '@ethersproject/units';
22
23
  import { hexlify, hexZeroPad } from '@ethersproject/bytes';
23
24
  import { getAddress } from '@ethersproject/address';
24
25
  import bs58 from 'bs58';
25
- import '@ethersproject/units';
26
26
 
27
27
  /**
28
28
  * Type-safe registry for managing and executing blockchain action handlers.
@@ -2846,9 +2846,71 @@ const RECOVERABILITY_VALUES = [
2846
2846
  'RESUMABLE',
2847
2847
  'FATAL',
2848
2848
  ];
2849
+ /**
2850
+ * Error type constants for categorizing errors by origin.
2851
+ *
2852
+ * This const object provides a reference for error types, enabling
2853
+ * IDE autocomplete and preventing typos when creating custom errors.
2854
+ *
2855
+ * @remarks
2856
+ * While internal error definitions use string literals with type annotations
2857
+ * for strict type safety, this constant is useful for developers creating
2858
+ * custom error instances or checking error types programmatically.
2859
+ *
2860
+ * @example
2861
+ * ```typescript
2862
+ * import { ERROR_TYPES, KitError } from '@core/errors'
2863
+ *
2864
+ * // Use for type checking
2865
+ * if (error.type === ERROR_TYPES.BALANCE) {
2866
+ * console.log('This is a balance error')
2867
+ * }
2868
+ * ```
2869
+ *
2870
+ * @example
2871
+ * ```typescript
2872
+ * // Use as reference when creating custom errors
2873
+ * const error = new KitError({
2874
+ * code: 9999,
2875
+ * name: 'CUSTOM_ERROR',
2876
+ * type: ERROR_TYPES.BALANCE, // IDE autocomplete works here
2877
+ * recoverability: 'FATAL',
2878
+ * message: 'Custom balance error'
2879
+ * })
2880
+ * ```
2881
+ */
2882
+ const ERROR_TYPES = {
2883
+ /** User input validation and parameter checking */
2884
+ INPUT: 'INPUT',
2885
+ /** Insufficient token balances and amount validation */
2886
+ BALANCE: 'BALANCE',
2887
+ /** On-chain execution: reverts, gas issues, transaction failures */
2888
+ ONCHAIN: 'ONCHAIN',
2889
+ /** Blockchain RPC provider issues and endpoint problems */
2890
+ RPC: 'RPC',
2891
+ /** Internet connectivity, DNS resolution, connection issues */
2892
+ NETWORK: 'NETWORK',
2893
+ };
2894
+ /**
2895
+ * Array of valid error type values for validation.
2896
+ * Derived from ERROR_TYPES const object.
2897
+ */
2898
+ const ERROR_TYPE_VALUES = Object.values(ERROR_TYPES);
2849
2899
 
2850
- // Create a mutable array for Zod enum validation
2900
+ // Create mutable arrays for Zod enum validation
2851
2901
  const RECOVERABILITY_ARRAY = [...RECOVERABILITY_VALUES];
2902
+ const ERROR_TYPE_ARRAY = [...ERROR_TYPE_VALUES];
2903
+ /**
2904
+ * Error code ranges for validation.
2905
+ * Single source of truth for valid error code ranges.
2906
+ */
2907
+ const ERROR_CODE_RANGES = [
2908
+ { min: 1000, max: 1999, type: 'INPUT' },
2909
+ { min: 3000, max: 3999, type: 'NETWORK' },
2910
+ { min: 4000, max: 4999, type: 'RPC' },
2911
+ { min: 5000, max: 5999, type: 'ONCHAIN' },
2912
+ { min: 9000, max: 9999, type: 'BALANCE' },
2913
+ ];
2852
2914
  /**
2853
2915
  * Zod schema for validating ErrorDetails objects.
2854
2916
  *
@@ -2861,7 +2923,8 @@ const RECOVERABILITY_ARRAY = [...RECOVERABILITY_VALUES];
2861
2923
  *
2862
2924
  * const result = errorDetailsSchema.safeParse({
2863
2925
  * code: 1001,
2864
- * name: 'NETWORK_MISMATCH',
2926
+ * name: 'INPUT_NETWORK_MISMATCH',
2927
+ * type: 'INPUT',
2865
2928
  * recoverability: 'FATAL',
2866
2929
  * message: 'Source and destination networks must be different'
2867
2930
  * })
@@ -2870,30 +2933,56 @@ const RECOVERABILITY_ARRAY = [...RECOVERABILITY_VALUES];
2870
2933
  * console.error('Validation failed:', result.error.issues)
2871
2934
  * }
2872
2935
  * ```
2936
+ *
2937
+ * @example
2938
+ * ```typescript
2939
+ * // Runtime error
2940
+ * const result = errorDetailsSchema.safeParse({
2941
+ * code: 9001,
2942
+ * name: 'BALANCE_INSUFFICIENT_TOKEN',
2943
+ * type: 'BALANCE',
2944
+ * recoverability: 'FATAL',
2945
+ * message: 'Insufficient USDC balance'
2946
+ * })
2947
+ * ```
2873
2948
  */
2874
2949
  const errorDetailsSchema = z.object({
2875
- /** Numeric identifier following standardized ranges (1000+ for INPUT errors) */
2950
+ /**
2951
+ * Numeric identifier following standardized ranges:
2952
+ * - 1000-1999: INPUT errors - Parameter validation
2953
+ * - 3000-3999: NETWORK errors - Connectivity issues
2954
+ * - 4000-4999: RPC errors - Provider issues, gas estimation
2955
+ * - 5000-5999: ONCHAIN errors - Transaction/simulation failures
2956
+ * - 9000-9999: BALANCE errors - Insufficient funds
2957
+ */
2876
2958
  code: z
2877
2959
  .number()
2878
2960
  .int('Error code must be an integer')
2879
- .min(1000, 'Error code must be within valid range (1000+)')
2880
- .max(1099, 'Error code must be within valid range (1099 max)'),
2881
- /** Human-readable ID (e.g., "NETWORK_MISMATCH") */
2961
+ .refine((code) => ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
2962
+ message: 'Error code must be in valid ranges: 1000-1999 (INPUT), 3000-3999 (NETWORK), 4000-4999 (RPC), 5000-5999 (ONCHAIN), 9000-9999 (BALANCE)',
2963
+ }),
2964
+ /** Human-readable ID (e.g., "INPUT_NETWORK_MISMATCH", "BALANCE_INSUFFICIENT_TOKEN") */
2882
2965
  name: z
2883
2966
  .string()
2884
2967
  .min(1, 'Error name must be a non-empty string')
2885
2968
  .regex(/^[A-Z_][A-Z0-9_]*$/, 'Error name must match pattern: ^[A-Z_][A-Z0-9_]*$'),
2969
+ /** Error category indicating where the error originated */
2970
+ type: z.enum(ERROR_TYPE_ARRAY, {
2971
+ errorMap: () => ({
2972
+ message: 'Error type must be one of: INPUT, BALANCE, ONCHAIN, RPC, NETWORK',
2973
+ }),
2974
+ }),
2886
2975
  /** Error handling strategy */
2887
2976
  recoverability: z.enum(RECOVERABILITY_ARRAY, {
2888
2977
  errorMap: () => ({
2889
2978
  message: 'Recoverability must be one of: RETRYABLE, RESUMABLE, FATAL',
2890
2979
  }),
2891
2980
  }),
2892
- /** User-friendly explanation with network context */
2981
+ /** User-friendly explanation with context */
2893
2982
  message: z
2894
2983
  .string()
2895
2984
  .min(1, 'Error message must be a non-empty string')
2896
- .max(500, 'Error message must be 500 characters or less'),
2985
+ .max(1000, 'Error message must be 1000 characters or less'),
2897
2986
  /** Raw error details, context, or the original error that caused this one. */
2898
2987
  cause: z
2899
2988
  .object({
@@ -2908,7 +2997,7 @@ const errorDetailsSchema = z.object({
2908
2997
  *
2909
2998
  * @param details - The object to validate
2910
2999
  * @returns The validated ErrorDetails object
2911
- * @throws {TypeError} When validation fails
3000
+ * @throws TypeError When validation fails
2912
3001
  *
2913
3002
  * @example
2914
3003
  * ```typescript
@@ -2985,6 +3074,8 @@ class KitError extends Error {
2985
3074
  code;
2986
3075
  /** Human-readable ID (e.g., "NETWORK_MISMATCH") */
2987
3076
  name;
3077
+ /** Error category indicating where the error originated */
3078
+ type;
2988
3079
  /** Error handling strategy */
2989
3080
  recoverability;
2990
3081
  /** Raw error details, context, or the original error that caused this one. */
@@ -3013,6 +3104,12 @@ class KitError extends Error {
3013
3104
  enumerable: true,
3014
3105
  configurable: false,
3015
3106
  },
3107
+ type: {
3108
+ value: validatedDetails.type,
3109
+ writable: false,
3110
+ enumerable: true,
3111
+ configurable: false,
3112
+ },
3016
3113
  recoverability: {
3017
3114
  value: validatedDetails.recoverability,
3018
3115
  writable: false,
@@ -3032,14 +3129,19 @@ class KitError extends Error {
3032
3129
  }
3033
3130
 
3034
3131
  /**
3035
- * Minimum error code for INPUT type errors.
3036
- * INPUT errors represent validation failures and invalid parameters.
3132
+ * Standardized error code ranges for consistent categorization:
3133
+ *
3134
+ * - 1000-1999: INPUT errors - Parameter validation, input format errors
3135
+ * - 3000-3999: NETWORK errors - Internet connectivity, DNS, connection issues
3136
+ * - 4000-4999: RPC errors - Blockchain provider issues, gas estimation, nonce errors
3137
+ * - 5000-5999: ONCHAIN errors - Transaction/simulation failures, gas exhaustion, reverts
3138
+ * - 9000-9999: BALANCE errors - Insufficient funds, token balance, allowance
3037
3139
  */
3038
3140
  /**
3039
3141
  * Standardized error definitions for INPUT type errors.
3040
3142
  *
3041
- * Each entry combines the numeric error code with its corresponding
3042
- * string name to ensure consistency when creating error instances.
3143
+ * Each entry combines the numeric error code, string name, and type
3144
+ * to ensure consistency when creating error instances.
3043
3145
  *
3044
3146
  * Error codes follow a hierarchical numbering scheme where the first digit
3045
3147
  * indicates the error category (1 = INPUT) and subsequent digits provide
@@ -3056,48 +3158,609 @@ class KitError extends Error {
3056
3158
  * message: 'Source and destination networks must be different'
3057
3159
  * })
3058
3160
  *
3059
- * // Access code and name individually if needed
3161
+ * // Access code, name, and type individually if needed
3060
3162
  * console.log(InputError.NETWORK_MISMATCH.code) // 1001
3061
3163
  * console.log(InputError.NETWORK_MISMATCH.name) // 'INPUT_NETWORK_MISMATCH'
3164
+ * console.log(InputError.NETWORK_MISMATCH.type) // 'INPUT'
3062
3165
  * ```
3063
3166
  */
3064
3167
  const InputError = {
3065
- /** Unsupported or invalid bridge route configuration */
3066
- UNSUPPORTED_ROUTE: {
3067
- code: 1003,
3068
- name: 'INPUT_UNSUPPORTED_ROUTE',
3168
+ /** Invalid or unsupported chain identifier */
3169
+ INVALID_CHAIN: {
3170
+ code: 1005,
3171
+ name: 'INPUT_INVALID_CHAIN',
3172
+ type: 'INPUT',
3173
+ }};
3174
+ /**
3175
+ * Standardized error definitions for BALANCE type errors.
3176
+ *
3177
+ * BALANCE errors indicate insufficient funds or allowance issues
3178
+ * that prevent transaction execution.
3179
+ *
3180
+ * @example
3181
+ * ```typescript
3182
+ * import { BalanceError } from '@core/errors'
3183
+ *
3184
+ * const error = new KitError({
3185
+ * ...BalanceError.INSUFFICIENT_TOKEN,
3186
+ * recoverability: 'FATAL',
3187
+ * message: 'Insufficient USDC balance on Ethereum',
3188
+ * cause: { trace: { required: '100', available: '50' } }
3189
+ * })
3190
+ * ```
3191
+ */
3192
+ const BalanceError = {
3193
+ /** Insufficient token balance for transaction */
3194
+ INSUFFICIENT_TOKEN: {
3195
+ code: 9001,
3196
+ name: 'BALANCE_INSUFFICIENT_TOKEN',
3197
+ type: 'BALANCE',
3198
+ },
3199
+ /** Insufficient native token (ETH/SOL/etc) for gas fees */
3200
+ INSUFFICIENT_GAS: {
3201
+ code: 9002,
3202
+ name: 'BALANCE_INSUFFICIENT_GAS',
3203
+ type: 'BALANCE',
3204
+ }};
3205
+ /**
3206
+ * Standardized error definitions for ONCHAIN type errors.
3207
+ *
3208
+ * ONCHAIN errors occur during transaction execution, simulation,
3209
+ * or interaction with smart contracts on the blockchain.
3210
+ *
3211
+ * @example
3212
+ * ```typescript
3213
+ * import { OnchainError } from '@core/errors'
3214
+ *
3215
+ * const error = new KitError({
3216
+ * ...OnchainError.SIMULATION_FAILED,
3217
+ * recoverability: 'FATAL',
3218
+ * message: 'Simulation failed: ERC20 transfer amount exceeds balance',
3219
+ * cause: { trace: { reason: 'ERC20: transfer amount exceeds balance' } }
3220
+ * })
3221
+ * ```
3222
+ */
3223
+ const OnchainError = {
3224
+ /** Transaction reverted on-chain after execution */
3225
+ TRANSACTION_REVERTED: {
3226
+ code: 5001,
3227
+ name: 'ONCHAIN_TRANSACTION_REVERTED',
3228
+ type: 'ONCHAIN',
3229
+ },
3230
+ /** Pre-flight transaction simulation failed */
3231
+ SIMULATION_FAILED: {
3232
+ code: 5002,
3233
+ name: 'ONCHAIN_SIMULATION_FAILED',
3234
+ type: 'ONCHAIN',
3235
+ },
3236
+ /** Transaction ran out of gas during execution */
3237
+ OUT_OF_GAS: {
3238
+ code: 5003,
3239
+ name: 'ONCHAIN_OUT_OF_GAS',
3240
+ type: 'ONCHAIN',
3241
+ }};
3242
+ /**
3243
+ * Standardized error definitions for RPC type errors.
3244
+ *
3245
+ * RPC errors occur when communicating with blockchain RPC providers,
3246
+ * including endpoint failures, invalid responses, and provider-specific issues.
3247
+ *
3248
+ * @example
3249
+ * ```typescript
3250
+ * import { RpcError } from '@core/errors'
3251
+ *
3252
+ * const error = new KitError({
3253
+ * ...RpcError.ENDPOINT_ERROR,
3254
+ * recoverability: 'RETRYABLE',
3255
+ * message: 'RPC endpoint unavailable on Ethereum',
3256
+ * cause: { trace: { endpoint: 'https://mainnet.infura.io' } }
3257
+ * })
3258
+ * ```
3259
+ */
3260
+ const RpcError = {
3261
+ /** RPC endpoint returned error or is unavailable */
3262
+ ENDPOINT_ERROR: {
3263
+ code: 4001,
3264
+ name: 'RPC_ENDPOINT_ERROR',
3265
+ type: 'RPC',
3266
+ }};
3267
+ /**
3268
+ * Standardized error definitions for NETWORK type errors.
3269
+ *
3270
+ * NETWORK errors indicate connectivity issues at the network layer,
3271
+ * including DNS failures, connection timeouts, and unreachable endpoints.
3272
+ *
3273
+ * @example
3274
+ * ```typescript
3275
+ * import { NetworkError } from '@core/errors'
3276
+ *
3277
+ * const error = new KitError({
3278
+ * ...NetworkError.CONNECTION_FAILED,
3279
+ * recoverability: 'RETRYABLE',
3280
+ * message: 'Failed to connect to Ethereum network',
3281
+ * cause: { trace: { error: 'ECONNREFUSED' } }
3282
+ * })
3283
+ * ```
3284
+ */
3285
+ const NetworkError = {
3286
+ /** Network connection failed or unreachable */
3287
+ CONNECTION_FAILED: {
3288
+ code: 3001,
3289
+ name: 'NETWORK_CONNECTION_FAILED',
3290
+ type: 'NETWORK',
3069
3291
  }};
3070
3292
 
3071
3293
  /**
3072
- * Creates error for unsupported bridge route.
3294
+ * Creates error for invalid chain configuration.
3073
3295
  *
3074
- * This error is thrown when attempting to bridge between chains that don't
3075
- * have a supported bridge route configured.
3296
+ * This error is thrown when the provided chain doesn't meet the required
3297
+ * configuration or is not supported for the operation.
3076
3298
  *
3077
- * @param source - Source chain name
3078
- * @param destination - Destination chain name
3079
- * @returns KitError with specific route details
3299
+ * @param chain - The invalid chain name or identifier
3300
+ * @param reason - Specific reason why chain is invalid
3301
+ * @returns KitError with chain details and validation rule
3080
3302
  *
3081
3303
  * @example
3082
3304
  * ```typescript
3083
- * import { createUnsupportedRouteError } from '@core/errors'
3305
+ * import { createInvalidChainError } from '@core/errors'
3084
3306
  *
3085
- * throw createUnsupportedRouteError('Ethereum', 'Solana')
3086
- * // Message: "Route from Ethereum to Solana is not supported"
3307
+ * throw createInvalidChainError('UnknownChain', 'Chain is not supported by this bridge')
3308
+ * // Message: "Invalid chain 'UnknownChain': Chain is not supported by this bridge"
3087
3309
  * ```
3088
3310
  */
3089
- function createUnsupportedRouteError(source, destination) {
3311
+ function createInvalidChainError(chain, reason) {
3090
3312
  const errorDetails = {
3091
- ...InputError.UNSUPPORTED_ROUTE,
3313
+ ...InputError.INVALID_CHAIN,
3092
3314
  recoverability: 'FATAL',
3093
- message: `Route from ${source} to ${destination} is not supported.`,
3315
+ message: `Invalid chain '${chain}': ${reason}`,
3094
3316
  cause: {
3095
- trace: { source, destination },
3317
+ trace: { chain, reason },
3096
3318
  },
3097
3319
  };
3098
3320
  return new KitError(errorDetails);
3099
3321
  }
3100
3322
 
3323
+ /**
3324
+ * Creates error for insufficient token balance.
3325
+ *
3326
+ * This error is thrown when a wallet does not have enough tokens to
3327
+ * complete a transaction. The error is FATAL as it requires user
3328
+ * intervention to add funds.
3329
+ *
3330
+ * @param chain - The blockchain network where the balance check failed
3331
+ * @param token - The token symbol (e.g., 'USDC', 'ETH')
3332
+ * @param rawError - The original error from the underlying system (optional)
3333
+ * @returns KitError with insufficient token balance details
3334
+ *
3335
+ * @example
3336
+ * ```typescript
3337
+ * import { createInsufficientTokenBalanceError } from '@core/errors'
3338
+ *
3339
+ * throw createInsufficientTokenBalanceError('Ethereum', 'USDC')
3340
+ * // Message: "Insufficient USDC balance on Ethereum"
3341
+ * ```
3342
+ *
3343
+ * @example
3344
+ * ```typescript
3345
+ * // With raw error for debugging
3346
+ * try {
3347
+ * await transfer(...)
3348
+ * } catch (error) {
3349
+ * throw createInsufficientTokenBalanceError('Base', 'USDC', error)
3350
+ * }
3351
+ * ```
3352
+ */
3353
+ function createInsufficientTokenBalanceError(chain, token, rawError) {
3354
+ return new KitError({
3355
+ ...BalanceError.INSUFFICIENT_TOKEN,
3356
+ recoverability: 'FATAL',
3357
+ message: `Insufficient ${token} balance on ${chain}`,
3358
+ cause: {
3359
+ trace: {
3360
+ chain,
3361
+ token,
3362
+ rawError,
3363
+ },
3364
+ },
3365
+ });
3366
+ }
3367
+ /**
3368
+ * Creates error for insufficient gas funds.
3369
+ *
3370
+ * This error is thrown when a wallet does not have enough native tokens
3371
+ * (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
3372
+ * as it requires user intervention to add gas funds.
3373
+ *
3374
+ * @param chain - The blockchain network where the gas check failed
3375
+ * @param rawError - The original error from the underlying system (optional)
3376
+ * @returns KitError with insufficient gas details
3377
+ *
3378
+ * @example
3379
+ * ```typescript
3380
+ * import { createInsufficientGasError } from '@core/errors'
3381
+ *
3382
+ * throw createInsufficientGasError('Ethereum')
3383
+ * // Message: "Insufficient gas funds on Ethereum"
3384
+ * ```
3385
+ */
3386
+ function createInsufficientGasError(chain, rawError) {
3387
+ return new KitError({
3388
+ ...BalanceError.INSUFFICIENT_GAS,
3389
+ recoverability: 'FATAL',
3390
+ message: `Insufficient gas funds on ${chain}`,
3391
+ cause: {
3392
+ trace: {
3393
+ chain,
3394
+ rawError,
3395
+ },
3396
+ },
3397
+ });
3398
+ }
3399
+
3400
+ /**
3401
+ * Creates error for transaction simulation failures.
3402
+ *
3403
+ * This error is thrown when a pre-flight transaction simulation fails,
3404
+ * typically due to contract logic that would revert. The error is FATAL
3405
+ * as it indicates the transaction would fail if submitted.
3406
+ *
3407
+ * @param chain - The blockchain network where the simulation failed
3408
+ * @param reason - The reason for simulation failure (e.g., revert message)
3409
+ * @param rawError - The original error from the underlying system (optional)
3410
+ * @returns KitError with simulation failure details
3411
+ *
3412
+ * @example
3413
+ * ```typescript
3414
+ * import { createSimulationFailedError } from '@core/errors'
3415
+ *
3416
+ * throw createSimulationFailedError('Ethereum', 'ERC20: insufficient allowance')
3417
+ * // Message: "Simulation failed on Ethereum: ERC20: insufficient allowance"
3418
+ * ```
3419
+ */
3420
+ function createSimulationFailedError(chain, reason, rawError) {
3421
+ return new KitError({
3422
+ ...OnchainError.SIMULATION_FAILED,
3423
+ recoverability: 'FATAL',
3424
+ message: `Simulation failed on ${chain}: ${reason}`,
3425
+ cause: {
3426
+ trace: {
3427
+ chain,
3428
+ reason,
3429
+ rawError,
3430
+ },
3431
+ },
3432
+ });
3433
+ }
3434
+ /**
3435
+ * Creates error for transaction reverts.
3436
+ *
3437
+ * This error is thrown when a transaction is submitted and confirmed
3438
+ * but reverts on-chain. The error is FATAL as it indicates the
3439
+ * transaction executed but failed.
3440
+ *
3441
+ * @param chain - The blockchain network where the transaction reverted
3442
+ * @param reason - The reason for the revert (e.g., revert message)
3443
+ * @param rawError - The original error from the underlying system (optional)
3444
+ * @returns KitError with transaction revert details
3445
+ *
3446
+ * @example
3447
+ * ```typescript
3448
+ * import { createTransactionRevertedError } from '@core/errors'
3449
+ *
3450
+ * throw createTransactionRevertedError('Base', 'Slippage exceeded')
3451
+ * // Message: "Transaction reverted on Base: Slippage exceeded"
3452
+ * ```
3453
+ */
3454
+ function createTransactionRevertedError(chain, reason, rawError) {
3455
+ return new KitError({
3456
+ ...OnchainError.TRANSACTION_REVERTED,
3457
+ recoverability: 'FATAL',
3458
+ message: `Transaction reverted on ${chain}: ${reason}`,
3459
+ cause: {
3460
+ trace: {
3461
+ chain,
3462
+ reason,
3463
+ rawError,
3464
+ },
3465
+ },
3466
+ });
3467
+ }
3468
+ /**
3469
+ * Creates error for out of gas failures.
3470
+ *
3471
+ * This error is thrown when a transaction runs out of gas during execution.
3472
+ * The error is FATAL as it requires adjusting gas limits or transaction logic.
3473
+ *
3474
+ * @param chain - The blockchain network where the transaction ran out of gas
3475
+ * @param rawError - The original error from the underlying system (optional)
3476
+ * @returns KitError with out of gas details
3477
+ *
3478
+ * @example
3479
+ * ```typescript
3480
+ * import { createOutOfGasError } from '@core/errors'
3481
+ *
3482
+ * throw createOutOfGasError('Polygon')
3483
+ * // Message: "Transaction ran out of gas on Polygon"
3484
+ * ```
3485
+ */
3486
+ function createOutOfGasError(chain, rawError) {
3487
+ return new KitError({
3488
+ ...OnchainError.OUT_OF_GAS,
3489
+ recoverability: 'FATAL',
3490
+ message: `Transaction ran out of gas on ${chain}`,
3491
+ cause: {
3492
+ trace: {
3493
+ chain,
3494
+ rawError,
3495
+ },
3496
+ },
3497
+ });
3498
+ }
3499
+
3500
+ /**
3501
+ * Creates error for RPC endpoint failures.
3502
+ *
3503
+ * This error is thrown when an RPC provider endpoint fails, returns an error,
3504
+ * or is unavailable. The error is RETRYABLE as RPC issues are often temporary.
3505
+ *
3506
+ * @param chain - The blockchain network where the RPC error occurred
3507
+ * @param rawError - The original error from the underlying system (optional)
3508
+ * @returns KitError with RPC endpoint error details
3509
+ *
3510
+ * @example
3511
+ * ```typescript
3512
+ * import { createRpcEndpointError } from '@core/errors'
3513
+ *
3514
+ * throw createRpcEndpointError('Ethereum')
3515
+ * // Message: "RPC endpoint error on Ethereum"
3516
+ * ```
3517
+ */
3518
+ function createRpcEndpointError(chain, rawError) {
3519
+ return new KitError({
3520
+ ...RpcError.ENDPOINT_ERROR,
3521
+ recoverability: 'RETRYABLE',
3522
+ message: `RPC endpoint error on ${chain}`,
3523
+ cause: {
3524
+ trace: {
3525
+ chain,
3526
+ rawError,
3527
+ },
3528
+ },
3529
+ });
3530
+ }
3531
+
3532
+ /**
3533
+ * Creates error for network connection failures.
3534
+ *
3535
+ * This error is thrown when network connectivity issues prevent reaching
3536
+ * the blockchain network. The error is RETRYABLE as network issues are
3537
+ * often temporary.
3538
+ *
3539
+ * @param chain - The blockchain network where the connection failed
3540
+ * @param rawError - The original error from the underlying system (optional)
3541
+ * @returns KitError with network connection error details
3542
+ *
3543
+ * @example
3544
+ * ```typescript
3545
+ * import { createNetworkConnectionError } from '@core/errors'
3546
+ *
3547
+ * throw createNetworkConnectionError('Ethereum')
3548
+ * // Message: "Network connection failed for Ethereum"
3549
+ * ```
3550
+ */
3551
+ function createNetworkConnectionError(chain, rawError) {
3552
+ return new KitError({
3553
+ ...NetworkError.CONNECTION_FAILED,
3554
+ recoverability: 'RETRYABLE',
3555
+ message: `Network connection failed for ${chain}`,
3556
+ cause: {
3557
+ trace: {
3558
+ chain,
3559
+ rawError,
3560
+ },
3561
+ },
3562
+ });
3563
+ }
3564
+
3565
+ /**
3566
+ * Parses raw blockchain errors into structured KitError instances.
3567
+ *
3568
+ * This function uses pattern matching to identify common blockchain error
3569
+ * types and converts them into standardized KitError format. It handles
3570
+ * errors from viem, ethers, Solana web3.js, and other blockchain libraries.
3571
+ *
3572
+ * The parser recognizes 5 main error patterns:
3573
+ * 1. Insufficient balance errors
3574
+ * 2. Simulation/execution reverted errors
3575
+ * 3. Gas-related errors
3576
+ * 4. Network connectivity errors
3577
+ * 5. RPC provider errors
3578
+ *
3579
+ * @param error - The raw error from the blockchain library
3580
+ * @param context - Context information including chain and optional token
3581
+ * @returns A structured KitError instance
3582
+ *
3583
+ * @example
3584
+ * ```typescript
3585
+ * import { parseBlockchainError } from '@core/errors'
3586
+ *
3587
+ * try {
3588
+ * await walletClient.sendTransaction(...)
3589
+ * } catch (error) {
3590
+ * throw parseBlockchainError(error, {
3591
+ * chain: 'Ethereum',
3592
+ * token: 'USDC',
3593
+ * operation: 'transfer'
3594
+ * })
3595
+ * }
3596
+ * ```
3597
+ *
3598
+ * @example
3599
+ * ```typescript
3600
+ * // Minimal usage
3601
+ * try {
3602
+ * await connection.sendTransaction(...)
3603
+ * } catch (error) {
3604
+ * throw parseBlockchainError(error, { chain: 'Solana' })
3605
+ * }
3606
+ * ```
3607
+ */
3608
+ function parseBlockchainError(error, context) {
3609
+ const msg = extractMessage(error);
3610
+ const token = context.token ?? 'token';
3611
+ // Pattern 1: Insufficient balance errors
3612
+ // Matches balance-related errors from ERC20 contracts, native transfers, and Solana programs
3613
+ if (/transfer amount exceeds balance|insufficient (balance|funds)|burn amount exceeded/i.test(msg)) {
3614
+ return createInsufficientTokenBalanceError(context.chain, token, error);
3615
+ }
3616
+ // Pattern 2: Simulation and execution reverts
3617
+ // Matches contract revert errors and simulation failures
3618
+ if (/execution reverted|simulation failed|transaction reverted|transaction failed/i.test(msg)) {
3619
+ const reason = extractRevertReason(msg) ?? 'Transaction reverted';
3620
+ // Distinguish between simulation failures and transaction reverts
3621
+ // "simulation failed" or "eth_call" indicates pre-flight simulation
3622
+ // "transaction failed" or context.operation === 'transaction' indicates post-execution
3623
+ if (/simulation failed/i.test(msg) || context.operation === 'simulation') {
3624
+ return createSimulationFailedError(context.chain, reason, error);
3625
+ }
3626
+ // Transaction execution failures or reverts
3627
+ return createTransactionRevertedError(context.chain, reason, error);
3628
+ }
3629
+ // Pattern 3: Gas-related errors
3630
+ // Matches gas estimation failures and gas exhaustion
3631
+ // Check specific patterns first, then generic "gas" patterns
3632
+ // Gas estimation failures are RPC issues
3633
+ if (/gas estimation failed|cannot estimate gas/i.test(msg)) {
3634
+ return createRpcEndpointError(context.chain, error);
3635
+ }
3636
+ // Gas exhaustion errors
3637
+ // Use specific patterns without wildcards to avoid ReDoS
3638
+ if (/out of gas|gas limit exceeded|exceeds block gas limit/i.test(msg)) {
3639
+ return createOutOfGasError(context.chain, error);
3640
+ }
3641
+ // Insufficient funds for gas
3642
+ if (/insufficient funds for gas/i.test(msg)) {
3643
+ return createInsufficientGasError(context.chain, error);
3644
+ }
3645
+ // Pattern 4: Network connectivity errors
3646
+ // Matches connection failures, DNS errors, and timeouts
3647
+ if (/connection (refused|failed)|network|timeout|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
3648
+ return createNetworkConnectionError(context.chain, error);
3649
+ }
3650
+ // Pattern 5: RPC provider errors
3651
+ // Matches RPC endpoint errors, invalid responses, and rate limits
3652
+ if (/rpc|invalid response|rate limit|too many requests/i.test(msg)) {
3653
+ return createRpcEndpointError(context.chain, error);
3654
+ }
3655
+ // Fallback based on operation context
3656
+ // Gas-related operations are RPC calls
3657
+ if (context.operation === 'estimateGas' ||
3658
+ context.operation === 'getGasPrice') {
3659
+ return createRpcEndpointError(context.chain, error);
3660
+ }
3661
+ // Fallback for unrecognized errors
3662
+ // Defaults to simulation failed as transaction execution is the most common failure point
3663
+ return createSimulationFailedError(context.chain, msg.length > 0 ? msg : 'Unknown error', error);
3664
+ }
3665
+ /**
3666
+ * Type guard to check if error has Solana-Kit structure with logs.
3667
+ *
3668
+ * Checks if the error object contains a context with logs array,
3669
+ * which is the structure used by Solana Kit errors.
3670
+ *
3671
+ * @param error - Unknown error to check
3672
+ * @returns True if error has Solana-Kit logs structure
3673
+ */
3674
+ function hasSolanaLogs(error) {
3675
+ return (error !== null &&
3676
+ typeof error === 'object' &&
3677
+ 'context' in error &&
3678
+ error.context !== null &&
3679
+ typeof error.context === 'object' &&
3680
+ 'logs' in error.context &&
3681
+ Array.isArray(error.context.logs));
3682
+ }
3683
+ /**
3684
+ * Extracts a human-readable error message from various error types.
3685
+ *
3686
+ * Handles Error objects, string errors, objects with message properties,
3687
+ * Solana-Kit errors with context logs, and falls back to string representation.
3688
+ * For Solana-Kit errors, extracts Anchor error messages from transaction logs.
3689
+ *
3690
+ * @param error - Unknown error to extract message from
3691
+ * @returns Extracted error message string
3692
+ *
3693
+ * @example
3694
+ * ```typescript
3695
+ * const msg1 = extractMessage(new Error('test')) // 'test'
3696
+ * const msg2 = extractMessage('string error') // 'string error'
3697
+ * const msg3 = extractMessage({ message: 'obj' }) // 'obj'
3698
+ * ```
3699
+ */
3700
+ function extractMessage(error) {
3701
+ // Check for Solana-Kit errors with context.logs
3702
+ if (hasSolanaLogs(error)) {
3703
+ // Extract Anchor error message from logs
3704
+ const anchorLog = error.context.logs.find((log) => log.includes('AnchorError') || log.includes('Error Message'));
3705
+ if (anchorLog !== undefined) {
3706
+ // Return the anchor error log which contains the detailed message
3707
+ return anchorLog;
3708
+ }
3709
+ }
3710
+ if (error instanceof Error) {
3711
+ return error.message;
3712
+ }
3713
+ if (typeof error === 'string') {
3714
+ return error;
3715
+ }
3716
+ if (typeof error === 'object' && error !== null && 'message' in error) {
3717
+ return String(error.message);
3718
+ }
3719
+ return String(error);
3720
+ }
3721
+ /**
3722
+ * Extracts the revert reason from an error message.
3723
+ *
3724
+ * Attempts to parse out the meaningful reason from execution revert errors,
3725
+ * removing common prefixes like "execution reverted:" or "reverted:".
3726
+ *
3727
+ * @param msg - The error message to extract from
3728
+ * @returns The extracted revert reason, or null if not found
3729
+ *
3730
+ * @example
3731
+ * ```typescript
3732
+ * const reason = extractRevertReason(
3733
+ * 'execution reverted: ERC20: transfer amount exceeds balance'
3734
+ * )
3735
+ * // Returns: 'ERC20: transfer amount exceeds balance'
3736
+ * ```
3737
+ *
3738
+ * @example
3739
+ * ```typescript
3740
+ * const reason = extractRevertReason(
3741
+ * 'Simulation failed: Execution reverted with reason: Insufficient allowance'
3742
+ * )
3743
+ * // Returns: 'Insufficient allowance'
3744
+ * ```
3745
+ */
3746
+ function extractRevertReason(msg) {
3747
+ // Try to extract reason after "execution reverted:" or "reason:"
3748
+ // Use [^\n.]+ instead of .+? to avoid ReDoS vulnerability
3749
+ const patterns = [
3750
+ /(?:execution reverted|reverted):\s*([^\n.]+)/i,
3751
+ /reason:\s*([^\n.]+)/i,
3752
+ /with reason:\s*([^\n.]+)/i,
3753
+ ];
3754
+ for (const pattern of patterns) {
3755
+ const match = pattern.exec(msg);
3756
+ const extractedReason = match?.at(1);
3757
+ if (extractedReason !== undefined && extractedReason.length > 0) {
3758
+ return extractedReason.trim();
3759
+ }
3760
+ }
3761
+ return null;
3762
+ }
3763
+
3101
3764
  /**
3102
3765
  * Abstract class defining the standard interface for an adapter that interacts with a specific blockchain.
3103
3766
  *
@@ -3243,20 +3906,24 @@ class Adapter {
3243
3906
  * Validate that the target chain is supported by this adapter.
3244
3907
  *
3245
3908
  * @param targetChain - The chain to validate.
3246
- * @throws Error if the chain is not supported.
3909
+ * @throws KitError with INVALID_CHAIN code if the chain is not supported by this adapter.
3247
3910
  */
3248
3911
  validateChainSupport(targetChain) {
3249
3912
  if (this.capabilities?.supportedChains) {
3250
3913
  const isSupported = this.capabilities.supportedChains.some((supportedChain) => supportedChain.chain === targetChain.chain);
3251
3914
  if (!isSupported) {
3252
3915
  const supportedCount = this.capabilities.supportedChains.length;
3253
- const message = `Chain ${targetChain.name} (${targetChain.type}) is not supported by this adapter (supports ${supportedCount.toString()} chains)`;
3254
- throw createUnsupportedRouteError(targetChain.name, message);
3916
+ // List supported chain names for better user experience
3917
+ const supportedChainNames = this.capabilities.supportedChains
3918
+ .map((chainDef) => chainDef.name)
3919
+ .join(', ');
3920
+ const reason = `Not supported by this adapter. It supports ${supportedCount.toString()} ${supportedCount === 1 ? 'chain' : 'chains'}: ${supportedChainNames}`;
3921
+ throw createInvalidChainError(targetChain.name, reason);
3255
3922
  }
3256
3923
  }
3257
3924
  else if (this.chainType && targetChain.type !== this.chainType) {
3258
- const message = `Chain type mismatch: adapter supports ${this.chainType} chains, but received ${targetChain.type} chain: ${targetChain.name}`;
3259
- throw createUnsupportedRouteError(targetChain.name, message);
3925
+ const reason = `Chain type mismatch: adapter supports ${this.chainType} chains, but received ${targetChain.type ?? 'unknown'} chain`;
3926
+ throw createInvalidChainError(targetChain.name, reason);
3260
3927
  }
3261
3928
  }
3262
3929
  }
@@ -9952,14 +10619,17 @@ class EthersAdapter extends EvmAdapter {
9952
10619
  /**
9953
10620
  * Simulates a contract function call using Ethers v6 `.staticCall`.
9954
10621
  */
9955
- async simulateFunctionCall(contract, functionName, args) {
10622
+ async simulateFunctionCall(contract, functionName, args, chain) {
9956
10623
  try {
9957
10624
  const func = contract.getFunction(functionName);
9958
10625
  await func.staticCall(...args);
9959
10626
  }
9960
10627
  catch (err) {
9961
- const msg = err instanceof Error ? err.message : String(err);
9962
- throw new Error(`Simulation failed for ${functionName}: ${msg}`);
10628
+ // Wrap simulation errors with structured error format
10629
+ throw parseBlockchainError(err, {
10630
+ chain: chain.name,
10631
+ operation: 'simulation',
10632
+ });
9963
10633
  }
9964
10634
  }
9965
10635
  /**
@@ -9972,7 +10642,7 @@ class EthersAdapter extends EvmAdapter {
9972
10642
  * @param overrides - Optional estimate overrides.
9973
10643
  * @returns Promise resolving to the estimated gas, gas price, and total fee.
9974
10644
  */
9975
- async estimateGasForFunction(contract, functionName, args, chain, overrides) {
10645
+ async estimateGasForFunction(contract, functionName, args, chain, overrides, fallback) {
9976
10646
  let gas;
9977
10647
  try {
9978
10648
  const contractFunction = contract.getFunction(functionName);
@@ -9981,7 +10651,16 @@ class EthersAdapter extends EvmAdapter {
9981
10651
  : await contractFunction.estimateGas(...args);
9982
10652
  }
9983
10653
  catch (error) {
9984
- throw new Error(`Gas estimation failed: ${error.message}`);
10654
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
10655
+ if (fallback &&
10656
+ errorMessage.toLocaleLowerCase().includes('execution reverted')) {
10657
+ return fallback;
10658
+ }
10659
+ // Wrap gas estimation errors with structured error format
10660
+ throw parseBlockchainError(error, {
10661
+ chain: chain.name,
10662
+ operation: 'estimateGas',
10663
+ });
9985
10664
  }
9986
10665
  let gasPrice;
9987
10666
  try {
@@ -9989,7 +10668,11 @@ class EthersAdapter extends EvmAdapter {
9989
10668
  gasPrice = await this.fetchGasPrice(chain);
9990
10669
  }
9991
10670
  catch (error) {
9992
- throw new Error(`Gas price retrieval failed: ${error.message}`);
10671
+ // Wrap gas price errors with structured error format
10672
+ throw parseBlockchainError(error, {
10673
+ chain: chain.name,
10674
+ operation: 'getGasPrice',
10675
+ });
9993
10676
  }
9994
10677
  return { gas, gasPrice, fee: (gas * gasPrice).toString() };
9995
10678
  }
@@ -10045,7 +10728,11 @@ class EthersAdapter extends EvmAdapter {
10045
10728
  const shouldRetry = ethersNonceManager.shouldRetryNonceError(error, userProvidedNonce);
10046
10729
  // Don't retry if: not a nonce error, user provided nonce, or this is the last attempt
10047
10730
  if (!shouldRetry || attempt === maxAttempts) {
10048
- throw error;
10731
+ // Wrap transaction execution errors with structured error format
10732
+ throw parseBlockchainError(error, {
10733
+ chain: chain.name,
10734
+ operation: 'sendTransaction',
10735
+ });
10049
10736
  }
10050
10737
  // Retry with resync on nonce-related errors (this will set a new nonce)
10051
10738
  txRequest.nonce = await ethersNonceManager.resyncAndAllocate(provider, chain.chainId, fromAddress);
@@ -10206,18 +10893,19 @@ class EthersAdapter extends EvmAdapter {
10206
10893
  }
10207
10894
  // For state-changing functions, use the original logic
10208
10895
  const contract = this.createContractInstance(params, signer, resolvedContext.address, provider);
10209
- await this.simulateFunctionCall(contract, functionName, args);
10210
10896
  return {
10211
10897
  type: 'evm',
10212
- estimate: async (overrides) => {
10898
+ estimate: async (overrides, fallback) => {
10213
10899
  await this.ensureChain(targetChain);
10214
10900
  // Reconnect the contract with the correct provider for the target chain to ensure
10215
10901
  // gas estimation and fee data retrieval use the correct network.
10216
10902
  const estimationProvider = await this.getProvider(targetChain);
10217
10903
  const contractForEstimation = contract.connect(estimationProvider);
10218
- return this.estimateGasForFunction(contractForEstimation, functionName, args, targetChain, overrides);
10904
+ return this.estimateGasForFunction(contractForEstimation, functionName, args, targetChain, overrides, fallback);
10219
10905
  },
10220
10906
  execute: async (overrides) => {
10907
+ // Simulate the function call to catch errors before submission
10908
+ await this.simulateFunctionCall(contract, functionName, args, targetChain);
10221
10909
  await this.ensureChain(targetChain);
10222
10910
  // Reconnect the contract with the current signer, which is on the correct
10223
10911
  // chain after `ensureChain`, to ensure the transaction is populated and
@@ -10246,7 +10934,7 @@ class EthersAdapter extends EvmAdapter {
10246
10934
  * adapter interface, it is effectively required and enforced at runtime. This ensures
10247
10935
  * the adapter is connected to the correct chain before querying the signer.
10248
10936
  *
10249
- * @param chain - The chain to use for address resolution (provided by OperationContext)
10937
+ * @param chain - The chain to use for address resolution.
10250
10938
  * @returns A promise that resolves to the signer's address
10251
10939
  * @throws Error when no chain is provided
10252
10940
  *
@@ -10258,26 +10946,15 @@ class EthersAdapter extends EvmAdapter {
10258
10946
  * const address = await adapter.getAddress(Ethereum)
10259
10947
  * console.log('Wallet address:', address)
10260
10948
  * ```
10261
- *
10262
- * @example
10263
- * ```typescript
10264
- * // In practice, address resolution happens automatically
10265
- * const prepared = await adapter.prepare(params, {
10266
- * chain: 'Ethereum'
10267
- * // Address automatically resolved via getAddress() internally
10268
- * })
10269
- * ```
10270
10949
  */
10271
10950
  async getAddress(chain) {
10272
10951
  // Prevent calling getAddress on developer-controlled adapters
10273
10952
  if (this.capabilities?.addressContext === 'developer-controlled') {
10274
- throw new Error('Cannot call getAddress() on developer-controlled adapters. ' +
10275
- 'Address must be provided explicitly in the operation context.');
10953
+ throw new Error('Cannot call getAddress() on developer-controlled adapters. Address must be provided explicitly in the operation context.');
10276
10954
  }
10277
10955
  // Chain parameter should now be provided by resolveOperationContext
10278
10956
  if (!chain) {
10279
- throw new Error('Chain parameter is required for address resolution. ' +
10280
- 'This should be provided by the OperationContext pattern.');
10957
+ throw new Error('Chain parameter is required for address resolution. This should be provided by the OperationContext pattern.');
10281
10958
  }
10282
10959
  // Ensure we're on the correct chain before getting address
10283
10960
  await this.ensureChain(chain);