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