@circle-fin/provider-cctp-v2 1.6.1 → 1.6.2
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/CHANGELOG.md +8 -0
- package/index.cjs +1024 -12
- package/index.mjs +1024 -12
- package/package.json +6 -2
package/index.cjs
CHANGED
|
@@ -189,8 +189,9 @@ var Blockchain;
|
|
|
189
189
|
/**
|
|
190
190
|
* Enum representing chains that support same-chain swaps through the Swap Kit.
|
|
191
191
|
*
|
|
192
|
-
* Unlike the full {@link Blockchain} enum, SwapChain includes
|
|
193
|
-
* networks
|
|
192
|
+
* Unlike the full {@link Blockchain} enum, SwapChain includes mainnet
|
|
193
|
+
* networks and explicitly whitelisted testnets (e.g., {@link Arc_Testnet})
|
|
194
|
+
* where adapter contracts are deployed (CCTPv2 support).
|
|
194
195
|
*
|
|
195
196
|
* Dynamic validation via {@link isSwapSupportedChain} ensures chains
|
|
196
197
|
* automatically work when adapter contracts and supported tokens are deployed.
|
|
@@ -235,6 +236,8 @@ var SwapChain;
|
|
|
235
236
|
SwapChain["XDC"] = "XDC";
|
|
236
237
|
SwapChain["HyperEVM"] = "HyperEVM";
|
|
237
238
|
SwapChain["Monad"] = "Monad";
|
|
239
|
+
// Testnet chains with swap support
|
|
240
|
+
SwapChain["Arc_Testnet"] = "Arc_Testnet";
|
|
238
241
|
})(SwapChain || (SwapChain = {}));
|
|
239
242
|
// -----------------------------------------------------------------------------
|
|
240
243
|
// Bridge Chain Enum (CCTPv2 Supported Chains)
|
|
@@ -331,6 +334,31 @@ var BridgeChain;
|
|
|
331
334
|
BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
|
|
332
335
|
BridgeChain["XDC_Apothem"] = "XDC_Apothem";
|
|
333
336
|
})(BridgeChain || (BridgeChain = {}));
|
|
337
|
+
// -----------------------------------------------------------------------------
|
|
338
|
+
// Earn Chain Enum
|
|
339
|
+
// -----------------------------------------------------------------------------
|
|
340
|
+
/**
|
|
341
|
+
* Enumeration of blockchains that support earn (vault deposit/withdraw)
|
|
342
|
+
* operations through the Earn Kit.
|
|
343
|
+
*
|
|
344
|
+
* Currently only Ethereum mainnet is supported. Additional chains
|
|
345
|
+
* will be added as vault protocol support expands.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* import { EarnChain } from '@core/chains'
|
|
350
|
+
*
|
|
351
|
+
* const result = await earnKit.deposit({
|
|
352
|
+
* from: { adapter, chain: EarnChain.Ethereum },
|
|
353
|
+
* vaultAddress: '0x...',
|
|
354
|
+
* amount: '100',
|
|
355
|
+
* })
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
var EarnChain;
|
|
359
|
+
(function (EarnChain) {
|
|
360
|
+
EarnChain["Ethereum"] = "Ethereum";
|
|
361
|
+
})(EarnChain || (EarnChain = {}));
|
|
334
362
|
|
|
335
363
|
/**
|
|
336
364
|
* Helper function to define a chain with proper TypeScript typing.
|
|
@@ -633,6 +661,14 @@ const BRIDGE_CONTRACT_EVM_MAINNET = '0xB3FA262d0fB521cc93bE83d87b322b8A23DAf3F0'
|
|
|
633
661
|
* on EVM-compatible chains. Use this address for mainnet adapter integrations.
|
|
634
662
|
*/
|
|
635
663
|
const ADAPTER_CONTRACT_EVM_MAINNET = '0x7FB8c7260b63934d8da38aF902f87ae6e284a845';
|
|
664
|
+
/**
|
|
665
|
+
* The adapter contract address for EVM testnet networks.
|
|
666
|
+
*
|
|
667
|
+
* This contract serves as an adapter for integrating with various protocols
|
|
668
|
+
* on EVM-compatible testnet chains. Use this address for testnet adapter
|
|
669
|
+
* integrations (e.g., Arc Testnet).
|
|
670
|
+
*/
|
|
671
|
+
const ADAPTER_CONTRACT_EVM_TESTNET = '0xBBD70b01a1CAbc96d5b7b129Ae1AAabdf50dd40b';
|
|
636
672
|
|
|
637
673
|
/**
|
|
638
674
|
* Arc Testnet chain definition
|
|
@@ -681,6 +717,7 @@ const ArcTestnet = defineChain({
|
|
|
681
717
|
},
|
|
682
718
|
kitContracts: {
|
|
683
719
|
bridge: BRIDGE_CONTRACT_EVM_TESTNET,
|
|
720
|
+
adapter: ADAPTER_CONTRACT_EVM_TESTNET,
|
|
684
721
|
},
|
|
685
722
|
});
|
|
686
723
|
|
|
@@ -1371,7 +1408,7 @@ const HyperEVM = defineChain({
|
|
|
1371
1408
|
},
|
|
1372
1409
|
chainId: 999,
|
|
1373
1410
|
isTestnet: false,
|
|
1374
|
-
explorerUrl: 'https://
|
|
1411
|
+
explorerUrl: 'https://hyperevmscan.io/tx/{hash}',
|
|
1375
1412
|
rpcEndpoints: ['https://rpc.hyperliquid.xyz/evm'],
|
|
1376
1413
|
eurcAddress: null,
|
|
1377
1414
|
usdcAddress: '0xb88339CB7199b77E23DB6E890353E22632Ba630f',
|
|
@@ -3336,6 +3373,41 @@ zod.z.union([
|
|
|
3336
3373
|
message: `Chain "${chainDef.name}" (${chainDef.chain}) is not supported for bridging. Only chains in the BridgeChain enum support CCTPv2 bridging.`,
|
|
3337
3374
|
})),
|
|
3338
3375
|
]);
|
|
3376
|
+
/**
|
|
3377
|
+
* Zod schema for validating earn-specific chain identifiers.
|
|
3378
|
+
*
|
|
3379
|
+
* Validate that the provided chain is supported for earn (vault
|
|
3380
|
+
* deposit/withdraw) operations. Currently only Ethereum is
|
|
3381
|
+
* supported.
|
|
3382
|
+
*
|
|
3383
|
+
* Accept an EarnChain enum value, a matching string literal, or
|
|
3384
|
+
* a ChainDefinition for a supported chain.
|
|
3385
|
+
*
|
|
3386
|
+
* @example
|
|
3387
|
+
* ```typescript
|
|
3388
|
+
* import { earnChainIdentifierSchema } from '@core/chains'
|
|
3389
|
+
* import { EarnChain, Ethereum } from '@core/chains'
|
|
3390
|
+
*
|
|
3391
|
+
* // Valid
|
|
3392
|
+
* earnChainIdentifierSchema.parse(EarnChain.Ethereum)
|
|
3393
|
+
* earnChainIdentifierSchema.parse('Ethereum')
|
|
3394
|
+
* earnChainIdentifierSchema.parse(Ethereum)
|
|
3395
|
+
*
|
|
3396
|
+
* // Invalid (throws ZodError)
|
|
3397
|
+
* earnChainIdentifierSchema.parse('Solana')
|
|
3398
|
+
* ```
|
|
3399
|
+
*/
|
|
3400
|
+
zod.z.union([
|
|
3401
|
+
zod.z.string().refine((val) => val in EarnChain, (val) => ({
|
|
3402
|
+
message: `"${val}" is not a supported earn chain. ` +
|
|
3403
|
+
`Supported chains: ${Object.values(EarnChain).join(', ')}`,
|
|
3404
|
+
})),
|
|
3405
|
+
zod.z.nativeEnum(EarnChain),
|
|
3406
|
+
chainDefinitionSchema$2.refine((chain) => chain.chain in EarnChain, (chain) => ({
|
|
3407
|
+
message: `"${chain.chain}" is not a supported earn chain. ` +
|
|
3408
|
+
`Supported chains: ${Object.values(EarnChain).join(', ')}`,
|
|
3409
|
+
})),
|
|
3410
|
+
]);
|
|
3339
3411
|
|
|
3340
3412
|
/**
|
|
3341
3413
|
* @packageDocumentation
|
|
@@ -4212,6 +4284,12 @@ const InputError = {
|
|
|
4212
4284
|
name: 'INPUT_UNSUPPORTED_ACTION',
|
|
4213
4285
|
type: 'INPUT',
|
|
4214
4286
|
},
|
|
4287
|
+
/** No route satisfies the slippage or minimum-output constraint */
|
|
4288
|
+
SLIPPAGE_CONSTRAINT_NOT_MET: {
|
|
4289
|
+
code: 1009,
|
|
4290
|
+
name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
|
|
4291
|
+
type: 'INPUT',
|
|
4292
|
+
},
|
|
4215
4293
|
/** General validation failure for complex validation rules */
|
|
4216
4294
|
VALIDATION_FAILED: {
|
|
4217
4295
|
code: 1098,
|
|
@@ -4275,11 +4353,61 @@ const BalanceError = {
|
|
|
4275
4353
|
* ```
|
|
4276
4354
|
*/
|
|
4277
4355
|
const OnchainError = {
|
|
4356
|
+
/** Transaction reverted on-chain after execution */
|
|
4357
|
+
TRANSACTION_REVERTED: {
|
|
4358
|
+
code: 5001,
|
|
4359
|
+
name: 'ONCHAIN_TRANSACTION_REVERTED',
|
|
4360
|
+
type: 'ONCHAIN',
|
|
4361
|
+
},
|
|
4278
4362
|
/** Pre-flight transaction simulation failed */
|
|
4279
4363
|
SIMULATION_FAILED: {
|
|
4280
4364
|
code: 5002,
|
|
4281
4365
|
name: 'ONCHAIN_SIMULATION_FAILED',
|
|
4282
4366
|
type: 'ONCHAIN',
|
|
4367
|
+
},
|
|
4368
|
+
/** Transaction ran out of gas during execution */
|
|
4369
|
+
OUT_OF_GAS: {
|
|
4370
|
+
code: 5003,
|
|
4371
|
+
name: 'ONCHAIN_OUT_OF_GAS',
|
|
4372
|
+
type: 'ONCHAIN',
|
|
4373
|
+
},
|
|
4374
|
+
/** Transaction size exceeds blockchain limit */
|
|
4375
|
+
TRANSACTION_TOO_LARGE: {
|
|
4376
|
+
code: 5005,
|
|
4377
|
+
name: 'ONCHAIN_TRANSACTION_TOO_LARGE',
|
|
4378
|
+
type: 'ONCHAIN',
|
|
4379
|
+
},
|
|
4380
|
+
/** Unknown blockchain error that cannot be categorized */
|
|
4381
|
+
UNKNOWN_BLOCKCHAIN_ERROR: {
|
|
4382
|
+
code: 5099,
|
|
4383
|
+
name: 'ONCHAIN_UNKNOWN_BLOCKCHAIN_ERROR',
|
|
4384
|
+
type: 'ONCHAIN',
|
|
4385
|
+
},
|
|
4386
|
+
};
|
|
4387
|
+
/**
|
|
4388
|
+
* Standardized error definitions for RPC type errors.
|
|
4389
|
+
*
|
|
4390
|
+
* RPC errors occur when communicating with blockchain RPC providers,
|
|
4391
|
+
* including endpoint failures, invalid responses, and provider-specific issues.
|
|
4392
|
+
*
|
|
4393
|
+
* @example
|
|
4394
|
+
* ```typescript
|
|
4395
|
+
* import { RpcError } from '@core/errors'
|
|
4396
|
+
*
|
|
4397
|
+
* const error = new KitError({
|
|
4398
|
+
* ...RpcError.ENDPOINT_ERROR,
|
|
4399
|
+
* recoverability: 'RETRYABLE',
|
|
4400
|
+
* message: 'RPC endpoint unavailable on Ethereum',
|
|
4401
|
+
* cause: { trace: { endpoint: 'https://mainnet.infura.io' } }
|
|
4402
|
+
* })
|
|
4403
|
+
* ```
|
|
4404
|
+
*/
|
|
4405
|
+
const RpcError = {
|
|
4406
|
+
/** RPC endpoint returned error or is unavailable */
|
|
4407
|
+
ENDPOINT_ERROR: {
|
|
4408
|
+
code: 4001,
|
|
4409
|
+
name: 'RPC_ENDPOINT_ERROR',
|
|
4410
|
+
type: 'RPC',
|
|
4283
4411
|
}};
|
|
4284
4412
|
/**
|
|
4285
4413
|
* Standardized error definitions for NETWORK type errors.
|
|
@@ -4300,6 +4428,12 @@ const OnchainError = {
|
|
|
4300
4428
|
* ```
|
|
4301
4429
|
*/
|
|
4302
4430
|
const NetworkError = {
|
|
4431
|
+
/** Network connection failed or unreachable */
|
|
4432
|
+
CONNECTION_FAILED: {
|
|
4433
|
+
code: 3001,
|
|
4434
|
+
name: 'NETWORK_CONNECTION_FAILED',
|
|
4435
|
+
type: 'NETWORK',
|
|
4436
|
+
},
|
|
4303
4437
|
/** Circle relayer failed to process the forwarding/mint transaction */
|
|
4304
4438
|
RELAYER_FORWARD_FAILED: {
|
|
4305
4439
|
code: 3003,
|
|
@@ -4660,6 +4794,687 @@ function createSimulationFailedError(chain, reason, trace) {
|
|
|
4660
4794
|
},
|
|
4661
4795
|
});
|
|
4662
4796
|
}
|
|
4797
|
+
/**
|
|
4798
|
+
* Creates error for transaction reverts.
|
|
4799
|
+
*
|
|
4800
|
+
* This error is thrown when a transaction is submitted and confirmed
|
|
4801
|
+
* but reverts on-chain. The error is FATAL as it indicates the
|
|
4802
|
+
* transaction executed but failed.
|
|
4803
|
+
*
|
|
4804
|
+
* @param chain - The blockchain network where the transaction reverted
|
|
4805
|
+
* @param reason - The reason for the revert (e.g., revert message)
|
|
4806
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
4807
|
+
* @param txHash - The transaction hash if the transaction was submitted (optional)
|
|
4808
|
+
* @param explorerUrl - The block explorer URL for the transaction (optional)
|
|
4809
|
+
* @returns KitError with transaction revert details
|
|
4810
|
+
*
|
|
4811
|
+
* @example
|
|
4812
|
+
* ```typescript
|
|
4813
|
+
* import { createTransactionRevertedError } from '@core/errors'
|
|
4814
|
+
*
|
|
4815
|
+
* throw createTransactionRevertedError('Base', 'Slippage exceeded')
|
|
4816
|
+
* // Message: "Transaction reverted on Base: Slippage exceeded"
|
|
4817
|
+
* ```
|
|
4818
|
+
*
|
|
4819
|
+
* @example
|
|
4820
|
+
* ```typescript
|
|
4821
|
+
* // With trace context and transaction details for debugging
|
|
4822
|
+
* throw createTransactionRevertedError(
|
|
4823
|
+
* 'Base',
|
|
4824
|
+
* 'Slippage exceeded',
|
|
4825
|
+
* { rawError: error },
|
|
4826
|
+
* '0x123...',
|
|
4827
|
+
* 'https://basescan.org/tx/0x123...'
|
|
4828
|
+
* )
|
|
4829
|
+
* ```
|
|
4830
|
+
*/
|
|
4831
|
+
function createTransactionRevertedError(chain, reason, trace, txHash, explorerUrl) {
|
|
4832
|
+
return new KitError({
|
|
4833
|
+
...OnchainError.TRANSACTION_REVERTED,
|
|
4834
|
+
recoverability: 'FATAL',
|
|
4835
|
+
message: `Transaction reverted on ${chain}: ${reason}`,
|
|
4836
|
+
cause: {
|
|
4837
|
+
trace: {
|
|
4838
|
+
...trace,
|
|
4839
|
+
chain,
|
|
4840
|
+
reason,
|
|
4841
|
+
...(txHash !== undefined && txHash !== '' && { txHash }),
|
|
4842
|
+
...(explorerUrl !== undefined && explorerUrl !== '' && { explorerUrl }),
|
|
4843
|
+
},
|
|
4844
|
+
},
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
/**
|
|
4848
|
+
* Creates error for out of gas failures.
|
|
4849
|
+
*
|
|
4850
|
+
* This error is thrown when a transaction runs out of gas during execution.
|
|
4851
|
+
* The error is FATAL as it requires adjusting gas limits or transaction logic.
|
|
4852
|
+
*
|
|
4853
|
+
* @param chain - The blockchain network where the transaction ran out of gas
|
|
4854
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
4855
|
+
* @param txHash - The transaction hash if the transaction was submitted (optional)
|
|
4856
|
+
* @param explorerUrl - The block explorer URL for the transaction (optional)
|
|
4857
|
+
* @returns KitError with out of gas details
|
|
4858
|
+
*
|
|
4859
|
+
* @example
|
|
4860
|
+
* ```typescript
|
|
4861
|
+
* import { createOutOfGasError } from '@core/errors'
|
|
4862
|
+
*
|
|
4863
|
+
* throw createOutOfGasError('Polygon')
|
|
4864
|
+
* // Message: "Transaction ran out of gas on Polygon"
|
|
4865
|
+
* ```
|
|
4866
|
+
*
|
|
4867
|
+
* @example
|
|
4868
|
+
* ```typescript
|
|
4869
|
+
* // With trace context for debugging
|
|
4870
|
+
* throw createOutOfGasError('Polygon', {
|
|
4871
|
+
* gasUsed: '50000',
|
|
4872
|
+
* gasLimit: '45000',
|
|
4873
|
+
* },
|
|
4874
|
+
* '0xabc...',
|
|
4875
|
+
* 'https://polygonscan.com/tx/0xabc...'
|
|
4876
|
+
* )
|
|
4877
|
+
* ```
|
|
4878
|
+
*/
|
|
4879
|
+
function createOutOfGasError(chain, trace, txHash, explorerUrl) {
|
|
4880
|
+
return new KitError({
|
|
4881
|
+
...OnchainError.OUT_OF_GAS,
|
|
4882
|
+
recoverability: 'FATAL',
|
|
4883
|
+
message: `Transaction ran out of gas on ${chain}`,
|
|
4884
|
+
cause: {
|
|
4885
|
+
trace: {
|
|
4886
|
+
...trace,
|
|
4887
|
+
chain,
|
|
4888
|
+
...(txHash !== undefined && txHash !== '' && { txHash }),
|
|
4889
|
+
...(explorerUrl !== undefined && explorerUrl !== '' && { explorerUrl }),
|
|
4890
|
+
},
|
|
4891
|
+
},
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
/**
|
|
4895
|
+
* Creates error for transaction size exceeding blockchain limits.
|
|
4896
|
+
*
|
|
4897
|
+
* This error is thrown when a transaction's serialized size exceeds
|
|
4898
|
+
* the blockchain's maximum transaction size limit. The error is FATAL
|
|
4899
|
+
* as it requires reducing the transaction size (e.g., fewer instructions,
|
|
4900
|
+
* smaller data payloads, or splitting into multiple transactions).
|
|
4901
|
+
*
|
|
4902
|
+
* Common on Solana where transactions have strict size limits (e.g., max 1232 bytes).
|
|
4903
|
+
*
|
|
4904
|
+
* @param chain - The blockchain network where the size limit was exceeded
|
|
4905
|
+
* @param rawError - The original error from the underlying system (optional)
|
|
4906
|
+
* @returns KitError with transaction size exceeded details
|
|
4907
|
+
*
|
|
4908
|
+
* @example
|
|
4909
|
+
* ```typescript
|
|
4910
|
+
* import { createTransactionTooLargeError } from '@core/errors'
|
|
4911
|
+
*
|
|
4912
|
+
* throw createTransactionTooLargeError('Solana')
|
|
4913
|
+
* // Message: "Transaction size exceeds limit on Solana"
|
|
4914
|
+
* ```
|
|
4915
|
+
*
|
|
4916
|
+
* @example
|
|
4917
|
+
* ```typescript
|
|
4918
|
+
* import { createTransactionTooLargeError } from '@core/errors'
|
|
4919
|
+
*
|
|
4920
|
+
* // With raw error containing size details
|
|
4921
|
+
* throw createTransactionTooLargeError(
|
|
4922
|
+
* 'Solana',
|
|
4923
|
+
* new Error('transaction too large: max: encoded/raw 1644/1232')
|
|
4924
|
+
* )
|
|
4925
|
+
* // Error includes size information in cause.trace
|
|
4926
|
+
* ```
|
|
4927
|
+
*/
|
|
4928
|
+
function createTransactionTooLargeError(chain, rawError) {
|
|
4929
|
+
return new KitError({
|
|
4930
|
+
...OnchainError.TRANSACTION_TOO_LARGE,
|
|
4931
|
+
recoverability: 'FATAL',
|
|
4932
|
+
message: `Transaction size exceeds limit on ${chain}`,
|
|
4933
|
+
cause: {
|
|
4934
|
+
trace: {
|
|
4935
|
+
chain,
|
|
4936
|
+
rawError,
|
|
4937
|
+
},
|
|
4938
|
+
},
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
/**
|
|
4942
|
+
* Creates error for unknown blockchain errors that cannot be categorized.
|
|
4943
|
+
*
|
|
4944
|
+
* This error is used as a fallback when a blockchain error doesn't match
|
|
4945
|
+
* any known patterns (balance, simulation, gas, network, RPC, etc.).
|
|
4946
|
+
* The error is FATAL as we cannot determine if it's retriable without
|
|
4947
|
+
* understanding the underlying cause.
|
|
4948
|
+
*
|
|
4949
|
+
* The original error message and context are preserved in the trace for
|
|
4950
|
+
* debugging purposes.
|
|
4951
|
+
*
|
|
4952
|
+
* @param chain - The blockchain network where the error occurred
|
|
4953
|
+
* @param originalMessage - The original error message from the blockchain
|
|
4954
|
+
* @param rawError - The original error object from the underlying system (optional)
|
|
4955
|
+
* @returns KitError with unknown blockchain error details
|
|
4956
|
+
*
|
|
4957
|
+
* @example
|
|
4958
|
+
* ```typescript
|
|
4959
|
+
* import { createUnknownBlockchainError } from '@core/errors'
|
|
4960
|
+
*
|
|
4961
|
+
* throw createUnknownBlockchainError('Ethereum', 'Unknown protocol error')
|
|
4962
|
+
* // Message: "Unknown blockchain error on Ethereum: Unknown protocol error"
|
|
4963
|
+
* ```
|
|
4964
|
+
*
|
|
4965
|
+
* @example
|
|
4966
|
+
* ```typescript
|
|
4967
|
+
* import { createUnknownBlockchainError } from '@core/errors'
|
|
4968
|
+
*
|
|
4969
|
+
* // With raw error for debugging
|
|
4970
|
+
* const rawError = new Error('Unexpected blockchain state')
|
|
4971
|
+
* throw createUnknownBlockchainError('Solana', rawError.message, rawError)
|
|
4972
|
+
* // Error preserves full context in cause.trace
|
|
4973
|
+
* ```
|
|
4974
|
+
*/
|
|
4975
|
+
function createUnknownBlockchainError(chain, originalMessage, rawError) {
|
|
4976
|
+
const errorMessage = originalMessage
|
|
4977
|
+
? 'Unknown blockchain error on ' + chain + ': ' + originalMessage
|
|
4978
|
+
: 'Unknown blockchain error on ' + chain;
|
|
4979
|
+
return new KitError({
|
|
4980
|
+
...OnchainError.UNKNOWN_BLOCKCHAIN_ERROR,
|
|
4981
|
+
recoverability: 'FATAL',
|
|
4982
|
+
message: errorMessage,
|
|
4983
|
+
cause: {
|
|
4984
|
+
trace: {
|
|
4985
|
+
chain,
|
|
4986
|
+
rawError,
|
|
4987
|
+
},
|
|
4988
|
+
},
|
|
4989
|
+
});
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
/**
|
|
4993
|
+
* Create error for RPC endpoint failures.
|
|
4994
|
+
*
|
|
4995
|
+
* Throw when an RPC provider endpoint fails, returns an error,
|
|
4996
|
+
* or is unavailable. The error is RETRYABLE as RPC issues are often temporary.
|
|
4997
|
+
*
|
|
4998
|
+
* @param chain - The blockchain network where the RPC error occurred.
|
|
4999
|
+
* @param trace - Optional trace context (can include rawError and debugging data).
|
|
5000
|
+
* @returns KitError with RPC endpoint error details.
|
|
5001
|
+
*
|
|
5002
|
+
* @example
|
|
5003
|
+
* ```typescript
|
|
5004
|
+
* import { createRpcEndpointError } from '@core/errors'
|
|
5005
|
+
*
|
|
5006
|
+
* throw createRpcEndpointError('Ethereum')
|
|
5007
|
+
* // Message: "RPC endpoint error on Ethereum"
|
|
5008
|
+
* ```
|
|
5009
|
+
*
|
|
5010
|
+
* @example
|
|
5011
|
+
* ```typescript
|
|
5012
|
+
* throw createRpcEndpointError('Ethereum', {
|
|
5013
|
+
* rawError: error,
|
|
5014
|
+
* endpoint: 'https://mainnet.infura.io/v3/...',
|
|
5015
|
+
* statusCode: 429,
|
|
5016
|
+
* })
|
|
5017
|
+
* ```
|
|
5018
|
+
*/
|
|
5019
|
+
function createRpcEndpointError(chain, trace) {
|
|
5020
|
+
return new KitError({
|
|
5021
|
+
...RpcError.ENDPOINT_ERROR,
|
|
5022
|
+
recoverability: 'RETRYABLE',
|
|
5023
|
+
message: `RPC endpoint error on ${chain}`,
|
|
5024
|
+
cause: {
|
|
5025
|
+
trace: {
|
|
5026
|
+
...trace,
|
|
5027
|
+
chain,
|
|
5028
|
+
},
|
|
5029
|
+
},
|
|
5030
|
+
});
|
|
5031
|
+
}
|
|
5032
|
+
|
|
5033
|
+
/**
|
|
5034
|
+
* Create error for network connection failures.
|
|
5035
|
+
*
|
|
5036
|
+
* Throw when network connectivity issues prevent reaching
|
|
5037
|
+
* the blockchain network. The error is RETRYABLE as network issues are
|
|
5038
|
+
* often temporary.
|
|
5039
|
+
*
|
|
5040
|
+
* @param chain - The blockchain network where the connection failed.
|
|
5041
|
+
* @param trace - Optional trace context (can include rawError and debugging data).
|
|
5042
|
+
* @returns KitError with network connection error details.
|
|
5043
|
+
*
|
|
5044
|
+
* @example
|
|
5045
|
+
* ```typescript
|
|
5046
|
+
* import { createNetworkConnectionError } from '@core/errors'
|
|
5047
|
+
*
|
|
5048
|
+
* throw createNetworkConnectionError('Ethereum')
|
|
5049
|
+
* // Message: "Network connection failed for Ethereum"
|
|
5050
|
+
* ```
|
|
5051
|
+
*
|
|
5052
|
+
* @example
|
|
5053
|
+
* ```typescript
|
|
5054
|
+
* throw createNetworkConnectionError('Ethereum', {
|
|
5055
|
+
* rawError: error,
|
|
5056
|
+
* endpoint: 'https://eth-mainnet.g.alchemy.com/v2/...',
|
|
5057
|
+
* retryCount: 3,
|
|
5058
|
+
* })
|
|
5059
|
+
* ```
|
|
5060
|
+
*/
|
|
5061
|
+
function createNetworkConnectionError(chain, trace) {
|
|
5062
|
+
return new KitError({
|
|
5063
|
+
...NetworkError.CONNECTION_FAILED,
|
|
5064
|
+
recoverability: 'RETRYABLE',
|
|
5065
|
+
message: `Network connection failed for ${chain}`,
|
|
5066
|
+
cause: {
|
|
5067
|
+
trace: {
|
|
5068
|
+
...trace,
|
|
5069
|
+
chain,
|
|
5070
|
+
},
|
|
5071
|
+
},
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
/**
|
|
5076
|
+
* Normalizes optional transaction details for error factories.
|
|
5077
|
+
*
|
|
5078
|
+
* Converts empty strings to undefined to ensure clean error traces.
|
|
5079
|
+
*
|
|
5080
|
+
* @param txHash - The transaction hash.
|
|
5081
|
+
* @param explorerUrl - The explorer URL.
|
|
5082
|
+
* @returns Normalized values or undefined.
|
|
5083
|
+
*/
|
|
5084
|
+
function normalizeTransactionDetails(txHash, explorerUrl) {
|
|
5085
|
+
const normalized = {};
|
|
5086
|
+
if (txHash !== undefined && txHash !== '') {
|
|
5087
|
+
normalized.txHash = txHash;
|
|
5088
|
+
}
|
|
5089
|
+
if (explorerUrl !== undefined && explorerUrl !== '') {
|
|
5090
|
+
normalized.explorerUrl = explorerUrl;
|
|
5091
|
+
}
|
|
5092
|
+
return normalized;
|
|
5093
|
+
}
|
|
5094
|
+
/**
|
|
5095
|
+
* Pattern that matches on-chain revert reasons caused by slippage or
|
|
5096
|
+
* price constraints. When a revert matches, the error is surfaced as
|
|
5097
|
+
* {@link InputError.SLIPPAGE_CONSTRAINT_NOT_MET} (RETRYABLE) instead of
|
|
5098
|
+
* a generic simulation-failed / transaction-reverted error.
|
|
5099
|
+
*
|
|
5100
|
+
* @internal
|
|
5101
|
+
*/
|
|
5102
|
+
const SLIPPAGE_REVERT_PATTERN = /slippage|lower than the minimum required|less than the initial balance|minimum.?output|price.?impact|stop.?limit|InsufficientOutput|InsufficientFinalBalance/i;
|
|
5103
|
+
/**
|
|
5104
|
+
* Handle simulation / execution revert errors, distinguishing slippage
|
|
5105
|
+
* constraint failures from generic reverts and simulation failures.
|
|
5106
|
+
*
|
|
5107
|
+
* @internal
|
|
5108
|
+
*/
|
|
5109
|
+
function handleRevertError(msg, error, context) {
|
|
5110
|
+
const reason = extractRevertReason(msg, error) ?? 'Transaction reverted';
|
|
5111
|
+
if (SLIPPAGE_REVERT_PATTERN.test(reason)) {
|
|
5112
|
+
return new KitError({
|
|
5113
|
+
...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
|
|
5114
|
+
recoverability: 'RETRYABLE',
|
|
5115
|
+
message: `Transaction on ${context.chain} reverted: "${reason}". ` +
|
|
5116
|
+
'Try increasing slippageBps or adjusting stopLimit.',
|
|
5117
|
+
cause: { trace: { rawError: error, chain: context.chain, reason } },
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
if (/simulation failed/i.test(msg) || context.operation === 'simulation') {
|
|
5121
|
+
return createSimulationFailedError(context.chain, reason, {
|
|
5122
|
+
rawError: error,
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
|
|
5126
|
+
return createTransactionRevertedError(context.chain, reason, { rawError: error }, txHash, explorerUrl);
|
|
5127
|
+
}
|
|
5128
|
+
/**
|
|
5129
|
+
* Parses raw blockchain errors into structured KitError instances.
|
|
5130
|
+
*
|
|
5131
|
+
* This function uses pattern matching to identify common blockchain error
|
|
5132
|
+
* types and converts them into standardized KitError format. It handles
|
|
5133
|
+
* errors from viem, ethers, Solana web3.js, and other blockchain libraries.
|
|
5134
|
+
*
|
|
5135
|
+
* The parser recognizes 6 main error patterns:
|
|
5136
|
+
* 1. Insufficient balance errors
|
|
5137
|
+
* 2. Simulation/execution reverted errors
|
|
5138
|
+
* 3. Gas-related errors
|
|
5139
|
+
* 4. Network connectivity errors
|
|
5140
|
+
* 5. RPC provider errors
|
|
5141
|
+
* 6. Transaction size limit errors
|
|
5142
|
+
*
|
|
5143
|
+
* When errors don't match known patterns, the parser uses operation context
|
|
5144
|
+
* (e.g., 'simulation', 'estimateGas') to categorize them appropriately.
|
|
5145
|
+
* Unrecognized errors without context are categorized as UNKNOWN_BLOCKCHAIN_ERROR.
|
|
5146
|
+
*
|
|
5147
|
+
* @param error - The raw error from the blockchain library
|
|
5148
|
+
* @param context - Context information including chain and optional token
|
|
5149
|
+
* @returns A structured KitError instance
|
|
5150
|
+
*
|
|
5151
|
+
* @example
|
|
5152
|
+
* ```typescript
|
|
5153
|
+
* import { parseBlockchainError } from '@core/errors'
|
|
5154
|
+
*
|
|
5155
|
+
* try {
|
|
5156
|
+
* await walletClient.sendTransaction(...)
|
|
5157
|
+
* } catch (error) {
|
|
5158
|
+
* throw parseBlockchainError(error, {
|
|
5159
|
+
* chain: 'Ethereum',
|
|
5160
|
+
* token: 'USDC',
|
|
5161
|
+
* operation: 'transfer'
|
|
5162
|
+
* })
|
|
5163
|
+
* }
|
|
5164
|
+
* ```
|
|
5165
|
+
*
|
|
5166
|
+
* @example
|
|
5167
|
+
* ```typescript
|
|
5168
|
+
* // Minimal usage
|
|
5169
|
+
* try {
|
|
5170
|
+
* await connection.sendTransaction(...)
|
|
5171
|
+
* } catch (error) {
|
|
5172
|
+
* throw parseBlockchainError(error, { chain: 'Solana' })
|
|
5173
|
+
* }
|
|
5174
|
+
* ```
|
|
5175
|
+
*
|
|
5176
|
+
* @example
|
|
5177
|
+
* ```typescript
|
|
5178
|
+
* // With transaction hash and explorer URL
|
|
5179
|
+
* import { buildExplorerUrl } from '@core/utils'
|
|
5180
|
+
* import { Ethereum } from '@core/chains'
|
|
5181
|
+
*
|
|
5182
|
+
* try {
|
|
5183
|
+
* const txHash = await walletClient.sendTransaction(...)
|
|
5184
|
+
* await waitForTransaction(txHash)
|
|
5185
|
+
* } catch (error) {
|
|
5186
|
+
* const explorerUrl = buildExplorerUrl(Ethereum, txHash)
|
|
5187
|
+
* throw parseBlockchainError(error, {
|
|
5188
|
+
* chain: 'Ethereum',
|
|
5189
|
+
* txHash,
|
|
5190
|
+
* explorerUrl
|
|
5191
|
+
* })
|
|
5192
|
+
* }
|
|
5193
|
+
* ```
|
|
5194
|
+
*/
|
|
5195
|
+
function parseBlockchainError(error, context) {
|
|
5196
|
+
const msg = extractMessage(error);
|
|
5197
|
+
const token = context.token ?? 'token';
|
|
5198
|
+
// Pattern 1: Insufficient balance errors
|
|
5199
|
+
// Matches balance-related errors from ERC20 contracts, native transfers, and Solana programs
|
|
5200
|
+
if (/transfer amount exceeds balance|insufficient (balance|funds)|burn amount exceeded/i.test(msg)) {
|
|
5201
|
+
return createInsufficientTokenBalanceError(context.chain, token, {
|
|
5202
|
+
rawError: error,
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
// Pattern 2: Simulation and execution reverts
|
|
5206
|
+
// Matches contract revert errors and simulation failures
|
|
5207
|
+
if (/execution reverted|simulation failed|transaction reverted|transaction failed/i.test(msg)) {
|
|
5208
|
+
return handleRevertError(msg, error, context);
|
|
5209
|
+
}
|
|
5210
|
+
// Pattern 3: Gas-related errors
|
|
5211
|
+
// Matches gas estimation failures and gas exhaustion
|
|
5212
|
+
// Check specific patterns first, then generic "gas" patterns
|
|
5213
|
+
// Gas estimation failures are RPC issues
|
|
5214
|
+
if (/gas estimation failed|cannot estimate gas/i.test(msg)) {
|
|
5215
|
+
return createRpcEndpointError(context.chain, { rawError: error });
|
|
5216
|
+
}
|
|
5217
|
+
// Gas exhaustion errors
|
|
5218
|
+
// Use specific patterns without wildcards to avoid ReDoS
|
|
5219
|
+
if (/out of gas|gas limit exceeded|exceeds block gas limit/i.test(msg)) {
|
|
5220
|
+
const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
|
|
5221
|
+
return createOutOfGasError(context.chain, { rawError: error }, txHash, explorerUrl);
|
|
5222
|
+
}
|
|
5223
|
+
// Insufficient funds for gas
|
|
5224
|
+
if (/insufficient funds for gas/i.test(msg)) {
|
|
5225
|
+
return createInsufficientGasError(context.chain, { rawError: error });
|
|
5226
|
+
}
|
|
5227
|
+
// Pattern 4: Network connectivity errors
|
|
5228
|
+
// Matches connection failures, DNS errors, and timeouts
|
|
5229
|
+
if (/connection (refused|failed)|network|timeout|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
|
|
5230
|
+
return createNetworkConnectionError(context.chain, { rawError: error });
|
|
5231
|
+
}
|
|
5232
|
+
// Pattern 5: RPC provider errors
|
|
5233
|
+
// Matches RPC endpoint errors, invalid responses, rate limits, and
|
|
5234
|
+
// transient JSON-RPC internal errors (e.g. ethers.js "could not coalesce error").
|
|
5235
|
+
// Note: "internal error" alone is too broad — contracts like USDT emit
|
|
5236
|
+
// "An internal error was received" for on-chain assertion failures.
|
|
5237
|
+
// We require JSON-RPC context (codes -32603/-32000) instead.
|
|
5238
|
+
if (/rpc|invalid response|rate limit|too many requests|could not coalesce|no response|server error|json-rpc\s+internal|internal json-rpc|-32603|-32000/i.test(msg)) {
|
|
5239
|
+
return createRpcEndpointError(context.chain, { rawError: error });
|
|
5240
|
+
}
|
|
5241
|
+
// Pattern 6: Transaction size limit errors
|
|
5242
|
+
// Matches transaction size errors from various blockchains:
|
|
5243
|
+
// - Solana: "transaction too large: max: encoded/raw 1644/1232"
|
|
5244
|
+
// - Generic: "transaction exceeds size limit", "max transaction size"
|
|
5245
|
+
// Split into two checks to reduce regex complexity for SonarQube
|
|
5246
|
+
const isSizeError = /transaction (?:too large|exceeds size limit|size exceeds)|encoded\/raw\s+\d+\/\d+|max transaction size/i.test(msg) || /(?:tx|transaction) size is too big:\s*\d+,\s*max:\s*\d+/i.test(msg);
|
|
5247
|
+
if (isSizeError) {
|
|
5248
|
+
return createTransactionTooLargeError(context.chain, error);
|
|
5249
|
+
}
|
|
5250
|
+
// Fallback based on operation context
|
|
5251
|
+
// Gas-related operations are RPC calls
|
|
5252
|
+
if (context.operation === 'estimateGas' ||
|
|
5253
|
+
context.operation === 'getGasPrice') {
|
|
5254
|
+
return createRpcEndpointError(context.chain, { rawError: error });
|
|
5255
|
+
}
|
|
5256
|
+
// Simulation operations that don't match standard patterns should still be
|
|
5257
|
+
// categorized as simulation failures. This handles cases where blockchain
|
|
5258
|
+
// libraries throw generic errors (e.g., "fail", "error") during staticCall
|
|
5259
|
+
// operations without descriptive messages. The operation context is authoritative
|
|
5260
|
+
// about what was being attempted, even when error messages are unclear.
|
|
5261
|
+
//
|
|
5262
|
+
// Example: ethers.js staticCall rejection with message "fail"
|
|
5263
|
+
// - Message doesn't match /execution reverted|simulation failed/
|
|
5264
|
+
// - But context.operation === 'simulation' tells us it was a simulation
|
|
5265
|
+
// - Result: SIMULATION_FAILED (accurate) vs UNKNOWN_BLOCKCHAIN_ERROR (misleading)
|
|
5266
|
+
if (context.operation === 'simulation') {
|
|
5267
|
+
return createSimulationFailedError(context.chain, msg.length > 0 ? msg : 'Simulation failed', { rawError: error });
|
|
5268
|
+
}
|
|
5269
|
+
// Final fallback for truly unrecognized errors
|
|
5270
|
+
// Only errors that don't match any pattern AND lack operation context
|
|
5271
|
+
// are categorized as unknown.
|
|
5272
|
+
return createUnknownBlockchainError(context.chain, msg.length > 0 ? msg : 'Unknown error', { rawError: error });
|
|
5273
|
+
}
|
|
5274
|
+
/**
|
|
5275
|
+
* Type guard to check if error has Solana-Kit structure with logs.
|
|
5276
|
+
*
|
|
5277
|
+
* Checks if the error object contains a context with logs array,
|
|
5278
|
+
* which is the structure used by Solana Kit errors.
|
|
5279
|
+
*
|
|
5280
|
+
* @param error - Unknown error to check
|
|
5281
|
+
* @returns True if error has Solana-Kit logs structure
|
|
5282
|
+
*/
|
|
5283
|
+
function hasSolanaLogs(error) {
|
|
5284
|
+
return (error !== null &&
|
|
5285
|
+
typeof error === 'object' &&
|
|
5286
|
+
'context' in error &&
|
|
5287
|
+
error.context !== null &&
|
|
5288
|
+
typeof error.context === 'object' &&
|
|
5289
|
+
'logs' in error.context &&
|
|
5290
|
+
Array.isArray(error.context.logs));
|
|
5291
|
+
}
|
|
5292
|
+
/**
|
|
5293
|
+
* Extracts a human-readable error message from various error types.
|
|
5294
|
+
*
|
|
5295
|
+
* Handles Error objects, string errors, objects with message properties,
|
|
5296
|
+
* Solana-Kit errors with context logs, and falls back to string representation.
|
|
5297
|
+
* For Solana-Kit errors, extracts Anchor error messages from transaction logs.
|
|
5298
|
+
*
|
|
5299
|
+
* @param error - Unknown error to extract message from
|
|
5300
|
+
* @returns Extracted error message string
|
|
5301
|
+
*
|
|
5302
|
+
* @example
|
|
5303
|
+
* ```typescript
|
|
5304
|
+
* const msg1 = extractMessage(new Error('test')) // 'test'
|
|
5305
|
+
* const msg2 = extractMessage('string error') // 'string error'
|
|
5306
|
+
* const msg3 = extractMessage({ message: 'obj' }) // 'obj'
|
|
5307
|
+
* ```
|
|
5308
|
+
*/
|
|
5309
|
+
function extractMessage(error) {
|
|
5310
|
+
// Check for Solana-Kit errors with context.logs
|
|
5311
|
+
if (hasSolanaLogs(error)) {
|
|
5312
|
+
// Extract Anchor error message from logs
|
|
5313
|
+
const anchorLog = error.context.logs.find((log) => log.includes('AnchorError') || log.includes('Error Message'));
|
|
5314
|
+
if (anchorLog !== undefined) {
|
|
5315
|
+
// Return the anchor error log which contains the detailed message
|
|
5316
|
+
return anchorLog;
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
if (error instanceof Error) {
|
|
5320
|
+
return error.message;
|
|
5321
|
+
}
|
|
5322
|
+
if (typeof error === 'string') {
|
|
5323
|
+
return error;
|
|
5324
|
+
}
|
|
5325
|
+
if (typeof error === 'object' && error !== null && 'message' in error) {
|
|
5326
|
+
return String(error.message);
|
|
5327
|
+
}
|
|
5328
|
+
return String(error);
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* Mapping of custom error selectors to human-readable error names.
|
|
5332
|
+
*
|
|
5333
|
+
* These selectors correspond to custom errors from the contracts.
|
|
5334
|
+
* When a transaction reverts with a custom error, the 4-byte selector
|
|
5335
|
+
* (first 4 bytes of keccak256 of the error signature) is included in the error message.
|
|
5336
|
+
*
|
|
5337
|
+
*/
|
|
5338
|
+
const CUSTOM_ERROR_SELECTORS = {
|
|
5339
|
+
'0xd93c0665': 'This contract is currently paused', // EnforcedPause()
|
|
5340
|
+
'0x3ee5aeb5': 'Security check failed due to a reentrancy attempt', // ReentrancyGuardReentrantCall()
|
|
5341
|
+
'0x8baa579f': 'The provided signature is invalid or could not be verified', // InvalidSignature()
|
|
5342
|
+
'0x1ab7da6b': 'This request has expired', // DeadlineExpired()
|
|
5343
|
+
'0x64eee62e': 'No instructions provided', // EmptyInstructions()
|
|
5344
|
+
'0x3c46992e': 'Too many execution steps were provided', // TooManyInstructions()
|
|
5345
|
+
'0xe066e60e': 'Exceeds maximum token inputs limit', // TooManyTokenInputs()
|
|
5346
|
+
'0x5566df5c': 'The beneficiary address is invalid', // InvalidBeneficiary()
|
|
5347
|
+
'0xf41d7bc6': 'Execution ID already used', // ExecIdUsed()
|
|
5348
|
+
'0x948726e2': 'The account balance does not meet the required minimum', // InvalidBalanceState()
|
|
5349
|
+
'0x01aa0452': 'The permit type provided is not supported', // UnsupportedPermitType()
|
|
5350
|
+
'0x13be252b': 'Token approval failed and the allowance is insufficient', // InsufficientAllowance()
|
|
5351
|
+
'0x5274afe7': 'Token transfer or approval failed', // SafeERC20FailedOperation(address)
|
|
5352
|
+
'0x00bc1dcb': 'One of the execution targets is invalid', // InvalidInstruction(uint256)
|
|
5353
|
+
'0x5d693841': 'Cannot approve a zero address or a native token', // InvalidApprovalConfig(uint256)
|
|
5354
|
+
'0x3ec5cfe1': 'Cannot validate output for a zero address token', // InvalidOutputConfig(uint256)
|
|
5355
|
+
'0x492fd70e': 'Instruction call failed without revert data', // ExecutionFailed(uint256)
|
|
5356
|
+
'0xba235e1a': 'The received token amount is lower than the minimum required', // InsufficientOutput(uint256,address,uint256,uint256)
|
|
5357
|
+
'0x9147d463': 'Final balance is less than the initial balance', // InsufficientFinalBalance(address,uint256,uint256)
|
|
5358
|
+
'0xf4b3b1bc': 'Native token transfer to the beneficiary failed', // NativeTransferFailed()
|
|
5359
|
+
};
|
|
5360
|
+
/**
|
|
5361
|
+
* Attempts to extract an error selector from an error object or message.
|
|
5362
|
+
*
|
|
5363
|
+
* Checks multiple sources in priority order:
|
|
5364
|
+
* 1. `error.data` field
|
|
5365
|
+
* 2. Message pattern: "custom error 0x8baa579f"
|
|
5366
|
+
*
|
|
5367
|
+
* @param msg - The error message
|
|
5368
|
+
* @param error - The error object (optional)
|
|
5369
|
+
* @returns The 4 bytes selector , or undefined if not found
|
|
5370
|
+
*/
|
|
5371
|
+
function extractErrorSelector(msg, error) {
|
|
5372
|
+
// Check error.data field
|
|
5373
|
+
if (error !== null && typeof error === 'object' && 'data' in error) {
|
|
5374
|
+
if (typeof error.data === 'string' && /^0x[0-9a-f]{8}$/i.test(error.data)) {
|
|
5375
|
+
return error.data.toLowerCase();
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
// Check for "custom error 0x8baa579f" in the message
|
|
5379
|
+
const match = /custom error (0x[0-9a-f]{8})\b/i.exec(msg);
|
|
5380
|
+
const selector = match?.[1];
|
|
5381
|
+
if (selector !== undefined && selector.length > 0) {
|
|
5382
|
+
return selector.toLowerCase();
|
|
5383
|
+
}
|
|
5384
|
+
return undefined;
|
|
5385
|
+
}
|
|
5386
|
+
/**
|
|
5387
|
+
* Extracts the revert reason from an error message and error object.
|
|
5388
|
+
*
|
|
5389
|
+
* Attempts to parse out the meaningful reason from execution revert errors,
|
|
5390
|
+
* removing common prefixes like "execution reverted:" or "reverted:".
|
|
5391
|
+
* If the error contains a custom error selector, it will be resolved to its
|
|
5392
|
+
* human-readable description with the selector included for developer reference.
|
|
5393
|
+
*
|
|
5394
|
+
* Supports multiple error formats:
|
|
5395
|
+
* - Custom error with selector in `error.data` field (e.g., `{ data: "0x8baa579f", ... }`)
|
|
5396
|
+
* - Message with selector pattern: `"Execution reverted with reason: custom error 0x8baa579f"`
|
|
5397
|
+
* - Standard revert: `"execution reverted: Insufficient funds"`
|
|
5398
|
+
*
|
|
5399
|
+
* @param msg - The error message to extract from
|
|
5400
|
+
* @param error - The full error object (optional, used to extract selector from `data` field)
|
|
5401
|
+
* @returns The extracted revert reason, or null if not found
|
|
5402
|
+
*
|
|
5403
|
+
* @example
|
|
5404
|
+
* ```typescript
|
|
5405
|
+
* const reason = extractRevertReason(
|
|
5406
|
+
* 'execution reverted: ERC20: transfer amount exceeds balance'
|
|
5407
|
+
* )
|
|
5408
|
+
* // Returns: 'ERC20: transfer amount exceeds balance'
|
|
5409
|
+
* ```
|
|
5410
|
+
*
|
|
5411
|
+
* @example
|
|
5412
|
+
* ```typescript
|
|
5413
|
+
* const reason = extractRevertReason(
|
|
5414
|
+
* 'Simulation failed: Execution reverted with reason: Insufficient allowance'
|
|
5415
|
+
* )
|
|
5416
|
+
* // Returns: 'Insufficient allowance'
|
|
5417
|
+
* ```
|
|
5418
|
+
*
|
|
5419
|
+
* @example
|
|
5420
|
+
* ```typescript
|
|
5421
|
+
* // Custom error with selector in message
|
|
5422
|
+
* const reason = extractRevertReason(
|
|
5423
|
+
* 'Execution reverted with reason: custom error 0x8baa579f'
|
|
5424
|
+
* )
|
|
5425
|
+
* // Returns: 'The provided signature is invalid or could not be verified (0x8baa579f)'
|
|
5426
|
+
* ```
|
|
5427
|
+
*
|
|
5428
|
+
* @example
|
|
5429
|
+
* ```typescript
|
|
5430
|
+
* // Error with data field
|
|
5431
|
+
* const reason = extractRevertReason(
|
|
5432
|
+
* 'execution reverted (unknown custom error)',
|
|
5433
|
+
* { data: '0x8baa579f' }
|
|
5434
|
+
* )
|
|
5435
|
+
* // Returns: 'The provided signature is invalid or could not be verified (0x8baa579f)'
|
|
5436
|
+
* ```
|
|
5437
|
+
*
|
|
5438
|
+
* @example
|
|
5439
|
+
* ```typescript
|
|
5440
|
+
* // Unrecognized selector preserved for debugging
|
|
5441
|
+
* const reason = extractRevertReason(
|
|
5442
|
+
* 'execution reverted (unknown custom error)',
|
|
5443
|
+
* { data: '0xdeadbeef' }
|
|
5444
|
+
* )
|
|
5445
|
+
* // Returns: 'Transaction reverted with error (0xdeadbeef)'
|
|
5446
|
+
* ```
|
|
5447
|
+
*/
|
|
5448
|
+
function extractRevertReason(msg, error) {
|
|
5449
|
+
// Try to extract and decode custom error selector
|
|
5450
|
+
const selector = extractErrorSelector(msg, error);
|
|
5451
|
+
if (selector !== undefined) {
|
|
5452
|
+
const errorMessage = CUSTOM_ERROR_SELECTORS[selector];
|
|
5453
|
+
if (errorMessage !== undefined) {
|
|
5454
|
+
return `${errorMessage} (${selector})`;
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
// Try to extract reason after "execution reverted:" or "reason:"
|
|
5458
|
+
// Use [^\n.]+ instead of .+? to avoid ReDoS vulnerability
|
|
5459
|
+
// Fall back to standard revert reason extraction
|
|
5460
|
+
const patterns = [
|
|
5461
|
+
/(?:execution reverted|reverted):\s*([^\n.]+)/i,
|
|
5462
|
+
/reason:\s*([^\n.]+)/i,
|
|
5463
|
+
/with reason:\s*([^\n.]+)/i,
|
|
5464
|
+
];
|
|
5465
|
+
for (const pattern of patterns) {
|
|
5466
|
+
const match = pattern.exec(msg);
|
|
5467
|
+
const extractedReason = match?.at(1);
|
|
5468
|
+
if (extractedReason !== undefined && extractedReason.length > 0) {
|
|
5469
|
+
return extractedReason.trim();
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
// Preserve unrecognized selector so it appears in error output for debugging
|
|
5473
|
+
if (selector !== undefined) {
|
|
5474
|
+
return `Transaction reverted with error (${selector})`;
|
|
5475
|
+
}
|
|
5476
|
+
return null;
|
|
5477
|
+
}
|
|
4663
5478
|
|
|
4664
5479
|
/**
|
|
4665
5480
|
* Type guard to check if an error is a KitError instance.
|
|
@@ -4717,6 +5532,102 @@ function isKitError(error) {
|
|
|
4717
5532
|
function isFatalError(error) {
|
|
4718
5533
|
return isKitError(error) && error.recoverability === 'FATAL';
|
|
4719
5534
|
}
|
|
5535
|
+
/**
|
|
5536
|
+
* Error codes that are considered retryable by default.
|
|
5537
|
+
*
|
|
5538
|
+
* @remarks
|
|
5539
|
+
* These are typically transient errors that may succeed on retry:
|
|
5540
|
+
* - Network connectivity issues (3001, 3002)
|
|
5541
|
+
* - Provider unavailability (4001, 4002)
|
|
5542
|
+
* - RPC nonce errors (4003)
|
|
5543
|
+
*/
|
|
5544
|
+
const DEFAULT_RETRYABLE_ERROR_CODES = [
|
|
5545
|
+
// Network errors
|
|
5546
|
+
3001, // NETWORK_CONNECTION_FAILED
|
|
5547
|
+
3002, // NETWORK_TIMEOUT
|
|
5548
|
+
// Provider errors
|
|
5549
|
+
4001, // PROVIDER_UNAVAILABLE
|
|
5550
|
+
4002, // PROVIDER_TIMEOUT
|
|
5551
|
+
4003, // RPC_NONCE_ERROR
|
|
5552
|
+
];
|
|
5553
|
+
/**
|
|
5554
|
+
* Checks if an error is retryable.
|
|
5555
|
+
*
|
|
5556
|
+
* @remarks
|
|
5557
|
+
* Check order for KitError instances:
|
|
5558
|
+
* 1. If `recoverability === 'RETRYABLE'`, return `true` immediately (priority check).
|
|
5559
|
+
* 2. Otherwise, check if `error.code` is in `DEFAULT_RETRYABLE_ERROR_CODES` (fallback check).
|
|
5560
|
+
* 3. Non-KitError instances always return `false`.
|
|
5561
|
+
*
|
|
5562
|
+
* This two-tier approach allows both explicit recoverability control and
|
|
5563
|
+
* backward-compatible code-based retry logic.
|
|
5564
|
+
*
|
|
5565
|
+
* RETRYABLE errors indicate transient failures that may succeed on
|
|
5566
|
+
* subsequent attempts, such as network timeouts or temporary service
|
|
5567
|
+
* unavailability. These errors are safe to retry after a delay.
|
|
5568
|
+
*
|
|
5569
|
+
* @param error - Unknown error to check
|
|
5570
|
+
* @returns True if error is retryable
|
|
5571
|
+
*
|
|
5572
|
+
* @example
|
|
5573
|
+
* ```typescript
|
|
5574
|
+
* import { isRetryableError } from '@core/errors'
|
|
5575
|
+
*
|
|
5576
|
+
* try {
|
|
5577
|
+
* await kit.bridge(params)
|
|
5578
|
+
* } catch (error) {
|
|
5579
|
+
* if (isRetryableError(error)) {
|
|
5580
|
+
* // Implement retry logic with exponential backoff
|
|
5581
|
+
* setTimeout(() => retryOperation(), 5000)
|
|
5582
|
+
* }
|
|
5583
|
+
* }
|
|
5584
|
+
* ```
|
|
5585
|
+
*
|
|
5586
|
+
* @example
|
|
5587
|
+
* ```typescript
|
|
5588
|
+
* import { isRetryableError, createNetworkConnectionError, KitError } from '@core/errors'
|
|
5589
|
+
*
|
|
5590
|
+
* // KitError with RETRYABLE recoverability (priority check)
|
|
5591
|
+
* const error1 = createNetworkConnectionError('Ethereum')
|
|
5592
|
+
* isRetryableError(error1) // true
|
|
5593
|
+
*
|
|
5594
|
+
* // KitError with default retryable code (fallback check)
|
|
5595
|
+
* const error2 = new KitError({
|
|
5596
|
+
* code: 3002, // NETWORK_TIMEOUT - in DEFAULT_RETRYABLE_ERROR_CODES
|
|
5597
|
+
* name: 'NETWORK_TIMEOUT',
|
|
5598
|
+
* type: 'NETWORK',
|
|
5599
|
+
* recoverability: 'FATAL', // Not RETRYABLE
|
|
5600
|
+
* message: 'Timeout',
|
|
5601
|
+
* })
|
|
5602
|
+
* isRetryableError(error2) // true (code 3002 is in default list)
|
|
5603
|
+
*
|
|
5604
|
+
* // KitError with non-retryable code and FATAL recoverability
|
|
5605
|
+
* const error3 = new KitError({
|
|
5606
|
+
* code: 1001,
|
|
5607
|
+
* name: 'INPUT_NETWORK_MISMATCH',
|
|
5608
|
+
* type: 'INPUT',
|
|
5609
|
+
* recoverability: 'FATAL',
|
|
5610
|
+
* message: 'Invalid input',
|
|
5611
|
+
* })
|
|
5612
|
+
* isRetryableError(error3) // false
|
|
5613
|
+
*
|
|
5614
|
+
* // Non-KitError
|
|
5615
|
+
* const error4 = new Error('Standard error')
|
|
5616
|
+
* isRetryableError(error4) // false
|
|
5617
|
+
* ```
|
|
5618
|
+
*/
|
|
5619
|
+
function isRetryableError$1(error) {
|
|
5620
|
+
// Use proper type guard to check if it's a KitError
|
|
5621
|
+
if (isKitError(error)) {
|
|
5622
|
+
// Priority check: explicit recoverability
|
|
5623
|
+
if (error.recoverability === 'RETRYABLE') {
|
|
5624
|
+
return true;
|
|
5625
|
+
}
|
|
5626
|
+
// Fallback check: error code against default retryable codes
|
|
5627
|
+
return DEFAULT_RETRYABLE_ERROR_CODES.includes(error.code);
|
|
5628
|
+
}
|
|
5629
|
+
return false;
|
|
5630
|
+
}
|
|
4720
5631
|
/**
|
|
4721
5632
|
* Safely extracts error message from any error type.
|
|
4722
5633
|
*
|
|
@@ -5095,8 +6006,15 @@ const pollApiWithValidation = async (url, method, isValidType, config = {}, body
|
|
|
5095
6006
|
}
|
|
5096
6007
|
}
|
|
5097
6008
|
}
|
|
5098
|
-
//
|
|
5099
|
-
|
|
6009
|
+
// Preserve responseBody from the last attempt so upstream parsers
|
|
6010
|
+
// (e.g. parseApiError) can inspect the server's structured response.
|
|
6011
|
+
const retryError = new Error(`Maximum retry attempts (${String(effectiveConfig.maxRetries)}) exceeded: ${String(lastError?.message)}`);
|
|
6012
|
+
if (lastError !== undefined &&
|
|
6013
|
+
'responseBody' in lastError &&
|
|
6014
|
+
lastError.responseBody !== undefined) {
|
|
6015
|
+
retryError.responseBody = lastError.responseBody;
|
|
6016
|
+
}
|
|
6017
|
+
throw retryError;
|
|
5100
6018
|
};
|
|
5101
6019
|
/**
|
|
5102
6020
|
* Convenience function for making GET requests with validation.
|
|
@@ -5637,6 +6555,7 @@ const USDC = {
|
|
|
5637
6555
|
// =========================================================================
|
|
5638
6556
|
// Testnets (alphabetically sorted)
|
|
5639
6557
|
// =========================================================================
|
|
6558
|
+
[Blockchain.Arc_Testnet]: '0x3600000000000000000000000000000000000000',
|
|
5640
6559
|
[Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
|
|
5641
6560
|
[Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
|
|
5642
6561
|
[Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
@@ -5713,6 +6632,8 @@ const EURC = {
|
|
|
5713
6632
|
[Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
|
|
5714
6633
|
[Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
|
|
5715
6634
|
[Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
|
|
6635
|
+
// Testnets
|
|
6636
|
+
[Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
|
|
5716
6637
|
},
|
|
5717
6638
|
};
|
|
5718
6639
|
|
|
@@ -6403,6 +7324,86 @@ function buildForwardingHookData() {
|
|
|
6403
7324
|
return cachedHookDataHex;
|
|
6404
7325
|
}
|
|
6405
7326
|
|
|
7327
|
+
const DEFAULTS = {
|
|
7328
|
+
maxRetries: 3,
|
|
7329
|
+
baseDelayMs: 1000,
|
|
7330
|
+
maxDelayMs: 15_000,
|
|
7331
|
+
deadlineMs: undefined,
|
|
7332
|
+
jitter: true,
|
|
7333
|
+
isRetryable: () => true,
|
|
7334
|
+
};
|
|
7335
|
+
function resolveOptions(options) {
|
|
7336
|
+
if (options === undefined)
|
|
7337
|
+
return DEFAULTS;
|
|
7338
|
+
return {
|
|
7339
|
+
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
7340
|
+
baseDelayMs: options.baseDelayMs ?? DEFAULTS.baseDelayMs,
|
|
7341
|
+
maxDelayMs: options.maxDelayMs ?? DEFAULTS.maxDelayMs,
|
|
7342
|
+
deadlineMs: options.deadlineMs ?? DEFAULTS.deadlineMs,
|
|
7343
|
+
jitter: options.jitter ?? DEFAULTS.jitter,
|
|
7344
|
+
isRetryable: options.isRetryable ?? DEFAULTS.isRetryable,
|
|
7345
|
+
};
|
|
7346
|
+
}
|
|
7347
|
+
/**
|
|
7348
|
+
* Calculate exponential backoff delay with optional jitter.
|
|
7349
|
+
*
|
|
7350
|
+
* @param attempt - 1-indexed retry attempt number.
|
|
7351
|
+
* @param config - Resolved retry configuration.
|
|
7352
|
+
* @returns Delay in milliseconds.
|
|
7353
|
+
*/
|
|
7354
|
+
function calculateDelay(attempt, config) {
|
|
7355
|
+
let delay = config.baseDelayMs * Math.pow(2, attempt - 1);
|
|
7356
|
+
delay = Math.min(delay, config.maxDelayMs);
|
|
7357
|
+
if (config.jitter) {
|
|
7358
|
+
const jitterFactor = 0.75 + Math.random() * 0.5; // NOSONAR - not security-sensitive
|
|
7359
|
+
delay = Math.round(delay * jitterFactor);
|
|
7360
|
+
}
|
|
7361
|
+
return delay;
|
|
7362
|
+
}
|
|
7363
|
+
/**
|
|
7364
|
+
* Retry an async function with exponential backoff and jitter.
|
|
7365
|
+
*
|
|
7366
|
+
* This is a lightweight standalone utility with no `@core/runtime`
|
|
7367
|
+
* dependency, suitable for use in adapters and providers that do not
|
|
7368
|
+
* participate in the middleware pipeline.
|
|
7369
|
+
*
|
|
7370
|
+
* @typeParam T - The resolved value type.
|
|
7371
|
+
* @param fn - The async function to execute (and potentially retry).
|
|
7372
|
+
* @param options - Retry configuration.
|
|
7373
|
+
* @returns The resolved value of `fn`.
|
|
7374
|
+
* @throws The last error when all retry attempts are exhausted, or
|
|
7375
|
+
* immediately when `isRetryable` returns `false`.
|
|
7376
|
+
*
|
|
7377
|
+
* @example
|
|
7378
|
+
* ```typescript
|
|
7379
|
+
* import { retryAsync } from '@core/utils'
|
|
7380
|
+
*
|
|
7381
|
+
* const receipt = await retryAsync(
|
|
7382
|
+
* () => adapter.waitForTransaction(txHash, { confirmations: 1 }, chain),
|
|
7383
|
+
* { maxRetries: 3, isRetryable: (err) => isTransientRpcError(err) },
|
|
7384
|
+
* )
|
|
7385
|
+
* ```
|
|
7386
|
+
*/
|
|
7387
|
+
async function retryAsync(fn, options) {
|
|
7388
|
+
const config = resolveOptions(options);
|
|
7389
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
7390
|
+
try {
|
|
7391
|
+
return await fn();
|
|
7392
|
+
}
|
|
7393
|
+
catch (error) {
|
|
7394
|
+
const pastDeadline = config.deadlineMs !== undefined && Date.now() >= config.deadlineMs;
|
|
7395
|
+
const isLastAttempt = attempt >= config.maxRetries;
|
|
7396
|
+
if (isLastAttempt || pastDeadline || !config.isRetryable(error)) {
|
|
7397
|
+
throw error;
|
|
7398
|
+
}
|
|
7399
|
+
const delayMs = calculateDelay(attempt + 1, config);
|
|
7400
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
7401
|
+
}
|
|
7402
|
+
}
|
|
7403
|
+
/* istanbul ignore next: unreachable safety throw for TypeScript */
|
|
7404
|
+
throw new Error('retryAsync: unreachable');
|
|
7405
|
+
}
|
|
7406
|
+
|
|
6406
7407
|
/**
|
|
6407
7408
|
* Transfer speed options for cross-chain operations.
|
|
6408
7409
|
*
|
|
@@ -8052,10 +9053,13 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
|
|
|
8052
9053
|
}
|
|
8053
9054
|
const txHash = await request.execute();
|
|
8054
9055
|
step.txHash = txHash;
|
|
8055
|
-
const
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
9056
|
+
const retryOptions = {
|
|
9057
|
+
isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
|
|
9058
|
+
};
|
|
9059
|
+
if (timeout !== undefined) {
|
|
9060
|
+
retryOptions.deadlineMs = Date.now() + timeout;
|
|
9061
|
+
}
|
|
9062
|
+
const transaction = await retryAsync(async () => adapter.waitForTransaction(txHash, { confirmations, timeout }, chain), retryOptions);
|
|
8059
9063
|
step.state = transaction.blockNumber ? 'success' : 'error';
|
|
8060
9064
|
step.data = transaction;
|
|
8061
9065
|
// Generate explorer URL for the step
|
|
@@ -9670,7 +10674,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
|
|
|
9670
10674
|
return step;
|
|
9671
10675
|
}
|
|
9672
10676
|
try {
|
|
9673
|
-
const transaction = await adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain)
|
|
10677
|
+
const transaction = await retryAsync(async () => adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain), {
|
|
10678
|
+
isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, {
|
|
10679
|
+
chain: chain.name,
|
|
10680
|
+
txHash: receipt.txHash,
|
|
10681
|
+
})),
|
|
10682
|
+
});
|
|
9674
10683
|
step.state = transaction.blockNumber === undefined ? 'error' : 'success';
|
|
9675
10684
|
step.data = transaction;
|
|
9676
10685
|
if (transaction.blockNumber === undefined) {
|
|
@@ -9686,7 +10695,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
|
|
|
9686
10695
|
return step;
|
|
9687
10696
|
}
|
|
9688
10697
|
|
|
9689
|
-
var version = "1.6.
|
|
10698
|
+
var version = "1.6.2";
|
|
9690
10699
|
var pkg = {
|
|
9691
10700
|
version: version};
|
|
9692
10701
|
|
|
@@ -10802,7 +11811,10 @@ async function waitForPendingTransaction(pendingStep, adapter, chain) {
|
|
|
10802
11811
|
message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`,
|
|
10803
11812
|
});
|
|
10804
11813
|
}
|
|
10805
|
-
const
|
|
11814
|
+
const txHash = pendingStep.txHash;
|
|
11815
|
+
const txReceipt = await retryAsync(async () => adapter.waitForTransaction(txHash, undefined, chain), {
|
|
11816
|
+
isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
|
|
11817
|
+
});
|
|
10806
11818
|
// Check if transaction was confirmed on-chain
|
|
10807
11819
|
if (!txReceipt.blockNumber) {
|
|
10808
11820
|
return {
|