@circle-fin/bridge-kit 1.1.1 → 1.2.0

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.cjs CHANGED
@@ -19,10 +19,10 @@
19
19
  'use strict';
20
20
 
21
21
  var zod = require('zod');
22
- var units = require('@ethersproject/units');
23
22
  require('@ethersproject/bytes');
24
23
  require('@ethersproject/address');
25
24
  require('bs58');
25
+ var units = require('@ethersproject/units');
26
26
  var providerCctpV2 = require('@circle-fin/provider-cctp-v2');
27
27
 
28
28
  /**
@@ -66,200 +66,961 @@ const registerKit = (kitIdentifier) => {
66
66
  }
67
67
  };
68
68
 
69
- // -----------------------------------------------------------------------------
70
- // Blockchain Enum
71
- // -----------------------------------------------------------------------------
72
69
  /**
73
- * Enumeration of all blockchains supported by this library.
74
- * @enum
75
- * @category Enums
76
- * @description Provides string identifiers for each supported blockchain.
70
+ * Valid recoverability values for error handling strategies.
71
+ *
72
+ * - FATAL errors are thrown immediately (invalid inputs, insufficient funds)
73
+ * - RETRYABLE errors are returned when a flow fails to start but could work later
74
+ * - RESUMABLE errors are returned when a flow fails mid-execution but can be continued
77
75
  */
78
- exports.Blockchain = void 0;
79
- (function (Blockchain) {
80
- Blockchain["Algorand"] = "Algorand";
81
- Blockchain["Algorand_Testnet"] = "Algorand_Testnet";
82
- Blockchain["Aptos"] = "Aptos";
83
- Blockchain["Aptos_Testnet"] = "Aptos_Testnet";
84
- Blockchain["Arc_Testnet"] = "Arc_Testnet";
85
- Blockchain["Arbitrum"] = "Arbitrum";
86
- Blockchain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
87
- Blockchain["Avalanche"] = "Avalanche";
88
- Blockchain["Avalanche_Fuji"] = "Avalanche_Fuji";
89
- Blockchain["Base"] = "Base";
90
- Blockchain["Base_Sepolia"] = "Base_Sepolia";
91
- Blockchain["Celo"] = "Celo";
92
- Blockchain["Celo_Alfajores_Testnet"] = "Celo_Alfajores_Testnet";
93
- Blockchain["Codex"] = "Codex";
94
- Blockchain["Codex_Testnet"] = "Codex_Testnet";
95
- Blockchain["Ethereum"] = "Ethereum";
96
- Blockchain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
97
- Blockchain["Hedera"] = "Hedera";
98
- Blockchain["Hedera_Testnet"] = "Hedera_Testnet";
99
- Blockchain["HyperEVM"] = "HyperEVM";
100
- Blockchain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
101
- Blockchain["Ink"] = "Ink";
102
- Blockchain["Ink_Testnet"] = "Ink_Testnet";
103
- Blockchain["Linea"] = "Linea";
104
- Blockchain["Linea_Sepolia"] = "Linea_Sepolia";
105
- Blockchain["NEAR"] = "NEAR";
106
- Blockchain["NEAR_Testnet"] = "NEAR_Testnet";
107
- Blockchain["Noble"] = "Noble";
108
- Blockchain["Noble_Testnet"] = "Noble_Testnet";
109
- Blockchain["Optimism"] = "Optimism";
110
- Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
111
- Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
112
- Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
113
- Blockchain["Plume"] = "Plume";
114
- Blockchain["Plume_Testnet"] = "Plume_Testnet";
115
- Blockchain["Polygon"] = "Polygon";
116
- Blockchain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
117
- Blockchain["Sei"] = "Sei";
118
- Blockchain["Sei_Testnet"] = "Sei_Testnet";
119
- Blockchain["Solana"] = "Solana";
120
- Blockchain["Solana_Devnet"] = "Solana_Devnet";
121
- Blockchain["Sonic"] = "Sonic";
122
- Blockchain["Sonic_Testnet"] = "Sonic_Testnet";
123
- Blockchain["Stellar"] = "Stellar";
124
- Blockchain["Stellar_Testnet"] = "Stellar_Testnet";
125
- Blockchain["Sui"] = "Sui";
126
- Blockchain["Sui_Testnet"] = "Sui_Testnet";
127
- Blockchain["Unichain"] = "Unichain";
128
- Blockchain["Unichain_Sepolia"] = "Unichain_Sepolia";
129
- Blockchain["World_Chain"] = "World_Chain";
130
- Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
131
- Blockchain["XDC"] = "XDC";
132
- Blockchain["XDC_Apothem"] = "XDC_Apothem";
133
- Blockchain["ZKSync_Era"] = "ZKSync_Era";
134
- Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
135
- })(exports.Blockchain || (exports.Blockchain = {}));
136
-
76
+ const RECOVERABILITY_VALUES = [
77
+ 'RETRYABLE',
78
+ 'RESUMABLE',
79
+ 'FATAL',
80
+ ];
137
81
  /**
138
- * Helper function to define a chain with proper TypeScript typing.
82
+ * Error type constants for categorizing errors by origin.
139
83
  *
140
- * This utility function works with TypeScript's `as const` assertion to create
141
- * strongly-typed, immutable chain definition objects. It preserves literal types
142
- * from the input and ensures the resulting object maintains all type information.
84
+ * This const object provides a reference for error types, enabling
85
+ * IDE autocomplete and preventing typos when creating custom errors.
86
+ *
87
+ * @remarks
88
+ * While internal error definitions use string literals with type annotations
89
+ * for strict type safety, this constant is useful for developers creating
90
+ * custom error instances or checking error types programmatically.
143
91
  *
144
- * When used with `as const`, it allows TypeScript to infer the most specific
145
- * possible types for all properties, including string literals and numeric literals,
146
- * rather than widening them to general types like string or number.
147
- * @typeParam T - The specific chain definition type (must extend ChainDefinition)
148
- * @param chain - The chain definition object, typically with an `as const` assertion
149
- * @returns The same chain definition with preserved literal types
150
92
  * @example
151
93
  * ```typescript
152
- * // Define an EVM chain with literal types preserved
153
- * const Ethereum = defineChain({
154
- * type: 'evm',
155
- * chain: Blockchain.Ethereum,
156
- * chainId: 1,
157
- * name: 'Ethereum',
158
- * nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
159
- * isTestnet: false,
160
- * usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
161
- * eurcAddress: null,
162
- * cctp: {
163
- * domain: 0,
164
- * contracts: {
165
- * TokenMessengerV1: '0xbd3fa81b58ba92a82136038b25adec7066af3155',
166
- * MessageTransmitterV1: '0x0a992d191deec32afe36203ad87d7d289a738f81'
167
- * }
168
- * }
169
- * } as const);
94
+ * import { ERROR_TYPES, KitError } from '@core/errors'
95
+ *
96
+ * // Use for type checking
97
+ * if (error.type === ERROR_TYPES.BALANCE) {
98
+ * console.log('This is a balance error')
99
+ * }
100
+ * ```
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * // Use as reference when creating custom errors
105
+ * const error = new KitError({
106
+ * code: 9999,
107
+ * name: 'CUSTOM_ERROR',
108
+ * type: ERROR_TYPES.BALANCE, // IDE autocomplete works here
109
+ * recoverability: 'FATAL',
110
+ * message: 'Custom balance error'
111
+ * })
170
112
  * ```
171
113
  */
172
- function defineChain(chain) {
173
- return chain;
174
- }
175
-
114
+ const ERROR_TYPES = {
115
+ /** User input validation and parameter checking */
116
+ INPUT: 'INPUT',
117
+ /** Insufficient token balances and amount validation */
118
+ BALANCE: 'BALANCE',
119
+ /** On-chain execution: reverts, gas issues, transaction failures */
120
+ ONCHAIN: 'ONCHAIN',
121
+ /** Blockchain RPC provider issues and endpoint problems */
122
+ RPC: 'RPC',
123
+ /** Internet connectivity, DNS resolution, connection issues */
124
+ NETWORK: 'NETWORK',
125
+ };
176
126
  /**
177
- * Algorand Mainnet chain definition
178
- * @remarks
179
- * This represents the official production network for the Algorand blockchain.
127
+ * Array of valid error type values for validation.
128
+ * Derived from ERROR_TYPES const object.
180
129
  */
181
- const Algorand = defineChain({
182
- type: 'algorand',
183
- chain: exports.Blockchain.Algorand,
184
- name: 'Algorand',
185
- title: 'Algorand Mainnet',
186
- nativeCurrency: {
187
- name: 'Algo',
188
- symbol: 'ALGO',
189
- decimals: 6,
190
- },
191
- isTestnet: false,
192
- explorerUrl: 'https://explorer.perawallet.app/tx/{hash}',
193
- rpcEndpoints: ['https://mainnet-api.algonode.cloud'],
194
- eurcAddress: null,
195
- usdcAddress: '31566704',
196
- cctp: null,
197
- });
130
+ const ERROR_TYPE_VALUES = Object.values(ERROR_TYPES);
198
131
 
132
+ // Create mutable arrays for Zod enum validation
133
+ const RECOVERABILITY_ARRAY = [...RECOVERABILITY_VALUES];
134
+ const ERROR_TYPE_ARRAY = [...ERROR_TYPE_VALUES];
199
135
  /**
200
- * Algorand Testnet chain definition
201
- * @remarks
202
- * This represents the official testnet for the Algorand blockchain.
136
+ * Error code ranges for validation.
137
+ * Single source of truth for valid error code ranges.
203
138
  */
204
- const AlgorandTestnet = defineChain({
205
- type: 'algorand',
206
- chain: exports.Blockchain.Algorand_Testnet,
207
- name: 'Algorand Testnet',
208
- title: 'Algorand Test Network',
209
- nativeCurrency: {
210
- name: 'Algo',
211
- symbol: 'ALGO',
212
- decimals: 6,
213
- },
214
- isTestnet: true,
215
- explorerUrl: 'https://testnet.explorer.perawallet.app/tx/{hash}',
216
- rpcEndpoints: ['https://testnet-api.algonode.cloud'],
217
- eurcAddress: null,
218
- usdcAddress: '10458941',
219
- cctp: null,
220
- });
221
-
139
+ const ERROR_CODE_RANGES = [
140
+ { min: 1000, max: 1999, type: 'INPUT' },
141
+ { min: 3000, max: 3999, type: 'NETWORK' },
142
+ { min: 4000, max: 4999, type: 'RPC' },
143
+ { min: 5000, max: 5999, type: 'ONCHAIN' },
144
+ { min: 9000, max: 9999, type: 'BALANCE' },
145
+ ];
222
146
  /**
223
- * Aptos Mainnet chain definition
224
- * @remarks
225
- * This represents the official production network for the Aptos blockchain.
147
+ * Zod schema for validating ErrorDetails objects.
148
+ *
149
+ * This schema provides runtime validation for all ErrorDetails properties,
150
+ * ensuring type safety and proper error handling for JavaScript consumers.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * import { errorDetailsSchema } from '@core/errors'
155
+ *
156
+ * const result = errorDetailsSchema.safeParse({
157
+ * code: 1001,
158
+ * name: 'INPUT_NETWORK_MISMATCH',
159
+ * type: 'INPUT',
160
+ * recoverability: 'FATAL',
161
+ * message: 'Source and destination networks must be different'
162
+ * })
163
+ *
164
+ * if (!result.success) {
165
+ * console.error('Validation failed:', result.error.issues)
166
+ * }
167
+ * ```
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * // Runtime error
172
+ * const result = errorDetailsSchema.safeParse({
173
+ * code: 9001,
174
+ * name: 'BALANCE_INSUFFICIENT_TOKEN',
175
+ * type: 'BALANCE',
176
+ * recoverability: 'FATAL',
177
+ * message: 'Insufficient USDC balance'
178
+ * })
179
+ * ```
226
180
  */
