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