227
- const Aptos = defineChain({
228
- type: 'aptos',
229
- chain: exports.Blockchain.Aptos,
230
- name: 'Aptos',
231
- title: 'Aptos Mainnet',
232
- nativeCurrency: {
233
- name: 'Aptos',
234
- symbol: 'APT',
235
- decimals: 8,
236
- },
237
- isTestnet: false,
238
- explorerUrl: 'https://explorer.aptoslabs.com/txn/{hash}?network=mainnet',
239
- rpcEndpoints: ['https://fullnode.mainnet.aptoslabs.com/v1'],
240
- eurcAddress: null,
241
- usdcAddress: '0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b',
242
- cctp: {
243
- domain: 9,
244
- contracts: {
245
- v1: {
246
- type: 'split',
247
- tokenMessenger: '0x9bce6734f7b63e835108e3bd8c36743d4709fe435f44791918801d0989640a9d',
248
- messageTransmitter: '0x177e17751820e4b4371873ca8c30279be63bdea63b88ed0f2239c2eea10f1772',
249
- confirmations: 1,
250
- },
251
- },
252
- },
181
+ const errorDetailsSchema = zod.z.object({
182
+ /**
183
+ * Numeric identifier following standardized ranges:
184
+ * - 1000-1999: INPUT errors - Parameter validation
185
+ * - 3000-3999: NETWORK errors - Connectivity issues
186
+ * - 4000-4999: RPC errors - Provider issues, gas estimation
187
+ * - 5000-5999: ONCHAIN errors - Transaction/simulation failures
188
+ * - 9000-9999: BALANCE errors - Insufficient funds
189
+ */
190
+ code: zod.z
191
+ .number()
192
+ .int('Error code must be an integer')
193
+ .refine((code) => ERROR_CODE_RANGES.some((range) => code >= range.min && code <= range.max), {
194
+ message: 'Error code must be in valid ranges: 1000-1999 (INPUT), 3000-3999 (NETWORK), 4000-4999 (RPC), 5000-5999 (ONCHAIN), 9000-9999 (BALANCE)',
195
+ }),
196
+ /** Human-readable ID (e.g., "INPUT_NETWORK_MISMATCH", "BALANCE_INSUFFICIENT_TOKEN") */
197
+ name: zod.z
198
+ .string()
199
+ .min(1, 'Error name must be a non-empty string')
200
+ .regex(/^[A-Z_][A-Z0-9_]*$/, 'Error name must match pattern: ^[A-Z_][A-Z0-9_]*$'),
201
+ /** Error category indicating where the error originated */
202
+ type: zod.z.enum(ERROR_TYPE_ARRAY, {
203
+ errorMap: () => ({
204
+ message: 'Error type must be one of: INPUT, BALANCE, ONCHAIN, RPC, NETWORK',
205
+ }),
206
+ }),
207
+ /** Error handling strategy */
208
+ recoverability: zod.z.enum(RECOVERABILITY_ARRAY, {
209
+ errorMap: () => ({
210
+ message: 'Recoverability must be one of: RETRYABLE, RESUMABLE, FATAL',
211
+ }),
212
+ }),
213
+ /** User-friendly explanation with context */
214
+ message: zod.z
215
+ .string()
216
+ .min(1, 'Error message must be a non-empty string')
217
+ .max(1000, 'Error message must be 1000 characters or less'),
218
+ /** Raw error details, context, or the original error that caused this one. */
219
+ cause: zod.z
220
+ .object({
221
+ /** Free-form error payload from underlying system */
222
+ trace: zod.z.unknown().optional(),
223
+ })
224
+ .optional(),
253
225
  });
254
226
 
255
227
  /**
256
- * Aptos Testnet chain definition
257
- * @remarks
258
- * This represents the official test network for the Aptos blockchain.
259
- */
260
- const AptosTestnet = defineChain({
261
- type: 'aptos',
262
- chain: exports.Blockchain.Aptos_Testnet,
228
+ * Validates an ErrorDetails object using Zod schema.
229
+ *
230
+ * @param details - The object to validate
231
+ * @returns The validated ErrorDetails object
232
+ * @throws TypeError When validation fails
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * import { validateErrorDetails } from '@core/errors'
237
+ *
238
+ * try {
239
+ * const validDetails = validateErrorDetails({
240
+ * code: 1001,
241
+ * name: 'NETWORK_MISMATCH',
242
+ * recoverability: 'FATAL',
243
+ * message: 'Source and destination networks must be different'
244
+ * })
245
+ * } catch (error) {
246
+ * console.error('Validation failed:', error.message)
247
+ * }
248
+ * ```
249
+ */
250
+ function validateErrorDetails(details) {
251
+ const result = errorDetailsSchema.safeParse(details);
252
+ if (!result.success) {
253
+ const issues = result.error.issues
254
+ .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
255
+ .join(', ');
256
+ throw new TypeError(`Invalid ErrorDetails: ${issues}`);
257
+ }
258
+ return result.data;
259
+ }
260
+
261
+ /**
262
+ * Maximum length for error messages in fallback validation errors.
263
+ *
264
+ * KitError enforces a 1000-character limit on error messages. When creating
265
+ * fallback validation errors that combine multiple Zod issues, we use 950
266
+ * characters to leave a 50-character buffer for:
267
+ * - The error message prefix ("Invalid bridge parameters: ")
268
+ * - Potential encoding differences or formatting overhead
269
+ * - Safety margin to prevent KitError constructor failures
270
+ *
271
+ * This ensures that even with concatenated issue summaries, the final message
272
+ * stays within KitError's constraints.
273
+ */
274
+ const MAX_MESSAGE_LENGTH = 950;
275
+
276
+ /**
277
+ * Structured error class for Stablecoin Kit operations.
278
+ *
279
+ * This class extends the native Error class while implementing the ErrorDetails
280
+ * interface, providing a consistent error format for programmatic handling
281
+ * across the Stablecoin Kits ecosystem. All properties are immutable to ensure
282
+ * error objects cannot be modified after creation.
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * import { KitError } from '@core/errors'
287
+ *
288
+ * const error = new KitError({
289
+ * code: 1001,
290
+ * name: 'INPUT_NETWORK_MISMATCH',
291
+ * recoverability: 'FATAL',
292
+ * message: 'Cannot bridge between mainnet and testnet'
293
+ * })
294
+ *
295
+ * if (error instanceof KitError) {
296
+ * console.log(`Error ${error.code}: ${error.name}`)
297
+ * // → "Error 1001: INPUT_NETWORK_MISMATCH"
298
+ * }
299
+ * ```
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * import { KitError } from '@core/errors'
304
+ *
305
+ * // Error with cause information
306
+ * const error = new KitError({
307
+ * code: 1002,
308
+ * name: 'INVALID_AMOUNT',
309
+ * recoverability: 'FATAL',
310
+ * message: 'Amount must be greater than zero',
311
+ * cause: {
312
+ * trace: { providedAmount: -100, minimumAmount: 0 }
313
+ * }
314
+ * })
315
+ *
316
+ * throw error
317
+ * ```
318
+ */
319
+ class KitError extends Error {
320
+ /** Numeric identifier following standardized ranges (1000+ for INPUT errors) */
321
+ code;
322
+ /** Human-readable ID (e.g., "NETWORK_MISMATCH") */
323
+ name;
324
+ /** Error category indicating where the error originated */
325
+ type;
326
+ /** Error handling strategy */
327
+ recoverability;
328
+ /** Raw error details, context, or the original error that caused this one. */
329
+ cause;
330
+ /**
331
+ * Create a new KitError instance.
332
+ *
333
+ * @param details - The error details object containing all required properties.
334
+ * @throws \{TypeError\} When details parameter is missing or invalid.
335
+ */
336
+ constructor(details) {
337
+ // Truncate message if it exceeds maximum length to prevent validation errors
338
+ let message = details.message;
339
+ if (message.length > MAX_MESSAGE_LENGTH) {
340
+ message = `${message.slice(0, MAX_MESSAGE_LENGTH - 3)}...`;
341
+ }
342
+ const truncatedDetails = { ...details, message };
343
+ // Validate input at runtime for JavaScript consumers using Zod
344
+ const validatedDetails = validateErrorDetails(truncatedDetails);
345
+ super(validatedDetails.message);
346
+ // Set properties as readonly at runtime
347
+ Object.defineProperties(this, {
348
+ name: {
349
+ value: validatedDetails.name,
350
+ writable: false,
351
+ enumerable: true,
352
+ configurable: false,
353
+ },
354
+ code: {
355
+ value: validatedDetails.code,
356
+ writable: false,
357
+ enumerable: true,
358
+ configurable: false,
359
+ },
360
+ type: {
361
+ value: validatedDetails.type,
362
+ writable: false,
363
+ enumerable: true,
364
+ configurable: false,
365
+ },
366
+ recoverability: {
367
+ value: validatedDetails.recoverability,
368
+ writable: false,
369
+ enumerable: true,
370
+ configurable: false,
371
+ },
372
+ ...(validatedDetails.cause && {
373
+ cause: {
374
+ value: validatedDetails.cause,
375
+ writable: false,
376
+ enumerable: true,
377
+ configurable: false,
378
+ },
379
+ }),
380
+ });
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Standardized error code ranges for consistent categorization:
386
+ *
387
+ * - 1000-1999: INPUT errors - Parameter validation, input format errors
388
+ * - 3000-3999: NETWORK errors - Internet connectivity, DNS, connection issues
389
+ * - 4000-4999: RPC errors - Blockchain provider issues, gas estimation, nonce errors
390
+ * - 5000-5999: ONCHAIN errors - Transaction/simulation failures, gas exhaustion, reverts
391
+ * - 9000-9999: BALANCE errors - Insufficient funds, token balance, allowance
392
+ */
393
+ /**
394
+ * Standardized error definitions for INPUT type errors.
395
+ *
396
+ * Each entry combines the numeric error code, string name, and type
397
+ * to ensure consistency when creating error instances.
398
+ *
399
+ * Error codes follow a hierarchical numbering scheme where the first digit
400
+ * indicates the error category (1 = INPUT) and subsequent digits provide
401
+ * specific error identification within that category.
402
+ *
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * import { InputError } from '@core/errors'
407
+ *
408
+ * const error = new KitError({
409
+ * ...InputError.NETWORK_MISMATCH,
410
+ * recoverability: 'FATAL',
411
+ * message: 'Source and destination networks must be different'
412
+ * })
413
+ *
414
+ * // Access code, name, and type individually if needed
415
+ * console.log(InputError.NETWORK_MISMATCH.code) // 1001
416
+ * console.log(InputError.NETWORK_MISMATCH.name) // 'INPUT_NETWORK_MISMATCH'
417
+ * console.log(InputError.NETWORK_MISMATCH.type) // 'INPUT'
418
+ * ```
419
+ */
420
+ const InputError = {
421
+ /** Network type mismatch between chains (mainnet vs testnet) */
422
+ NETWORK_MISMATCH: {
423
+ code: 1001,
424
+ name: 'INPUT_NETWORK_MISMATCH',
425
+ type: 'INPUT',
426
+ },
427
+ /** Invalid amount format or value (negative, zero, or malformed) */
428
+ INVALID_AMOUNT: {
429
+ code: 1002,
430
+ name: 'INPUT_INVALID_AMOUNT',
431
+ type: 'INPUT',
432
+ },
433
+ /** Unsupported or invalid bridge route configuration */
434
+ UNSUPPORTED_ROUTE: {
435
+ code: 1003,
436
+ name: 'INPUT_UNSUPPORTED_ROUTE',
437
+ type: 'INPUT',
438
+ },
439
+ /** Invalid wallet or contract address format */
440
+ INVALID_ADDRESS: {
441
+ code: 1004,
442
+ name: 'INPUT_INVALID_ADDRESS',
443
+ type: 'INPUT',
444
+ },
445
+ /** Invalid or unsupported chain identifier */
446
+ INVALID_CHAIN: {
447
+ code: 1005,
448
+ name: 'INPUT_INVALID_CHAIN',
449
+ type: 'INPUT',
450
+ },
451
+ /** General validation failure for complex validation rules */
452
+ VALIDATION_FAILED: {
453
+ code: 1098,
454
+ name: 'INPUT_VALIDATION_FAILED',
455
+ type: 'INPUT',
456
+ },
457
+ };
458
+
459
+ /**
460
+ * Creates error for network type mismatch between source and destination.
461
+ *
462
+ * This error is thrown when attempting to bridge between chains that have
463
+ * different network types (e.g., mainnet to testnet), which is not supported
464
+ * for security reasons.
465
+ *
466
+ * @param sourceChain - The source chain definition
467
+ * @param destChain - The destination chain definition
468
+ * @returns KitError with specific network mismatch details
469
+ *
470
+ * @example
471
+ * ```typescript
472
+ * import { createNetworkMismatchError } from '@core/errors'
473
+ * import { Ethereum, BaseSepolia } from '@core/chains'
474
+ *
475
+ * // This will throw a detailed error
476
+ * throw createNetworkMismatchError(Ethereum, BaseSepolia)
477
+ * // Message: "Cannot bridge between Ethereum (mainnet) and Base Sepolia (testnet). Source and destination networks must both be testnet or both be mainnet."
478
+ * ```
479
+ */
480
+ function createNetworkMismatchError(sourceChain, destChain) {
481
+ const sourceNetworkType = sourceChain.isTestnet ? 'testnet' : 'mainnet';
482
+ const destNetworkType = destChain.isTestnet ? 'testnet' : 'mainnet';
483
+ const errorDetails = {
484
+ ...InputError.NETWORK_MISMATCH,
485
+ recoverability: 'FATAL',
486
+ message: `Cannot bridge between ${sourceChain.name} (${sourceNetworkType}) and ${destChain.name} (${destNetworkType}). Source and destination networks must both be testnet or both be mainnet.`,
487
+ cause: {
488
+ trace: { sourceChain: sourceChain.name, destChain: destChain.name },
489
+ },
490
+ };
491
+ return new KitError(errorDetails);
492
+ }
493
+ /**
494
+ * Creates error for unsupported bridge route.
495
+ *
496
+ * This error is thrown when attempting to bridge between chains that don't
497
+ * have a supported bridge route configured.
498
+ *
499
+ * @param source - Source chain name
500
+ * @param destination - Destination chain name
501
+ * @returns KitError with specific route details
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * import { createUnsupportedRouteError } from '@core/errors'
506
+ *
507
+ * throw createUnsupportedRouteError('Ethereum', 'Solana')
508
+ * // Message: "Route from Ethereum to Solana is not supported"
509
+ * ```
510
+ */
511
+ function createUnsupportedRouteError(source, destination) {
512
+ const errorDetails = {
513
+ ...InputError.UNSUPPORTED_ROUTE,
514
+ recoverability: 'FATAL',
515
+ message: `Route from ${source} to ${destination} is not supported.`,
516
+ cause: {
517
+ trace: { source, destination },
518
+ },
519
+ };
520
+ return new KitError(errorDetails);
521
+ }
522
+ /**
523
+ * Creates error for invalid amount format or precision.
524
+ *
525
+ * This error is thrown when the provided amount doesn't meet validation
526
+ * requirements such as precision, range, or format.
527
+ *
528
+ * @param amount - The invalid amount string
529
+ * @param reason - Specific reason why amount is invalid
530
+ * @returns KitError with amount details and validation rule
531
+ *
532
+ * @example
533
+ * ```typescript
534
+ * import { createInvalidAmountError } from '@core/errors'
535
+ *
536
+ * throw createInvalidAmountError('0.000001', 'Amount must be at least 0.01 USDC')
537
+ * // Message: "Invalid amount '0.000001': Amount must be at least 0.01 USDC"
538
+ *
539
+ * throw createInvalidAmountError('1,000.50', 'Amount must be a numeric string with dot (.) as decimal separator, with no thousand separators or comma decimals')
540
+ * // Message: "Invalid amount '1,000.50': Amount must be a numeric string with dot (.) as decimal separator, with no thousand separators or comma decimals."
541
+ * ```
542
+ */
543
+ function createInvalidAmountError(amount, reason) {
544
+ const errorDetails = {
545
+ ...InputError.INVALID_AMOUNT,
546
+ recoverability: 'FATAL',
547
+ message: `Invalid amount '${amount}': ${reason}.`,
548
+ cause: {
549
+ trace: { amount, reason },
550
+ },
551
+ };
552
+ return new KitError(errorDetails);
553
+ }
554
+ /**
555
+ * Creates error for invalid wallet address format.
556
+ *
557
+ * This error is thrown when the provided address doesn't match the expected
558
+ * format for the specified chain.
559
+ *
560
+ * @param address - The invalid address string
561
+ * @param chain - Chain name where address is invalid
562
+ * @param expectedFormat - Description of expected address format
563
+ * @returns KitError with address details and format requirements
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * import { createInvalidAddressError } from '@core/errors'
568
+ *
569
+ * throw createInvalidAddressError('0x123', 'Ethereum', '42-character hex string starting with 0x')
570
+ * // Message: "Invalid address '0x123' for Ethereum. Expected 42-character hex string starting with 0x."
571
+ *
572
+ * throw createInvalidAddressError('invalid', 'Solana', 'base58-encoded string')
573
+ * // Message: "Invalid address 'invalid' for Solana. Expected base58-encoded string."
574
+ * ```
575
+ */
576
+ function createInvalidAddressError(address, chain, expectedFormat) {
577
+ const errorDetails = {
578
+ ...InputError.INVALID_ADDRESS,
579
+ recoverability: 'FATAL',
580
+ message: `Invalid address '${address}' for ${chain}. Expected ${expectedFormat}.`,
581
+ cause: {
582
+ trace: { address, chain, expectedFormat },
583
+ },
584
+ };
585
+ return new KitError(errorDetails);
586
+ }
587
+ /**
588
+ * Creates error for invalid chain configuration.
589
+ *
590
+ * This error is thrown when the provided chain doesn't meet the required
591
+ * configuration or is not supported for the operation.
592
+ *
593
+ * @param chain - The invalid chain name or identifier
594
+ * @param reason - Specific reason why chain is invalid
595
+ * @returns KitError with chain details and validation rule
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * import { createInvalidChainError } from '@core/errors'
600
+ *
601
+ * throw createInvalidChainError('UnknownChain', 'Chain is not supported by this bridge')
602
+ * // Message: "Invalid chain 'UnknownChain': Chain is not supported by this bridge"
603
+ * ```
604
+ */
605
+ function createInvalidChainError(chain, reason) {
606
+ const errorDetails = {
607
+ ...InputError.INVALID_CHAIN,
608
+ recoverability: 'FATAL',
609
+ message: `Invalid chain '${chain}': ${reason}`,
610
+ cause: {
611
+ trace: { chain, reason },
612
+ },
613
+ };
614
+ return new KitError(errorDetails);
615
+ }
616
+ /**
617
+ * Creates error for general validation failure.
618
+ *
619
+ * This error is thrown when input validation fails for reasons not covered
620
+ * by more specific error types.
621
+ *
622
+ * @param field - The field that failed validation
623
+ * @param value - The invalid value (can be any type)
624
+ * @param reason - Specific reason why validation failed
625
+ * @returns KitError with validation details
626
+ *
627
+ * @example
628
+ * ```typescript
629
+ * import { createValidationFailedError } from '@core/errors'
630
+ *
631
+ * throw createValidationFailedError('recipient', 'invalid@email', 'Must be a valid wallet address')
632
+ * // Message: "Validation failed for 'recipient': 'invalid@email' - Must be a valid wallet address"
633
+ *
634
+ * throw createValidationFailedError('chainId', 999, 'Unsupported chain ID')
635
+ * // Message: "Validation failed for 'chainId': 999 - Unsupported chain ID"
636
+ *
637
+ * throw createValidationFailedError('config', { invalid: true }, 'Missing required properties')
638
+ * // Message: "Validation failed for 'config': [object Object] - Missing required properties"
639
+ * ```
640
+ */
641
+ function createValidationFailedError$1(field, value, reason) {
642
+ // Convert value to string for display, handling different types appropriately
643
+ let valueString;
644
+ if (typeof value === 'string') {
645
+ valueString = `'${value}'`;
646
+ }
647
+ else if (typeof value === 'object' && value !== null) {
648
+ valueString = JSON.stringify(value);
649
+ }
650
+ else {
651
+ valueString = String(value);
652
+ }
653
+ const errorDetails = {
654
+ ...InputError.VALIDATION_FAILED,
655
+ recoverability: 'FATAL',
656
+ message: `Validation failed for '${field}': ${valueString} - ${reason}.`,
657
+ cause: {
658
+ trace: { field, value, reason },
659
+ },
660
+ };
661
+ return new KitError(errorDetails);
662
+ }
663
+ /**
664
+ * Creates a KitError from a Zod validation error with detailed error information.
665
+ *
666
+ * This factory function converts Zod validation failures into standardized KitError
667
+ * instances with INPUT_VALIDATION_FAILED code (1098). It extracts all Zod issues
668
+ * and includes them in both the error message and trace for debugging, providing
669
+ * developers with comprehensive validation feedback.
670
+ *
671
+ * The error message includes all validation errors concatenated with semicolons.
672
+ *
673
+ * @param zodError - The Zod validation error containing one or more validation issues
674
+ * @param context - Context string describing what was being validated (e.g., 'bridge parameters', 'user input')
675
+ * @returns KitError with INPUT_VALIDATION_FAILED code and structured validation details
676
+ *
677
+ * @example
678
+ * ```typescript
679
+ * import { createValidationErrorFromZod } from '@core/errors'
680
+ * import { z } from 'zod'
681
+ *
682
+ * const schema = z.object({
683
+ * name: z.string().min(3),
684
+ * age: z.number().positive()
685
+ * })
686
+ *
687
+ * const result = schema.safeParse({ name: 'ab', age: -1 })
688
+ * if (!result.success) {
689
+ * throw createValidationErrorFromZod(result.error, 'user data')
690
+ * }
691
+ * // Throws: KitError with message:
692
+ * // "Invalid user data: name: String must contain at least 3 character(s); age: Number must be greater than 0"
693
+ * // And cause.trace.validationErrors containing all validation errors as an array
694
+ * ```
695
+ *
696
+ * @example
697
+ * ```typescript
698
+ * // Usage in validation functions
699
+ * import { createValidationErrorFromZod } from '@core/errors'
700
+ *
701
+ * function validateBridgeParams(params: unknown): asserts params is BridgeParams {
702
+ * const result = bridgeParamsSchema.safeParse(params)
703
+ * if (!result.success) {
704
+ * throw createValidationErrorFromZod(result.error, 'bridge parameters')
705
+ * }
706
+ * }
707
+ * ```
708
+ */
709
+ function createValidationErrorFromZod(zodError, context) {
710
+ // Format each Zod issue as "path: message"
711
+ const validationErrors = zodError.issues.map((issue) => {
712
+ const path = issue.path.length > 0 ? `${issue.path.join('.')}: ` : '';
713
+ return `${path}${issue.message}`;
714
+ });
715
+ // Join all errors with semicolons to show complete validation feedback
716
+ const issueSummary = validationErrors.join('; ');
717
+ const allErrors = issueSummary || 'Invalid Input';
718
+ // Build full message from context and validation errors
719
+ const fullMessage = `Invalid ${context}: ${allErrors}`;
720
+ const errorDetails = {
721
+ ...InputError.VALIDATION_FAILED,
722
+ recoverability: 'FATAL',
723
+ message: fullMessage,
724
+ cause: {
725
+ trace: {
726
+ validationErrors, // Array of formatted error strings for display
727
+ zodError: zodError.message, // Original Zod error message
728
+ zodIssues: zodError.issues, // Full Zod issues array for debugging
729
+ },
730
+ },
731
+ };
732
+ return new KitError(errorDetails);
733
+ }
734
+
735
+ // -----------------------------------------------------------------------------
736
+ // Blockchain Enum
737
+ // -----------------------------------------------------------------------------
738
+ /**
739
+ * Enumeration of all blockchains known to this library.
740
+ *
741
+ * This enum contains every blockchain that has a chain definition, regardless
742
+ * of whether bridging is currently supported. For chains that support bridging
743
+ * via CCTPv2, see {@link BridgeChain}.
744
+ *
745
+ * @enum
746
+ * @category Enums
747
+ * @description Provides string identifiers for each blockchain with a definition.
748
+ * @see {@link BridgeChain} for the subset of chains that support CCTPv2 bridging.
749
+ */
750
+ exports.Blockchain = void 0;
751
+ (function (Blockchain) {
752
+ Blockchain["Algorand"] = "Algorand";
753
+ Blockchain["Algorand_Testnet"] = "Algorand_Testnet";
754
+ Blockchain["Aptos"] = "Aptos";
755
+ Blockchain["Aptos_Testnet"] = "Aptos_Testnet";
756
+ Blockchain["Arc_Testnet"] = "Arc_Testnet";
757
+ Blockchain["Arbitrum"] = "Arbitrum";
758
+ Blockchain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
759
+ Blockchain["Avalanche"] = "Avalanche";
760
+ Blockchain["Avalanche_Fuji"] = "Avalanche_Fuji";
761
+ Blockchain["Base"] = "Base";
762
+ Blockchain["Base_Sepolia"] = "Base_Sepolia";
763
+ Blockchain["Celo"] = "Celo";
764
+ Blockchain["Celo_Alfajores_Testnet"] = "Celo_Alfajores_Testnet";
765
+ Blockchain["Codex"] = "Codex";
766
+ Blockchain["Codex_Testnet"] = "Codex_Testnet";
767
+ Blockchain["Ethereum"] = "Ethereum";
768
+ Blockchain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
769
+ Blockchain["Hedera"] = "Hedera";
770
+ Blockchain["Hedera_Testnet"] = "Hedera_Testnet";
771
+ Blockchain["HyperEVM"] = "HyperEVM";
772
+ Blockchain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
773
+ Blockchain["Ink"] = "Ink";
774
+ Blockchain["Ink_Testnet"] = "Ink_Testnet";
775
+ Blockchain["Linea"] = "Linea";
776
+ Blockchain["Linea_Sepolia"] = "Linea_Sepolia";
777
+ Blockchain["NEAR"] = "NEAR";
778
+ Blockchain["NEAR_Testnet"] = "NEAR_Testnet";
779
+ Blockchain["Noble"] = "Noble";
780
+ Blockchain["Noble_Testnet"] = "Noble_Testnet";
781
+ Blockchain["Optimism"] = "Optimism";
782
+ Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
783
+ Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
784
+ Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
785
+ Blockchain["Plume"] = "Plume";
786
+ Blockchain["Plume_Testnet"] = "Plume_Testnet";
787
+ Blockchain["Polygon"] = "Polygon";
788
+ Blockchain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
789
+ Blockchain["Sei"] = "Sei";
790
+ Blockchain["Sei_Testnet"] = "Sei_Testnet";
791
+ Blockchain["Solana"] = "Solana";
792
+ Blockchain["Solana_Devnet"] = "Solana_Devnet";
793
+ Blockchain["Sonic"] = "Sonic";
794
+ Blockchain["Sonic_Testnet"] = "Sonic_Testnet";
795
+ Blockchain["Stellar"] = "Stellar";
796
+ Blockchain["Stellar_Testnet"] = "Stellar_Testnet";
797
+ Blockchain["Sui"] = "Sui";
798
+ Blockchain["Sui_Testnet"] = "Sui_Testnet";
799
+ Blockchain["Unichain"] = "Unichain";
800
+ Blockchain["Unichain_Sepolia"] = "Unichain_Sepolia";
801
+ Blockchain["World_Chain"] = "World_Chain";
802
+ Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
803
+ Blockchain["XDC"] = "XDC";
804
+ Blockchain["XDC_Apothem"] = "XDC_Apothem";
805
+ Blockchain["ZKSync_Era"] = "ZKSync_Era";
806
+ Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
807
+ })(exports.Blockchain || (exports.Blockchain = {}));
808
+ // -----------------------------------------------------------------------------
809
+ // Bridge Chain Enum (CCTPv2 Supported Chains)
810
+ // -----------------------------------------------------------------------------
811
+ /**
812
+ * Enumeration of blockchains that support cross-chain bridging via CCTPv2.
813
+ *
814
+ * The enum is derived from the full {@link Blockchain} enum but filtered to only
815
+ * include chains with active CCTPv2 support. When new chains gain CCTPv2 support,
816
+ * they are added to this enum.
817
+ *
818
+ * @enum
819
+ * @category Enums
820
+ *
821
+ * @remarks
822
+ * - This enum is the **canonical source** of bridging-supported chains.
823
+ * - Use this enum (or its string literals) in `kit.bridge()` calls for type safety.
824
+ * - Attempting to use a chain not in this enum will produce a TypeScript compile error.
825
+ *
826
+ * @example
827
+ * ```typescript
828
+ * import { BridgeKit, BridgeChain } from '@circle-fin/bridge-kit'
829
+ *
830
+ * const kit = new BridgeKit()
831
+ *
832
+ * // ✅ Valid - autocomplete suggests only supported chains
833
+ * await kit.bridge({
834
+ * from: { adapter, chain: BridgeChain.Ethereum },
835
+ * to: { adapter, chain: BridgeChain.Base },
836
+ * amount: '100'
837
+ * })
838
+ *
839
+ * // ✅ Also valid - string literals work with autocomplete
840
+ * await kit.bridge({
841
+ * from: { adapter, chain: 'Ethereum_Sepolia' },
842
+ * to: { adapter, chain: 'Base_Sepolia' },
843
+ * amount: '100'
844
+ * })
845
+ *
846
+ * // ❌ Compile error - Algorand is not in BridgeChain
847
+ * await kit.bridge({
848
+ * from: { adapter, chain: 'Algorand' }, // TypeScript error!
849
+ * to: { adapter, chain: 'Base' },
850
+ * amount: '100'
851
+ * })
852
+ * ```
853
+ *
854
+ * @see {@link Blockchain} for the complete list of all known blockchains.
855
+ * @see {@link BridgeChainIdentifier} for the type that accepts these values.
856
+ */
857
+ exports.BridgeChain = void 0;
858
+ (function (BridgeChain) {
859
+ // Mainnet chains with CCTPv2 support
860
+ BridgeChain["Arbitrum"] = "Arbitrum";
861
+ BridgeChain["Avalanche"] = "Avalanche";
862
+ BridgeChain["Base"] = "Base";
863
+ BridgeChain["Codex"] = "Codex";
864
+ BridgeChain["Ethereum"] = "Ethereum";
865
+ BridgeChain["HyperEVM"] = "HyperEVM";
866
+ BridgeChain["Ink"] = "Ink";
867
+ BridgeChain["Linea"] = "Linea";
868
+ BridgeChain["Optimism"] = "Optimism";
869
+ BridgeChain["Plume"] = "Plume";
870
+ BridgeChain["Polygon"] = "Polygon";
871
+ BridgeChain["Sei"] = "Sei";
872
+ BridgeChain["Solana"] = "Solana";
873
+ BridgeChain["Sonic"] = "Sonic";
874
+ BridgeChain["Unichain"] = "Unichain";
875
+ BridgeChain["World_Chain"] = "World_Chain";
876
+ BridgeChain["XDC"] = "XDC";
877
+ // Testnet chains with CCTPv2 support
878
+ BridgeChain["Arc_Testnet"] = "Arc_Testnet";
879
+ BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
880
+ BridgeChain["Avalanche_Fuji"] = "Avalanche_Fuji";
881
+ BridgeChain["Base_Sepolia"] = "Base_Sepolia";
882
+ BridgeChain["Codex_Testnet"] = "Codex_Testnet";
883
+ BridgeChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
884
+ BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
885
+ BridgeChain["Ink_Testnet"] = "Ink_Testnet";
886
+ BridgeChain["Linea_Sepolia"] = "Linea_Sepolia";
887
+ BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
888
+ BridgeChain["Plume_Testnet"] = "Plume_Testnet";
889
+ BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
890
+ BridgeChain["Sei_Testnet"] = "Sei_Testnet";
891
+ BridgeChain["Solana_Devnet"] = "Solana_Devnet";
892
+ BridgeChain["Sonic_Testnet"] = "Sonic_Testnet";
893
+ BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
894
+ BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
895
+ BridgeChain["XDC_Apothem"] = "XDC_Apothem";
896
+ })(exports.BridgeChain || (exports.BridgeChain = {}));
897
+
898
+ /**
899
+ * Helper function to define a chain with proper TypeScript typing.
900
+ *
901
+ * This utility function works with TypeScript's `as const` assertion to create
902
+ * strongly-typed, immutable chain definition objects. It preserves literal types
903
+ * from the input and ensures the resulting object maintains all type information.
904
+ *
905
+ * When used with `as const`, it allows TypeScript to infer the most specific
906
+ * possible types for all properties, including string literals and numeric literals,
907
+ * rather than widening them to general types like string or number.
908
+ * @typeParam T - The specific chain definition type (must extend ChainDefinition)
909
+ * @param chain - The chain definition object, typically with an `as const` assertion
910
+ * @returns The same chain definition with preserved literal types
911
+ * @example
912
+ * ```typescript
913
+ * // Define an EVM chain with literal types preserved
914
+ * const Ethereum = defineChain({
915
+ * type: 'evm',
916
+ * chain: Blockchain.Ethereum,
917
+ * chainId: 1,
918
+ * name: 'Ethereum',
919
+ * nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
920
+ * isTestnet: false,
921
+ * usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
922
+ * eurcAddress: null,
923
+ * cctp: {
924
+ * domain: 0,
925
+ * contracts: {
926
+ * TokenMessengerV1: '0xbd3fa81b58ba92a82136038b25adec7066af3155',
927
+ * MessageTransmitterV1: '0x0a992d191deec32afe36203ad87d7d289a738f81'
928
+ * }
929
+ * }
930
+ * } as const);
931
+ * ```
932
+ */
933
+ function defineChain(chain) {
934
+ return chain;
935
+ }
936
+
937
+ /**
938
+ * Algorand Mainnet chain definition
939
+ * @remarks
940
+ * This represents the official production network for the Algorand blockchain.
941
+ */
942
+ const Algorand = defineChain({
943
+ type: 'algorand',
944
+ chain: exports.Blockchain.Algorand,
945
+ name: 'Algorand',
946
+ title: 'Algorand Mainnet',
947
+ nativeCurrency: {
948
+ name: 'Algo',
949
+ symbol: 'ALGO',
950
+ decimals: 6,
951
+ },
952
+ isTestnet: false,
953
+ explorerUrl: 'https://explorer.perawallet.app/tx/{hash}',
954
+ rpcEndpoints: ['https://mainnet-api.algonode.cloud'],
955
+ eurcAddress: null,
956
+ usdcAddress: '31566704',
957
+ cctp: null,
958
+ });
959
+
960
+ /**
961
+ * Algorand Testnet chain definition
962
+ * @remarks
963
+ * This represents the official testnet for the Algorand blockchain.
964
+ */
965
+ const AlgorandTestnet = defineChain({
966
+ type: 'algorand',
967
+ chain: exports.Blockchain.Algorand_Testnet,
968
+ name: 'Algorand Testnet',
969
+ title: 'Algorand Test Network',
970
+ nativeCurrency: {
971
+ name: 'Algo',
972
+ symbol: 'ALGO',
973
+ decimals: 6,
974
+ },
975
+ isTestnet: true,
976
+ explorerUrl: 'https://testnet.explorer.perawallet.app/tx/{hash}',
977
+ rpcEndpoints: ['https://testnet-api.algonode.cloud'],
978
+ eurcAddress: null,
979
+ usdcAddress: '10458941',
980
+ cctp: null,
981
+ });
982
+
983
+ /**
984
+ * Aptos Mainnet chain definition
985
+ * @remarks
986
+ * This represents the official production network for the Aptos blockchain.
987
+ */
988
+ const Aptos = defineChain({
989
+ type: 'aptos',
990
+ chain: exports.Blockchain.Aptos,
991
+ name: 'Aptos',
992
+ title: 'Aptos Mainnet',
993
+ nativeCurrency: {
994
+ name: 'Aptos',
995
+ symbol: 'APT',
996
+ decimals: 8,
997
+ },
998
+ isTestnet: false,
999
+ explorerUrl: 'https://explorer.aptoslabs.com/txn/{hash}?network=mainnet',
1000
+ rpcEndpoints: ['https://fullnode.mainnet.aptoslabs.com/v1'],
1001
+ eurcAddress: null,
1002
+ usdcAddress: '0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b',
1003
+ cctp: {
1004
+ domain: 9,
1005
+ contracts: {
1006
+ v1: {
1007
+ type: 'split',
1008
+ tokenMessenger: '0x9bce6734f7b63e835108e3bd8c36743d4709fe435f44791918801d0989640a9d',
1009
+ messageTransmitter: '0x177e17751820e4b4371873ca8c30279be63bdea63b88ed0f2239c2eea10f1772',
1010
+ confirmations: 1,
1011
+ },
1012
+ },
1013
+ },
1014
+ });
1015
+
1016
+ /**
1017
+ * Aptos Testnet chain definition
1018
+ * @remarks
1019
+ * This represents the official test network for the Aptos blockchain.
1020
+ */
1021
+ const AptosTestnet = defineChain({
1022
+ type: 'aptos',
1023
+ chain: exports.Blockchain.Aptos_Testnet,
263
1024
  name: 'Aptos Testnet',
264
1025
  title: 'Aptos Test Network',
265
1026
  nativeCurrency: {
@@ -2363,13 +3124,51 @@ const chainDefinitionSchema$1 = zod.z.discriminatedUnion('type', [
2363
3124
  * chainIdentifierSchema.parse(Ethereum)
2364
3125
  * ```
2365
3126
  */
2366
- const chainIdentifierSchema = zod.z.union([
3127
+ zod.z.union([
2367
3128
  zod.z
2368
3129
  .string()
2369
3130
  .refine((val) => val in exports.Blockchain, 'Must be a valid Blockchain enum value as string'),
2370
3131
  zod.z.nativeEnum(exports.Blockchain),
2371
3132
  chainDefinitionSchema$1,
2372
3133
  ]);
3134
+ /**
3135
+ * Zod schema for validating bridge chain identifiers.
3136
+ *
3137
+ * This schema validates that the provided chain is supported for CCTPv2 bridging.
3138
+ * It accepts either a BridgeChain enum value, a string matching a BridgeChain value,
3139
+ * or a ChainDefinition for a supported chain.
3140
+ *
3141
+ * Use this schema when validating chain parameters for bridge operations to ensure
3142
+ * only CCTPv2-supported chains are accepted at runtime.
3143
+ *
3144
+ * @example
3145
+ * ```typescript
3146
+ * import { bridgeChainIdentifierSchema } from '@core/chains/validation'
3147
+ * import { BridgeChain, Chains } from '@core/chains'
3148
+ *
3149
+ * // Valid - BridgeChain enum value
3150
+ * bridgeChainIdentifierSchema.parse(BridgeChain.Ethereum)
3151
+ *
3152
+ * // Valid - string literal
3153
+ * bridgeChainIdentifierSchema.parse('Base_Sepolia')
3154
+ *
3155
+ * // Valid - ChainDefinition (validated by CCTP support)
3156
+ * bridgeChainIdentifierSchema.parse(Chains.Solana)
3157
+ *
3158
+ * // Invalid - Algorand is not in BridgeChain (throws ZodError)
3159
+ * bridgeChainIdentifierSchema.parse('Algorand')
3160
+ * ```
3161
+ *
3162
+ * @see {@link BridgeChain} for the enum of supported chains.
3163
+ */
3164
+ const bridgeChainIdentifierSchema = zod.z.union([
3165
+ zod.z.string().refine((val) => val in exports.BridgeChain, (val) => ({
3166
+ message: `Chain "${val}" is not supported for bridging. Only chains in the BridgeChain enum support CCTPv2 bridging.`,
3167
+ })),
3168
+ chainDefinitionSchema$1.refine((chainDef) => chainDef.chain in exports.BridgeChain, (chainDef) => ({
3169
+ message: `Chain "${chainDef.name}" (${chainDef.chain}) is not supported for bridging. Only chains in the BridgeChain enum support CCTPv2 bridging.`,
3170
+ })),
3171
+ ]);
2373
3172
 
2374
3173
  /**
2375
3174
  * Retrieve a chain definition by its blockchain enum value.
@@ -2398,1464 +3197,1150 @@ const getChainByEnum = (blockchain) => {
2398
3197
  if (!chain) {
2399
3198
  throw new Error(`No chain definition found for blockchain: ${blockchain}`);
2400
3199
  }
2401
- return chain;
2402
- };
2403
-
2404
- /**
2405
- * Resolves a flexible chain identifier to a ChainDefinition.
2406
- *
2407
- * This function handles all three supported formats:
2408
- * - ChainDefinition objects (passed through unchanged)
2409
- * - Blockchain enum values (resolved via getChainByEnum)
2410
- * - String literals of blockchain values (resolved via getChainByEnum)
2411
- *
2412
- * @param chainIdentifier - The chain identifier to resolve
2413
- * @returns The resolved ChainDefinition object
2414
- * @throws Error if the chain identifier cannot be resolved
2415
- *
2416
- * @example
2417
- * ```typescript
2418
- * import { resolveChainIdentifier } from '@core/chains'
2419
- * import { Blockchain, Ethereum } from '@core/chains'
2420
- *
2421
- * // All of these resolve to the same ChainDefinition:
2422
- * const chain1 = resolveChainIdentifier(Ethereum)
2423
- * const chain2 = resolveChainIdentifier(Blockchain.Ethereum)
2424
- * const chain3 = resolveChainIdentifier('Ethereum')
2425
- * ```
2426
- */
2427
- function resolveChainIdentifier(chainIdentifier) {
2428
- // If it's already a ChainDefinition object, return it unchanged
2429
- if (typeof chainIdentifier === 'object') {
2430
- return chainIdentifier;
2431
- }
2432
- // If it's a string or enum value, resolve it via getChainByEnum
2433
- if (typeof chainIdentifier === 'string') {
2434
- return getChainByEnum(chainIdentifier);
2435
- }
2436
- // This should never happen with proper typing, but provide a fallback
2437
- throw new Error(`Invalid chain identifier type: ${typeof chainIdentifier}. Expected ChainDefinition object, Blockchain enum, or string literal.`);
2438
- }
2439
-
2440
- /**
2441
- * Convert a human-readable decimal string to its smallest unit representation.
2442
- *
2443
- * This function converts user-friendly decimal values into the integer representation
2444
- * required by blockchain operations, where all values are stored in the smallest
2445
- * denomination. Uses the battle-tested implementation from @ethersproject/units.
2446
- *
2447
- * @param value - The decimal string to convert (e.g., "1.0")
2448
- * @param decimals - The number of decimal places for the unit conversion
2449
- * @returns The value in smallest units as a bigint (e.g., 1000000n for 1 USDC with 6 decimals)
2450
- * @throws Error if the value is not a valid decimal string
2451
- *
2452
- * @example
2453
- * ```typescript
2454
- * import { parseUnits } from '@core/utils'
2455
- *
2456
- * // Parse USDC (6 decimals)
2457
- * const usdcParsed = parseUnits('1.0', 6)
2458
- * console.log(usdcParsed) // 1000000n
2459
- *
2460
- * // Parse ETH (18 decimals)
2461
- * const ethParsed = parseUnits('1.0', 18)
2462
- * console.log(ethParsed) // 1000000000000000000n
2463
- *
2464
- * // Parse fractional amount
2465
- * const fractionalParsed = parseUnits('1.5', 6)
2466
- * console.log(fractionalParsed) // 1500000n
2467
- *
2468
- * // Parse integer (no decimal point)
2469
- * const integerParsed = parseUnits('42', 6)
2470
- * console.log(integerParsed) // 42000000n
2471
- * ```
2472
- */
2473
- const parseUnits = (value, decimals) => {
2474
- return units.parseUnits(value, decimals).toBigInt();
2475
- };
2476
-
2477
- /**
2478
- * Custom error class for validation errors.
2479
- * Provides structured error information while hiding implementation details.
2480
- */
2481
- class ValidationError extends Error {
2482
- errors;
2483
- constructor(message, errors) {
2484
- super(message);
2485
- this.errors = errors;
2486
- this.name = 'ValidationError';
2487
- }
2488
- }
2489
-
2490
- /**
2491
- * Formats a Zod error for display.
2492
- * @internal
2493
- */
2494
- const formatZodError = (error) => {
2495
- const path = error.path.length > 0 ? `${error.path.join('.')}: ` : '';
2496
- return `${path}${error.message}`;
2497
- };
2498
- /**
2499
- * Validates data against a Zod schema with enhanced error reporting.
2500
- *
2501
- * This function performs validation using Zod schemas and provides detailed error
2502
- * messages that include the validation context. It's designed to give developers
2503
- * clear feedback about what went wrong during validation.
2504
- *
2505
- * @param schema - The Zod schema to validate against
2506
- * @param data - The data to validate
2507
- * @param context - Context string to include in error messages (e.g., 'bridge parameters')
2508
- * @returns The validated and parsed data
2509
- * @throws {ValidationError} If validation fails
2510
- *
2511
- * @example
2512
- * ```typescript
2513
- * const result = validate(BridgeParamsSchema, params, 'bridge parameters')
2514
- * ```
2515
- */
2516
- function validate(value, schema, context) {
2517
- const result = schema.safeParse(value);
2518
- if (!result.success) {
2519
- const errors = result.error.errors.map(formatZodError);
2520
- const firstError = errors[0] ?? 'Invalid value';
2521
- throw new ValidationError(`Invalid ${context}: ${firstError}`, errors);
2522
- }
2523
- }
3200
+ return chain;
3201
+ };
2524
3202
 
2525
3203
  /**
2526
- * Symbol used to track validation state on objects.
2527
- * This allows us to attach metadata to objects without interfering with their structure,
2528
- * enabling optimized validation by skipping already validated objects.
2529
- * @internal
2530
- */
2531
- const VALIDATION_STATE = Symbol('validationState');
2532
- /**
2533
- * Validates data against a Zod schema with state tracking and enhanced error reporting.
3204
+ * Resolves a flexible chain identifier to a ChainDefinition.
2534
3205
  *
2535
- * This function performs validation using Zod schemas while tracking validation state
2536
- * and providing detailed error messages. It's designed for use in scenarios where
2537
- * validation state needs to be monitored and reported.
3206
+ * This function handles all three supported formats:
3207
+ * - ChainDefinition objects (passed through unchanged)
3208
+ * - Blockchain enum values (resolved via getChainByEnum)
3209
+ * - String literals of blockchain values (resolved via getChainByEnum)
2538
3210
  *
2539
- * @param schema - The Zod schema to validate against
2540
- * @param data - The data to validate
2541
- * @param context - Context string to include in error messages (e.g., 'bridge parameters')
2542
- * @returns Object containing validation result and state information
2543
- * @throws {ValidationError} If validation fails
3211
+ * @param chainIdentifier - The chain identifier to resolve
3212
+ * @returns The resolved ChainDefinition object
3213
+ * @throws Error if the chain identifier cannot be resolved
2544
3214
  *
2545
3215
  * @example
2546
3216
  * ```typescript
2547
- * const result = validateWithStateTracking(BridgeParamsSchema, params, 'bridge parameters')
3217
+ * import { resolveChainIdentifier } from '@core/chains'
3218
+ * import { Blockchain, Ethereum } from '@core/chains'
3219
+ *
3220
+ * // All of these resolve to the same ChainDefinition:
3221
+ * const chain1 = resolveChainIdentifier(Ethereum)
3222
+ * const chain2 = resolveChainIdentifier(Blockchain.Ethereum)
3223
+ * const chain3 = resolveChainIdentifier('Ethereum')
2548
3224
  * ```
2549
3225
  */
2550
- function validateWithStateTracking(value, schema, context, validatorName) {
2551
- // Skip validation for null or undefined values
2552
- if (value === null) {
2553
- throw new ValidationError(`Invalid ${context}: Value is null`, [
2554
- `Value is null`,
2555
- ]);
2556
- }
2557
- if (value === undefined) {
2558
- throw new ValidationError(`Invalid ${context}: Value is undefined`, [
2559
- `Value is undefined`,
2560
- ]);
2561
- }
2562
- // Ensure value is an object that can hold validation state
2563
- if (typeof value !== 'object') {
2564
- throw new ValidationError(`Invalid ${context}: Value must be an object`, [
2565
- `Value must be an object, got ${typeof value}`,
2566
- ]);
3226
+ function resolveChainIdentifier(chainIdentifier) {
3227
+ // If it's already a ChainDefinition object, return it unchanged
3228
+ if (typeof chainIdentifier === 'object') {
3229
+ return chainIdentifier;
2567
3230
  }
2568
- // Get or initialize validation state
2569
- const valueWithState = value;
2570
- const state = valueWithState[VALIDATION_STATE] ?? { validatedBy: [] };
2571
- // Skip validation if already validated by this validator
2572
- if (state.validatedBy.includes(validatorName)) {
2573
- return;
3231
+ // If it's a string or enum value, resolve it via getChainByEnum
3232
+ if (typeof chainIdentifier === 'string') {
3233
+ return getChainByEnum(chainIdentifier);
2574
3234
  }
2575
- // Delegate to the validate function for actual validation
2576
- validate(value, schema, context);
2577
- // Update validation state
2578
- state.validatedBy.push(validatorName);
2579
- valueWithState[VALIDATION_STATE] = state;
3235
+ // This should never happen with proper typing, but provide a fallback
3236
+ throw new Error(`Invalid chain identifier type: ${typeof chainIdentifier}. Expected ChainDefinition object, Blockchain enum, or string literal.`);
2580
3237
  }
2581
3238
 
2582
3239
  /**
2583
- * Zod schema for validating chain definition objects used in buildExplorerUrl.
2584
- * This schema ensures the chain definition has the required properties for URL generation.
2585
- */
2586
- const chainDefinitionSchema = zod.z.object({
2587
- name: zod.z
2588
- .string({
2589
- required_error: 'Chain name is required',
2590
- invalid_type_error: 'Chain name must be a string',
2591
- })
2592
- .min(1, 'Chain name cannot be empty'),
2593
- explorerUrl: zod.z
2594
- .string({
2595
- required_error: 'Explorer URL template is required',
2596
- invalid_type_error: 'Explorer URL template must be a string',
2597
- })
2598
- .min(1, 'Explorer URL template cannot be empty')
2599
- .refine((url) => url.includes('{hash}'), 'Explorer URL template must contain a {hash} placeholder'),
2600
- });
2601
- /**
2602
- * Zod schema for validating transaction hash strings used in buildExplorerUrl.
2603
- * This schema ensures the transaction hash is a non-empty string.
2604
- */
2605
- const transactionHashSchema = zod.z
2606
- .string({
2607
- required_error: 'Transaction hash is required',
2608
- invalid_type_error: 'Transaction hash must be a string',
2609
- })
2610
- .min(1, 'Transaction hash cannot be empty')
2611
- .transform((hash) => hash.trim()) // Automatically trim whitespace
2612
- .refine((hash) => hash.length > 0, 'Transaction hash must not be empty or whitespace-only');
2613
- /**
2614
- * Zod schema for validating buildExplorerUrl function parameters.
2615
- * This schema validates both the chain definition and transaction hash together.
2616
- */
2617
- zod.z.object({
2618
- chainDef: chainDefinitionSchema,
2619
- txHash: transactionHashSchema,
2620
- });
2621
- /**
2622
- * Zod schema for validating the generated explorer URL.
2623
- * This schema ensures the generated URL is valid.
2624
- */
2625
- zod.z
2626
- .string()
2627
- .url('Generated explorer URL is invalid');
2628
-
2629
- /**
2630
- * A type-safe event emitter for managing action-based event subscriptions.
3240
+ * Extracts chain information including name, display name, and expected address format.
2631
3241
  *
2632
- * Actionable provides a strongly-typed publish/subscribe pattern for events,
2633
- * where each event (action) has its own specific payload type. Handlers can
2634
- * subscribe to specific events or use a wildcard to receive all events.
3242
+ * This function determines the chain type by checking the explicit `type` property first,
3243
+ * then falls back to name-based matching for Solana chains. The expected address format
3244
+ * is determined based on the chain type:
3245
+ * - EVM chains: 42-character hex address starting with 0x
3246
+ * - Solana chains: 44-character base58 encoded string
3247
+ * - Other chains: Generic format message based on chain type
2635
3248
  *
2636
- * @typeParam AllActions - A record mapping action names to their payload types.
3249
+ * @param chain - The chain identifier (ChainDefinition object, string name, or undefined/null)
3250
+ * @returns Chain information with name, display name, and expected address format
2637
3251
  *
2638
3252
  * @example
2639
3253
  * ```typescript
2640
- * import { Actionable } from '@circle-fin/bridge-kit/utils';
2641
- *
2642
- * // Define your action types
2643
- * type TransferActions = {
2644
- * started: { txHash: string; amount: string };
2645
- * completed: { txHash: string; destinationTxHash: string };
2646
- * failed: { error: Error };
2647
- * };
3254
+ * import { extractChainInfo } from '@core/chains'
2648
3255
  *
2649
- * // Create an actionable instance
2650
- * const transferEvents = new Actionable<TransferActions>();
3256
+ * // EVM chain with explicit type
3257
+ * const info1 = extractChainInfo({ name: 'Ethereum', type: 'evm' })
3258
+ * console.log(info1.name) // → "Ethereum"
3259
+ * console.log(info1.displayName) // → "Ethereum"
3260
+ * console.log(info1.expectedAddressFormat)
3261
+ * // → "42-character hex address starting with 0x"
2651
3262
  *
2652
- * // Subscribe to a specific event
2653
- * transferEvents.on('completed', (payload) => {
2654
- * console.log(`Transfer completed with hash: ${payload.destinationTxHash}`);
2655
- * });
3263
+ * // Solana chain (inferred from name)
3264
+ * const info2 = extractChainInfo('Solana')
3265
+ * console.log(info2.expectedAddressFormat)
3266
+ * // → "44-character base58 encoded string"
2656
3267
  *
2657
- * // Subscribe to all events
2658
- * transferEvents.on('*', (payload) => {
2659
- * console.log('Event received:', payload);
2660
- * });
3268
+ * // Non-EVM chain with explicit type
3269
+ * const info3 = extractChainInfo({ name: 'Algorand', type: 'algorand' })
3270
+ * console.log(info3.expectedAddressFormat)
3271
+ * // → "valid algorand address"
2661
3272
  *
2662
- * // Dispatch an event
2663
- * transferEvents.dispatch('completed', {
2664
- * txHash: '0x123',
2665
- * destinationTxHash: '0xabc'
2666
- * });
3273
+ * // Unknown chain
3274
+ * const info4 = extractChainInfo(undefined)
3275
+ * console.log(info4.name) // → "unknown"
3276
+ * console.log(info4.expectedAddressFormat) // → "valid blockchain address"
2667
3277
  * ```
2668
3278
  */
2669
- class Actionable {
2670
- // Store event handlers by action key
2671
- handlers = {};
2672
- // Store wildcard handlers that receive all events
2673
- wildcard = [];
2674
- // Implementation that handles both overloads
2675
- on(action, handler) {
2676
- if (action === '*') {
2677
- // Add to wildcard handlers array
2678
- this.wildcard.push(handler);
2679
- }
2680
- else {
2681
- // Initialize the action's handler array if it doesn't exist
2682
- if (!this.handlers[action]) {
2683
- this.handlers[action] = [];
2684
- }
2685
- // Add the handler to the specific action's array
2686
- this.handlers[action].push(handler);
2687
- }
3279
+ function extractChainInfo(chain) {
3280
+ const name = typeof chain === 'string' ? chain : (chain?.name ?? 'unknown');
3281
+ const chainType = typeof chain === 'object' && chain !== null ? chain.type : undefined;
3282
+ // Use explicit chain type if available, fallback to name matching
3283
+ const isSolana = chainType === undefined
3284
+ ? name.toLowerCase().includes('solana')
3285
+ : chainType === 'solana';
3286
+ // Default to EVM if not Solana and no explicit non-EVM type
3287
+ const isEVM = chainType === undefined
3288
+ ? !isSolana // Default to EVM for unknown chains (unless they're Solana)
3289
+ : chainType === 'evm';
3290
+ // Determine expected address format based on chain type
3291
+ let expectedAddressFormat;
3292
+ if (isSolana) {
3293
+ expectedAddressFormat = '44-character base58 encoded string';
2688
3294
  }
2689
- // Implementation that handles both overloads
2690
- off(action, handler) {
2691
- if (action === '*') {
2692
- // Find and remove the handler from wildcard array
2693
- const index = this.wildcard.indexOf(handler);
2694
- if (index !== -1) {
2695
- this.wildcard.splice(index, 1);
2696
- }
2697
- }
2698
- else if (this.handlers[action]) {
2699
- // Check if there are handlers for this action
2700
- // Find and remove the specific handler
2701
- const index = this.handlers[action].indexOf(handler);
2702
- if (index !== -1) {
2703
- this.handlers[action].splice(index, 1);
2704
- }
2705
- }
3295
+ else if (isEVM) {
3296
+ expectedAddressFormat = '42-character hex address starting with 0x';
2706
3297
  }
2707
- /**
2708
- * Dispatch an action with its payload to all registered handlers.
2709
- *
2710
- * This method notifies both:
2711
- * - Handlers registered specifically for this action
2712
- * - Wildcard handlers registered for all actions
2713
- *
2714
- * @param action - The action key identifying the event type.
2715
- * @param payload - The data associated with the action.
2716
- *
2717
- * @example
2718
- * ```typescript
2719
- * type Actions = {
2720
- * transferStarted: { amount: string; destination: string };
2721
- * transferComplete: { txHash: string };
2722
- * };
2723
- *
2724
- * const events = new Actionable<Actions>();
2725
- *
2726
- * // Dispatch an event
2727
- * events.dispatch('transferStarted', {
2728
- * amount: '100',
2729
- * destination: '0xABC123'
2730
- * });
2731
- * ```
2732
- */
2733
- dispatch(action, payload) {
2734
- // Execute all handlers registered for this specific action
2735
- for (const h of this.handlers[action] ?? [])
2736
- h(payload);
2737
- // Execute all wildcard handlers
2738
- for (const h of this.wildcard)
2739
- h(payload);
3298
+ else {
3299
+ expectedAddressFormat = `valid ${chainType ?? 'blockchain'} address`;
2740
3300
  }
3301
+ return {
3302
+ name,
3303
+ displayName: name.replaceAll('_', ' '),
3304
+ expectedAddressFormat,
3305
+ };
2741
3306
  }
2742
3307
 
2743
- var name = "@circle-fin/bridge-kit";
2744
- var version = "1.1.1";
2745
- var pkg = {
2746
- name: name,
2747
- version: version};
2748
-
2749
3308
  /**
2750
- * Valid recoverability values for error handling strategies.
3309
+ * Type guard to check if an error is a KitError instance.
2751
3310
  *
2752
- * - FATAL errors are thrown immediately (invalid inputs, insufficient funds)
2753
- * - RETRYABLE errors are returned when a flow fails to start but could work later
2754
- * - RESUMABLE errors are returned when a flow fails mid-execution but can be continued
2755
- */
2756
- const RECOVERABILITY_VALUES = [
2757
- 'RETRYABLE',
2758
- 'RESUMABLE',
2759
- 'FATAL',
2760
- ];
2761
-
2762
- // Create a mutable array for Zod enum validation
2763
- const RECOVERABILITY_ARRAY = [...RECOVERABILITY_VALUES];
2764
- /**
2765
- * Zod schema for validating ErrorDetails objects.
3311
+ * This guard enables TypeScript to narrow the type from `unknown` to
3312
+ * `KitError`, providing access to structured error properties like
3313
+ * code, name, and recoverability.
2766
3314
  *
2767
- * This schema provides runtime validation for all ErrorDetails properties,
2768
- * ensuring type safety and proper error handling for JavaScript consumers.
3315
+ * @param error - Unknown error to check
3316
+ * @returns True if error is KitError with proper type narrowing
2769
3317
  *
2770
3318
  * @example
2771
3319
  * ```typescript
2772
- * import { errorDetailsSchema } from '@core/errors'
2773
- *
2774
- * const result = errorDetailsSchema.safeParse({
2775
- * code: 1001,
2776
- * name: 'NETWORK_MISMATCH',
2777
- * recoverability: 'FATAL',
2778
- * message: 'Source and destination networks must be different'
2779
- * })
3320
+ * import { isKitError } from '@core/errors'
2780
3321
  *
2781
- * if (!result.success) {
2782
- * console.error('Validation failed:', result.error.issues)
3322
+ * try {
3323
+ * await kit.bridge(params)
3324
+ * } catch (error) {
3325
+ * if (isKitError(error)) {
3326
+ * // TypeScript knows this is KitError
3327
+ * console.log(`Structured error: ${error.name} (${error.code})`)
3328
+ * } else {
3329
+ * console.log('Regular error:', error)
3330
+ * }
2783
3331
  * }
2784
3332
  * ```
2785
3333
  */
2786
- const errorDetailsSchema = zod.z.object({
2787
- /** Numeric identifier following standardized ranges (1000+ for INPUT errors) */
2788
- code: zod.z
2789
- .number()
2790
- .int('Error code must be an integer')
2791
- .min(1000, 'Error code must be within valid range (1000+)')
2792
- .max(1099, 'Error code must be within valid range (1099 max)'),
2793
- /** Human-readable ID (e.g., "NETWORK_MISMATCH") */
2794
- name: zod.z
2795
- .string()
2796
- .min(1, 'Error name must be a non-empty string')
2797
- .regex(/^[A-Z_][A-Z0-9_]*$/, 'Error name must match pattern: ^[A-Z_][A-Z0-9_]*$'),
2798
- /** Error handling strategy */
2799
- recoverability: zod.z.enum(RECOVERABILITY_ARRAY, {
2800
- errorMap: () => ({
2801
- message: 'Recoverability must be one of: RETRYABLE, RESUMABLE, FATAL',
2802
- }),
2803
- }),
2804
- /** User-friendly explanation with network context */
2805
- message: zod.z
2806
- .string()
2807
- .min(1, 'Error message must be a non-empty string')
2808
- .max(500, 'Error message must be 500 characters or less'),
2809
- /** Raw error details, context, or the original error that caused this one. */
2810
- cause: zod.z
2811
- .object({
2812
- /** Free-form error payload from underlying system */
2813
- trace: zod.z.unknown().optional(),
2814
- })
2815
- .optional(),
2816
- });
2817
-
3334
+ function isKitError(error) {
3335
+ return error instanceof KitError;
3336
+ }
2818
3337
  /**
2819
- * Validates an ErrorDetails object using Zod schema.
3338
+ * Checks if an error is a KitError with FATAL recoverability.
2820
3339
  *
2821
- * @param details - The object to validate
2822
- * @returns The validated ErrorDetails object
2823
- * @throws {TypeError} When validation fails
3340
+ * FATAL errors indicate issues that cannot be resolved through retries,
3341
+ * such as invalid inputs, configuration problems, or business rule
3342
+ * violations. These errors require user intervention to fix.
3343
+ *
3344
+ * @param error - Unknown error to check
3345
+ * @returns True if error is a KitError with FATAL recoverability
2824
3346
  *
2825
3347
  * @example
2826
3348
  * ```typescript
2827
- * import { validateErrorDetails } from '@core/errors'
3349
+ * import { isFatalError } from '@core/errors'
2828
3350
  *
2829
3351
  * try {
2830
- * const validDetails = validateErrorDetails({
2831
- * code: 1001,
2832
- * name: 'NETWORK_MISMATCH',
2833
- * recoverability: 'FATAL',
2834
- * message: 'Source and destination networks must be different'
2835
- * })
3352
+ * await kit.bridge(params)
2836
3353
  * } catch (error) {
2837
- * console.error('Validation failed:', error.message)
3354
+ * if (isFatalError(error)) {
3355
+ * // Show user-friendly error message - don't retry
3356
+ * showUserError(error.message)
3357
+ * }
2838
3358
  * }
2839
3359
  * ```
2840
3360
  */
2841
- function validateErrorDetails(details) {
2842
- const result = errorDetailsSchema.safeParse(details);
2843
- if (!result.success) {
2844
- const issues = result.error.issues
2845
- .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
2846
- .join(', ');
2847
- throw new TypeError(`Invalid ErrorDetails: ${issues}`);
2848
- }
2849
- return result.data;
3361
+ function isFatalError(error) {
3362
+ return isKitError(error) && error.recoverability === 'FATAL';
2850
3363
  }
2851
-
2852
3364
  /**
2853
- * Structured error class for Stablecoin Kit operations.
3365
+ * Checks if an error is a KitError with RETRYABLE recoverability.
2854
3366
  *
2855
- * This class extends the native Error class while implementing the ErrorDetails
2856
- * interface, providing a consistent error format for programmatic handling
2857
- * across the Stablecoin Kits ecosystem. All properties are immutable to ensure
2858
- * error objects cannot be modified after creation.
3367
+ * RETRYABLE errors indicate transient failures that may succeed on
3368
+ * subsequent attempts, such as network timeouts or temporary service
3369
+ * unavailability. These errors are safe to retry after a delay.
3370
+ *
3371
+ * @param error - Unknown error to check
3372
+ * @returns True if error is a KitError with RETRYABLE recoverability
2859
3373
  *
2860
3374
  * @example
2861
3375
  * ```typescript
2862
- * import { KitError } from '@core/errors'
2863
- *
2864
- * const error = new KitError({
2865
- * code: 1001,
2866
- * name: 'INPUT_NETWORK_MISMATCH',
2867
- * recoverability: 'FATAL',
2868
- * message: 'Cannot bridge between mainnet and testnet'
2869
- * })
3376
+ * import { isRetryableError } from '@core/errors'
2870
3377
  *
2871
- * if (error instanceof KitError) {
2872
- * console.log(`Error ${error.code}: ${error.name}`)
2873
- * // "Error 1001: INPUT_NETWORK_MISMATCH"
3378
+ * try {
3379
+ * await kit.bridge(params)
3380
+ * } catch (error) {
3381
+ * if (isRetryableError(error)) {
3382
+ * // Implement retry logic with exponential backoff
3383
+ * setTimeout(() => retryOperation(), 5000)
3384
+ * }
2874
3385
  * }
2875
3386
  * ```
3387
+ */
3388
+ function isRetryableError(error) {
3389
+ return isKitError(error) && error.recoverability === 'RETRYABLE';
3390
+ }
3391
+ /**
3392
+ * Type guard to check if error is KitError with INPUT type.
3393
+ *
3394
+ * INPUT errors represent validation failures, invalid parameters,
3395
+ * or user input problems. These errors are always FATAL and require
3396
+ * the user to correct their input before retrying.
3397
+ *
3398
+ * @param error - Unknown error to check
3399
+ * @returns True if error is KitError with INPUT type
2876
3400
  *
2877
3401
  * @example
2878
3402
  * ```typescript
2879
- * import { KitError } from '@core/errors'
3403
+ * import { isInputError } from '@core/errors'
2880
3404
  *
2881
- * // Error with cause information
2882
- * const error = new KitError({
2883
- * code: 1002,
2884
- * name: 'INVALID_AMOUNT',
2885
- * recoverability: 'FATAL',
2886
- * message: 'Amount must be greater than zero',
2887
- * cause: {
2888
- * trace: { providedAmount: -100, minimumAmount: 0 }
3405
+ * try {
3406
+ * await kit.bridge(params)
3407
+ * } catch (error) {
3408
+ * if (isInputError(error)) {
3409
+ * console.log('Validation error:', error.message)
3410
+ * showValidationUI()
2889
3411
  * }
2890
- * })
2891
- *
2892
- * throw error
3412
+ * }
2893
3413
  * ```
2894
3414
  */
2895
- class KitError extends Error {
2896
- /** Numeric identifier following standardized ranges (1000+ for INPUT errors) */
2897
- code;
2898
- /** Human-readable ID (e.g., "NETWORK_MISMATCH") */
2899
- name;
2900
- /** Error handling strategy */
2901
- recoverability;
2902
- /** Raw error details, context, or the original error that caused this one. */
2903
- cause;
2904
- /**
2905
- * Create a new KitError instance.
2906
- *
2907
- * @param details - The error details object containing all required properties.
2908
- * @throws \{TypeError\} When details parameter is missing or invalid.
2909
- */
2910
- constructor(details) {
2911
- // Validate input at runtime for JavaScript consumers using Zod
2912
- const validatedDetails = validateErrorDetails(details);
2913
- super(validatedDetails.message);
2914
- // Set properties as readonly at runtime
2915
- Object.defineProperties(this, {
2916
- name: {
2917
- value: validatedDetails.name,
2918
- writable: false,
2919
- enumerable: true,
2920
- configurable: false,
2921
- },
2922
- code: {
2923
- value: validatedDetails.code,
2924
- writable: false,
2925
- enumerable: true,
2926
- configurable: false,
2927
- },
2928
- recoverability: {
2929
- value: validatedDetails.recoverability,
2930
- writable: false,
2931
- enumerable: true,
2932
- configurable: false,
2933
- },
2934
- ...(validatedDetails.cause && {
2935
- cause: {
2936
- value: validatedDetails.cause,
2937
- writable: false,
2938
- enumerable: true,
2939
- configurable: false,
2940
- },
2941
- }),
2942
- });
2943
- }
3415
+ function isInputError(error) {
3416
+ return isKitError(error) && error.type === ERROR_TYPES.INPUT;
2944
3417
  }
2945
-
2946
3418
  /**
2947
- * Minimum error code for INPUT type errors.
2948
- * INPUT errors represent validation failures and invalid parameters.
2949
- */
2950
- const INPUT_ERROR_CODE_MIN = 1000;
2951
- /**
2952
- * Maximum error code for INPUT type errors (exclusive).
2953
- * INPUT error codes range from 1000 to 1099 inclusive.
2954
- */
2955
- const INPUT_ERROR_CODE_MAX = 1100;
2956
- /**
2957
- * Standardized error definitions for INPUT type errors.
2958
- *
2959
- * Each entry combines the numeric error code with its corresponding
2960
- * string name to ensure consistency when creating error instances.
3419
+ * Safely extracts error message from any error type.
2961
3420
  *
2962
- * Error codes follow a hierarchical numbering scheme where the first digit
2963
- * indicates the error category (1 = INPUT) and subsequent digits provide
2964
- * specific error identification within that category.
3421
+ * This utility handles different error types gracefully, extracting
3422
+ * meaningful messages from Error instances, string errors, or providing
3423
+ * a fallback for unknown error types. Never throws.
2965
3424
  *
3425
+ * @param error - Unknown error to extract message from
3426
+ * @returns Error message string, or fallback message
2966
3427
  *
2967
3428
  * @example
2968
3429
  * ```typescript
2969
- * import { InputError } from '@core/errors'
2970
- *
2971
- * const error = new KitError({
2972
- * ...InputError.NETWORK_MISMATCH,
2973
- * recoverability: 'FATAL',
2974
- * message: 'Source and destination networks must be different'
2975
- * })
3430
+ * import { getErrorMessage } from '@core/errors'
2976
3431
  *
2977
- * // Access code and name individually if needed
2978
- * console.log(InputError.NETWORK_MISMATCH.code) // 1001
2979
- * console.log(InputError.NETWORK_MISMATCH.name) // 'INPUT_NETWORK_MISMATCH'
3432
+ * try {
3433
+ * await riskyOperation()
3434
+ * } catch (error) {
3435
+ * const message = getErrorMessage(error)
3436
+ * console.log('Error occurred:', message)
3437
+ * // Works with Error, KitError, string, or any other type
3438
+ * }
2980
3439
  * ```
2981
3440
  */
2982
- const InputError = {
2983
- /** Network type mismatch between chains (mainnet vs testnet) */
2984
- NETWORK_MISMATCH: {
2985
- code: 1001,
2986
- name: 'INPUT_NETWORK_MISMATCH',
2987
- },
2988
- /** Invalid amount format or value (negative, zero, or malformed) */
2989
- INVALID_AMOUNT: {
2990
- code: 1002,
2991
- name: 'INPUT_INVALID_AMOUNT',
2992
- },
2993
- /** Unsupported or invalid bridge route configuration */
2994
- UNSUPPORTED_ROUTE: {
2995
- code: 1003,
2996
- name: 'INPUT_UNSUPPORTED_ROUTE',
2997
- },
2998
- /** Invalid wallet or contract address format */
2999
- INVALID_ADDRESS: {
3000
- code: 1004,
3001
- name: 'INPUT_INVALID_ADDRESS',
3002
- },
3003
- /** Invalid or unsupported chain identifier */
3004
- INVALID_CHAIN: {
3005
- code: 1005,
3006
- name: 'INPUT_INVALID_CHAIN',
3007
- },
3008
- /** General validation failure for complex validation rules */
3009
- VALIDATION_FAILED: {
3010
- code: 1098,
3011
- name: 'INPUT_VALIDATION_FAILED',
3012
- },
3013
- };
3014
-
3441
+ function getErrorMessage(error) {
3442
+ if (error instanceof Error) {
3443
+ return error.message;
3444
+ }
3445
+ if (typeof error === 'string') {
3446
+ return error;
3447
+ }
3448
+ return 'An unknown error occurred';
3449
+ }
3015
3450
  /**
3016
- * Creates error for network type mismatch between source and destination.
3451
+ * Gets the error code from a KitError, or null if not applicable.
3017
3452
  *
3018
- * This error is thrown when attempting to bridge between chains that have
3019
- * different network types (e.g., mainnet to testnet), which is not supported
3020
- * for security reasons.
3453
+ * This utility safely extracts the numeric error code from KitError
3454
+ * instances, returning null for non-KitError types. Useful for
3455
+ * programmatic error handling based on specific error codes.
3021
3456
  *
3022
- * @param sourceChain - The source chain definition
3023
- * @param destChain - The destination chain definition
3024
- * @returns KitError with specific network mismatch details
3457
+ * @param error - Unknown error to extract code from
3458
+ * @returns Error code number, or null if not a KitError
3025
3459
  *
3026
3460
  * @example
3027
3461
  * ```typescript
3028
- * import { createNetworkMismatchError } from '@core/errors'
3029
- * import { Ethereum, BaseSepolia } from '@core/chains'
3462
+ * import { getErrorCode, InputError } from '@core/errors'
3030
3463
  *
3031
- * // This will throw a detailed error
3032
- * throw createNetworkMismatchError(Ethereum, BaseSepolia)
3033
- * // Message: "Cannot bridge between Ethereum (mainnet) and Base Sepolia (testnet). Source and destination networks must both be testnet or both be mainnet."
3464
+ * try {
3465
+ * await kit.bridge(params)
3466
+ * } catch (error) {
3467
+ * const code = getErrorCode(error)
3468
+ * if (code === InputError.NETWORK_MISMATCH.code) {
3469
+ * // Handle network mismatch specifically
3470
+ * showNetworkMismatchHelp()
3471
+ * }
3472
+ * }
3034
3473
  * ```
3035
3474
  */
3036
- function createNetworkMismatchError(sourceChain, destChain) {
3037
- const sourceNetworkType = sourceChain.isTestnet ? 'testnet' : 'mainnet';
3038
- const destNetworkType = destChain.isTestnet ? 'testnet' : 'mainnet';
3039
- const errorDetails = {
3040
- ...InputError.NETWORK_MISMATCH,
3041
- recoverability: 'FATAL',
3042
- message: `Cannot bridge between ${sourceChain.name} (${sourceNetworkType}) and ${destChain.name} (${destNetworkType}). Source and destination networks must both be testnet or both be mainnet.`,
3043
- cause: {
3044
- trace: { sourceChain: sourceChain.name, destChain: destChain.name },
3045
- },
3046
- };
3047
- return new KitError(errorDetails);
3475
+ function getErrorCode(error) {
3476
+ return isKitError(error) ? error.code : null;
3048
3477
  }
3478
+
3049
3479
  /**
3050
- * Creates error for unsupported bridge route.
3480
+ * Validates if an address format is correct for the specified chain.
3051
3481
  *
3052
- * This error is thrown when attempting to bridge between chains that don't
3053
- * have a supported bridge route configured.
3482
+ * This function checks the explicit chain `type` property first, then falls back
3483
+ * to name-based matching to determine whether to validate as a Solana or EVM address.
3054
3484
  *
3055
- * @param source - Source chain name
3056
- * @param destination - Destination chain name
3057
- * @returns KitError with specific route details
3485
+ * @param address - The address to validate
3486
+ * @param chain - The chain identifier or chain definition (cannot be null)
3487
+ * @returns True if the address format is valid for the chain, false otherwise
3058
3488
  *
3059
3489
  * @example
3060
3490
  * ```typescript
3061
- * import { createUnsupportedRouteError } from '@core/errors'
3491
+ * import { isValidAddressForChain } from '@core/errors'
3062
3492
  *
3063
- * throw createUnsupportedRouteError('Ethereum', 'Solana')
3064
- * // Message: "Route from Ethereum to Solana is not supported"
3493
+ * // EVM address validation
3494
+ * isValidAddressForChain('0x742d35Cc6634C0532925a3b8D0C0C1C4C5C6C7C8', 'Ethereum')
3495
+ * // → true
3496
+ *
3497
+ * // Solana address validation
3498
+ * isValidAddressForChain('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', { name: 'Solana', type: 'solana' })
3499
+ * // → true
3500
+ *
3501
+ * // Invalid format
3502
+ * isValidAddressForChain('invalid', 'Ethereum')
3503
+ * // → false
3065
3504
  * ```
3066
3505
  */
3067
- function createUnsupportedRouteError(source, destination) {
3068
- const errorDetails = {
3069
- ...InputError.UNSUPPORTED_ROUTE,
3070
- recoverability: 'FATAL',
3071
- message: `Route from ${source} to ${destination} is not supported.`,
3072
- cause: {
3073
- trace: { source, destination },
3074
- },
3075
- };
3076
- return new KitError(errorDetails);
3506
+ function isValidAddressForChain(address, chain) {
3507
+ const chainType = typeof chain === 'object' ? chain.type : undefined;
3508
+ const name = typeof chain === 'string' ? chain : chain.name;
3509
+ // Use explicit chain type if available, fallback to name matching
3510
+ const isSolana = chainType !== undefined
3511
+ ? chainType === 'solana'
3512
+ : name.toLowerCase().includes('solana');
3513
+ if (isSolana) {
3514
+ // Solana base58 address: 32-44 characters from base58 alphabet
3515
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
3516
+ }
3517
+ // EVM hex address: 0x followed by 40 hex characters
3518
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
3077
3519
  }
3078
3520
  /**
3079
- * Creates error for invalid amount format or precision.
3521
+ * Type guard to check if a value is an object.
3080
3522
  *
3081
- * This error is thrown when the provided amount doesn't meet validation
3082
- * requirements such as precision, range, or format.
3523
+ * @param val - The value to check
3524
+ * @returns True if the value is a non-null object
3525
+ */
3526
+ function isObject(val) {
3527
+ return typeof val === 'object' && val !== null;
3528
+ }
3529
+ /**
3530
+ * Type guard to check if a value is a chain with isTestnet property.
3083
3531
  *
3084
- * @param amount - The invalid amount string
3085
- * @param reason - Specific reason why amount is invalid
3086
- * @returns KitError with amount details and validation rule
3532
+ * @param chain - The value to check
3533
+ * @returns True if the value is a chain object with name and isTestnet properties
3087
3534
  *
3088
3535
  * @example
3089
3536
  * ```typescript
3090
- * import { createInvalidAmountError } from '@core/errors'
3537
+ * import { isChainWithTestnet } from '@core/errors'
3091
3538
  *
3092
- * throw createInvalidAmountError('0.000001', 'Amount must be at least 0.01 USDC')
3093
- * // Message: "Invalid amount '0.000001': Amount must be at least 0.01 USDC"
3539
+ * const chain1 = { name: 'Ethereum', isTestnet: false }
3540
+ * isChainWithTestnet(chain1) // true
3541
+ *
3542
+ * const chain2 = { name: 'Ethereum' }
3543
+ * isChainWithTestnet(chain2) // → false
3094
3544
  *
3095
- * throw createInvalidAmountError('abc', 'Amount must be a valid number')
3096
- * // Message: "Invalid amount 'abc': Amount must be a valid number"
3545
+ * const chain3 = 'Ethereum'
3546
+ * isChainWithTestnet(chain3) // false
3097
3547
  * ```
3098
3548
  */
3099
- function createInvalidAmountError(amount, reason) {
3100
- const errorDetails = {
3101
- ...InputError.INVALID_AMOUNT,
3102
- recoverability: 'FATAL',
3103
- message: `Invalid amount '${amount}': ${reason}.`,
3104
- cause: {
3105
- trace: { amount, reason },
3106
- },
3107
- };
3108
- return new KitError(errorDetails);
3549
+ function isChainWithTestnet(chain) {
3550
+ return (isObject(chain) &&
3551
+ 'isTestnet' in chain &&
3552
+ typeof chain['isTestnet'] === 'boolean' &&
3553
+ 'name' in chain &&
3554
+ typeof chain['name'] === 'string');
3109
3555
  }
3110
3556
  /**
3111
- * Creates error for invalid wallet address format.
3557
+ * Gets chain identifier from toData object.
3112
3558
  *
3113
- * This error is thrown when the provided address doesn't match the expected
3114
- * format for the specified chain.
3559
+ * Looks for chain in context.chain first, then falls back to direct chain property.
3115
3560
  *
3116
- * @param address - The invalid address string
3117
- * @param chain - Chain name where address is invalid
3118
- * @param expectedFormat - Description of expected address format
3119
- * @returns KitError with address details and format requirements
3561
+ * @param toData - The destination data object
3562
+ * @returns The chain identifier or null
3563
+ */
3564
+ function getChainFromToData(toData) {
3565
+ const context = toData['context'];
3566
+ const chain = context?.['chain'] ?? toData['chain'];
3567
+ return chain;
3568
+ }
3569
+ /**
3570
+ * Gets chain name from params object.
3120
3571
  *
3121
- * @example
3122
- * ```typescript
3123
- * import { createInvalidAddressError } from '@core/errors'
3572
+ * @param params - The parameters object
3573
+ * @returns The chain name as a string, or null if not found
3574
+ */
3575
+ function getChainName(params) {
3576
+ if (isObject(params)) {
3577
+ const chain = params['chain'];
3578
+ if (typeof chain === 'string') {
3579
+ return chain;
3580
+ }
3581
+ if (isObject(chain) && 'name' in chain) {
3582
+ return chain.name;
3583
+ }
3584
+ }
3585
+ return null;
3586
+ }
3587
+ /**
3588
+ * Extracts from data from params object.
3124
3589
  *
3125
- * throw createInvalidAddressError('0x123', 'Ethereum', '42-character hex string starting with 0x')
3126
- * // Message: "Invalid address '0x123' for Ethereum. Expected 42-character hex string starting with 0x."
3590
+ * Supports both 'from' and 'source' property names.
3127
3591
  *
3128
- * throw createInvalidAddressError('invalid', 'Solana', 'base58-encoded string')
3129
- * // Message: "Invalid address 'invalid' for Solana. Expected base58-encoded string."
3130
- * ```
3592
+ * @param params - The parameters object
3593
+ * @returns The from/source data or undefined
3131
3594
  */
3132
- function createInvalidAddressError(address, chain, expectedFormat) {
3133
- const errorDetails = {
3134
- ...InputError.INVALID_ADDRESS,
3135
- recoverability: 'FATAL',
3136
- message: `Invalid address '${address}' for ${chain}. Expected ${expectedFormat}.`,
3137
- cause: {
3138
- trace: { address, chain, expectedFormat },
3139
- },
3140
- };
3141
- return new KitError(errorDetails);
3595
+ function extractFromData(params) {
3596
+ return (params['from'] ?? params['source']);
3142
3597
  }
3143
3598
  /**
3144
- * Creates error for invalid chain configuration.
3599
+ * Extracts to data from params object.
3145
3600
  *
3146
- * This error is thrown when the provided chain doesn't meet the required
3147
- * configuration or is not supported for the operation.
3601
+ * Supports both 'to' and 'destination' property names.
3148
3602
  *
3149
- * @param chain - The invalid chain name or identifier
3150
- * @param reason - Specific reason why chain is invalid
3151
- * @returns KitError with chain details and validation rule
3603
+ * @param params - The parameters object
3604
+ * @returns The to/destination data or undefined
3605
+ */
3606
+ function extractToData(params) {
3607
+ return (params['to'] ?? params['destination']);
3608
+ }
3609
+ /**
3610
+ * Gets address from params object using a dot-separated path.
3611
+ *
3612
+ * Traverses nested objects to extract the address value at the specified path.
3613
+ *
3614
+ * @param params - The parameters object
3615
+ * @param path - Dot-separated path to extract address from (e.g., 'to.recipientAddress')
3616
+ * @returns The address as a string, or 'unknown' if not found
3152
3617
  *
3153
3618
  * @example
3154
3619
  * ```typescript
3155
- * import { createInvalidChainError } from '@core/errors'
3156
- *
3157
- * throw createInvalidChainError('UnknownChain', 'Chain is not supported by this bridge')
3158
- * // Message: "Invalid chain 'UnknownChain': Chain is not supported by this bridge"
3620
+ * // Extract from specific path
3621
+ * getAddressFromParams(params, 'to.recipientAddress')
3622
+ * // Returns the value at params.to.recipientAddress
3159
3623
  * ```
3160
3624
  */
3161
- function createInvalidChainError(chain, reason) {
3162
- const errorDetails = {
3163
- ...InputError.INVALID_CHAIN,
3164
- recoverability: 'FATAL',
3165
- message: `Invalid chain '${chain}': ${reason}`,
3166
- cause: {
3167
- trace: { chain, reason },
3168
- },
3169
- };
3170
- return new KitError(errorDetails);
3625
+ function getAddressFromParams(params, path) {
3626
+ const parts = path.split('.');
3627
+ let current = params;
3628
+ for (const part of parts) {
3629
+ if (current !== null &&
3630
+ current !== undefined &&
3631
+ typeof current === 'object' &&
3632
+ part in current) {
3633
+ current = current[part];
3634
+ }
3635
+ else {
3636
+ return 'unknown';
3637
+ }
3638
+ }
3639
+ return typeof current === 'string' ? current : 'unknown';
3640
+ }
3641
+ /**
3642
+ * Gets chain identifier from params object.
3643
+ *
3644
+ * Looks for chain in to.context.chain, to.chain, or direct chain property.
3645
+ *
3646
+ * @param params - The parameters object
3647
+ * @returns The chain identifier or null
3648
+ */
3649
+ function getChainFromParams(params) {
3650
+ const to = extractToData(params);
3651
+ const chain = to?.['chain'] ?? params['chain'];
3652
+ return chain;
3171
3653
  }
3172
3654
 
3173
3655
  /**
3174
- * Type guard to check if an error is a KitError instance.
3175
- *
3176
- * This guard enables TypeScript to narrow the type from `unknown` to
3177
- * `KitError`, providing access to structured error properties like
3178
- * code, name, and recoverability.
3179
- *
3180
- * @param error - Unknown error to check
3181
- * @returns True if error is KitError with proper type narrowing
3656
+ * Converts a Zod validation error into a specific KitError instance using structured pattern matching.
3182
3657
  *
3183
- * @example
3184
- * ```typescript
3185
- * import { isKitError } from '@core/errors'
3658
+ * This function inspects Zod's error details (path, code, message) and delegates each issue
3659
+ * to specialized handlers that generate domain-specific KitError objects. It leverages
3660
+ * Zod's error codes and path information for robust matching, avoiding fragile string checks.
3186
3661
  *
3187
- * try {
3188
- * await kit.bridge(params)
3189
- * } catch (error) {
3190
- * if (isKitError(error)) {
3191
- * // TypeScript knows this is KitError
3192
- * console.log(`Structured error: ${error.name} (${error.code})`)
3193
- * } else {
3194
- * console.log('Regular error:', error)
3195
- * }
3196
- * }
3197
- * ```
3662
+ * @param zodError - The Zod validation error containing one or more issues
3663
+ * @param params - The original parameters that failed validation (used to extract invalid values)
3664
+ * @returns A specific KitError instance with actionable error details
3198
3665
  */
3199
- function isKitError(error) {
3200
- return error instanceof KitError;
3666
+ function convertZodErrorToStructured(zodError, params) {
3667
+ // Handle null/undefined params gracefully
3668
+ if (params === null || params === undefined) {
3669
+ return createValidationFailedError(zodError);
3670
+ }
3671
+ const paramsObj = params;
3672
+ const toData = extractToData(paramsObj);
3673
+ const fromData = extractFromData(paramsObj);
3674
+ for (const issue of zodError.issues) {
3675
+ const path = issue.path.join('.');
3676
+ const code = issue.code;
3677
+ // Try to handle specific error types
3678
+ const amountError = handleAmountError(path, code, issue.message, paramsObj);
3679
+ if (amountError)
3680
+ return amountError;
3681
+ const chainError = handleChainError(path, code, issue.message, fromData, toData);
3682
+ if (chainError)
3683
+ return chainError;
3684
+ const addressError = handleAddressError(path, code, issue.message, paramsObj);
3685
+ if (addressError)
3686
+ return addressError;
3687
+ }
3688
+ // Fallback
3689
+ return createValidationFailedError(zodError);
3201
3690
  }
3202
3691
  /**
3203
- * Checks if an error is a KitError with FATAL recoverability.
3204
- *
3205
- * FATAL errors indicate issues that cannot be resolved through retries,
3206
- * such as invalid inputs, configuration problems, or business rule
3207
- * violations. These errors require user intervention to fix.
3692
+ * Creates a generic validation failed error for null/undefined params or unmapped error cases.
3208
3693
  *
3209
- * @param error - Unknown error to check
3210
- * @returns True if error is a KitError with FATAL recoverability
3694
+ * This is a fallback handler used when:
3695
+ * - Parameters are null or undefined
3696
+ * - No specific error handler matches the Zod error
3697
+ * - The error doesn't fit into amount/chain/address categories
3211
3698
  *
3212
- * @example
3213
- * ```typescript
3214
- * import { isFatalError } from '@core/errors'
3699
+ * This function delegates to createValidationErrorFromZod with a generic "parameters" context.
3215
3700
  *
3216
- * try {
3217
- * await kit.bridge(params)
3218
- * } catch (error) {
3219
- * if (isFatalError(error)) {
3220
- * // Show user-friendly error message - don't retry
3221
- * showUserError(error.message)
3222
- * }
3223
- * }
3224
- * ```
3701
+ * @param zodError - The Zod validation error with all issues
3702
+ * @returns A generic KitError with INPUT_VALIDATION_FAILED code
3225
3703
  */
3226
- function isFatalError(error) {
3227
- return isKitError(error) && error.recoverability === 'FATAL';
3704
+ function createValidationFailedError(zodError) {
3705
+ return createValidationErrorFromZod(zodError, 'parameters');
3228
3706
  }
3707
+ const AMOUNT_FORMAT_ERROR_MESSAGE = 'Amount must be a numeric string with dot (.) as decimal separator, with no thousand separators or comma decimals';
3229
3708
  /**
3230
- * Checks if an error is a KitError with RETRYABLE recoverability.
3231
- *
3232
- * RETRYABLE errors indicate transient failures that may succeed on
3233
- * subsequent attempts, such as network timeouts or temporary service
3234
- * unavailability. These errors are safe to retry after a delay.
3235
- *
3236
- * @param error - Unknown error to check
3237
- * @returns True if error is a KitError with RETRYABLE recoverability
3709
+ * Handles amount-related validation errors from Zod.
3238
3710
  *
3239
- * @example
3240
- * ```typescript
3241
- * import { isRetryableError } from '@core/errors'
3711
+ * Checks if the validation error path includes 'amount' and attempts
3712
+ * to convert generic Zod errors into specific KitError instances with
3713
+ * actionable messages. Delegates to specialized handlers in order of specificity:
3714
+ * 1. Negative amount errors (too_small)
3715
+ * 2. Custom validation errors (decimal places, numeric string)
3716
+ * 3. Invalid string format errors
3717
+ * 4. Invalid type errors
3242
3718
  *
3243
- * try {
3244
- * await kit.bridge(params)
3245
- * } catch (error) {
3246
- * if (isRetryableError(error)) {
3247
- * // Implement retry logic with exponential backoff
3248
- * setTimeout(() => retryOperation(), 5000)
3249
- * }
3250
- * }
3251
- * ```
3719
+ * @param path - The Zod error path (e.g., 'amount' or 'config.amount')
3720
+ * @param code - The Zod error code (e.g., 'too_small', 'invalid_string', 'custom')
3721
+ * @param message - The original Zod error message
3722
+ * @param paramsObj - The original params object for extracting the invalid amount value
3723
+ * @returns KitError with INPUT_INVALID_AMOUNT code if this is an amount error, null otherwise
3252
3724
  */
3253
- function isRetryableError(error) {
3254
- return isKitError(error) && error.recoverability === 'RETRYABLE';
3725
+ function handleAmountError(path, code, message, paramsObj) {
3726
+ if (!path.includes('amount'))
3727
+ return null;
3728
+ const amount = typeof paramsObj['amount'] === 'string' ? paramsObj['amount'] : 'unknown';
3729
+ // Try different error handlers in order of specificity
3730
+ const negativeError = handleNegativeAmountError(code, message, amount);
3731
+ if (negativeError)
3732
+ return negativeError;
3733
+ const customError = handleCustomAmountError(code, message, amount);
3734
+ if (customError)
3735
+ return customError;
3736
+ const stringFormatError = handleInvalidStringAmountError(code, message, amount);
3737
+ if (stringFormatError)
3738
+ return stringFormatError;
3739
+ const typeError = handleInvalidTypeAmountError(code, amount);
3740
+ if (typeError)
3741
+ return typeError;
3742
+ return null;
3255
3743
  }
3256
3744
  /**
3257
- * Combined type guard to check if error is KitError with INPUT type.
3258
- *
3259
- * INPUT errors have error codes in the 1000-1099 range and represent
3260
- * validation failures, invalid parameters, or user input problems.
3261
- * These errors are always FATAL and require the user to correct their
3262
- * input before retrying.
3745
+ * Handles negative or too-small amount validation errors.
3263
3746
  *
3264
- * @param error - Unknown error to check
3265
- * @returns True if error is KitError with INPUT error code range
3747
+ * Detects Zod 'too_small' error codes or messages containing 'greater than'
3748
+ * and creates a specific error indicating the amount must be positive.
3266
3749
  *
3267
- * @example
3268
- * ```typescript
3269
- * import { isInputError } from '@core/errors'
3270
- * import { createBridgeKit } from '@core/bridge'
3750
+ * @param code - The Zod error code
3751
+ * @param message - The Zod error message
3752
+ * @param amount - The invalid amount value as a string
3753
+ * @returns KitError if this is a negative/too-small amount error, null otherwise
3754
+ */
3755
+ function handleNegativeAmountError(code, message, amount) {
3756
+ if (code === 'too_small' || message.includes('greater than')) {
3757
+ return createInvalidAmountError(amount, 'Amount must be greater than 0');
3758
+ }
3759
+ return null;
3760
+ }
3761
+ /**
3762
+ * Handles custom Zod refinement validation errors for amounts.
3271
3763
  *
3272
- * async function handleBridge() {
3273
- * const kit = createBridgeKit(config)
3274
- * const params = { amount: '100', from: 'ethereum', to: 'polygon' }
3764
+ * Processes Zod 'custom' error codes from .refine() validators and matches
3765
+ * against known patterns:
3766
+ * - 'non-negative' - amount must be \>= 0
3767
+ * - 'decimal places' - too many decimal places
3768
+ * - 'numeric string' - value is not a valid numeric string
3275
3769
  *
3276
- * try {
3277
- * await kit.bridge(params)
3278
- * } catch (error) {
3279
- * if (isInputError(error)) {
3280
- * console.log(`Input error: ${error.message} (code: ${error.code})`)
3281
- * }
3282
- * }
3283
- * }
3284
- * ```
3770
+ * @param code - The Zod error code (must be 'custom')
3771
+ * @param message - The custom error message from the refinement
3772
+ * @param amount - The invalid amount value as a string
3773
+ * @returns KitError with specific message if pattern matches, null otherwise
3285
3774
  */
3286
- function isInputError(error) {
3287
- return (isKitError(error) &&
3288
- error.code >= INPUT_ERROR_CODE_MIN &&
3289
- error.code < INPUT_ERROR_CODE_MAX);
3775
+ function handleCustomAmountError(code, message, amount) {
3776
+ if (code !== 'custom')
3777
+ return null;
3778
+ if (message.includes('non-negative')) {
3779
+ return createInvalidAmountError(amount, 'Amount must be non-negative');
3780
+ }
3781
+ if (message.includes('greater than 0')) {
3782
+ return createInvalidAmountError(amount, 'Amount must be greater than 0');
3783
+ }
3784
+ if (message.includes('decimal places')) {
3785
+ return createInvalidAmountError(amount, message);
3786
+ }
3787
+ if (message.includes('numeric string')) {
3788
+ return createInvalidAmountError(amount, AMOUNT_FORMAT_ERROR_MESSAGE);
3789
+ }
3790
+ return null;
3290
3791
  }
3291
3792
  /**
3292
- * Safely extracts error message from any error type.
3293
- *
3294
- * This utility handles different error types gracefully, extracting
3295
- * meaningful messages from Error instances, string errors, or providing
3296
- * a fallback for unknown error types. Never throws.
3793
+ * Handles Zod 'invalid_string' errors for amount values.
3297
3794
  *
3298
- * @param error - Unknown error to extract message from
3299
- * @returns Error message string, or fallback message
3795
+ * Processes string validation failures (e.g., regex mismatches) and categorizes them:
3796
+ * 1. Negative numbers that pass Number() but fail other validations
3797
+ * 2. Decimal places validation failures (too many decimal places)
3798
+ * 3. Numeric format validation failures (invalid characters, comma decimals, thousand separators)
3799
+ * 4. Generic invalid number strings (must use dot decimal notation)
3300
3800
  *
3301
- * @example
3302
- * ```typescript
3303
- * import { getErrorMessage } from '@core/errors'
3801
+ * Note: The SDK enforces strict dot-decimal notation. Comma decimals (e.g., "1,5") and
3802
+ * thousand separators (e.g., "1,000.50") are not allowed. UI layers should normalize
3803
+ * locale-specific formats before passing values to the SDK.
3304
3804
  *
3305
- * try {
3306
- * await riskyOperation()
3307
- * } catch (error) {
3308
- * const message = getErrorMessage(error)
3309
- * console.log('Error occurred:', message)
3310
- * // Works with Error, KitError, string, or any other type
3311
- * }
3312
- * ```
3805
+ * @param code - The Zod error code (must be 'invalid_string')
3806
+ * @param message - The Zod error message from string validation
3807
+ * @param amount - The invalid amount value as a string
3808
+ * @returns KitError with context-specific message if pattern matches, null otherwise
3313
3809
  */
3314
- function getErrorMessage(error) {
3315
- if (error instanceof Error) {
3316
- return error.message;
3810
+ function handleInvalidStringAmountError(code, message, amount) {
3811
+ if (code !== 'invalid_string')
3812
+ return null;
3813
+ // Check for decimal places validation
3814
+ if (isDecimalPlacesError(message)) {
3815
+ return createInvalidAmountError(amount, 'Maximum supported decimal places: 6');
3317
3816
  }
3318
- if (typeof error === 'string') {
3319
- return error;
3817
+ // Check for numeric format validation
3818
+ if (isNumericFormatError(message)) {
3819
+ return createInvalidAmountError(amount, AMOUNT_FORMAT_ERROR_MESSAGE);
3320
3820
  }
3321
- return 'An unknown error occurred';
3821
+ // For other cases like 'abc', return specific error
3822
+ if (!message.includes('valid number format')) {
3823
+ return createInvalidAmountError(amount, AMOUNT_FORMAT_ERROR_MESSAGE);
3824
+ }
3825
+ return null;
3322
3826
  }
3323
3827
  /**
3324
- * Gets the error code from a KitError, or null if not applicable.
3828
+ * Handles Zod 'invalid_type' errors for amount values.
3325
3829
  *
3326
- * This utility safely extracts the numeric error code from KitError
3327
- * instances, returning null for non-KitError types. Useful for
3328
- * programmatic error handling based on specific error codes.
3830
+ * Triggered when the amount is not a string type (e.g., number, boolean, object).
3831
+ * Creates an error indicating the amount must be a string representation of a number
3832
+ * using strict dot-decimal notation (no comma decimals or thousand separators).
3329
3833
  *
3330
- * @param error - Unknown error to extract code from
3331
- * @returns Error code number, or null if not a KitError
3834
+ * @param code - The Zod error code (must be 'invalid_type')
3835
+ * @param amount - The invalid amount value (will be converted to string for error message)
3836
+ * @returns KitError if this is a type mismatch, null otherwise
3837
+ */
3838
+ function handleInvalidTypeAmountError(code, amount) {
3839
+ if (code === 'invalid_type') {
3840
+ return createInvalidAmountError(amount, AMOUNT_FORMAT_ERROR_MESSAGE);
3841
+ }
3842
+ return null;
3843
+ }
3844
+ /**
3845
+ * Checks if an error message indicates a decimal places validation failure.
3332
3846
  *
3333
- * @example
3334
- * ```typescript
3335
- * import { getErrorCode, InputError } from '@core/errors'
3847
+ * Looks for keywords like 'maximum', 'at most', and 'decimal places' to identify
3848
+ * errors related to too many decimal digits in an amount value.
3336
3849
  *
3337
- * try {
3338
- * await kit.bridge(params)
3339
- * } catch (error) {
3340
- * const code = getErrorCode(error)
3341
- * if (code === InputError.NETWORK_MISMATCH.code) {
3342
- * // Handle network mismatch specifically
3343
- * showNetworkMismatchHelp()
3344
- * }
3345
- * }
3346
- * ```
3850
+ * @param message - The error message to analyze
3851
+ * @returns True if the message indicates a decimal places error, false otherwise
3347
3852
  */
3348
- function getErrorCode(error) {
3349
- return isKitError(error) ? error.code : null;
3853
+ function isDecimalPlacesError(message) {
3854
+ return ((message.includes('maximum') || message.includes('at most')) &&
3855
+ message.includes('decimal places'));
3350
3856
  }
3351
-
3352
3857
  /**
3353
- * Extracts chain information including name, display name, and expected address format.
3858
+ * Checks if an error message indicates a numeric format validation failure.
3354
3859
  *
3355
- * This function determines the chain type by checking the explicit `type` property first,
3356
- * then falls back to name-based matching. This approach is more robust than relying solely
3357
- * on string matching and future-proofs the code for new chain ecosystems.
3860
+ * Identifies errors where a value contains 'numeric' but is not specifically
3861
+ * about 'valid number format' or 'numeric string' (to avoid false positives).
3358
3862
  *
3359
- * @param chain - The chain identifier (string, chain object, or undefined/null)
3360
- * @returns Chain information with name, display name, and expected address format
3863
+ * @param message - The error message to analyze
3864
+ * @returns True if the message indicates a numeric format error, false otherwise
3865
+ */
3866
+ function isNumericFormatError(message) {
3867
+ return (message.includes('numeric') &&
3868
+ !message.includes('valid number format') &&
3869
+ !message.includes('numeric string'));
3870
+ }
3871
+ /**
3872
+ * Handles chain-related validation errors from Zod.
3361
3873
  *
3362
- * @example
3363
- * ```typescript
3364
- * import { extractChainInfo } from '@core/errors'
3874
+ * Checks if the validation error path includes 'chain' and extracts the
3875
+ * chain name from either the source (from) or destination (to) data.
3876
+ * Creates a KitError with the invalid chain name and original error message.
3365
3877
  *
3366
- * // With chain object
3367
- * const info1 = extractChainInfo({ name: 'Ethereum', type: 'evm' })
3368
- * console.log(info1.expectedAddressFormat)
3369
- * // "42-character hex address starting with 0x"
3878
+ * @param path - The Zod error path (e.g., 'from.chain' or 'to.chain')
3879
+ * @param _code - The Zod error code (unused, prefixed with _ to indicate intentionally ignored)
3880
+ * @param message - The original Zod error message
3881
+ * @param fromData - The source/from data object (may contain chain info)
3882
+ * @param toData - The destination/to data object (may contain chain info)
3883
+ * @returns KitError with INPUT_INVALID_CHAIN code if this is a chain error, null otherwise
3884
+ */
3885
+ function handleChainError(path, _code, message, fromData, toData) {
3886
+ if (!path.includes('chain'))
3887
+ return null;
3888
+ const chain = getChainName(fromData) ?? getChainName(toData);
3889
+ return createInvalidChainError(chain ?? 'unknown', message);
3890
+ }
3891
+ /**
3892
+ * Handles address-related validation errors from Zod.
3370
3893
  *
3371
- * // With string
3372
- * const info2 = extractChainInfo('Solana')
3373
- * console.log(info2.expectedAddressFormat)
3374
- * // "44-character base58 encoded string"
3894
+ * Checks if the validation error path includes 'address' and extracts both
3895
+ * the invalid address from the path and the target chain from the params.
3896
+ * Uses chain utilities to determine the expected address format (EVM or Solana)
3897
+ * and creates a context-specific error message.
3375
3898
  *
3376
- * // With undefined
3377
- * const info3 = extractChainInfo(undefined)
3378
- * console.log(info3.name) // "unknown"
3379
- * ```
3899
+ * @param path - The Zod error path (e.g., 'to.recipientAddress')
3900
+ * @param _code - The Zod error code (unused, prefixed with _ to indicate intentionally ignored)
3901
+ * @param _message - The original Zod error message (unused, we create a more specific message)
3902
+ * @param paramsObj - The original params object for extracting address and chain info
3903
+ * @returns KitError with INPUT_INVALID_ADDRESS code if this is an address error, null otherwise
3380
3904
  */
3381
- function extractChainInfo(chain) {
3382
- const name = typeof chain === 'string' ? chain : (chain?.name ?? 'unknown');
3383
- const chainType = typeof chain === 'object' && chain !== null ? chain.type : undefined;
3384
- // Use explicit chain type if available, fallback to name matching
3385
- const isSolana = chainType !== undefined
3386
- ? chainType === 'solana'
3387
- : name.toLowerCase().includes('solana');
3388
- return {
3389
- name,
3390
- displayName: name.replace(/_/g, ' '),
3391
- expectedAddressFormat: isSolana
3392
- ? '44-character base58 encoded string'
3393
- : '42-character hex address starting with 0x',
3394
- };
3905
+ function handleAddressError(path, _code, _message, paramsObj) {
3906
+ if (!path.toLowerCase().includes('address'))
3907
+ return null;
3908
+ const address = getAddressFromParams(paramsObj, path);
3909
+ const chain = getChainFromParams(paramsObj);
3910
+ const chainInfo = extractChainInfo(chain);
3911
+ return createInvalidAddressError(address, chainInfo.displayName, chainInfo.expectedAddressFormat);
3395
3912
  }
3913
+
3396
3914
  /**
3397
- * Validates if an address format is correct for the specified chain.
3915
+ * Validates data against a Zod schema with enhanced error reporting.
3398
3916
  *
3399
- * This function checks the explicit chain `type` property first, then falls back
3400
- * to name-based matching to determine whether to validate as a Solana or EVM address.
3917
+ * This function performs validation using Zod schemas and provides detailed error
3918
+ * messages that include the validation context. It's designed to give developers
3919
+ * clear feedback about what went wrong during validation.
3401
3920
  *
3402
- * @param address - The address to validate
3403
- * @param chain - The chain identifier or chain definition (cannot be null)
3404
- * @returns True if the address format is valid for the chain, false otherwise
3921
+ * @param value - The value to validate
3922
+ * @param schema - The Zod schema to validate against
3923
+ * @param context - Context string to include in error messages (e.g., 'bridge parameters')
3924
+ * @returns Asserts that value is of type T (type narrowing)
3925
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098)
3405
3926
  *
3406
3927
  * @example
3407
3928
  * ```typescript
3408
- * import { isValidAddressForChain } from '@core/errors'
3409
- *
3410
- * // EVM address validation
3411
- * isValidAddressForChain('0x742d35Cc6634C0532925a3b8D0C0C1C4C5C6C7C8', 'Ethereum')
3412
- * // → true
3413
- *
3414
- * // Solana address validation
3415
- * isValidAddressForChain('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', { name: 'Solana', type: 'solana' })
3416
- * // → true
3417
- *
3418
- * // Invalid format
3419
- * isValidAddressForChain('invalid', 'Ethereum')
3420
- * // → false
3929
+ * validate(params, BridgeParamsSchema, 'bridge parameters')
3930
+ * // After this call, TypeScript knows params is of type BridgeParams
3421
3931
  * ```
3422
3932
  */
3423
- function isValidAddressForChain(address, chain) {
3424
- const chainType = typeof chain === 'object' ? chain.type : undefined;
3425
- const name = typeof chain === 'string' ? chain : chain.name;
3426
- // Use explicit chain type if available, fallback to name matching
3427
- const isSolana = chainType !== undefined
3428
- ? chainType === 'solana'
3429
- : name.toLowerCase().includes('solana');
3430
- if (isSolana) {
3431
- // Solana base58 address: 32-44 characters from base58 alphabet
3432
- return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
3933
+ function validate(value, schema, context) {
3934
+ const result = schema.safeParse(value);
3935
+ if (!result.success) {
3936
+ throw createValidationErrorFromZod(result.error, context);
3433
3937
  }
3434
- // EVM hex address: 0x followed by 40 hex characters
3435
- return /^0x[a-fA-F0-9]{40}$/.test(address);
3436
3938
  }
3939
+
3437
3940
  /**
3438
- * Type guard to check if a value is an object.
3439
- *
3440
- * @param val - The value to check
3441
- * @returns True if the value is a non-null object
3941
+ * Symbol used to track validation state on objects.
3942
+ * This allows us to attach metadata to objects without interfering with their structure,
3943
+ * enabling optimized validation by skipping already validated objects.
3944
+ * @internal
3442
3945
  */
3443
- function isObject(val) {
3444
- return typeof val === 'object' && val !== null;
3445
- }
3946
+ const VALIDATION_STATE = Symbol('validationState');
3446
3947
  /**
3447
- * Type guard to check if a value is a chain with isTestnet property.
3948
+ * Validates data against a Zod schema with state tracking and enhanced error reporting.
3448
3949
  *
3449
- * @param chain - The value to check
3450
- * @returns True if the value is a chain object with name and isTestnet properties
3950
+ * This function performs validation using Zod schemas while tracking validation state
3951
+ * and providing detailed error messages. It's designed for use in scenarios where
3952
+ * validation state needs to be monitored and reported.
3953
+ *
3954
+ * @param value - The value to validate
3955
+ * @param schema - The Zod schema to validate against
3956
+ * @param context - Context string to include in error messages (e.g., 'bridge parameters')
3957
+ * @param validatorName - Symbol identifying the validator for state tracking
3958
+ * @returns Asserts that value is of type T (type narrowing)
3959
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098)
3451
3960
  *
3452
3961
  * @example
3453
3962
  * ```typescript
3454
- * import { isChainWithTestnet } from '@core/errors'
3455
- *
3456
- * const chain1 = { name: 'Ethereum', isTestnet: false }
3457
- * isChainWithTestnet(chain1) // → true
3458
- *
3459
- * const chain2 = { name: 'Ethereum' }
3460
- * isChainWithTestnet(chain2) // → false
3461
- *
3462
- * const chain3 = 'Ethereum'
3463
- * isChainWithTestnet(chain3) // → false
3963
+ * const result = validateWithStateTracking(BridgeParamsSchema, params, 'bridge parameters')
3464
3964
  * ```
3465
3965
  */
3466
- function isChainWithTestnet(chain) {
3467
- return (isObject(chain) &&
3468
- 'isTestnet' in chain &&
3469
- typeof chain['isTestnet'] === 'boolean' &&
3470
- 'name' in chain &&
3471
- typeof chain['name'] === 'string');
3966
+ function validateWithStateTracking(value, schema, context, validatorName) {
3967
+ // Skip validation for null or undefined values
3968
+ if (value === null) {
3969
+ throw new KitError({
3970
+ ...InputError.VALIDATION_FAILED,
3971
+ recoverability: 'FATAL',
3972
+ message: `Invalid ${context}: Value is null`,
3973
+ cause: {
3974
+ trace: {
3975
+ validationErrors: ['Value is null'],
3976
+ },
3977
+ },
3978
+ });
3979
+ }
3980
+ if (value === undefined) {
3981
+ throw new KitError({
3982
+ ...InputError.VALIDATION_FAILED,
3983
+ recoverability: 'FATAL',
3984
+ message: `Invalid ${context}: Value is undefined`,
3985
+ cause: {
3986
+ trace: {
3987
+ validationErrors: ['Value is undefined'],
3988
+ },
3989
+ },
3990
+ });
3991
+ }
3992
+ // Ensure value is an object that can hold validation state
3993
+ if (typeof value !== 'object') {
3994
+ throw new KitError({
3995
+ ...InputError.VALIDATION_FAILED,
3996
+ recoverability: 'FATAL',
3997
+ message: `Invalid ${context}: Value must be an object`,
3998
+ cause: {
3999
+ trace: {
4000
+ validationErrors: [`Value must be an object, got ${typeof value}`],
4001
+ },
4002
+ },
4003
+ });
4004
+ }
4005
+ // Get or initialize validation state
4006
+ const valueWithState = value;
4007
+ const state = valueWithState[VALIDATION_STATE] ?? { validatedBy: [] };
4008
+ // Skip validation if already validated by this validator
4009
+ if (state.validatedBy.includes(validatorName)) {
4010
+ return;
4011
+ }
4012
+ // Delegate to the validate function for actual validation (now throws KitError)
4013
+ validate(value, schema, context);
4014
+ // Update validation state
4015
+ state.validatedBy.push(validatorName);
4016
+ valueWithState[VALIDATION_STATE] = state;
3472
4017
  }
4018
+
3473
4019
  /**
3474
- * Gets chain identifier from toData object.
3475
- *
3476
- * Looks for chain in context.chain first, then falls back to direct chain property.
3477
- *
3478
- * @param toData - The destination data object
3479
- * @returns The chain identifier or null
4020
+ * Zod schema for validating chain definition objects used in buildExplorerUrl.
4021
+ * This schema ensures the chain definition has the required properties for URL generation.
3480
4022
  */
3481
- function getChainFromToData(toData) {
3482
- const context = toData['context'];
3483
- const chain = context?.['chain'] ?? toData['chain'];
3484
- return chain;
3485
- }
4023
+ const chainDefinitionSchema = zod.z.object({
4024
+ name: zod.z
4025
+ .string({
4026
+ required_error: 'Chain name is required',
4027
+ invalid_type_error: 'Chain name must be a string',
4028
+ })
4029
+ .min(1, 'Chain name cannot be empty'),
4030
+ explorerUrl: zod.z
4031
+ .string({
4032
+ required_error: 'Explorer URL template is required',
4033
+ invalid_type_error: 'Explorer URL template must be a string',
4034
+ })
4035
+ .min(1, 'Explorer URL template cannot be empty')
4036
+ .refine((url) => url.includes('{hash}'), 'Explorer URL template must contain a {hash} placeholder'),
4037
+ });
3486
4038
  /**
3487
- * Gets chain name from params object.
3488
- *
3489
- * @param params - The parameters object
3490
- * @returns The chain name as a string, or null if not found
4039
+ * Zod schema for validating transaction hash strings used in buildExplorerUrl.
4040
+ * This schema ensures the transaction hash is a non-empty string.
3491
4041
  */
3492
- function getChainName(params) {
3493
- if (isObject(params)) {
3494
- const chain = params['chain'];
3495
- if (typeof chain === 'string') {
3496
- return chain;
3497
- }
3498
- if (isObject(chain) && 'name' in chain) {
3499
- return chain.name;
3500
- }
3501
- }
3502
- return null;
3503
- }
4042
+ const transactionHashSchema = zod.z
4043
+ .string({
4044
+ required_error: 'Transaction hash is required',
4045
+ invalid_type_error: 'Transaction hash must be a string',
4046
+ })
4047
+ .min(1, 'Transaction hash cannot be empty')
4048
+ .transform((hash) => hash.trim()) // Automatically trim whitespace
4049
+ .refine((hash) => hash.length > 0, 'Transaction hash must not be empty or whitespace-only');
3504
4050
  /**
3505
- * Extracts from data from params object.
3506
- *
3507
- * Supports both 'from' and 'source' property names.
3508
- *
3509
- * @param params - The parameters object
3510
- * @returns The from/source data or undefined
4051
+ * Zod schema for validating buildExplorerUrl function parameters.
4052
+ * This schema validates both the chain definition and transaction hash together.
3511
4053
  */
3512
- function extractFromData(params) {
3513
- return (params['from'] ?? params['source']);
3514
- }
4054
+ zod.z.object({
4055
+ chainDef: chainDefinitionSchema,
4056
+ txHash: transactionHashSchema,
4057
+ });
3515
4058
  /**
3516
- * Extracts to data from params object.
3517
- *
3518
- * Supports both 'to' and 'destination' property names.
3519
- *
3520
- * @param params - The parameters object
3521
- * @returns The to/destination data or undefined
4059
+ * Zod schema for validating the generated explorer URL.
4060
+ * This schema ensures the generated URL is valid.
3522
4061
  */
3523
- function extractToData(params) {
3524
- return (params['to'] ?? params['destination']);
3525
- }
4062
+ zod.z
4063
+ .string()
4064
+ .url('Generated explorer URL is invalid');
4065
+
3526
4066
  /**
3527
- * Gets address from params object using a dot-separated path.
4067
+ * A type-safe event emitter for managing action-based event subscriptions.
3528
4068
  *
3529
- * Traverses nested objects to extract the address value at the specified path.
4069
+ * Actionable provides a strongly-typed publish/subscribe pattern for events,
4070
+ * where each event (action) has its own specific payload type. Handlers can
4071
+ * subscribe to specific events or use a wildcard to receive all events.
3530
4072
  *
3531
- * @param params - The parameters object
3532
- * @param path - Dot-separated path to extract address from (e.g., 'to.recipientAddress')
3533
- * @returns The address as a string, or 'unknown' if not found
4073
+ * @typeParam AllActions - A record mapping action names to their payload types.
3534
4074
  *
3535
4075
  * @example
3536
4076
  * ```typescript
3537
- * // Extract from specific path
3538
- * getAddressFromParams(params, 'to.recipientAddress')
3539
- * // Returns the value at params.to.recipientAddress
3540
- * ```
3541
- */
3542
- function getAddressFromParams(params, path) {
3543
- const parts = path.split('.');
3544
- let current = params;
3545
- for (const part of parts) {
3546
- if (current !== null &&
3547
- current !== undefined &&
3548
- typeof current === 'object' &&
3549
- part in current) {
3550
- current = current[part];
3551
- }
3552
- else {
3553
- return 'unknown';
3554
- }
3555
- }
3556
- return typeof current === 'string' ? current : 'unknown';
3557
- }
3558
- /**
3559
- * Gets chain identifier from params object.
3560
- *
3561
- * Looks for chain in to.context.chain, to.chain, or direct chain property.
4077
+ * import { Actionable } from '@circle-fin/bridge-kit/utils';
3562
4078
  *
3563
- * @param params - The parameters object
3564
- * @returns The chain identifier or null
3565
- */
3566
- function getChainFromParams(params) {
3567
- const to = extractToData(params);
3568
- const chain = to?.['chain'] ?? params['chain'];
3569
- return chain;
3570
- }
3571
-
3572
- /**
3573
- * Maximum length for error messages in fallback validation errors.
4079
+ * // Define your action types
4080
+ * type TransferActions = {
4081
+ * started: { txHash: string; amount: string };
4082
+ * completed: { txHash: string; destinationTxHash: string };
4083
+ * failed: { error: Error };
4084
+ * };
3574
4085
  *
3575
- * KitError enforces a 500-character limit on error messages. When creating
3576
- * fallback validation errors that combine multiple Zod issues, we use 450
3577
- * characters to leave a 50-character buffer for:
3578
- * - The error message prefix ("Invalid bridge parameters: ")
3579
- * - Potential encoding differences or formatting overhead
3580
- * - Safety margin to prevent KitError constructor failures
4086
+ * // Create an actionable instance
4087
+ * const transferEvents = new Actionable<TransferActions>();
3581
4088
  *
3582
- * This ensures that even with concatenated issue summaries, the final message
3583
- * stays within KitError's constraints.
3584
- */
3585
- const MAX_MESSAGE_LENGTH = 450;
3586
-
3587
- /**
3588
- * Converts a Zod validation error into a specific KitError instance using structured pattern matching.
4089
+ * // Subscribe to a specific event
4090
+ * transferEvents.on('completed', (payload) => {
4091
+ * console.log(`Transfer completed with hash: ${payload.destinationTxHash}`);
4092
+ * });
3589
4093
  *
3590
- * This function inspects Zod's error details (path, code, message) and delegates each issue
3591
- * to specialized handlers that generate domain-specific KitError objects. It leverages
3592
- * Zod's error codes and path information for robust matching, avoiding fragile string checks.
4094
+ * // Subscribe to all events
4095
+ * transferEvents.on('*', (payload) => {
4096
+ * console.log('Event received:', payload);
4097
+ * });
3593
4098
  *
3594
- * @param zodError - The Zod validation error containing one or more issues
3595
- * @param params - The original parameters that failed validation (used to extract invalid values)
3596
- * @returns A specific KitError instance with actionable error details
4099
+ * // Dispatch an event
4100
+ * transferEvents.dispatch('completed', {
4101
+ * txHash: '0x123',
4102
+ * destinationTxHash: '0xabc'
4103
+ * });
4104
+ * ```
3597
4105
  */
3598
- function convertZodErrorToStructured(zodError, params) {
3599
- // Handle null/undefined params gracefully
3600
- if (params === null || params === undefined) {
3601
- return createValidationFailedError(zodError);
4106
+ class Actionable {
4107
+ // Store event handlers by action key
4108
+ handlers = {};
4109
+ // Store wildcard handlers that receive all events
4110
+ wildcard = [];
4111
+ // Implementation that handles both overloads
4112
+ on(action, handler) {
4113
+ if (action === '*') {
4114
+ // Add to wildcard handlers array
4115
+ this.wildcard.push(handler);
4116
+ }
4117
+ else {
4118
+ // Initialize the action's handler array if it doesn't exist
4119
+ if (!this.handlers[action]) {
4120
+ this.handlers[action] = [];
4121
+ }
4122
+ // Add the handler to the specific action's array
4123
+ this.handlers[action].push(handler);
4124
+ }
3602
4125
  }
3603
- const paramsObj = params;
3604
- const toData = extractToData(paramsObj);
3605
- const fromData = extractFromData(paramsObj);
3606
- for (const issue of zodError.issues) {
3607
- const path = issue.path.join('.');
3608
- const code = issue.code;
3609
- // Try to handle specific error types
3610
- const amountError = handleAmountError(path, code, issue.message, paramsObj);
3611
- if (amountError)
3612
- return amountError;
3613
- const chainError = handleChainError(path, code, issue.message, fromData, toData);
3614
- if (chainError)
3615
- return chainError;
3616
- const addressError = handleAddressError(path, code, issue.message, paramsObj);
3617
- if (addressError)
3618
- return addressError;
4126
+ // Implementation that handles both overloads
4127
+ off(action, handler) {
4128
+ if (action === '*') {
4129
+ // Find and remove the handler from wildcard array
4130
+ const index = this.wildcard.indexOf(handler);
4131
+ if (index !== -1) {
4132
+ this.wildcard.splice(index, 1);
4133
+ }
4134
+ }
4135
+ else if (this.handlers[action]) {
4136
+ // Check if there are handlers for this action
4137
+ // Find and remove the specific handler
4138
+ const index = this.handlers[action].indexOf(handler);
4139
+ if (index !== -1) {
4140
+ this.handlers[action].splice(index, 1);
4141
+ }
4142
+ }
4143
+ }
4144
+ /**
4145
+ * Dispatch an action with its payload to all registered handlers.
4146
+ *
4147
+ * This method notifies both:
4148
+ * - Handlers registered specifically for this action
4149
+ * - Wildcard handlers registered for all actions
4150
+ *
4151
+ * @param action - The action key identifying the event type.
4152
+ * @param payload - The data associated with the action.
4153
+ *
4154
+ * @example
4155
+ * ```typescript
4156
+ * type Actions = {
4157
+ * transferStarted: { amount: string; destination: string };
4158
+ * transferComplete: { txHash: string };
4159
+ * };
4160
+ *
4161
+ * const events = new Actionable<Actions>();
4162
+ *
4163
+ * // Dispatch an event
4164
+ * events.dispatch('transferStarted', {
4165
+ * amount: '100',
4166
+ * destination: '0xABC123'
4167
+ * });
4168
+ * ```
4169
+ */
4170
+ dispatch(action, payload) {
4171
+ // Execute all handlers registered for this specific action
4172
+ for (const h of this.handlers[action] ?? [])
4173
+ h(payload);
4174
+ // Execute all wildcard handlers
4175
+ for (const h of this.wildcard)
4176
+ h(payload);
3619
4177
  }
3620
- // Fallback
3621
- return createValidationFailedError(zodError);
3622
- }
3623
- /**
3624
- * Creates a generic validation failed error for null/undefined params or unmapped error cases.
3625
- *
3626
- * This is a fallback handler used when:
3627
- * - Parameters are null or undefined
3628
- * - No specific error handler matches the Zod error
3629
- * - The error doesn't fit into amount/chain/address categories
3630
- *
3631
- * The function truncates the error message to stay within KitError's 500-character limit.
3632
- *
3633
- * @param zodError - The Zod validation error with all issues
3634
- * @param params - The original parameters (may be null/undefined)
3635
- * @returns A generic KitError with INPUT_VALIDATION_FAILED code
3636
- */
3637
- function createValidationFailedError(zodError) {
3638
- const issueSummary = zodError.issues
3639
- .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
3640
- .join('; ');
3641
- // Truncate message to avoid KitError constructor failure.
3642
- const fullMessage = `Invalid parameters: ${issueSummary}`;
3643
- const truncatedMessage = fullMessage.length > MAX_MESSAGE_LENGTH
3644
- ? `${fullMessage.substring(0, MAX_MESSAGE_LENGTH)}...`
3645
- : fullMessage;
3646
- return new KitError({
3647
- ...InputError.VALIDATION_FAILED,
3648
- recoverability: 'FATAL',
3649
- message: truncatedMessage,
3650
- cause: {
3651
- trace: {
3652
- originalError: zodError.message,
3653
- zodIssues: zodError.issues,
3654
- },
3655
- },
3656
- });
3657
- }
3658
- /**
3659
- * Handles amount-related validation errors from Zod.
3660
- *
3661
- * Checks if the validation error path includes 'amount' and attempts
3662
- * to convert generic Zod errors into specific KitError instances with
3663
- * actionable messages. Delegates to specialized handlers in order of specificity:
3664
- * 1. Negative amount errors (too_small)
3665
- * 2. Custom validation errors (decimal places, numeric string)
3666
- * 3. Invalid string format errors
3667
- * 4. Invalid type errors
3668
- *
3669
- * @param path - The Zod error path (e.g., 'amount' or 'config.amount')
3670
- * @param code - The Zod error code (e.g., 'too_small', 'invalid_string', 'custom')
3671
- * @param message - The original Zod error message
3672
- * @param paramsObj - The original params object for extracting the invalid amount value
3673
- * @returns KitError with INPUT_INVALID_AMOUNT code if this is an amount error, null otherwise
3674
- */
3675
- function handleAmountError(path, code, message, paramsObj) {
3676
- if (!path.includes('amount'))
3677
- return null;
3678
- const amount = typeof paramsObj['amount'] === 'string' ? paramsObj['amount'] : 'unknown';
3679
- // Try different error handlers in order of specificity
3680
- const negativeError = handleNegativeAmountError(code, message, amount);
3681
- if (negativeError)
3682
- return negativeError;
3683
- const customError = handleCustomAmountError(code, message, amount);
3684
- if (customError)
3685
- return customError;
3686
- const stringFormatError = handleInvalidStringAmountError(code, message, amount);
3687
- if (stringFormatError)
3688
- return stringFormatError;
3689
- const typeError = handleInvalidTypeAmountError(code, amount);
3690
- if (typeError)
3691
- return typeError;
3692
- return null;
3693
4178
  }
4179
+
3694
4180
  /**
3695
- * Handles negative or too-small amount validation errors.
4181
+ * Convert a value from its smallest unit representation to a human-readable decimal string.
3696
4182
  *
3697
- * Detects Zod 'too_small' error codes or messages containing 'greater than'
3698
- * and creates a specific error indicating the amount must be positive.
4183
+ * This function normalizes token values from their blockchain representation (where
4184
+ * everything is stored as integers in the smallest denomination) to human-readable
4185
+ * decimal format. Uses the battle-tested implementation from @ethersproject/units.
3699
4186
  *
3700
- * @param code - The Zod error code
3701
- * @param message - The Zod error message
3702
- * @param amount - The invalid amount value as a string
3703
- * @returns KitError if this is a negative/too-small amount error, null otherwise
3704
- */
3705
- function handleNegativeAmountError(code, message, amount) {
3706
- if (code === 'too_small' || message.includes('greater than')) {
3707
- return createInvalidAmountError(amount, 'Amount must be greater than 0');
3708
- }
3709
- return null;
3710
- }
3711
- /**
3712
- * Handles custom Zod refinement validation errors for amounts.
4187
+ * @param value - The value in smallest units (e.g., "1000000" for 1 USDC with 6 decimals)
4188
+ * @param decimals - The number of decimal places for the unit conversion
4189
+ * @returns A human-readable decimal string (e.g., "1.0")
4190
+ * @throws Error if the value is not a valid numeric string
3713
4191
  *
3714
- * Processes Zod 'custom' error codes from .refine() validators and matches
3715
- * against known patterns:
3716
- * - 'non-negative' - amount must be \>= 0
3717
- * - 'decimal places' - too many decimal places
3718
- * - 'numeric string' - value is not a valid numeric string
4192
+ * @example
4193
+ * ```typescript
4194
+ * import { formatUnits } from '@core/utils'
3719
4195
  *
3720
- * @param code - The Zod error code (must be 'custom')
3721
- * @param message - The custom error message from the refinement
3722
- * @param amount - The invalid amount value as a string
3723
- * @returns KitError with specific message if pattern matches, null otherwise
3724
- */
3725
- function handleCustomAmountError(code, message, amount) {
3726
- if (code !== 'custom')
3727
- return null;
3728
- if (message.includes('non-negative')) {
3729
- return createInvalidAmountError(amount, 'Amount must be non-negative');
3730
- }
3731
- if (message.includes('decimal places')) {
3732
- return createInvalidAmountError(amount, message);
3733
- }
3734
- if (message.includes('numeric string')) {
3735
- return createInvalidAmountError(amount, 'Amount must be a numeric string');
3736
- }
3737
- return null;
3738
- }
3739
- /**
3740
- * Handles Zod 'invalid_string' errors for amount values.
4196
+ * // Format USDC (6 decimals)
4197
+ * const usdcFormatted = formatUnits('1000000', 6)
4198
+ * console.log(usdcFormatted) // "1.0"
3741
4199
  *
3742
- * Processes string validation failures (e.g., regex mismatches) and categorizes them:
3743
- * 1. Negative numbers that pass Number() but fail other validations
3744
- * 2. Decimal places validation failures (too many decimal places)
3745
- * 3. Numeric format validation failures (contains non-numeric characters)
3746
- * 4. Generic invalid number strings
4200
+ * // Format ETH (18 decimals)
4201
+ * const ethFormatted = formatUnits('1000000000000000000', 18)
4202
+ * console.log(ethFormatted) // "1.0"
3747
4203
  *
3748
- * @param code - The Zod error code (must be 'invalid_string')
3749
- * @param message - The Zod error message from string validation
3750
- * @param amount - The invalid amount value as a string
3751
- * @returns KitError with context-specific message if pattern matches, null otherwise
4204
+ * // Format with fractional part
4205
+ * const fractionalFormatted = formatUnits('1500000', 6)
4206
+ * console.log(fractionalFormatted) // "1.5"
4207
+ * ```
3752
4208
  */
3753
- function handleInvalidStringAmountError(code, message, amount) {
3754
- if (code !== 'invalid_string')
3755
- return null;
3756
- // Check if it's a negative number first
3757
- if (amount.startsWith('-') && !isNaN(Number(amount))) {
3758
- return createInvalidAmountError(amount, 'Amount must be greater than 0');
3759
- }
3760
- // Check for decimal places validation
3761
- if (isDecimalPlacesError(message)) {
3762
- return createInvalidAmountError(amount, 'Maximum supported decimal places: 6');
3763
- }
3764
- // Check for numeric format validation
3765
- if (isNumericFormatError(message)) {
3766
- return createInvalidAmountError(amount, 'Amount must be a valid number format');
3767
- }
3768
- // For other cases like 'abc', return specific error
3769
- if (!message.includes('valid number format')) {
3770
- return createInvalidAmountError(amount, 'Amount must be a valid number string');
3771
- }
3772
- return null;
3773
- }
4209
+ const formatUnits = (value, decimals) => {
4210
+ return units.formatUnits(value, decimals);
4211
+ };
3774
4212
  /**
3775
- * Handles Zod 'invalid_type' errors for amount values.
4213
+ * Convert a human-readable decimal string to its smallest unit representation.
3776
4214
  *
3777
- * Triggered when the amount is not a string type (e.g., number, boolean, object).
3778
- * Creates an error indicating the amount must be a string representation of a number.
4215
+ * This function converts user-friendly decimal values into the integer representation
4216
+ * required by blockchain operations, where all values are stored in the smallest
4217
+ * denomination. Uses the battle-tested implementation from @ethersproject/units.
3779
4218
  *
3780
- * @param code - The Zod error code (must be 'invalid_type')
3781
- * @param amount - The invalid amount value (will be converted to string for error message)
3782
- * @returns KitError if this is a type mismatch, null otherwise
3783
- */
3784
- function handleInvalidTypeAmountError(code, amount) {
3785
- if (code === 'invalid_type') {
3786
- return createInvalidAmountError(amount, 'Amount must be a valid number string');
3787
- }
3788
- return null;
3789
- }
3790
- /**
3791
- * Checks if an error message indicates a decimal places validation failure.
4219
+ * @param value - The decimal string to convert (e.g., "1.0")
4220
+ * @param decimals - The number of decimal places for the unit conversion
4221
+ * @returns The value in smallest units as a bigint (e.g., 1000000n for 1 USDC with 6 decimals)
4222
+ * @throws Error if the value is not a valid decimal string
3792
4223
  *
3793
- * Looks for keywords like 'maximum', 'at most', and 'decimal places' to identify
3794
- * errors related to too many decimal digits in an amount value.
4224
+ * @example
4225
+ * ```typescript
4226
+ * import { parseUnits } from '@core/utils'
3795
4227
  *
3796
- * @param message - The error message to analyze
3797
- * @returns True if the message indicates a decimal places error, false otherwise
3798
- */
3799
- function isDecimalPlacesError(message) {
3800
- return ((message.includes('maximum') || message.includes('at most')) &&
3801
- message.includes('decimal places'));
3802
- }
3803
- /**
3804
- * Checks if an error message indicates a numeric format validation failure.
4228
+ * // Parse USDC (6 decimals)
4229
+ * const usdcParsed = parseUnits('1.0', 6)
4230
+ * console.log(usdcParsed) // 1000000n
3805
4231
  *
3806
- * Identifies errors where a value contains 'numeric' but is not specifically
3807
- * about 'valid number format' or 'numeric string' (to avoid false positives).
4232
+ * // Parse ETH (18 decimals)
4233
+ * const ethParsed = parseUnits('1.0', 18)
4234
+ * console.log(ethParsed) // 1000000000000000000n
3808
4235
  *
3809
- * @param message - The error message to analyze
3810
- * @returns True if the message indicates a numeric format error, false otherwise
4236
+ * // Parse fractional amount
4237
+ * const fractionalParsed = parseUnits('1.5', 6)
4238
+ * console.log(fractionalParsed) // 1500000n
4239
+ *
4240
+ * // Parse integer (no decimal point)
4241
+ * const integerParsed = parseUnits('42', 6)
4242
+ * console.log(integerParsed) // 42000000n
4243
+ * ```
3811
4244
  */
3812
- function isNumericFormatError(message) {
3813
- return (message.includes('numeric') &&
3814
- !message.includes('valid number format') &&
3815
- !message.includes('numeric string'));
3816
- }
4245
+ const parseUnits = (value, decimals) => {
4246
+ return units.parseUnits(value, decimals).toBigInt();
4247
+ };
4248
+
3817
4249
  /**
3818
- * Handles chain-related validation errors from Zod.
4250
+ * Format a token amount into a human-readable decimal string.
3819
4251
  *
3820
- * Checks if the validation error path includes 'chain' and extracts the
3821
- * chain name from either the source (from) or destination (to) data.
3822
- * Creates a KitError with the invalid chain name and original error message.
4252
+ * Accepts a smallest-unit string and either assumes USDC's 6 decimals or derives the
4253
+ * native decimals from the provided chain definition. Delegates to {@link formatUnits}
4254
+ * to preserve consistent rounding and formatting behaviour across the SDK.
3823
4255
  *
3824
- * @param path - The Zod error path (e.g., 'from.chain' or 'to.chain')
3825
- * @param _code - The Zod error code (unused, prefixed with _ to indicate intentionally ignored)
3826
- * @param message - The original Zod error message
3827
- * @param fromData - The source/from data object (may contain chain info)
3828
- * @param toData - The destination/to data object (may contain chain info)
3829
- * @returns KitError with INPUT_INVALID_CHAIN code if this is a chain error, null otherwise
4256
+ * @remarks
4257
+ * When `token` is `'native'`, supply a chain identifier that {@link resolveChainIdentifier}
4258
+ * can resolve so the native currency decimals can be determined.
4259
+ *
4260
+ * @param params - The formatting input including the raw value and token selector.
4261
+ * @returns The decimal string representation of the amount.
4262
+ * @throws Error if the value cannot be parsed or if the chain identifier is unknown.
4263
+ *
4264
+ * @example
4265
+ * ```typescript
4266
+ * import { formatAmount } from '@core/utils'
4267
+ * import { Ethereum } from '@core/chains'
4268
+ *
4269
+ * const usdcAmount = formatAmount({ value: '1000000', token: 'USDC' })
4270
+ * console.log(usdcAmount) // "1"
4271
+ *
4272
+ * const ethAmount = formatAmount({
4273
+ * value: '3141592000000000000',
4274
+ * token: 'native',
4275
+ * chain: Ethereum,
4276
+ * })
4277
+ * console.log(ethAmount) // "3.141592"
4278
+ * ```
3830
4279
  */
3831
- function handleChainError(path, _code, message, fromData, toData) {
3832
- if (!path.includes('chain'))
3833
- return null;
3834
- const chain = getChainName(fromData) ?? getChainName(toData);
3835
- return createInvalidChainError(chain ?? 'unknown', message);
3836
- }
4280
+ const formatAmount = (params) => {
4281
+ const { value, token } = params;
4282
+ switch (token) {
4283
+ case 'USDC':
4284
+ return formatUnits(value, 6);
4285
+ case 'native':
4286
+ return formatUnits(value, resolveChainIdentifier(params.chain).nativeCurrency.decimals);
4287
+ default:
4288
+ // This will cause a compile-time error if a new token type is added to
4289
+ // `FormatAmountParams` but not handled in this switch statement, ensuring exhaustiveness.
4290
+ throw new Error(`formatAmount: Unhandled token type: ${token}`);
4291
+ }
4292
+ };
4293
+
3837
4294
  /**
3838
- * Handles address-related validation errors from Zod.
4295
+ * Parse a human-readable token amount into its smallest unit representation.
3839
4296
  *
3840
- * Checks if the validation error path includes 'address' and extracts both
3841
- * the invalid address from the path and the target chain from the params.
3842
- * Uses chain utilities to determine the expected address format (EVM or Solana)
3843
- * and creates a context-specific error message.
4297
+ * Accepts a decimal string and either assumes USDC's 6 decimals or derives the
4298
+ * native decimals from the provided chain definition. Delegates to {@link parseUnits}
4299
+ * to preserve deterministic rounding and bigint conversions across the SDK.
3844
4300
  *
3845
- * @param path - The Zod error path (e.g., 'to.recipientAddress')
3846
- * @param _code - The Zod error code (unused, prefixed with _ to indicate intentionally ignored)
3847
- * @param _message - The original Zod error message (unused, we create a more specific message)
3848
- * @param paramsObj - The original params object for extracting address and chain info
3849
- * @returns KitError with INPUT_INVALID_ADDRESS code if this is an address error, null otherwise
4301
+ * @remarks
4302
+ * When `token` is `'native'`, supply a chain identifier that {@link resolveChainIdentifier}
4303
+ * can resolve so the native currency decimals can be determined.
4304
+ *
4305
+ * @param params - The parsing input including the amount value, token, and optional chain identifier.
4306
+ * @returns The bigint representation of the amount in smallest units.
4307
+ * @throws Error if the value cannot be parsed or if the chain identifier is unknown.
4308
+ *
4309
+ * @example
4310
+ * ```typescript
4311
+ * import { parseAmount } from '@core/utils'
4312
+ * import { Ethereum } from '@core/chains'
4313
+ *
4314
+ * const usdcAmount = parseAmount({ value: '1', token: 'USDC' })
4315
+ * console.log(usdcAmount) // 1000000n
4316
+ *
4317
+ * const ethAmount = parseAmount({
4318
+ * value: '3.141592',
4319
+ * token: 'native',
4320
+ * chain: Ethereum,
4321
+ * })
4322
+ * console.log(ethAmount) // 3141592000000000000n
4323
+ * ```
3850
4324
  */
3851
- function handleAddressError(path, _code, _message, paramsObj) {
3852
- if (!path.toLowerCase().includes('address'))
3853
- return null;
3854
- const address = getAddressFromParams(paramsObj, path);
3855
- const chain = getChainFromParams(paramsObj);
3856
- const chainInfo = extractChainInfo(chain);
3857
- return createInvalidAddressError(address, chainInfo.displayName, chainInfo.expectedAddressFormat);
3858
- }
4325
+ const parseAmount = (params) => {
4326
+ const { value, token } = params;
4327
+ switch (token) {
4328
+ case 'USDC':
4329
+ return parseUnits(value, 6);
4330
+ case 'native':
4331
+ return parseUnits(value, resolveChainIdentifier(params.chain).nativeCurrency.decimals);
4332
+ default:
4333
+ // This will cause a compile-time error if a new token type is added to
4334
+ // `FormatAmountParams` but not handled in this switch statement, ensuring exhaustiveness.
4335
+ throw new Error(`parseAmount: Unhandled token type: ${token}`);
4336
+ }
4337
+ };
4338
+
4339
+ var name = "@circle-fin/bridge-kit";
4340
+ var version = "1.2.0";
4341
+ var pkg = {
4342
+ name: name,
4343
+ version: version};
3859
4344
 
3860
4345
  const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
3861
4346
  /**
@@ -3864,17 +4349,22 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
3864
4349
  * Validates the shape of {@link CustomFeePolicy}, which lets SDK consumers
3865
4350
  * provide custom fee calculation and fee-recipient resolution logic.
3866
4351
  *
3867
- * - calculateFee: required function that returns a fee as a string (or Promise<string>).
4352
+ * - computeFee: optional function (recommended) that receives human-readable amounts
4353
+ * and returns a fee as a string (or Promise<string>).
4354
+ * - calculateFee: optional function (deprecated) that receives smallest-unit amounts
4355
+ * and returns a fee as a string (or Promise<string>).
3868
4356
  * - resolveFeeRecipientAddress: required function that returns a recipient address as a
3869
4357
  * string (or Promise<string>).
3870
4358
  *
4359
+ * Exactly one of `computeFee` or `calculateFee` must be provided (not both).
4360
+ *
3871
4361
  * This schema only ensures the presence and return types of the functions; it
3872
4362
  * does not validate their argument types.
3873
4363
  *
3874
4364
  * @example
3875
4365
  * ```ts
3876
4366
  * const config = {
3877
- * calculateFee: async () => '1000000',
4367
+ * computeFee: async () => '1', // 1 USDC (human-readable)
3878
4368
  * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
3879
4369
  * }
3880
4370
  * const result = customFeePolicySchema.safeParse(config)
@@ -3883,12 +4373,27 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
3883
4373
  */
3884
4374
  const customFeePolicySchema = zod.z
3885
4375
  .object({
3886
- calculateFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
4376
+ computeFee: zod.z
4377
+ .function()
4378
+ .returns(zod.z.string().or(zod.z.promise(zod.z.string())))
4379
+ .optional(),
4380
+ calculateFee: zod.z
4381
+ .function()
4382
+ .returns(zod.z.string().or(zod.z.promise(zod.z.string())))
4383
+ .optional(),
3887
4384
  resolveFeeRecipientAddress: zod.z
3888
4385
  .function()
3889
4386
  .returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
3890
4387
  })
3891
- .strict();
4388
+ .strict()
4389
+ .refine((data) => {
4390
+ const hasComputeFee = data.computeFee !== undefined;
4391
+ const hasCalculateFee = data.calculateFee !== undefined;
4392
+ // XOR: exactly one must be provided
4393
+ return hasComputeFee !== hasCalculateFee;
4394
+ }, {
4395
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.',
4396
+ });
3892
4397
  /**
3893
4398
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
3894
4399
  *
@@ -3900,7 +4405,7 @@ const customFeePolicySchema = zod.z
3900
4405
  * @example
3901
4406
  * ```ts
3902
4407
  * const config = {
3903
- * calculateFee: () => '1000000',
4408
+ * computeFee: () => '1', // 1 USDC (human-readable)
3904
4409
  * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
3905
4410
  * }
3906
4411
  * assertCustomFeePolicy(config)
@@ -3967,16 +4472,16 @@ function assertBridgeParams(params, schema) {
3967
4472
  validateWithStateTracking(params, schema, 'bridge parameters', ASSERT_BRIDGE_PARAMS_SYMBOL);
3968
4473
  }
3969
4474
  catch (error) {
3970
- // Convert ValidationError to structured KitError
3971
- if (error instanceof ValidationError) {
3972
- // Extract the underlying Zod error from ValidationError
3973
- // ValidationError wraps the Zod validation failure
4475
+ // Convert generic KitError validation failure to structured KitError with specific codes
4476
+ if (error instanceof KitError &&
4477
+ error.code === InputError.VALIDATION_FAILED.code) {
4478
+ // Re-parse to get the underlying Zod error for enhanced error mapping
3974
4479
  const result = schema.safeParse(params);
3975
4480
  if (!result.success) {
3976
4481
  throw convertZodErrorToStructured(result.error, params);
3977
4482
  }
3978
4483
  }
3979
- // Re-throw if it's not a ValidationError
4484
+ // Re-throw if it's not a validation error or couldn't be parsed
3980
4485
  throw error;
3981
4486
  }
3982
4487
  // Additional business logic checks that Zod cannot handle
@@ -4006,7 +4511,7 @@ function assertBridgeParams(params, schema) {
4006
4511
  * This schema does not validate length, making it suitable for various hex string types
4007
4512
  * like addresses, transaction hashes, and other hex-encoded data.
4008
4513
  *
4009
- * @throws {ValidationError} If validation fails, with details about which properties failed
4514
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
4010
4515
  *
4011
4516
  * @example
4012
4517
  * ```typescript
@@ -4037,7 +4542,7 @@ const hexStringSchema = zod.z
4037
4542
  * - Must be a valid hex string with '0x' prefix
4038
4543
  * - Must be exactly 42 characters long (0x + 40 hex characters)
4039
4544
  *
4040
- * @throws {ValidationError} If validation fails, with details about which properties failed
4545
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
4041
4546
  *
4042
4547
  * @example
4043
4548
  * ```typescript
@@ -4057,7 +4562,7 @@ hexStringSchema.refine((value) => value.length === 42, 'EVM address must be exac
4057
4562
  * - Must be a valid hex string with '0x' prefix
4058
4563
  * - Must be exactly 66 characters long (0x + 64 hex characters)
4059
4564
  *
4060
- * @throws {ValidationError} If validation fails, with details about which properties failed
4565
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
4061
4566
  *
4062
4567
  * @example
4063
4568
  * ```typescript
@@ -4083,7 +4588,7 @@ hexStringSchema.refine((value) => value.length === 66, 'Transaction hash must be
4083
4588
  * This schema does not validate length, making it suitable for various base58-encoded data
4084
4589
  * like Solana addresses, transaction signatures, and other base58-encoded data.
4085
4590
  *
4086
- * @throws {ValidationError} If validation fails, with details about which properties failed
4591
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
4087
4592
  *
4088
4593
  * @example
4089
4594
  * ```typescript
@@ -4115,7 +4620,7 @@ const base58StringSchema = zod.z
4115
4620
  * - Must be a valid base58-encoded string
4116
4621
  * - Must be between 32-44 characters long (typical length for base58-encoded 32-byte addresses)
4117
4622
  *
4118
- * @throws {ValidationError} If validation fails, with details about which properties failed
4623
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
4119
4624
  *
4120
4625
  * @example
4121
4626
  * ```typescript
@@ -4135,7 +4640,7 @@ base58StringSchema.refine((value) => value.length >= 32 && value.length <= 44, '
4135
4640
  * - Must be a valid base58-encoded string
4136
4641
  * - Must be between 86-88 characters long (typical length for base58-encoded 64-byte signatures)
4137
4642
  *
4138
- * @throws {ValidationError} If validation fails, with details about which properties failed
4643
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
4139
4644
  *
4140
4645
  * @example
4141
4646
  * ```typescript
@@ -4173,23 +4678,35 @@ exports.TransferSpeed = void 0;
4173
4678
  })(exports.TransferSpeed || (exports.TransferSpeed = {}));
4174
4679
 
4175
4680
  /**
4176
- * Factory to validate a numeric string that may include thousand separators and decimal part.
4177
- * Supports either comma or dot as decimal separator and the other as thousands.
4681
+ * Factory to validate a numeric string with strict dot-decimal notation.
4682
+ * Only accepts dot (.) as the decimal separator. Thousand separators are not allowed.
4683
+ *
4684
+ * This enforces an unambiguous format for SDK inputs. Internationalization concerns
4685
+ * (comma vs dot decimal separators) should be handled in the UI layer before passing
4686
+ * values to the SDK.
4687
+ *
4688
+ * Accepts the following formats:
4689
+ * - Whole numbers: "1", "100", "1000"
4690
+ * - Leading zero decimals: "0.1", "0.5", "0.001"
4691
+ * - Shorthand decimals: ".1", ".5", ".001"
4692
+ * - Standard decimals: "1.23", "100.50"
4693
+ *
4694
+ * Does NOT accept:
4695
+ * - Comma decimal separator: "1,5" (use "1.5" instead)
4696
+ * - Thousand separators: "1,000.50" or "1.000,50" (use "1000.50" instead)
4697
+ * - Multiple decimal points: "1.2.3"
4698
+ * - Negative numbers: "-100"
4699
+ * - Non-numeric characters: "abc", "100a"
4178
4700
  *
4179
4701
  * Behavior differences controlled by options:
4180
4702
  * - allowZero: when false, value must be strictly greater than 0; when true, non-negative.
4181
4703
  * - regexMessage: error message when the basic numeric format fails.
4704
+ * - maxDecimals: maximum number of decimal places allowed (e.g., 6 for USDC).
4182
4705
  */
4183
4706
  const createDecimalStringValidator = (options) => (schema) => schema
4184
- .regex(/^\d+(?:[.,]\d{3})*(?:[.,]\d+)?$/, options.regexMessage)
4707
+ .regex(/^-?(?:\d+(?:\.\d+)?|\.\d+)$/, options.regexMessage)
4185
4708
  .superRefine((val, ctx) => {
4186
- const lastSeparator = val.lastIndexOf(',');
4187
- const lastDot = val.lastIndexOf('.');
4188
- const isCommaSeparated = lastSeparator > lastDot;
4189
- const normalizedValue = val
4190
- .replace(isCommaSeparated ? /\./g : /,/g, '')
4191
- .replace(isCommaSeparated ? ',' : '.', '.');
4192
- const amount = parseFloat(normalizedValue);
4709
+ const amount = Number.parseFloat(val);
4193
4710
  if (Number.isNaN(amount)) {
4194
4711
  ctx.addIssue({
4195
4712
  code: zod.z.ZodIssueCode.custom,
@@ -4199,7 +4716,7 @@ const createDecimalStringValidator = (options) => (schema) => schema
4199
4716
  }
4200
4717
  // Check decimal precision if maxDecimals is specified
4201
4718
  if (options.maxDecimals !== undefined) {
4202
- const decimalPart = normalizedValue.split('.')[1];
4719
+ const decimalPart = val.split('.')[1];
4203
4720
  if (decimalPart && decimalPart.length > options.maxDecimals) {
4204
4721
  ctx.addIssue({
4205
4722
  code: zod.z.ZodIssueCode.custom,
@@ -4226,7 +4743,7 @@ const createDecimalStringValidator = (options) => (schema) => schema
4226
4743
  * This ensures the basic structure of a chain definition is valid.
4227
4744
  * A chain definition must include at minimum a name and type.
4228
4745
  *
4229
- * @throws ValidationError If validation fails, with details about which properties failed
4746
+ * @throws KitError if validation fails
4230
4747
  *
4231
4748
  * @example
4232
4749
  * ```typescript
@@ -4253,7 +4770,7 @@ zod.z.object({
4253
4770
  * - A valid Ethereum address
4254
4771
  * - A valid chain definition with required properties
4255
4772
  *
4256
- * @throws ValidationError If validation fails, with details about which properties failed
4773
+ * @throws KitError if validation fails
4257
4774
  *
4258
4775
  * @example
4259
4776
  * ```typescript
@@ -4294,7 +4811,7 @@ const walletContextSchema = zod.z.object({
4294
4811
  * - An optional fee amount as string
4295
4812
  * - An optional fee recipient as string address
4296
4813
  *
4297
- * @throws ValidationError If validation fails
4814
+ * @throws KitError if validation fails
4298
4815
  *
4299
4816
  * @example
4300
4817
  * ```typescript
@@ -4313,11 +4830,12 @@ const customFeeSchema = zod.z
4313
4830
  .object({
4314
4831
  /**
4315
4832
  * The fee to charge for the transfer as string.
4316
- * Must be a non-negative value.
4833
+ * Must be a non-negative value using dot (.) as decimal separator,
4834
+ * with no thousand separators or comma decimals.
4317
4835
  */
4318
4836
  value: createDecimalStringValidator({
4319
4837
  allowZero: true,
4320
- regexMessage: 'Value must be non-negative',
4838
+ regexMessage: 'Value must be a non-negative numeric string with dot (.) as decimal separator, with no thousand separators or comma decimals.',
4321
4839
  attributeName: 'value',
4322
4840
  })(zod.z.string()).optional(),
4323
4841
  /**
@@ -4340,7 +4858,7 @@ const customFeeSchema = zod.z
4340
4858
  * - USDC as the token
4341
4859
  * - Optional config with transfer speed and max fee settings
4342
4860
  *
4343
- * @throws ValidationError If validation fails, with details about which properties failed
4861
+ * @throws KitError if validation fails
4344
4862
  *
4345
4863
  * @example
4346
4864
  * ```typescript
@@ -4361,9 +4879,9 @@ const customFeeSchema = zod.z
4361
4879
  * token: 'USDC',
4362
4880
  * config: {
4363
4881
  * transferSpeed: 'FAST',
4364
- * maxFee: '1.5', // Decimal format
4882
+ * maxFee: '1.5', // Must use dot as decimal separator
4365
4883
  * customFee: {
4366
- * value: '0.5', // Decimal format
4884
+ * value: '0.5', // Must use dot as decimal separator
4367
4885
  * recipientAddress: '0x1234567890123456789012345678901234567890'
4368
4886
  * }
4369
4887
  * }
@@ -4379,7 +4897,7 @@ zod.z.object({
4379
4897
  .min(1, 'Required')
4380
4898
  .pipe(createDecimalStringValidator({
4381
4899
  allowZero: false,
4382
- regexMessage: 'Amount must be a numeric string with optional decimal places (e.g., 10.5, 10,5, 1.000,50 or 1,000.50)',
4900
+ regexMessage: 'Amount must be a numeric string with dot (.) as decimal separator (e.g., "0.1", ".1", "10.5", "1000.50"), with no thousand separators or comma decimals.',
4383
4901
  attributeName: 'amount',
4384
4902
  maxDecimals: 6,
4385
4903
  })(zod.z.string())),
@@ -4392,7 +4910,7 @@ zod.z.object({
4392
4910
  .string()
4393
4911
  .pipe(createDecimalStringValidator({
4394
4912
  allowZero: true,
4395
- regexMessage: 'maxFee must be a numeric string with optional decimal places (e.g., "1", "0.5", "1.5")',
4913
+ regexMessage: 'maxFee must be a numeric string with dot (.) as decimal separator (e.g., "1", "0.5", ".5", "1.5"), with no thousand separators or comma decimals.',
4396
4914
  attributeName: 'maxFee',
4397
4915
  maxDecimals: 6,
4398
4916
  })(zod.z.string()))
@@ -4402,13 +4920,19 @@ zod.z.object({
4402
4920
  });
4403
4921
 
4404
4922
  /**
4405
- * Schema for validating AdapterContext.
4923
+ * Error message constants for validation
4924
+ */
4925
+ const AMOUNT_VALIDATION_MESSAGE = 'Amount must be a numeric string with dot (.) as decimal separator (e.g., "0.1", ".1", "10.5", "1000.50"), with no thousand separators or comma decimals.';
4926
+ const MAX_FEE_VALIDATION_MESSAGE = 'maxFee must be a numeric string with dot (.) as decimal separator (e.g., "1", "0.5", ".5", "1.5"), with no thousand separators or comma decimals.';
4927
+ /**
4928
+ * Schema for validating AdapterContext for bridge operations.
4406
4929
  * Must always contain both adapter and chain explicitly.
4930
+ *
4407
4931
  * Optionally includes address for developer-controlled adapters.
4408
4932
  */
4409
4933
  const adapterContextSchema = zod.z.object({
4410
4934
  adapter: adapterSchema,
4411
- chain: chainIdentifierSchema,
4935
+ chain: bridgeChainIdentifierSchema,
4412
4936
  address: zod.z.string().optional(),
4413
4937
  });
4414
4938
  /**
@@ -4494,7 +5018,7 @@ const bridgeParamsWithChainIdentifierSchema = zod.z.object({
4494
5018
  .min(1, 'Required')
4495
5019
  .pipe(createDecimalStringValidator({
4496
5020
  allowZero: false,
4497
- regexMessage: 'Amount must be a numeric string with optional decimal places (e.g., 10.5, 10,5, 1.000,50 or 1,000.50)',
5021
+ regexMessage: AMOUNT_VALIDATION_MESSAGE,
4498
5022
  attributeName: 'amount',
4499
5023
  maxDecimals: 6,
4500
5024
  })(zod.z.string())),
@@ -4507,7 +5031,7 @@ const bridgeParamsWithChainIdentifierSchema = zod.z.object({
4507
5031
  .min(1, 'Required')
4508
5032
  .pipe(createDecimalStringValidator({
4509
5033
  allowZero: true,
4510
- regexMessage: 'Max fee must be a numeric string with optional decimal places (e.g., 1, 0.5, 1.5)',
5034
+ regexMessage: MAX_FEE_VALIDATION_MESSAGE,
4511
5035
  attributeName: 'maxFee',
4512
5036
  maxDecimals: 6,
4513
5037
  })(zod.z.string()))
@@ -4716,6 +5240,10 @@ function resolveConfig(params) {
4716
5240
  async function resolveBridgeParams(params) {
4717
5241
  const fromChain = resolveChainDefinition(params.from);
4718
5242
  const toChain = resolveChainDefinition(params.to);
5243
+ // Validate adapter chain support after resolution
5244
+ // This ensures adapters support the resolved chains before proceeding
5245
+ params.from.adapter.validateChainSupport(fromChain);
5246
+ params.to.adapter.validateChainSupport(toChain);
4719
5247
  const [fromAddress, toAddress] = await Promise.all([
4720
5248
  resolveAddress(params.from),
4721
5249
  resolveAddress(params.to),
@@ -4754,6 +5282,70 @@ async function resolveBridgeParams(params) {
4754
5282
  */
4755
5283
  const getDefaultProviders = () => [new providerCctpV2.CCTPV2BridgingProvider()];
4756
5284
 
5285
+ /**
5286
+ * A helper function to get a function that transforms an amount into a human-readable string or a bigint string.
5287
+ * @param formatDirection - The direction to format the amount in.
5288
+ * @returns A function that transforms an amount into a human-readable string or a bigint string.
5289
+ */
5290
+ const getAmountTransformer = (formatDirection) => formatDirection === 'to-human-readable'
5291
+ ? (params) => formatAmount(params)
5292
+ : (params) => parseAmount(params).toString();
5293
+ /**
5294
+ * Format the bridge result into human-readable string values for the user or bigint string values for internal use.
5295
+ * @param result - The bridge result to format.
5296
+ * @param formatDirection - The direction to format the result in.
5297
+ * - If 'to-human-readable', the result will be converted to human-readable string values.
5298
+ * - If 'to-internal', the result will be converted to bigint string values (usually for internal use).
5299
+ * @returns The formatted bridge result.
5300
+ *
5301
+ * @example
5302
+ * ```typescript
5303
+ * const result = await kit.bridge({
5304
+ * amount: '1000000',
5305
+ * token: 'USDC',
5306
+ * from: { adapter: adapter, chain: 'Ethereum' },
5307
+ * to: { adapter: adapter, chain: 'Base' },
5308
+ * })
5309
+ *
5310
+ * // Format the bridge result into human-readable string values for the user
5311
+ * const formattedResultHumanReadable = formatBridgeResult(result, 'to-human-readable')
5312
+ * console.log(formattedResultHumanReadable)
5313
+ *
5314
+ * // Format the bridge result into bigint string values for internal use
5315
+ * const formattedResultInternal = formatBridgeResult(result, 'to-internal')
5316
+ * console.log(formattedResultInternal)
5317
+ * ```
5318
+ */
5319
+ const formatBridgeResult = (result, formatDirection) => {
5320
+ const transform = getAmountTransformer(formatDirection);
5321
+ return {
5322
+ ...result,
5323
+ amount: transform({ value: result.amount, token: result.token }),
5324
+ ...(result.config && {
5325
+ config: {
5326
+ ...result.config,
5327
+ ...(result.config.maxFee && {
5328
+ maxFee: transform({
5329
+ value: result.config.maxFee,
5330
+ token: result.token,
5331
+ }),
5332
+ }),
5333
+ ...(result.config.customFee && {
5334
+ customFee: {
5335
+ ...result.config.customFee,
5336
+ value: result.config.customFee.value
5337
+ ? transform({
5338
+ value: result.config.customFee.value,
5339
+ token: result.token,
5340
+ })
5341
+ : undefined,
5342
+ },
5343
+ }),
5344
+ },
5345
+ }),
5346
+ };
5347
+ };
5348
+
4757
5349
  /**
4758
5350
  * Route cross-chain USDC bridging through Circle's Cross-Chain Transfer Protocol v2 (CCTPv2).
4759
5351
  *
@@ -4768,18 +5360,18 @@ const getDefaultProviders = () => [new providerCctpV2.CCTPV2BridgingProvider()];
4768
5360
  *
4769
5361
  * @param params - The bridge parameters containing source, destination, amount, and token
4770
5362
  * @returns Promise resolving to the bridge result with transaction details and steps
4771
- * @throws {ValidationError} If the parameters are invalid
5363
+ * @throws {KitError} If the parameters are invalid
4772
5364
  * @throws {BridgeError} If the bridging process fails
4773
5365
  * @throws {UnsupportedRouteError} If the route is not supported
4774
5366
  *
4775
5367
  * @example
4776
5368
  * ```typescript
4777
5369
  * import { BridgeKit } from '@circle-fin/bridge-kit'
4778
- * import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
5370
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
4779
5371
  *
4780
5372
  * // Create kit with default CCTPv2 provider
4781
5373
  * const kit = new BridgeKit()
4782
- * const adapter = createAdapterFromPrivateKey({ privateKey: '0x...' })
5374
+ * const adapter = createViemAdapterFromPrivateKey({ privateKey: '0x...' })
4783
5375
  *
4784
5376
  * // Execute cross-chain transfer
4785
5377
  * const result = await kit.bridge({
@@ -4852,18 +5444,18 @@ class BridgeKit {
4852
5444
  *
4853
5445
  * @param params - The transfer parameters containing source, destination, amount, and token
4854
5446
  * @returns Promise resolving to the transfer result with transaction details and steps
4855
- * @throws {ValidationError} When any parameter validation fails.
5447
+ * @throws {KitError} When any parameter validation fails.
4856
5448
  * @throws {Error} When CCTPv2 does not support the specified route.
4857
5449
  *
4858
5450
  * @example
4859
5451
  * ```typescript
4860
5452
  * import { BridgeKit } from '@circle-fin/bridge-kit'
4861
- * import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
5453
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
4862
5454
  *
4863
5455
  * const kit = new BridgeKit()
4864
5456
  *
4865
5457
  * // Create a single adapter that can work across chains
4866
- * const adapter = createAdapterFromPrivateKey({
5458
+ * const adapter = createViemAdapterFromPrivateKey({
4867
5459
  * privateKey: process.env.PRIVATE_KEY,
4868
5460
  * })
4869
5461
  *
@@ -4893,7 +5485,7 @@ class BridgeKit {
4893
5485
  async bridge(params) {
4894
5486
  // First validate the parameters
4895
5487
  assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
4896
- // Then resolve chain definitions
5488
+ // Then resolve chain definitions (includes adapter chain support validation)
4897
5489
  const resolvedParams = await resolveBridgeParams(params);
4898
5490
  // Validate network compatibility
4899
5491
  this.validateNetworkCompatibility(resolvedParams);
@@ -4902,7 +5494,8 @@ class BridgeKit {
4902
5494
  // Find a provider that supports this route
4903
5495
  const provider = this.findProviderForRoute(finalResolvedParams);
4904
5496
  // Execute the transfer using the provider
4905
- return provider.bridge(finalResolvedParams);
5497
+ // Format the bridge result into human-readable string values for the user
5498
+ return formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
4906
5499
  }
4907
5500
  /**
4908
5501
  * Retry a failed or incomplete cross-chain USDC bridge operation.
@@ -4935,10 +5528,14 @@ class BridgeKit {
4935
5528
  * @example
4936
5529
  * ```typescript
4937
5530
  * import { BridgeKit } from '@circle-fin/bridge-kit'
4938
- * import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
5531
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
4939
5532
  *
4940
5533
  * const kit = new BridgeKit()
4941
5534
  *
5535
+ * // Create adapters for source and destination chains
5536
+ * const sourceAdapter = createViemAdapterFromPrivateKey({ privateKey: '...' })
5537
+ * const destAdapter = createViemAdapterFromPrivateKey({ privateKey: '...' })
5538
+ *
4942
5539
  * // Assume we have a failed bridge result from a previous operation
4943
5540
  * const failedResult: BridgeResult = {
4944
5541
  * state: 'error',
@@ -4970,7 +5567,11 @@ class BridgeKit {
4970
5567
  if (!provider) {
4971
5568
  throw new Error(`Provider ${result.provider} not found`);
4972
5569
  }
4973
- return provider.retry(result, context);
5570
+ // Format the bridge result into bigint string values for internal use
5571
+ const formattedBridgeResultInternal = formatBridgeResult(result, 'to-internal');
5572
+ // Execute the retry using the provider
5573
+ // Format the bridge result into human-readable string values for the user
5574
+ return formatBridgeResult(await provider.retry(formattedBridgeResultInternal, context), 'to-human-readable');
4974
5575
  }
4975
5576
  /**
4976
5577
  * Estimate the cost and fees for a cross-chain USDC bridge operation.
@@ -4980,7 +5581,7 @@ class BridgeKit {
4980
5581
  * as the bridge method but stops before execution.
4981
5582
  * @param params - The bridge parameters for cost estimation
4982
5583
  * @returns Promise resolving to detailed cost breakdown including gas estimates
4983
- * @throws {ValidationError} When the parameters are invalid.
5584
+ * @throws {KitError} When the parameters are invalid.
4984
5585
  * @throws {UnsupportedRouteError} When the route is not supported.
4985
5586
  *
4986
5587
  * @example
@@ -4997,7 +5598,7 @@ class BridgeKit {
4997
5598
  async estimate(params) {
4998
5599
  // First validate the parameters
4999
5600
  assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
5000
- // Then resolve chain definitions
5601
+ // Then resolve chain definitions (includes adapter chain support validation)
5001
5602
  const resolvedParams = await resolveBridgeParams(params);
5002
5603
  // Validate network compatibility
5003
5604
  this.validateNetworkCompatibility(resolvedParams);
@@ -5097,7 +5698,7 @@ class BridgeKit {
5097
5698
  const existingFee = providerParams.config?.customFee?.value;
5098
5699
  const existingFeeRecipient = providerParams.config?.customFee?.recipientAddress;
5099
5700
  // Fill missing values using kit-level configuration (if available)
5100
- const fee = existingFee ?? (await this.customFeePolicy?.calculateFee(providerParams));
5701
+ const fee = existingFee ?? (await this.resolveFee(providerParams));
5101
5702
  const feeRecipient = existingFeeRecipient ??
5102
5703
  (await this.customFeePolicy?.resolveFeeRecipientAddress(providerParams.source.chain, providerParams));
5103
5704
  // Only attach customFee if at least one value is defined
@@ -5113,56 +5714,94 @@ class BridgeKit {
5113
5714
  return providerParams;
5114
5715
  }
5115
5716
  /**
5116
- * Sets the custom fee policy for the kit.
5717
+ * Resolve the custom fee for a bridge transfer.
5117
5718
  *
5118
- * Ensures the fee is represented in the smallest unit of the token.
5119
- * - If the token is USDC, the fee is converted to base units (6 decimals).
5120
- * - If the token is not USDC, the fee is returned as is.
5719
+ * Checks which fee function the user provided and executes accordingly:
5720
+ * - `computeFee`: receives human-readable amounts, returns human-readable fee
5721
+ * - `calculateFee` (deprecated): receives smallest units, returns smallest units
5121
5722
  *
5122
- * This allows developers to specify the kit-level fee in base units (e.g., USDC: 1, ETH: 0.000001),
5123
- * and the kit will handle conversion to the smallest unit as needed.
5723
+ * @param providerParams - The resolved bridge parameters (amounts in smallest units).
5724
+ * @returns The resolved fee in smallest units, or undefined if no fee policy is set.
5725
+ */
5726
+ async resolveFee(providerParams) {
5727
+ if (!this.customFeePolicy) {
5728
+ return undefined;
5729
+ }
5730
+ const token = providerParams.token ?? 'USDC';
5731
+ if (token !== 'USDC') {
5732
+ throw createValidationFailedError$1('token', token, 'Custom fee policy only supports USDC');
5733
+ }
5734
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- intentionally support deprecated calculateFee
5735
+ const { computeFee, calculateFee } = this.customFeePolicy;
5736
+ let fee;
5737
+ if (computeFee) {
5738
+ // Convert amount to human-readable for the user's computeFee
5739
+ const humanReadableParams = {
5740
+ ...providerParams,
5741
+ amount: formatUnits(providerParams.amount, 6),
5742
+ };
5743
+ fee = await computeFee(humanReadableParams);
5744
+ }
5745
+ // Fall back to deprecated calculateFee (receives smallest units)
5746
+ if (calculateFee) {
5747
+ fee = await calculateFee(providerParams);
5748
+ }
5749
+ if (fee) {
5750
+ return parseUnits(fee, 6).toString();
5751
+ }
5752
+ return undefined;
5753
+ }
5754
+ /**
5755
+ * Set the custom fee policy for the kit.
5756
+ *
5757
+ * Use `computeFee` (recommended) for human-readable amounts, or `calculateFee`
5758
+ * (deprecated) for smallest-unit amounts. Only one should be provided.
5759
+ *
5760
+ * ```text
5761
+ * Transfer amount (user input, e.g., 1,000 USDC)
5762
+ * ↓ Wallet signs for transfer + custom fee (e.g., 1,000 + 10 = 1,010 USDC)
5763
+ * ↓ Custom fee split (10% Circle, 90% your recipientAddress wallet)
5764
+ * ↓ Full transfer amount (1,000 USDC) forwarded to CCTPv2
5765
+ * ↓ CCTPv2 protocol fee (e.g., 0.1 USDC) deducted from transfer amount
5766
+ * ↓ User receives funds on destination chain (e.g., 999.9 USDC)
5767
+ * ```
5124
5768
  *
5125
5769
  * @param customFeePolicy - The custom fee policy to set.
5126
- * @throws {ValidationError} If the custom fee policy is invalid or missing required functions
5770
+ * @throws {KitError} If the custom fee policy is invalid or missing required functions
5127
5771
  *
5128
5772
  * @example
5129
5773
  * ```typescript
5130
5774
  * import { BridgeKit } from '@circle-fin/bridge-kit'
5131
- * import { Blockchain } from '@core/chains'
5132
5775
  *
5133
5776
  * const kit = new BridgeKit()
5134
5777
  *
5135
5778
  * kit.setCustomFeePolicy({
5136
- * calculateFee: (params) => {
5137
- * // Return decimal string - kit converts to smallest units automatically
5138
- * // '0.1' becomes '100000' (0.1 USDC with 6 decimals)
5139
- * return params.source.chain.chain === Blockchain.Ethereum_Sepolia
5140
- * ? '0.1' // 0.1 USDC
5141
- * : '0.2'; // 0.2 USDC
5779
+ * // computeFee receives human-readable amounts (e.g., '100' for 100 USDC)
5780
+ * computeFee: (params) => {
5781
+ * const amount = parseFloat(params.amount)
5782
+ *
5783
+ * // 1% fee, bounded to 5-50 USDC
5784
+ * const fee = Math.min(Math.max(amount * 0.01, 5), 50)
5785
+ * return fee.toFixed(6)
5142
5786
  * },
5143
- * resolveFeeRecipientAddress: (feePayoutChain, params) => {
5144
- * // Return valid address for the source chain
5145
- * return params.source.chain.chain === Blockchain.Ethereum_Sepolia
5146
- * ? '0x23f9a5BEA7B92a0638520607407BC7f0310aEeD4'
5147
- * : '0x1E1A18B7bD95bcFcFb4d6E245D289C1e95547b35';
5787
+ * resolveFeeRecipientAddress: (feePayoutChain) => {
5788
+ * return feePayoutChain.type === 'solana'
5789
+ * ? '9xQeWvG816bUx9EP9MnZ4buHh3A6E2dFQa4Xz6V7C7Gn'
5790
+ * : '0x23f9a5BEA7B92a0638520607407BC7f0310aEeD4'
5148
5791
  * },
5149
- * });
5792
+ * })
5793
+ *
5794
+ * // 100 USDC transfer + 5 USDC custom fee results:
5795
+ * // - Wallet signs for 105 USDC total.
5796
+ * // - Circle receives 0.5 USDC (10% share of the custom fee).
5797
+ * // - Your recipientAddress wallet receives 4.5 USDC.
5798
+ * // - CCTPv2 processes 100 USDC and later deducts its own protocol fee.
5150
5799
  * ```
5151
5800
  */
5152
5801
  setCustomFeePolicy(customFeePolicy) {
5153
5802
  assertCustomFeePolicy(customFeePolicy);
5154
- const { calculateFee, resolveFeeRecipientAddress } = customFeePolicy;
5155
- // Format the calculateFee function to convert the fee to the smallest unit of the token
5156
- const formattedCalculateFee = async (params) => {
5157
- const fee = await calculateFee(params);
5158
- const token = params.token ?? 'USDC';
5159
- return token === 'USDC' ? parseUnits(fee, 6).toString() : fee;
5160
- };
5161
- // Return a new custom fee policy with the formatted calculateFee function
5162
- this.customFeePolicy = {
5163
- calculateFee: formattedCalculateFee,
5164
- resolveFeeRecipientAddress,
5165
- };
5803
+ // Store the policy as-is; resolveFee handles the branching logic
5804
+ this.customFeePolicy = customFeePolicy;
5166
5805
  }
5167
5806
  /**
5168
5807
  * Remove the custom fee policy for the kit.
@@ -5196,5 +5835,6 @@ exports.isFatalError = isFatalError;
5196
5835
  exports.isInputError = isInputError;
5197
5836
  exports.isKitError = isKitError;
5198
5837
  exports.isRetryableError = isRetryableError;
5838
+ exports.resolveChainIdentifier = resolveChainIdentifier;
5199
5839
  exports.setExternalPrefix = setExternalPrefix;
5200
5840
  //# sourceMappingURL=index.cjs.map