@circle-fin/app-kit 1.11.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +56 -25
- package/bridge.cjs +1696 -1348
- package/bridge.d.cts +979 -2
- package/bridge.d.mts +979 -2
- package/bridge.d.ts +979 -2
- package/bridge.mjs +1696 -1348
- package/chains.cjs +106 -1
- package/chains.d.cts +96 -2
- package/chains.d.mts +96 -2
- package/chains.d.ts +96 -2
- package/chains.mjs +105 -2
- package/context.d.cts +978 -1
- package/context.d.mts +978 -1
- package/context.d.ts +978 -1
- package/earn.cjs +1728 -65
- package/earn.d.cts +978 -1
- package/earn.d.mts +978 -1
- package/earn.d.ts +978 -1
- package/earn.mjs +1728 -65
- package/estimateBridge.cjs +1695 -1347
- package/estimateBridge.d.cts +978 -1
- package/estimateBridge.d.mts +978 -1
- package/estimateBridge.d.ts +978 -1
- package/estimateBridge.mjs +1695 -1347
- package/estimateSwap.cjs +312 -17
- package/estimateSwap.d.cts +977 -1
- package/estimateSwap.d.mts +977 -1
- package/estimateSwap.d.ts +977 -1
- package/estimateSwap.mjs +312 -17
- package/index.cjs +3608 -1603
- package/index.d.cts +4181 -3426
- package/index.d.mts +4181 -3426
- package/index.d.ts +4181 -3426
- package/index.mjs +3607 -1604
- package/package.json +6 -6
- package/swap.cjs +312 -17
- package/swap.d.cts +977 -1
- package/swap.d.mts +977 -1
- package/swap.d.ts +977 -1
- package/swap.mjs +312 -17
- package/unifiedBalance.cjs +436 -122
- package/unifiedBalance.d.cts +132 -31
- package/unifiedBalance.d.mts +132 -31
- package/unifiedBalance.d.ts +132 -31
- package/unifiedBalance.mjs +436 -122
package/estimateBridge.cjs
CHANGED
|
@@ -40,6 +40,7 @@ var web3_js = require('@solana/web3.js');
|
|
|
40
40
|
require('bn.js');
|
|
41
41
|
require('@coral-xyz/anchor');
|
|
42
42
|
require('@noble/curves/ed25519');
|
|
43
|
+
require('viem');
|
|
43
44
|
var keccak256 = require('@ethersproject/keccak256');
|
|
44
45
|
|
|
45
46
|
function _interopDefault (e) { return e && e.__esModule ? e.default : e; }
|
|
@@ -2562,6 +2563,8 @@ class KitError extends Error {
|
|
|
2562
2563
|
Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
|
|
2563
2564
|
Blockchain["XDC"] = "XDC";
|
|
2564
2565
|
Blockchain["XDC_Apothem"] = "XDC_Apothem";
|
|
2566
|
+
Blockchain["X_Layer"] = "X_Layer";
|
|
2567
|
+
Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
|
|
2565
2568
|
Blockchain["ZKSync_Era"] = "ZKSync_Era";
|
|
2566
2569
|
Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
|
|
2567
2570
|
})(Blockchain || (Blockchain = {}));
|
|
@@ -2615,6 +2618,7 @@ var BridgeChain;
|
|
|
2615
2618
|
BridgeChain["Unichain"] = "Unichain";
|
|
2616
2619
|
BridgeChain["World_Chain"] = "World_Chain";
|
|
2617
2620
|
BridgeChain["XDC"] = "XDC";
|
|
2621
|
+
BridgeChain["X_Layer"] = "X_Layer";
|
|
2618
2622
|
// Testnet chains with CCTPv2 support
|
|
2619
2623
|
BridgeChain["Arc_Testnet"] = "Arc_Testnet";
|
|
2620
2624
|
BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
|
|
@@ -2640,6 +2644,7 @@ var BridgeChain;
|
|
|
2640
2644
|
BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
|
|
2641
2645
|
BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
|
|
2642
2646
|
BridgeChain["XDC_Apothem"] = "XDC_Apothem";
|
|
2647
|
+
BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
|
|
2643
2648
|
})(BridgeChain || (BridgeChain = {}));
|
|
2644
2649
|
var UnifiedBalanceChain;
|
|
2645
2650
|
(function(UnifiedBalanceChain) {
|
|
@@ -5177,7 +5182,8 @@ var EarnChain;
|
|
|
5177
5182
|
isTestnet: true,
|
|
5178
5183
|
explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
|
|
5179
5184
|
rpcEndpoints: [
|
|
5180
|
-
'https://
|
|
5185
|
+
'https://polygon-amoy-bor-rpc.publicnode.com',
|
|
5186
|
+
'https://polygon-amoy.drpc.org'
|
|
5181
5187
|
],
|
|
5182
5188
|
eurcAddress: null,
|
|
5183
5189
|
usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
|
|
@@ -6042,6 +6048,104 @@ var EarnChain;
|
|
|
6042
6048
|
}
|
|
6043
6049
|
});
|
|
6044
6050
|
|
|
6051
|
+
/**
|
|
6052
|
+
* X Layer Mainnet chain definition
|
|
6053
|
+
* @remarks
|
|
6054
|
+
* This represents the official production network for the X Layer blockchain.
|
|
6055
|
+
* X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
|
|
6056
|
+
* using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
|
|
6057
|
+
* OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
|
|
6058
|
+
*/ const XLayer = defineChain({
|
|
6059
|
+
type: 'evm',
|
|
6060
|
+
chain: Blockchain.X_Layer,
|
|
6061
|
+
name: 'X Layer',
|
|
6062
|
+
title: 'X Layer Mainnet',
|
|
6063
|
+
nativeCurrency: {
|
|
6064
|
+
name: 'OKB',
|
|
6065
|
+
symbol: 'OKB',
|
|
6066
|
+
decimals: 18
|
|
6067
|
+
},
|
|
6068
|
+
chainId: 196,
|
|
6069
|
+
isTestnet: false,
|
|
6070
|
+
explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
|
|
6071
|
+
rpcEndpoints: [
|
|
6072
|
+
'https://xlayerrpc.okx.com'
|
|
6073
|
+
],
|
|
6074
|
+
eurcAddress: null,
|
|
6075
|
+
usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
|
|
6076
|
+
usdtAddress: null,
|
|
6077
|
+
cctp: {
|
|
6078
|
+
domain: 37,
|
|
6079
|
+
contracts: {
|
|
6080
|
+
v2: {
|
|
6081
|
+
type: 'split',
|
|
6082
|
+
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
6083
|
+
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
6084
|
+
confirmations: 65,
|
|
6085
|
+
fastConfirmations: 1
|
|
6086
|
+
}
|
|
6087
|
+
},
|
|
6088
|
+
forwarderSupported: {
|
|
6089
|
+
source: false,
|
|
6090
|
+
destination: false
|
|
6091
|
+
}
|
|
6092
|
+
},
|
|
6093
|
+
kitContracts: {
|
|
6094
|
+
bridge: BRIDGE_CONTRACT_EVM_MAINNET
|
|
6095
|
+
}
|
|
6096
|
+
});
|
|
6097
|
+
|
|
6098
|
+
/**
|
|
6099
|
+
* X Layer Testnet chain definition
|
|
6100
|
+
* @remarks
|
|
6101
|
+
* This represents the official test network for the X Layer blockchain.
|
|
6102
|
+
* X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
|
|
6103
|
+
* using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
|
|
6104
|
+
* OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
|
|
6105
|
+
*/ const XLayerTestnet = defineChain({
|
|
6106
|
+
type: 'evm',
|
|
6107
|
+
chain: Blockchain.X_Layer_Testnet,
|
|
6108
|
+
name: 'X Layer Testnet',
|
|
6109
|
+
title: 'X Layer Testnet',
|
|
6110
|
+
nativeCurrency: {
|
|
6111
|
+
name: 'OKB',
|
|
6112
|
+
symbol: 'OKB',
|
|
6113
|
+
decimals: 18
|
|
6114
|
+
},
|
|
6115
|
+
chainId: 1952,
|
|
6116
|
+
isTestnet: true,
|
|
6117
|
+
// Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
|
|
6118
|
+
// testnet URL targets the deprecated pre-rebrand chain ID 195, not this
|
|
6119
|
+
// chain's ID (1952). Verified against the internal chain-expansion-scripts
|
|
6120
|
+
// config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
|
|
6121
|
+
explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
|
|
6122
|
+
rpcEndpoints: [
|
|
6123
|
+
'https://testrpc.xlayer.tech'
|
|
6124
|
+
],
|
|
6125
|
+
eurcAddress: null,
|
|
6126
|
+
usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
|
|
6127
|
+
usdtAddress: null,
|
|
6128
|
+
cctp: {
|
|
6129
|
+
domain: 37,
|
|
6130
|
+
contracts: {
|
|
6131
|
+
v2: {
|
|
6132
|
+
type: 'split',
|
|
6133
|
+
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
6134
|
+
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
6135
|
+
confirmations: 65,
|
|
6136
|
+
fastConfirmations: 1
|
|
6137
|
+
}
|
|
6138
|
+
},
|
|
6139
|
+
forwarderSupported: {
|
|
6140
|
+
source: false,
|
|
6141
|
+
destination: false
|
|
6142
|
+
}
|
|
6143
|
+
},
|
|
6144
|
+
kitContracts: {
|
|
6145
|
+
bridge: BRIDGE_CONTRACT_EVM_TESTNET
|
|
6146
|
+
}
|
|
6147
|
+
});
|
|
6148
|
+
|
|
6045
6149
|
/**
|
|
6046
6150
|
* ZKSync Era Mainnet chain definition
|
|
6047
6151
|
* @remarks
|
|
@@ -6161,6 +6265,8 @@ var Chains = {
|
|
|
6161
6265
|
WorldChainSepolia: WorldChainSepolia,
|
|
6162
6266
|
XDC: XDC,
|
|
6163
6267
|
XDCApothem: XDCApothem,
|
|
6268
|
+
XLayer: XLayer,
|
|
6269
|
+
XLayerTestnet: XLayerTestnet,
|
|
6164
6270
|
ZKSyncEra: ZKSyncEra,
|
|
6165
6271
|
ZKSyncEraSepolia: ZKSyncEraSepolia
|
|
6166
6272
|
};
|
|
@@ -8215,6 +8321,7 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8215
8321
|
[Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
|
|
8216
8322
|
[Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
|
|
8217
8323
|
[Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
|
|
8324
|
+
[Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
|
|
8218
8325
|
[Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
|
|
8219
8326
|
// =========================================================================
|
|
8220
8327
|
// Testnets (alphabetically sorted)
|
|
@@ -8223,6 +8330,7 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8223
8330
|
[Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
|
|
8224
8331
|
[Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
|
|
8225
8332
|
[Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
8333
|
+
[Blockchain.Celo_Alfajores_Testnet]: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B',
|
|
8226
8334
|
[Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
|
|
8227
8335
|
[Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
|
|
8228
8336
|
[Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
|
|
@@ -8249,6 +8357,7 @@ const swapTokenEnumSchema = zod.z.enum([
|
|
|
8249
8357
|
[Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
|
|
8250
8358
|
[Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
|
|
8251
8359
|
[Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
|
|
8360
|
+
[Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
|
|
8252
8361
|
[Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
|
|
8253
8362
|
}
|
|
8254
8363
|
};
|
|
@@ -9660,7 +9769,7 @@ function resolveOptions(options) {
|
|
|
9660
9769
|
}
|
|
9661
9770
|
|
|
9662
9771
|
var name$2 = "@circle-fin/bridge-kit";
|
|
9663
|
-
var version$3 = "1.
|
|
9772
|
+
var version$3 = "1.13.0";
|
|
9664
9773
|
var pkg$3 = {
|
|
9665
9774
|
name: name$2,
|
|
9666
9775
|
version: version$3};
|
|
@@ -9697,13 +9806,21 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
|
|
|
9697
9806
|
computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
|
|
9698
9807
|
calculateFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
|
|
9699
9808
|
resolveFeeRecipientAddress: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string())))
|
|
9700
|
-
}).strict().
|
|
9809
|
+
}).strict().superRefine((data, ctx)=>{
|
|
9701
9810
|
const hasComputeFee = data.computeFee !== undefined;
|
|
9702
9811
|
const hasCalculateFee = data.calculateFee !== undefined;
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9812
|
+
if (hasComputeFee && hasCalculateFee) {
|
|
9813
|
+
ctx.addIssue({
|
|
9814
|
+
code: zod.z.ZodIssueCode.custom,
|
|
9815
|
+
message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
|
|
9816
|
+
});
|
|
9817
|
+
}
|
|
9818
|
+
if (!hasComputeFee && !hasCalculateFee) {
|
|
9819
|
+
ctx.addIssue({
|
|
9820
|
+
code: zod.z.ZodIssueCode.custom,
|
|
9821
|
+
message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
|
|
9822
|
+
});
|
|
9823
|
+
}
|
|
9707
9824
|
});
|
|
9708
9825
|
/**
|
|
9709
9826
|
* Assert that the provided value conforms to {@link CustomFeePolicy}.
|
|
@@ -9801,6 +9918,44 @@ function assertBridgeParams(params, schema) {
|
|
|
9801
9918
|
}
|
|
9802
9919
|
}
|
|
9803
9920
|
|
|
9921
|
+
/**
|
|
9922
|
+
* Canonical list of actions that do not prepare or submit transactions.
|
|
9923
|
+
*
|
|
9924
|
+
* @internal
|
|
9925
|
+
*/ const READ_ACTION_KEYS = [
|
|
9926
|
+
'token.allowance',
|
|
9927
|
+
'token.balanceOf',
|
|
9928
|
+
'token.name',
|
|
9929
|
+
'native.balanceOf',
|
|
9930
|
+
'usdc.allowance',
|
|
9931
|
+
'usdc.balanceOf',
|
|
9932
|
+
'usdc.name',
|
|
9933
|
+
'gateway.v1.isDelegate',
|
|
9934
|
+
'gateway.v1.withdrawingBalance',
|
|
9935
|
+
'gateway.v1.withdrawalBlock',
|
|
9936
|
+
'gateway.v1.signBurnIntents'
|
|
9937
|
+
];
|
|
9938
|
+
const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
|
|
9939
|
+
/**
|
|
9940
|
+
* Check whether a runtime value identifies a read action.
|
|
9941
|
+
*
|
|
9942
|
+
* @param action - The value to classify.
|
|
9943
|
+
* @returns Whether the value is a registered read-action key.
|
|
9944
|
+
*
|
|
9945
|
+
* @example
|
|
9946
|
+
* ```typescript
|
|
9947
|
+
* import { isReadActionKey } from '@core/adapter'
|
|
9948
|
+
*
|
|
9949
|
+
* if (isReadActionKey(value)) {
|
|
9950
|
+
* await adapter.readAction(value, params, context)
|
|
9951
|
+
* }
|
|
9952
|
+
* ```
|
|
9953
|
+
*
|
|
9954
|
+
* @internal
|
|
9955
|
+
*/ function isReadActionKey(action) {
|
|
9956
|
+
return READ_ACTION_KEY_SET.has(action);
|
|
9957
|
+
}
|
|
9958
|
+
|
|
9804
9959
|
/**
|
|
9805
9960
|
* Resolves an operation context into concrete chain and address values.
|
|
9806
9961
|
*
|
|
@@ -9880,6 +10035,72 @@ function assertBridgeParams(params, schema) {
|
|
|
9880
10035
|
};
|
|
9881
10036
|
}
|
|
9882
10037
|
|
|
10038
|
+
/**
|
|
10039
|
+
* Create the standard error for a missing or non-read action.
|
|
10040
|
+
*
|
|
10041
|
+
* @param action - The unsupported action value.
|
|
10042
|
+
* @returns A fatal unsupported-action error.
|
|
10043
|
+
*
|
|
10044
|
+
* @internal
|
|
10045
|
+
*/ function createUnsupportedReadActionError(action) {
|
|
10046
|
+
return new KitError({
|
|
10047
|
+
...InputError.UNSUPPORTED_ACTION,
|
|
10048
|
+
recoverability: 'FATAL',
|
|
10049
|
+
message: `Read action "${String(action)}" is not registered in this adapter.`
|
|
10050
|
+
});
|
|
10051
|
+
}
|
|
10052
|
+
/**
|
|
10053
|
+
* Execute a read through the adapter's dedicated read seam when available.
|
|
10054
|
+
*
|
|
10055
|
+
* @remarks
|
|
10056
|
+
* Fall back to the legacy `prepareAction().execute()` contract so providers
|
|
10057
|
+
* remain runtime-compatible with adapter versions released before `readAction`.
|
|
10058
|
+
* Consumers must upgrade their adapter package for reads to bypass custom
|
|
10059
|
+
* `prepareAction` wrappers.
|
|
10060
|
+
*
|
|
10061
|
+
* @typeParam TAdapterCapabilities - The adapter capabilities type.
|
|
10062
|
+
* @typeParam TActionKey - The read action key.
|
|
10063
|
+
* @param adapter - The adapter that owns the read action.
|
|
10064
|
+
* @param action - The read action to execute.
|
|
10065
|
+
* @param params - The parameters for the read action.
|
|
10066
|
+
* @param ctx - The operation context.
|
|
10067
|
+
* @returns The raw read-action result.
|
|
10068
|
+
* @throws {KitError} When `action` is not a supported read-action key.
|
|
10069
|
+
*
|
|
10070
|
+
* @example
|
|
10071
|
+
* ```typescript
|
|
10072
|
+
* import { executeAdapterReadAction } from '@core/adapter'
|
|
10073
|
+
* import { Ethereum } from '@core/chains'
|
|
10074
|
+
*
|
|
10075
|
+
* const allowance = await executeAdapterReadAction(
|
|
10076
|
+
* adapter,
|
|
10077
|
+
* 'token.allowance',
|
|
10078
|
+
* { tokenAddress, delegate },
|
|
10079
|
+
* { chain: Ethereum },
|
|
10080
|
+
* )
|
|
10081
|
+
* ```
|
|
10082
|
+
*
|
|
10083
|
+
* @internal
|
|
10084
|
+
*/ async function executeAdapterReadAction(adapter, action, params, ctx) {
|
|
10085
|
+
if (!isReadActionKey(action)) {
|
|
10086
|
+
throw createUnsupportedReadActionError(action);
|
|
10087
|
+
}
|
|
10088
|
+
const runtimeAdapter = adapter;
|
|
10089
|
+
if (typeof runtimeAdapter.readAction === 'function') {
|
|
10090
|
+
return runtimeAdapter.readAction(action, params, ctx);
|
|
10091
|
+
}
|
|
10092
|
+
let request;
|
|
10093
|
+
try {
|
|
10094
|
+
request = await adapter.prepareAction(action, params, ctx);
|
|
10095
|
+
} catch (error) {
|
|
10096
|
+
if (error instanceof Error && error.message === `Action ${action} is not supported`) {
|
|
10097
|
+
throw createUnsupportedReadActionError(action);
|
|
10098
|
+
}
|
|
10099
|
+
throw error;
|
|
10100
|
+
}
|
|
10101
|
+
return request.execute();
|
|
10102
|
+
}
|
|
10103
|
+
|
|
9883
10104
|
/**
|
|
9884
10105
|
* Schema for validating hexadecimal strings with '0x' prefix.
|
|
9885
10106
|
*
|
|
@@ -10089,16 +10310,15 @@ function assertBridgeParams(params, schema) {
|
|
|
10089
10310
|
* ```
|
|
10090
10311
|
*/ const validateBalanceForTransaction = async (params)=>{
|
|
10091
10312
|
const { amount, adapter, token, tokenAddress, operationContext } = params;
|
|
10092
|
-
const
|
|
10313
|
+
const balance = await executeAdapterReadAction(adapter, 'usdc.balanceOf', {
|
|
10093
10314
|
walletAddress: operationContext.address
|
|
10094
10315
|
}, operationContext);
|
|
10095
|
-
|
|
10096
|
-
if (BigInt(balance) < BigInt(amount)) {
|
|
10316
|
+
if (BigInt(String(balance)) < BigInt(amount)) {
|
|
10097
10317
|
// Extract chain name from operationContext
|
|
10098
10318
|
const chainName = extractChainInfo(operationContext.chain).name;
|
|
10099
10319
|
// Create KitError with rich context in trace
|
|
10100
10320
|
throw createInsufficientTokenBalanceError(chainName, token, {
|
|
10101
|
-
balance: balance
|
|
10321
|
+
balance: String(balance),
|
|
10102
10322
|
amount,
|
|
10103
10323
|
tokenAddress,
|
|
10104
10324
|
walletAddress: operationContext.address
|
|
@@ -12464,15 +12684,52 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
|
|
|
12464
12684
|
;
|
|
12465
12685
|
const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
|
|
12466
12686
|
;
|
|
12467
|
-
//
|
|
12468
|
-
//
|
|
12469
|
-
//
|
|
12470
|
-
|
|
12687
|
+
// Gas FLOORS, not ceilings — kept separate from the fee-estimate averages
|
|
12688
|
+
// above. `executePreparedChainRequest` submits
|
|
12689
|
+
// max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
|
|
12690
|
+
// is covered by its own estimate, and a chain whose estimator under-reports
|
|
12691
|
+
// (Cronos: returns 30_600 where the EIP-7623 calldata floor is 45_000) is
|
|
12692
|
+
// covered by the floor.
|
|
12693
|
+
//
|
|
12694
|
+
// Two distinct chain surcharges drive these numbers, both measured live:
|
|
12695
|
+
// Sei — ~+51_500 per NEWLY CREATED storage slot (73_595 vs vanilla 22_100);
|
|
12696
|
+
// no flat per-tx surcharge (31_535, identical to Base).
|
|
12697
|
+
// Edge — ~+53_200 flat on EVERY tx (84_751 vs Base 31_535); storage priced
|
|
12698
|
+
// normally. Edge therefore fails warm as well as cold.
|
|
12699
|
+
// A floor must clear the worst COLD cost, since a slot that exists at estimate
|
|
12700
|
+
// time can be consumed before inclusion and cost a full step more on execution.
|
|
12701
|
+
// Each floor is therefore derived from the worst observed estimate *after* the
|
|
12702
|
+
// 1.25x buffer, plus headroom — sizing it below the buffered value would leave
|
|
12703
|
+
// the estimate governing and defeat the point of the floor.
|
|
12704
|
+
//
|
|
12705
|
+
// The `*_GAS_LIMIT_EVM` names are kept despite these being floors: they are
|
|
12706
|
+
// exported, so renaming to `*_GAS_FLOOR_EVM` would be a breaking change for
|
|
12707
|
+
// consumers. Read "LIMIT" here as "the limit we submit", never as a ceiling.
|
|
12708
|
+
const APPROVE_GAS_LIMIT_EVM = 150_000n // buffered worst cold 149_355 (Edge Testnet 119_484 x 1.25) + drift headroom
|
|
12471
12709
|
;
|
|
12472
|
-
const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM =
|
|
12710
|
+
const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 500_000n // buffered worst 474_078 (Sei 379_263 x 1.25) + ~26k headroom
|
|
12473
12711
|
;
|
|
12474
|
-
const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839
|
|
12712
|
+
const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears Cronos' calldata floor ~10x
|
|
12475
12713
|
;
|
|
12714
|
+
/**
|
|
12715
|
+
* The gas floor for each bridge step, keyed by step name.
|
|
12716
|
+
*
|
|
12717
|
+
* Two places need these and they must agree: each step module passes its floor
|
|
12718
|
+
* to `executePreparedChainRequest` for submission, and
|
|
12719
|
+
* `CCTPV2BridgingProvider.estimate()` quotes the resulting limit so a caller
|
|
12720
|
+
* can fund a wallet. A transaction is only admitted when the sender holds
|
|
12721
|
+
* `gasLimit * maxFeePerGas`, so a quote taken from anything other than the
|
|
12722
|
+
* submitted limit under-reports what the wallet actually needs — historically
|
|
12723
|
+
* the quote sat ~2.5x below the reserved limit.
|
|
12724
|
+
*
|
|
12725
|
+
* Both sides read this map so the two cannot drift apart. Change a floor here
|
|
12726
|
+
* and the quote moves with it; point one side at a different value and the
|
|
12727
|
+
* divergence is visible in review rather than silent at runtime.
|
|
12728
|
+
*/ const BRIDGE_STEP_GAS_FLOORS_EVM = {
|
|
12729
|
+
approve: APPROVE_GAS_LIMIT_EVM,
|
|
12730
|
+
burn: DEPOSIT_FOR_BURN_GAS_LIMIT_EVM,
|
|
12731
|
+
mint: RECEIVE_MESSAGE_GAS_LIMIT_EVM
|
|
12732
|
+
};
|
|
12476
12733
|
/**
|
|
12477
12734
|
* The minimum finality threshold for CCTPv2 transfers.
|
|
12478
12735
|
*
|
|
@@ -12491,1538 +12748,1595 @@ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
|
|
|
12491
12748
|
};
|
|
12492
12749
|
|
|
12493
12750
|
/**
|
|
12494
|
-
*
|
|
12495
|
-
* @internal
|
|
12496
|
-
*/ const DEFAULT_CONFIG = {
|
|
12497
|
-
timeout: 2_000,
|
|
12498
|
-
maxRetries: 30 * 20,
|
|
12499
|
-
retryDelay: 2_000,
|
|
12500
|
-
headers: {
|
|
12501
|
-
'Content-Type': 'application/json'
|
|
12502
|
-
}
|
|
12503
|
-
};
|
|
12504
|
-
/**
|
|
12505
|
-
* Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
|
|
12506
|
-
*
|
|
12507
|
-
* Headers are merged independently so caller-supplied headers augment the
|
|
12508
|
-
* defaults (such as `Content-Type`) rather than replacing them wholesale.
|
|
12509
|
-
*
|
|
12510
|
-
* @param config - Caller-provided polling configuration overrides
|
|
12511
|
-
* @param internalDefaults - Internal defaults applied before `config` (for example a
|
|
12512
|
-
* reduced `maxRetries` for one-shot requests); `config` still wins on conflict
|
|
12513
|
-
* @returns The effective polling configuration
|
|
12514
|
-
* @internal
|
|
12515
|
-
*/ const mergeAttestationConfig = (config, internalDefaults = {})=>({
|
|
12516
|
-
...DEFAULT_CONFIG,
|
|
12517
|
-
...internalDefaults,
|
|
12518
|
-
...config,
|
|
12519
|
-
headers: {
|
|
12520
|
-
...DEFAULT_CONFIG.headers,
|
|
12521
|
-
...internalDefaults.headers,
|
|
12522
|
-
...config.headers
|
|
12523
|
-
}
|
|
12524
|
-
});
|
|
12525
|
-
/**
|
|
12526
|
-
* Type guard that verifies if an unknown value matches the AttestationMessage shape
|
|
12527
|
-
* and has all required properties.
|
|
12528
|
-
*
|
|
12529
|
-
* @param obj - The value to check, typically an element from the messages array
|
|
12530
|
-
* @returns True if the object matches the AttestationMessage shape, false otherwise
|
|
12531
|
-
* @internal
|
|
12532
|
-
*/ const isValidAttestationMessage = (obj)=>{
|
|
12533
|
-
return typeof obj === 'object' && obj !== null && 'message' in obj && 'eventNonce' in obj && 'attestation' in obj && 'decodedMessage' in obj && 'cctpVersion' in obj && 'status' in obj && typeof obj.status === 'string';
|
|
12534
|
-
};
|
|
12535
|
-
/**
|
|
12536
|
-
* Type guard that verifies if an attestation message is complete.
|
|
12537
|
-
*
|
|
12538
|
-
* @param message - The attestation message to check
|
|
12539
|
-
* @returns True if the message status is 'complete', false otherwise
|
|
12540
|
-
* @internal
|
|
12541
|
-
*/ const isCompleteAttestation = (message)=>{
|
|
12542
|
-
return message.status === 'complete';
|
|
12543
|
-
};
|
|
12544
|
-
/**
|
|
12545
|
-
* Type guard that verifies if an unknown value has the correct structure
|
|
12546
|
-
* for an AttestationResponse, regardless of attestation completion status.
|
|
12547
|
-
*
|
|
12548
|
-
* @param obj - The value to check, typically a parsed JSON response
|
|
12549
|
-
* @returns True if the object matches the AttestationResponse shape
|
|
12550
|
-
* @internal
|
|
12551
|
-
*/ const hasValidAttestationStructure = (obj)=>{
|
|
12552
|
-
if (typeof obj !== 'object' || obj === null || !('messages' in obj) || !Array.isArray(obj.messages)) {
|
|
12553
|
-
return false;
|
|
12554
|
-
}
|
|
12555
|
-
const messages = obj.messages;
|
|
12556
|
-
// Validate all messages have the correct shape
|
|
12557
|
-
return messages.every(isValidAttestationMessage);
|
|
12558
|
-
};
|
|
12559
|
-
/**
|
|
12560
|
-
* Type guard that verifies if an unknown value matches the AttestationResponse shape
|
|
12561
|
-
* and contains a complete attestation.
|
|
12562
|
-
*
|
|
12563
|
-
* This function performs runtime validation to ensure that the provided value
|
|
12564
|
-
* conforms to the expected structure of an AttestationResponse and has at least
|
|
12565
|
-
* one complete attestation. It checks that:
|
|
12566
|
-
* 1. The value has valid AttestationResponse structure
|
|
12567
|
-
* 2. At least one message has status 'complete'
|
|
12568
|
-
*
|
|
12569
|
-
* @remarks
|
|
12570
|
-
* This type guard is used internally by the attestation fetcher to validate
|
|
12571
|
-
* responses from the IRIS API before processing them. It provides runtime
|
|
12572
|
-
* type safety for data coming from the network and ensures we have a complete
|
|
12573
|
-
* attestation before proceeding.
|
|
12574
|
-
*
|
|
12575
|
-
* If the response has valid structure but no complete attestation yet,
|
|
12576
|
-
* it throws a retryable error. If the response structure is invalid,
|
|
12577
|
-
* it throws a non-retryable validation error.
|
|
12578
|
-
*
|
|
12579
|
-
* @param obj - The value to check, typically a parsed JSON response
|
|
12580
|
-
* @returns True if the object matches the AttestationResponse shape and has a complete attestation
|
|
12581
|
-
* @throws {Error} With "Invalid attestation response structure" if structure is invalid (non-retryable)
|
|
12582
|
-
* @throws {Error} With "Attestation not ready" if no complete attestation yet (retryable)
|
|
12583
|
-
*
|
|
12584
|
-
* @example
|
|
12585
|
-
* ```typescript
|
|
12586
|
-
* const response = await fetch('https://iris-api.circle.com/...')
|
|
12587
|
-
* const data = await response.json()
|
|
12751
|
+
* CCTP bridge step names that can occur in the bridging flow.
|
|
12588
12752
|
*
|
|
12589
|
-
*
|
|
12590
|
-
*
|
|
12591
|
-
*
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
// If structure is invalid, this is a permanent failure - don't retry
|
|
12599
|
-
throw new Error('Invalid attestation response structure');
|
|
12600
|
-
}
|
|
12601
|
-
// Then check if at least one message is complete
|
|
12602
|
-
if (!obj.messages.some(isCompleteAttestation)) {
|
|
12603
|
-
// If no complete message, this is a temporary state - allow retry
|
|
12604
|
-
throw new Error('Attestation not ready');
|
|
12605
|
-
}
|
|
12606
|
-
return true;
|
|
12753
|
+
* This object provides type safety for step names and represents all possible
|
|
12754
|
+
* steps that can be executed during a CCTP bridge operation. Using const assertions
|
|
12755
|
+
* makes this tree-shakable and follows modern TypeScript best practices.
|
|
12756
|
+
*/ const CCTPv2StepName = {
|
|
12757
|
+
approve: 'approve',
|
|
12758
|
+
burn: 'burn',
|
|
12759
|
+
fetchAttestation: 'fetchAttestation',
|
|
12760
|
+
mint: 'mint',
|
|
12761
|
+
reAttest: 'reAttest'
|
|
12607
12762
|
};
|
|
12608
12763
|
/**
|
|
12609
|
-
*
|
|
12610
|
-
*
|
|
12611
|
-
* Constructs a properly formatted URL for the IRIS API v2 endpoint that provides
|
|
12612
|
-
* attestation messages for cross-chain transfers. The URL includes both the source
|
|
12613
|
-
* domain identifier and the transaction hash as query parameters. The base URL
|
|
12614
|
-
* is selected based on whether the operation is for testnet or mainnet.
|
|
12615
|
-
*
|
|
12616
|
-
* @param sourceDomainId - The CCTP domain ID of the source chain (numeric or string)
|
|
12617
|
-
* @param transactionHash - The transaction hash of the burn operation to fetch attestation for
|
|
12618
|
-
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
12619
|
-
* @returns A fully qualified URL string for the IRIS API endpoint
|
|
12764
|
+
* Conditional step transition rules for CCTP bridge flow.
|
|
12620
12765
|
*
|
|
12621
|
-
*
|
|
12622
|
-
*
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12766
|
+
* Rules are evaluated in order - the first matching condition determines the next step.
|
|
12767
|
+
* This approach supports flexible flow logic and makes it easy to extend with new patterns.
|
|
12768
|
+
*/ const STEP_TRANSITION_RULES = {
|
|
12769
|
+
// Starting state - no steps executed yet
|
|
12770
|
+
'': [
|
|
12771
|
+
{
|
|
12772
|
+
condition: ()=>true,
|
|
12773
|
+
nextStep: CCTPv2StepName.approve,
|
|
12774
|
+
reason: 'Start with approval step',
|
|
12775
|
+
isActionable: true
|
|
12776
|
+
}
|
|
12777
|
+
],
|
|
12778
|
+
// After Approve step
|
|
12779
|
+
[CCTPv2StepName.approve]: [
|
|
12780
|
+
{
|
|
12781
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
12782
|
+
nextStep: CCTPv2StepName.burn,
|
|
12783
|
+
reason: 'Approval successful, proceed to burn',
|
|
12784
|
+
isActionable: true
|
|
12785
|
+
},
|
|
12786
|
+
{
|
|
12787
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
12788
|
+
nextStep: CCTPv2StepName.approve,
|
|
12789
|
+
reason: 'Retry failed approval',
|
|
12790
|
+
isActionable: true
|
|
12791
|
+
},
|
|
12792
|
+
{
|
|
12793
|
+
condition: (ctx)=>ctx.lastStep?.state === 'noop',
|
|
12794
|
+
nextStep: CCTPv2StepName.burn,
|
|
12795
|
+
reason: 'No approval needed, proceed to burn',
|
|
12796
|
+
isActionable: true
|
|
12797
|
+
},
|
|
12798
|
+
{
|
|
12799
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
12800
|
+
nextStep: CCTPv2StepName.approve,
|
|
12801
|
+
reason: 'Continue pending approval',
|
|
12802
|
+
isActionable: false
|
|
12803
|
+
}
|
|
12804
|
+
],
|
|
12805
|
+
// After Burn step
|
|
12806
|
+
[CCTPv2StepName.burn]: [
|
|
12807
|
+
{
|
|
12808
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
12809
|
+
nextStep: CCTPv2StepName.fetchAttestation,
|
|
12810
|
+
reason: 'Burn successful, fetch attestation',
|
|
12811
|
+
isActionable: true
|
|
12812
|
+
},
|
|
12813
|
+
{
|
|
12814
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
12815
|
+
nextStep: CCTPv2StepName.burn,
|
|
12816
|
+
reason: 'Retry failed burn',
|
|
12817
|
+
isActionable: true
|
|
12818
|
+
},
|
|
12819
|
+
{
|
|
12820
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
12821
|
+
nextStep: CCTPv2StepName.burn,
|
|
12822
|
+
reason: 'Continue pending burn',
|
|
12823
|
+
isActionable: false
|
|
12824
|
+
}
|
|
12825
|
+
],
|
|
12826
|
+
// After FetchAttestation step
|
|
12827
|
+
[CCTPv2StepName.fetchAttestation]: [
|
|
12828
|
+
{
|
|
12829
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
12830
|
+
nextStep: CCTPv2StepName.mint,
|
|
12831
|
+
reason: 'Attestation fetched, proceed to mint',
|
|
12832
|
+
isActionable: true
|
|
12833
|
+
},
|
|
12834
|
+
{
|
|
12835
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
12836
|
+
nextStep: CCTPv2StepName.fetchAttestation,
|
|
12837
|
+
reason: 'Retry fetching attestation',
|
|
12838
|
+
isActionable: true
|
|
12839
|
+
},
|
|
12840
|
+
{
|
|
12841
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
12842
|
+
nextStep: CCTPv2StepName.fetchAttestation,
|
|
12843
|
+
reason: 'Continue pending attestation fetch',
|
|
12844
|
+
isActionable: false
|
|
12845
|
+
}
|
|
12846
|
+
],
|
|
12847
|
+
// After Mint step
|
|
12848
|
+
[CCTPv2StepName.mint]: [
|
|
12849
|
+
{
|
|
12850
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
12851
|
+
nextStep: null,
|
|
12852
|
+
reason: 'Bridge completed successfully',
|
|
12853
|
+
isActionable: false
|
|
12854
|
+
},
|
|
12855
|
+
{
|
|
12856
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
12857
|
+
nextStep: CCTPv2StepName.mint,
|
|
12858
|
+
reason: 'Retry failed mint',
|
|
12859
|
+
isActionable: true
|
|
12860
|
+
},
|
|
12861
|
+
{
|
|
12862
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
12863
|
+
nextStep: CCTPv2StepName.mint,
|
|
12864
|
+
reason: 'Continue pending mint',
|
|
12865
|
+
isActionable: false
|
|
12866
|
+
}
|
|
12867
|
+
],
|
|
12868
|
+
// After ReAttest step
|
|
12869
|
+
[CCTPv2StepName.reAttest]: [
|
|
12870
|
+
{
|
|
12871
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
12872
|
+
nextStep: CCTPv2StepName.mint,
|
|
12873
|
+
reason: 'Re-attestation successful, proceed to mint',
|
|
12874
|
+
isActionable: true
|
|
12875
|
+
},
|
|
12876
|
+
{
|
|
12877
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
12878
|
+
nextStep: CCTPv2StepName.mint,
|
|
12879
|
+
reason: 'Re-attestation failed, retry mint to re-initiate recovery',
|
|
12880
|
+
isActionable: true
|
|
12881
|
+
},
|
|
12882
|
+
{
|
|
12883
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
12884
|
+
nextStep: CCTPv2StepName.mint,
|
|
12885
|
+
reason: 'Re-attestation pending, retry mint to re-initiate recovery',
|
|
12886
|
+
isActionable: true
|
|
12887
|
+
}
|
|
12888
|
+
]
|
|
12636
12889
|
};
|
|
12637
12890
|
/**
|
|
12638
|
-
*
|
|
12891
|
+
* Analyze bridge steps to determine retry feasibility and continuation point.
|
|
12639
12892
|
*
|
|
12640
|
-
*
|
|
12641
|
-
*
|
|
12642
|
-
*
|
|
12893
|
+
* This function examines the current state of bridge steps to determine the optimal
|
|
12894
|
+
* continuation strategy. It uses a rule-based approach that makes it easy to extend
|
|
12895
|
+
* with new flow patterns and step types in the future.
|
|
12643
12896
|
*
|
|
12644
|
-
*
|
|
12645
|
-
*
|
|
12646
|
-
* - Retry delay: 2 000 ms between attempts
|
|
12647
|
-
* - Max retries: 600 (30 × 20)
|
|
12648
|
-
* - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
|
|
12897
|
+
* The current analysis supports the standard CCTP flow:
|
|
12898
|
+
* **Traditional flow**: Approve → Burn → FetchAttestation → Mint
|
|
12649
12899
|
*
|
|
12650
|
-
*
|
|
12651
|
-
*
|
|
12652
|
-
*
|
|
12653
|
-
*
|
|
12654
|
-
*
|
|
12655
|
-
*
|
|
12900
|
+
* Key features:
|
|
12901
|
+
* - Rule-based transitions: Easy to extend with new step types and logic
|
|
12902
|
+
* - Context-aware decisions: Considers execution history and step states
|
|
12903
|
+
* - Actionable logic: Distinguishes between steps requiring user action vs waiting
|
|
12904
|
+
* - Terminal states: Properly handles completion and non-actionable states
|
|
12905
|
+
*
|
|
12906
|
+
* @param bridgeResult - The bridge result containing step execution history.
|
|
12907
|
+
* @returns Analysis result with continuation step and actionability information.
|
|
12908
|
+
* @throws Error when bridgeResult is invalid or contains no steps array.
|
|
12656
12909
|
*
|
|
12657
12910
|
* @example
|
|
12658
12911
|
* ```typescript
|
|
12659
|
-
*
|
|
12660
|
-
* const response = await fetchAttestation(1, '0xabc...', false)
|
|
12661
|
-
* console.log(`Found ${response.messages.length} attestation messages`)
|
|
12662
|
-
*
|
|
12663
|
-
* // Fetch with custom timeout
|
|
12664
|
-
* const response2 = await fetchAttestation(1, '0xdef...', true, {
|
|
12665
|
-
* timeout: 5000,
|
|
12666
|
-
* maxRetries: 5
|
|
12667
|
-
* })
|
|
12668
|
-
* ```
|
|
12669
|
-
*/ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
12670
|
-
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
12671
|
-
const effectiveConfig = mergeAttestationConfig(config);
|
|
12672
|
-
return await pollApiGet(url, isAttestationResponse, effectiveConfig);
|
|
12673
|
-
};
|
|
12674
|
-
/**
|
|
12675
|
-
* Type guard that validates attestation response structure without requiring completion status.
|
|
12676
|
-
*
|
|
12677
|
-
* This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
|
|
12678
|
-
* attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
|
|
12679
|
-
* this function does not throw if no complete attestation is found.
|
|
12680
|
-
*
|
|
12681
|
-
* @param obj - The value to check, typically a parsed JSON response
|
|
12682
|
-
* @returns True if the object has valid attestation structure
|
|
12683
|
-
* @throws {Error} With "Invalid attestation response structure" if structure is invalid
|
|
12684
|
-
* @internal
|
|
12685
|
-
*/ const isAttestationResponseWithoutStatusCheck = (obj)=>{
|
|
12686
|
-
if (!hasValidAttestationStructure(obj)) {
|
|
12687
|
-
throw new Error('Invalid attestation response structure');
|
|
12688
|
-
}
|
|
12689
|
-
return true;
|
|
12690
|
-
};
|
|
12691
|
-
/**
|
|
12692
|
-
* Fetches attestation data without requiring the attestation to be complete.
|
|
12912
|
+
* import { analyzeSteps } from './analyzeSteps'
|
|
12693
12913
|
*
|
|
12694
|
-
*
|
|
12695
|
-
*
|
|
12696
|
-
*
|
|
12914
|
+
* // Failed approval step (requires user action)
|
|
12915
|
+
* const bridgeResult = {
|
|
12916
|
+
* steps: [
|
|
12917
|
+
* { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
|
|
12918
|
+
* ]
|
|
12919
|
+
* }
|
|
12697
12920
|
*
|
|
12698
|
-
*
|
|
12699
|
-
*
|
|
12700
|
-
*
|
|
12701
|
-
*
|
|
12702
|
-
* @returns The attestation response data (may contain incomplete/expired attestations)
|
|
12703
|
-
* @throws If the request fails, times out, or returns invalid data
|
|
12921
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
12922
|
+
* // Result: { continuationStep: 'Approve', isRetryable: true,
|
|
12923
|
+
* // reason: 'Retry failed approval' }
|
|
12924
|
+
* ```
|
|
12704
12925
|
*
|
|
12705
12926
|
* @example
|
|
12706
12927
|
* ```typescript
|
|
12707
|
-
* //
|
|
12708
|
-
* const
|
|
12709
|
-
*
|
|
12710
|
-
*
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
// Use minimal retries since we're just fetching existing data
|
|
12714
|
-
const effectiveConfig = mergeAttestationConfig(config, {
|
|
12715
|
-
maxRetries: 3
|
|
12716
|
-
});
|
|
12717
|
-
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
12718
|
-
};
|
|
12719
|
-
/**
|
|
12720
|
-
* Type guard that validates attestation response has expirationBlock === '0'.
|
|
12721
|
-
*
|
|
12722
|
-
* This is used after requestReAttestation() to poll until the attestation
|
|
12723
|
-
* is fully re-processed and has a zero expiration block (never expires).
|
|
12724
|
-
* The expiration block transitions from non-zero to zero when Circle
|
|
12725
|
-
* completes processing the re-attestation request.
|
|
12928
|
+
* // Pending transaction (requires waiting, not actionable)
|
|
12929
|
+
* const bridgeResult = {
|
|
12930
|
+
* steps: [
|
|
12931
|
+
* { name: 'Approve', state: 'pending' }
|
|
12932
|
+
* ]
|
|
12933
|
+
* }
|
|
12726
12934
|
*
|
|
12727
|
-
*
|
|
12728
|
-
*
|
|
12729
|
-
*
|
|
12935
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
12936
|
+
* // Result: { continuationStep: 'Approve', isRetryable: false,
|
|
12937
|
+
* // reason: 'Continue pending approval' }
|
|
12938
|
+
* ```
|
|
12730
12939
|
*
|
|
12731
12940
|
* @example
|
|
12732
12941
|
* ```typescript
|
|
12733
|
-
* //
|
|
12734
|
-
* const
|
|
12735
|
-
*
|
|
12736
|
-
*
|
|
12942
|
+
* // Completed bridge (nothing to do)
|
|
12943
|
+
* const bridgeResult = {
|
|
12944
|
+
* steps: [
|
|
12945
|
+
* { name: 'Approve', state: 'success' },
|
|
12946
|
+
* { name: 'Burn', state: 'success' },
|
|
12947
|
+
* { name: 'FetchAttestation', state: 'success' },
|
|
12948
|
+
* { name: 'Mint', state: 'success' }
|
|
12949
|
+
* ]
|
|
12950
|
+
* }
|
|
12737
12951
|
*
|
|
12738
|
-
*
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
//
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
// Re-attestation not yet complete - allow retry via polling
|
|
12747
|
-
throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
|
|
12952
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
12953
|
+
* // Result: { continuationStep: null, isRetryable: false,
|
|
12954
|
+
* // reason: 'Bridge completed successfully' }
|
|
12955
|
+
* ```
|
|
12956
|
+
*/ const analyzeSteps = (bridgeResult)=>{
|
|
12957
|
+
// Input validation
|
|
12958
|
+
if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
|
|
12959
|
+
throw new Error('Invalid bridgeResult: must contain a steps array');
|
|
12748
12960
|
}
|
|
12749
|
-
|
|
12961
|
+
const { steps } = bridgeResult;
|
|
12962
|
+
// Build execution context from step history
|
|
12963
|
+
const context = buildFlowContext(steps);
|
|
12964
|
+
// Determine continuation logic using rule engine
|
|
12965
|
+
const continuation = determineContinuationFromRules(context);
|
|
12966
|
+
return {
|
|
12967
|
+
continuationStep: continuation.nextStep,
|
|
12968
|
+
isActionable: continuation.isActionable,
|
|
12969
|
+
completedSteps: Array.from(context.completedSteps),
|
|
12970
|
+
failedSteps: Array.from(context.failedSteps),
|
|
12971
|
+
reason: continuation.reason
|
|
12972
|
+
};
|
|
12750
12973
|
};
|
|
12751
12974
|
/**
|
|
12752
|
-
*
|
|
12753
|
-
*
|
|
12754
|
-
* This function is used after calling requestReAttestation() to wait until
|
|
12755
|
-
* the attestation is fully re-processed. The expirationBlock transitions
|
|
12756
|
-
* from non-zero to zero when Circle completes the re-attestation.
|
|
12757
|
-
*
|
|
12758
|
-
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
12759
|
-
* @param transactionHash - The transaction hash to fetch attestation for
|
|
12760
|
-
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
12761
|
-
* @param config - Optional configuration overrides
|
|
12762
|
-
* @returns The re-attested attestation response with expirationBlock === '0'
|
|
12763
|
-
* @throws If the request fails, times out, or expirationBlock never becomes 0
|
|
12764
|
-
*
|
|
12765
|
-
* @example
|
|
12766
|
-
* ```typescript
|
|
12767
|
-
* // After requesting re-attestation
|
|
12768
|
-
* await requestReAttestation(nonce, isTestnet)
|
|
12975
|
+
* Build flow context from the execution history.
|
|
12769
12976
|
*
|
|
12770
|
-
*
|
|
12771
|
-
*
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12977
|
+
* @param steps - Array of executed bridge steps.
|
|
12978
|
+
* @returns Flow context with execution state and history.
|
|
12979
|
+
*/ function buildFlowContext(steps) {
|
|
12980
|
+
const completedSteps = new Set();
|
|
12981
|
+
const failedSteps = new Set();
|
|
12982
|
+
let lastStep;
|
|
12983
|
+
// Process step history to build context
|
|
12984
|
+
for (const step of steps){
|
|
12985
|
+
if (step.state === 'success' || step.state === 'noop') {
|
|
12986
|
+
completedSteps.add(step.name);
|
|
12987
|
+
} else if (step.state === 'error') {
|
|
12988
|
+
failedSteps.add(step.name);
|
|
12989
|
+
}
|
|
12990
|
+
// Track the last step for continuation logic
|
|
12991
|
+
lastStep = {
|
|
12992
|
+
name: step.name,
|
|
12993
|
+
state: step.state
|
|
12994
|
+
};
|
|
12995
|
+
}
|
|
12996
|
+
return {
|
|
12997
|
+
completedSteps,
|
|
12998
|
+
failedSteps,
|
|
12999
|
+
...lastStep && {
|
|
13000
|
+
lastStep
|
|
13001
|
+
}
|
|
13002
|
+
};
|
|
13003
|
+
}
|
|
12779
13004
|
/**
|
|
12780
|
-
*
|
|
13005
|
+
* Determine continuation step using the rule engine.
|
|
12781
13006
|
*
|
|
12782
|
-
*
|
|
12783
|
-
*
|
|
13007
|
+
* @param context - The flow context with execution history.
|
|
13008
|
+
* @returns Continuation decision with next step and actionability information.
|
|
13009
|
+
*/ function determineContinuationFromRules(context) {
|
|
13010
|
+
const lastStepName = context.lastStep?.name;
|
|
13011
|
+
// Handle initial state when no steps have been executed
|
|
13012
|
+
if (lastStepName === undefined) {
|
|
13013
|
+
const rules = STEP_TRANSITION_RULES[''];
|
|
13014
|
+
const matchingRule = rules?.find((rule)=>rule.condition(context));
|
|
13015
|
+
if (!matchingRule) {
|
|
13016
|
+
return {
|
|
13017
|
+
nextStep: null,
|
|
13018
|
+
isActionable: false,
|
|
13019
|
+
reason: 'No initial state rule found'
|
|
13020
|
+
};
|
|
13021
|
+
}
|
|
13022
|
+
return {
|
|
13023
|
+
nextStep: matchingRule.nextStep,
|
|
13024
|
+
isActionable: matchingRule.isActionable,
|
|
13025
|
+
reason: matchingRule.reason
|
|
13026
|
+
};
|
|
13027
|
+
}
|
|
13028
|
+
// A step with an empty name is ambiguous and should be treated as an unrecoverable state.
|
|
13029
|
+
if (lastStepName === '') {
|
|
13030
|
+
return {
|
|
13031
|
+
nextStep: null,
|
|
13032
|
+
isActionable: false,
|
|
13033
|
+
reason: 'No transition rules defined for step with empty name'
|
|
13034
|
+
};
|
|
13035
|
+
}
|
|
13036
|
+
const rules = STEP_TRANSITION_RULES[lastStepName];
|
|
13037
|
+
if (!rules) {
|
|
13038
|
+
return {
|
|
13039
|
+
nextStep: null,
|
|
13040
|
+
isActionable: false,
|
|
13041
|
+
reason: `No transition rules defined for step: ${lastStepName}`
|
|
13042
|
+
};
|
|
13043
|
+
}
|
|
13044
|
+
// Find the first matching rule
|
|
13045
|
+
const matchingRule = rules.find((rule)=>rule.condition(context));
|
|
13046
|
+
if (!matchingRule) {
|
|
13047
|
+
return {
|
|
13048
|
+
nextStep: null,
|
|
13049
|
+
isActionable: false,
|
|
13050
|
+
reason: `No matching transition rule for current context`
|
|
13051
|
+
};
|
|
13052
|
+
}
|
|
13053
|
+
return {
|
|
13054
|
+
nextStep: matchingRule.nextStep,
|
|
13055
|
+
isActionable: matchingRule.isActionable,
|
|
13056
|
+
reason: matchingRule.reason
|
|
13057
|
+
};
|
|
13058
|
+
}
|
|
13059
|
+
|
|
13060
|
+
/**
|
|
13061
|
+
* Find a step by name in the bridge result.
|
|
12784
13062
|
*
|
|
12785
|
-
* @param
|
|
12786
|
-
* @param
|
|
12787
|
-
* @returns
|
|
13063
|
+
* @param result - The bridge result to search.
|
|
13064
|
+
* @param stepName - The name of the step to find.
|
|
13065
|
+
* @returns The step if found, undefined otherwise.
|
|
12788
13066
|
*
|
|
12789
13067
|
* @example
|
|
12790
13068
|
* ```typescript
|
|
12791
|
-
*
|
|
12792
|
-
* const mainnetUrl = buildReAttestUrl('0xabc', false)
|
|
12793
|
-
* // => 'https://iris-api.circle.com/v2/reattest/0xabc'
|
|
13069
|
+
* import { findStepByName } from './findStep'
|
|
12794
13070
|
*
|
|
12795
|
-
*
|
|
12796
|
-
*
|
|
12797
|
-
*
|
|
13071
|
+
* const burnStep = findStepByName(result, 'burn')
|
|
13072
|
+
* if (burnStep) {
|
|
13073
|
+
* console.log('Burn tx:', burnStep.txHash)
|
|
13074
|
+
* }
|
|
12798
13075
|
* ```
|
|
12799
|
-
*/
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
return url.toString();
|
|
12803
|
-
};
|
|
12804
|
-
/**
|
|
12805
|
-
* Type guard that validates the re-attestation API response structure.
|
|
12806
|
-
*
|
|
12807
|
-
* @param obj - The value to check, typically a parsed JSON response
|
|
12808
|
-
* @returns True if the object matches the ReAttestationResponse shape
|
|
12809
|
-
* @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
|
|
12810
|
-
* @internal
|
|
12811
|
-
*/ const isReAttestationResponse = (obj)=>{
|
|
12812
|
-
if (typeof obj !== 'object' || obj === null || !('message' in obj) || !('nonce' in obj) || typeof obj.message !== 'string' || typeof obj.nonce !== 'string') {
|
|
12813
|
-
throw new Error('Invalid re-attestation response structure');
|
|
12814
|
-
}
|
|
12815
|
-
return true;
|
|
12816
|
-
};
|
|
13076
|
+
*/ function findStepByName(result, stepName) {
|
|
13077
|
+
return result.steps.find((step)=>step.name === stepName);
|
|
13078
|
+
}
|
|
12817
13079
|
/**
|
|
12818
|
-
*
|
|
13080
|
+
* Find a pending step by name and return it with its index.
|
|
12819
13081
|
*
|
|
12820
|
-
*
|
|
12821
|
-
* attestation for a previously issued nonce. After calling this function,
|
|
12822
|
-
* you should poll `fetchAttestation` to retrieve the new attestation.
|
|
13082
|
+
* Searches for a step that matches both the step name and has a pending state.
|
|
12823
13083
|
*
|
|
12824
|
-
* @param
|
|
12825
|
-
* @param
|
|
12826
|
-
* @
|
|
12827
|
-
* @
|
|
12828
|
-
* @throws If the request fails, times out, or returns invalid data
|
|
13084
|
+
* @param result - The bridge result containing steps to search through.
|
|
13085
|
+
* @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
|
|
13086
|
+
* @returns An object containing the step and its index in the steps array.
|
|
13087
|
+
* @throws KitError if the specified pending step is not found.
|
|
12829
13088
|
*
|
|
12830
13089
|
* @example
|
|
12831
13090
|
* ```typescript
|
|
12832
|
-
*
|
|
12833
|
-
* const response = await requestReAttestation('0xabc', true)
|
|
12834
|
-
* console.log(response.message) // "Re-attestation successfully requested for nonce."
|
|
13091
|
+
* import { findPendingStep } from './findStep'
|
|
12835
13092
|
*
|
|
12836
|
-
*
|
|
12837
|
-
*
|
|
13093
|
+
* const { step, index } = findPendingStep(result, 'burn')
|
|
13094
|
+
* console.log('Pending step:', step.name, 'at index:', index)
|
|
12838
13095
|
* ```
|
|
12839
|
-
*/
|
|
12840
|
-
const
|
|
12841
|
-
|
|
12842
|
-
const effectiveConfig = mergeAttestationConfig(config, {
|
|
12843
|
-
maxRetries: 3
|
|
12844
|
-
});
|
|
12845
|
-
return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
|
|
12846
|
-
};
|
|
12847
|
-
|
|
12848
|
-
/**
|
|
12849
|
-
* Type guard that checks if the relayer has confirmed the mint transaction.
|
|
12850
|
-
*
|
|
12851
|
-
* This function validates that:
|
|
12852
|
-
* 1. The response has valid AttestationResponse structure
|
|
12853
|
-
* 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
|
|
12854
|
-
*
|
|
12855
|
-
* If forwardState is 'FAILED', throws a non-retryable KitError.
|
|
12856
|
-
* If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
|
|
12857
|
-
*
|
|
12858
|
-
* @param obj - The value to check, typically a parsed JSON response
|
|
12859
|
-
* @returns True if the relayer has confirmed the mint
|
|
12860
|
-
* @throws {KitError} With FATAL recoverability if structure is invalid
|
|
12861
|
-
* @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
|
|
12862
|
-
* @throws {KitError} With RETRYABLE recoverability if still pending
|
|
12863
|
-
* @internal
|
|
12864
|
-
*/ const isRelayerMintConfirmed = (obj)=>{
|
|
12865
|
-
// First check if the structure is valid
|
|
12866
|
-
if (!hasValidAttestationStructure(obj)) {
|
|
13096
|
+
*/ function findPendingStep(result, stepName) {
|
|
13097
|
+
const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
|
|
13098
|
+
if (index === -1) {
|
|
12867
13099
|
throw new KitError({
|
|
12868
13100
|
...InputError.VALIDATION_FAILED,
|
|
12869
13101
|
recoverability: 'FATAL',
|
|
12870
|
-
message:
|
|
13102
|
+
message: `Pending step "${stepName}" not found in result`
|
|
12871
13103
|
});
|
|
12872
13104
|
}
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
if (!message) {
|
|
13105
|
+
const step = result.steps[index];
|
|
13106
|
+
if (!step) {
|
|
12876
13107
|
throw new KitError({
|
|
12877
13108
|
...InputError.VALIDATION_FAILED,
|
|
12878
13109
|
recoverability: 'FATAL',
|
|
12879
|
-
message: '
|
|
13110
|
+
message: 'Pending step is undefined'
|
|
12880
13111
|
});
|
|
12881
13112
|
}
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
message: 'Circle relayer failed to forward the mint transaction. The mint may still have succeeded if another party submitted it. Check the recipient wallet balance before retrying. If the mint did not occur, you can manually submit it using the attestation data in the error cause.',
|
|
12888
|
-
cause: {
|
|
12889
|
-
trace: {
|
|
12890
|
-
eventNonce: message.eventNonce,
|
|
12891
|
-
attestation: message.attestation,
|
|
12892
|
-
message: message.message
|
|
12893
|
-
}
|
|
12894
|
-
}
|
|
12895
|
-
});
|
|
12896
|
-
}
|
|
12897
|
-
// Check if mint is confirmed (or complete) with a valid transaction hash
|
|
12898
|
-
// We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
|
|
12899
|
-
if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
|
|
12900
|
-
return true;
|
|
12901
|
-
}
|
|
12902
|
-
// Still pending or not yet processed - throw RETRYABLE error to continue polling
|
|
12903
|
-
throw new KitError({
|
|
12904
|
-
...NetworkError.RELAYER_PENDING,
|
|
12905
|
-
recoverability: 'RETRYABLE',
|
|
12906
|
-
message: 'Relayer mint not ready. Waiting for confirmation.'
|
|
12907
|
-
});
|
|
12908
|
-
};
|
|
13113
|
+
return {
|
|
13114
|
+
step,
|
|
13115
|
+
index
|
|
13116
|
+
};
|
|
13117
|
+
}
|
|
12909
13118
|
/**
|
|
12910
|
-
*
|
|
13119
|
+
* Get the burn transaction hash from bridge result.
|
|
12911
13120
|
*
|
|
12912
|
-
*
|
|
12913
|
-
*
|
|
12914
|
-
* This function polls until the relayer has submitted and confirmed the mint transaction.
|
|
13121
|
+
* @param result - The bridge result.
|
|
13122
|
+
* @returns The burn transaction hash, or undefined if not found.
|
|
12915
13123
|
*
|
|
12916
|
-
* @
|
|
12917
|
-
*
|
|
12918
|
-
*
|
|
12919
|
-
* - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
|
|
12920
|
-
* - Returns the attestation message with `forwardTxHash` populated
|
|
13124
|
+
* @example
|
|
13125
|
+
* ```typescript
|
|
13126
|
+
* import { getBurnTxHash } from './findStep'
|
|
12921
13127
|
*
|
|
12922
|
-
*
|
|
12923
|
-
*
|
|
12924
|
-
*
|
|
12925
|
-
*
|
|
12926
|
-
*
|
|
12927
|
-
|
|
12928
|
-
|
|
13128
|
+
* const burnTxHash = getBurnTxHash(result)
|
|
13129
|
+
* if (burnTxHash) {
|
|
13130
|
+
* console.log('Burn tx hash:', burnTxHash)
|
|
13131
|
+
* }
|
|
13132
|
+
* ```
|
|
13133
|
+
*/ function getBurnTxHash(result) {
|
|
13134
|
+
return findStepByName(result, CCTPv2StepName.burn)?.txHash;
|
|
13135
|
+
}
|
|
13136
|
+
/**
|
|
13137
|
+
* Get the attestation data from bridge result.
|
|
13138
|
+
*
|
|
13139
|
+
* @param result - The bridge result.
|
|
13140
|
+
* @returns The attestation data, or undefined if not found.
|
|
12929
13141
|
*
|
|
12930
13142
|
* @example
|
|
12931
13143
|
* ```typescript
|
|
12932
|
-
*
|
|
12933
|
-
*
|
|
13144
|
+
* import { getAttestationData } from './findStep'
|
|
13145
|
+
*
|
|
13146
|
+
* const attestation = getAttestationData(result)
|
|
13147
|
+
* if (attestation) {
|
|
13148
|
+
* console.log('Attestation:', attestation.message)
|
|
13149
|
+
* }
|
|
12934
13150
|
* ```
|
|
12935
|
-
*/
|
|
12936
|
-
|
|
12937
|
-
const
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
|
|
12941
|
-
} catch (error) {
|
|
12942
|
-
// Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
|
|
12943
|
-
if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
|
|
12944
|
-
throw new KitError({
|
|
12945
|
-
...NetworkError.RELAYER_FORWARD_FAILED,
|
|
12946
|
-
recoverability: error.recoverability,
|
|
12947
|
-
message: error.message,
|
|
12948
|
-
cause: {
|
|
12949
|
-
...error.cause,
|
|
12950
|
-
trace: {
|
|
12951
|
-
...error.cause?.trace,
|
|
12952
|
-
burnTxHash: transactionHash
|
|
12953
|
-
}
|
|
12954
|
-
}
|
|
12955
|
-
});
|
|
12956
|
-
}
|
|
12957
|
-
throw error;
|
|
12958
|
-
}
|
|
12959
|
-
// Return the first message (which should have forwardTxHash)
|
|
12960
|
-
// Note: This check is needed for TypeScript type safety even though
|
|
12961
|
-
// isRelayerMintConfirmed validates messages[0] exists. The type guard
|
|
12962
|
-
// narrows the type at the call site, but TypeScript can't infer that
|
|
12963
|
-
// the array still has elements after pollApiGet returns.
|
|
12964
|
-
const message = response.messages[0];
|
|
12965
|
-
if (!message) {
|
|
12966
|
-
throw new KitError({
|
|
12967
|
-
...InputError.VALIDATION_FAILED,
|
|
12968
|
-
recoverability: 'FATAL',
|
|
12969
|
-
message: 'No attestation messages found in response after polling.'
|
|
12970
|
-
});
|
|
13151
|
+
*/ function getAttestationData(result) {
|
|
13152
|
+
// Prefer reAttest data (most recent attestation after expiry)
|
|
13153
|
+
const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
|
|
13154
|
+
if (reAttestStep?.state === 'success' && reAttestStep.data) {
|
|
13155
|
+
return reAttestStep.data;
|
|
12971
13156
|
}
|
|
12972
|
-
|
|
12973
|
-
|
|
13157
|
+
// Fall back to fetchAttestation step
|
|
13158
|
+
const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
|
|
13159
|
+
return fetchStep?.data;
|
|
13160
|
+
}
|
|
12974
13161
|
|
|
12975
|
-
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
12976
13162
|
/**
|
|
12977
|
-
*
|
|
12978
|
-
* The validation includes:
|
|
12979
|
-
* - Basic wallet context validation (adapter, address, chain)
|
|
12980
|
-
* - CCTPv2-specific chain validation (must be an EVM chain)
|
|
13163
|
+
* Check if the analysis indicates a non-actionable pending state.
|
|
12981
13164
|
*
|
|
12982
|
-
*
|
|
12983
|
-
*
|
|
13165
|
+
* A pending state is non-actionable when there's a continuation step but
|
|
13166
|
+
* the analysis marks it as not actionable, typically because we need to
|
|
13167
|
+
* wait for an ongoing operation to complete.
|
|
13168
|
+
*
|
|
13169
|
+
* @param analysis - The step analysis result from analyzeSteps.
|
|
13170
|
+
* @param result - The bridge result to check for pending steps.
|
|
13171
|
+
* @returns True if there is a pending step that we should wait for.
|
|
12984
13172
|
*
|
|
12985
13173
|
* @example
|
|
12986
13174
|
* ```typescript
|
|
12987
|
-
* import {
|
|
12988
|
-
* import {
|
|
13175
|
+
* import { hasPendingState } from './stepUtils'
|
|
13176
|
+
* import { analyzeSteps } from '../analyzeSteps'
|
|
12989
13177
|
*
|
|
12990
|
-
*
|
|
12991
|
-
*
|
|
12992
|
-
*
|
|
12993
|
-
* prepare: async () => ({ data: 'prepared transaction' }),
|
|
12994
|
-
* waitForTransaction: async () => ({ status: 'confirmed' })
|
|
12995
|
-
* },
|
|
12996
|
-
* address: '0x1234567890123456789012345678901234567890',
|
|
12997
|
-
* chain: {
|
|
12998
|
-
* ...Ethereum,
|
|
12999
|
-
* usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
13000
|
-
* cctp: {
|
|
13001
|
-
* domain: 1,
|
|
13002
|
-
* contracts: {
|
|
13003
|
-
* v2: {
|
|
13004
|
-
* tokenMessenger: '0xTokenMessenger',
|
|
13005
|
-
* messageTransmitter: '0xMessageTransmitter'
|
|
13006
|
-
* }
|
|
13007
|
-
* }
|
|
13008
|
-
* }
|
|
13009
|
-
* }
|
|
13178
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
13179
|
+
* if (hasPendingState(analysis, bridgeResult)) {
|
|
13180
|
+
* // Wait for the pending operation to complete
|
|
13010
13181
|
* }
|
|
13182
|
+
* ```
|
|
13183
|
+
*/ /**
|
|
13184
|
+
* Evaluate a transaction receipt and return the corresponding step state
|
|
13185
|
+
* and error message. Centralises the success/revert/unconfirmed logic so
|
|
13186
|
+
* every call-site behaves identically.
|
|
13011
13187
|
*
|
|
13012
|
-
*
|
|
13013
|
-
*
|
|
13188
|
+
* @param receipt - The transaction receipt containing status and block info.
|
|
13189
|
+
* @param txHash - The transaction hash used in error messages.
|
|
13190
|
+
* @returns An object with `state` and an optional `errorMessage`.
|
|
13014
13191
|
*
|
|
13015
|
-
*
|
|
13016
|
-
*
|
|
13192
|
+
* @example
|
|
13193
|
+
* ```typescript
|
|
13194
|
+
* const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
|
|
13195
|
+
* step.state = outcome.state
|
|
13196
|
+
* if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
|
|
13017
13197
|
* ```
|
|
13018
|
-
*/ function
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
// Validate USDC support
|
|
13024
|
-
if (context.chain.usdcAddress === null) {
|
|
13025
|
-
throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
|
|
13198
|
+
*/ function evaluateTransactionOutcome(receipt, txHash) {
|
|
13199
|
+
if (receipt.status === 'success' && receipt.blockNumber) {
|
|
13200
|
+
return {
|
|
13201
|
+
state: 'success'
|
|
13202
|
+
};
|
|
13026
13203
|
}
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13204
|
+
return {
|
|
13205
|
+
state: 'error',
|
|
13206
|
+
errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
|
|
13207
|
+
};
|
|
13208
|
+
}
|
|
13209
|
+
function hasPendingState(analysis, result) {
|
|
13210
|
+
// Check if there's a continuation step that's marked as non-actionable
|
|
13211
|
+
if (analysis.continuationStep === null || analysis.isActionable) {
|
|
13212
|
+
return false;
|
|
13030
13213
|
}
|
|
13214
|
+
// Verify that the continuation step actually exists and is in pending state
|
|
13215
|
+
const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
|
|
13216
|
+
return pendingStep !== undefined;
|
|
13031
13217
|
}
|
|
13032
|
-
|
|
13033
|
-
const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
13034
13218
|
/**
|
|
13035
|
-
*
|
|
13036
|
-
* The validation includes:
|
|
13037
|
-
* - Basic parameter structure and types
|
|
13038
|
-
* - Amount validation (non-empty numeric string \> 0)
|
|
13039
|
-
* - Wallet address format validation (must be valid Ethereum address)
|
|
13040
|
-
* - Chain definition validation (must be a valid chain with required properties)
|
|
13041
|
-
* - Adapter validation (must implement required methods)
|
|
13042
|
-
* - Optional config validation (transfer speed and max fee)
|
|
13043
|
-
* - Network compatibility (source and destination chains must both be testnet or both mainnet)
|
|
13044
|
-
* - CCTPv2-specific wallet context validations
|
|
13219
|
+
* Check if the step is the last one in the execution flow.
|
|
13045
13220
|
*
|
|
13046
|
-
* @param
|
|
13047
|
-
* @
|
|
13221
|
+
* @param step - The step object to check.
|
|
13222
|
+
* @param stepNames - The ordered list of step names in the execution flow.
|
|
13223
|
+
* @returns True if this is the last step in the flow.
|
|
13048
13224
|
*
|
|
13049
13225
|
* @example
|
|
13050
13226
|
* ```typescript
|
|
13051
|
-
* import {
|
|
13052
|
-
* import { Ethereum, Base } from '@core/chains'
|
|
13227
|
+
* import { isLastStep } from './stepUtils'
|
|
13053
13228
|
*
|
|
13054
|
-
*
|
|
13055
|
-
*
|
|
13056
|
-
*
|
|
13057
|
-
* source: {
|
|
13058
|
-
* adapter: sourceAdapter,
|
|
13059
|
-
* address: '0xSourceAddress',
|
|
13060
|
-
* chain: {
|
|
13061
|
-
* ...Ethereum,
|
|
13062
|
-
* cctp: {
|
|
13063
|
-
* domain: 1,
|
|
13064
|
-
* contracts: {
|
|
13065
|
-
* v2: {
|
|
13066
|
-
* tokenMessenger: '0xTokenMessenger',
|
|
13067
|
-
* messageTransmitter: '0xMessageTransmitter'
|
|
13068
|
-
* }
|
|
13069
|
-
* }
|
|
13070
|
-
* }
|
|
13071
|
-
* }
|
|
13072
|
-
* },
|
|
13073
|
-
* destination: {
|
|
13074
|
-
* adapter: destAdapter,
|
|
13075
|
-
* address: '0xDestAddress',
|
|
13076
|
-
* chain: {
|
|
13077
|
-
* ...Base,
|
|
13078
|
-
* cctp: {
|
|
13079
|
-
* domain: 2,
|
|
13080
|
-
* contracts: {
|
|
13081
|
-
* v2: {
|
|
13082
|
-
* tokenMessenger: '0xTokenMessenger',
|
|
13083
|
-
* messageTransmitter: '0xMessageTransmitter'
|
|
13084
|
-
* }
|
|
13085
|
-
* }
|
|
13086
|
-
* }
|
|
13087
|
-
* }
|
|
13088
|
-
* },
|
|
13089
|
-
* token: 'USDC',
|
|
13090
|
-
* config: {
|
|
13091
|
-
* transferSpeed: 'FAST',
|
|
13092
|
-
* maxFee: '1000000'
|
|
13093
|
-
* }
|
|
13094
|
-
* }
|
|
13095
|
-
*
|
|
13096
|
-
* // This will throw if validation fails
|
|
13097
|
-
* assertCCTPv2BridgeParams(params)
|
|
13098
|
-
*
|
|
13099
|
-
* // If we get here, params is guaranteed to be valid
|
|
13100
|
-
* console.log('CCTPv2 transfer parameters are valid')
|
|
13229
|
+
* const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
|
|
13230
|
+
* isLastStep({ name: 'mint' }, stepNames) // true
|
|
13231
|
+
* isLastStep({ name: 'burn' }, stepNames) // false
|
|
13101
13232
|
* ```
|
|
13102
|
-
*/ function
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
// After validation, we know params is CCTPV2BridgeParams
|
|
13106
|
-
const bridgeParams = params;
|
|
13107
|
-
// Enforce that source and destination chains are either testnet or mainnet
|
|
13108
|
-
if (bridgeParams.source.chain.isTestnet !== bridgeParams.destination.chain.isTestnet) {
|
|
13109
|
-
throw createNetworkMismatchError(bridgeParams.source.chain, bridgeParams.destination.chain);
|
|
13110
|
-
}
|
|
13111
|
-
assertCCTPV2Support(bridgeParams.source.chain, bridgeParams.destination.chain);
|
|
13112
|
-
// Validate that the destination chain supports forwarding when forwarder is enabled
|
|
13113
|
-
assertForwarderRouteSupport(bridgeParams.source.chain, bridgeParams.destination.chain, bridgeParams.destination.useForwarder);
|
|
13114
|
-
/**
|
|
13115
|
-
* Enforce that if fee is defined then feeRecipient must be defined.
|
|
13116
|
-
* We do not do this in the validation function itself because we want to allow
|
|
13117
|
-
* optional properties when calling `provider.bridge()` due to the custom fee
|
|
13118
|
-
* configuration being possible at the kit level as well.
|
|
13119
|
-
*/ if (bridgeParams.config?.customFee?.value !== undefined && bridgeParams.config?.customFee?.recipientAddress === undefined) {
|
|
13120
|
-
throw createValidationFailedError$1('recipientAddress', bridgeParams.config.customFee.value, 'Custom fee is defined but fee recipient is not. Please provide a fee recipient.');
|
|
13121
|
-
}
|
|
13122
|
-
// Check if this is a forwarder-only destination (no adapter, requires useForwarder: true)
|
|
13123
|
-
const isForwarderOnly = bridgeParams.destination.useForwarder === true && !('adapter' in bridgeParams.destination && bridgeParams.destination.adapter);
|
|
13124
|
-
// Forwarder-only destinations require recipientAddress
|
|
13125
|
-
if (isForwarderOnly) {
|
|
13126
|
-
if (!bridgeParams.destination.recipientAddress?.trim()) {
|
|
13127
|
-
throw createValidationFailedError$1('recipientAddress', bridgeParams.destination.recipientAddress, 'recipientAddress is required when using forwarder without a destination adapter.');
|
|
13128
|
-
}
|
|
13129
|
-
}
|
|
13130
|
-
// Validate CCTP v2 specific requirements for source wallet
|
|
13131
|
-
assertCCTPv2WalletContext(bridgeParams.source);
|
|
13132
|
-
// Validate that source adapter supports the chain (defense-in-depth)
|
|
13133
|
-
bridgeParams.source.adapter.validateChainSupport(bridgeParams.source.chain);
|
|
13134
|
-
// Only validate destination wallet context and adapter if not forwarder-only
|
|
13135
|
-
if (!isForwarderOnly) {
|
|
13136
|
-
assertCCTPv2WalletContext(bridgeParams.destination);
|
|
13137
|
-
// Validate that destination adapter supports the chain (defense-in-depth)
|
|
13138
|
-
bridgeParams.destination.adapter.validateChainSupport(bridgeParams.destination.chain);
|
|
13139
|
-
}
|
|
13233
|
+
*/ function isLastStep(step, stepNames) {
|
|
13234
|
+
const stepIndex = stepNames.indexOf(step.name);
|
|
13235
|
+
return stepIndex === -1 || stepIndex >= stepNames.length - 1;
|
|
13140
13236
|
}
|
|
13141
13237
|
/**
|
|
13142
|
-
*
|
|
13143
|
-
*/ /**
|
|
13144
|
-
* Throws a KitError if the given chain does not support CCTP v2.
|
|
13238
|
+
* Wait for a pending transaction to complete.
|
|
13145
13239
|
*
|
|
13146
|
-
*
|
|
13147
|
-
*
|
|
13148
|
-
* @param isSource - Whether this is the source chain (for error context)
|
|
13149
|
-
*/ function assertCCTPV2Support(source, destination) {
|
|
13150
|
-
if (!isCCTPV2Supported(source) || !isCCTPV2Supported(destination)) {
|
|
13151
|
-
throw createUnsupportedRouteError(source.name, destination.name);
|
|
13152
|
-
}
|
|
13153
|
-
}
|
|
13154
|
-
/**
|
|
13155
|
-
* Validates that the forwarder (relaying) feature is compatible with the route.
|
|
13240
|
+
* Poll the adapter until the transaction is confirmed on-chain and return
|
|
13241
|
+
* the updated step with success or error state based on the receipt.
|
|
13156
13242
|
*
|
|
13157
|
-
*
|
|
13158
|
-
*
|
|
13243
|
+
* @param pendingStep - The full step object containing the transaction hash.
|
|
13244
|
+
* @param adapter - The adapter to use for waiting.
|
|
13245
|
+
* @param chain - The chain where the transaction was submitted.
|
|
13246
|
+
* @returns The updated step object with success or error state.
|
|
13159
13247
|
*
|
|
13160
|
-
* @
|
|
13161
|
-
* @param destination - The destination chain definition
|
|
13162
|
-
* @param useForwarder - Whether the forwarder is enabled on the destination
|
|
13163
|
-
* @throws {KitError} If the forwarder is enabled and the destination chain does not support forwarding
|
|
13164
|
-
*/ function assertForwarderRouteSupport(source, destination, useForwarder) {
|
|
13165
|
-
if (useForwarder === true && !destination.cctp?.forwarderSupported.destination) {
|
|
13166
|
-
throw new KitError({
|
|
13167
|
-
...InputError.UNSUPPORTED_ROUTE,
|
|
13168
|
-
recoverability: 'FATAL',
|
|
13169
|
-
message: `Route from ${source.name} to ${destination.name} with forwarder is not supported (destination chain does not support forwarding).`,
|
|
13170
|
-
cause: {
|
|
13171
|
-
trace: {
|
|
13172
|
-
source: source.name,
|
|
13173
|
-
destination: destination.name
|
|
13174
|
-
}
|
|
13175
|
-
}
|
|
13176
|
-
});
|
|
13177
|
-
}
|
|
13178
|
-
}
|
|
13179
|
-
|
|
13180
|
-
/**
|
|
13181
|
-
* Checks if a decoded attestation field matches the corresponding transfer parameter.
|
|
13182
|
-
* If the values do not match, appends a descriptive error message to the errors array.
|
|
13248
|
+
* @throws KitError when the pending step has no transaction hash.
|
|
13183
13249
|
*
|
|
13184
|
-
* @
|
|
13185
|
-
*
|
|
13186
|
-
*
|
|
13187
|
-
* @param errors - The array to which error messages will be appended if a mismatch is found
|
|
13188
|
-
*/ function checkFieldMismatch(field, decoded, param, errors) {
|
|
13189
|
-
if (decoded !== param) {
|
|
13190
|
-
errors.push(`${field} mismatch: decoded=${String(decoded)}, params=${String(param)}`);
|
|
13191
|
-
}
|
|
13192
|
-
}
|
|
13193
|
-
/**
|
|
13194
|
-
* Asserts that the decoded message from attestation matches the provided transfer params.
|
|
13195
|
-
* Throws KitError if any field mismatches, with clear error messages.
|
|
13250
|
+
* @example
|
|
13251
|
+
* ```typescript
|
|
13252
|
+
* import { waitForPendingTransaction } from './bridgeStepUtils'
|
|
13196
13253
|
*
|
|
13197
|
-
*
|
|
13198
|
-
*
|
|
13199
|
-
*
|
|
13200
|
-
|
|
13201
|
-
|
|
13202
|
-
|
|
13203
|
-
const messageBody = message.decodedMessageBody;
|
|
13204
|
-
// Use recipientAddress if provided, otherwise use destination.address
|
|
13205
|
-
const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
|
|
13206
|
-
const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
|
|
13207
|
-
let sender;
|
|
13208
|
-
if (hasCustomContractSupport(params.source.chain, 'bridge')) {
|
|
13209
|
-
if (params.source.chain.type === 'solana') {
|
|
13210
|
-
// Solana: User → Bridge contract → CCTP (user remains sender)
|
|
13211
|
-
sender = params.source.address;
|
|
13212
|
-
} else {
|
|
13213
|
-
// Other chains (like EVM): Bridge contract → CCTP (bridge contract becomes sender)
|
|
13214
|
-
sender = params.source.chain.kitContracts?.bridge;
|
|
13215
|
-
}
|
|
13216
|
-
} else {
|
|
13217
|
-
sender = params.source.address;
|
|
13218
|
-
}
|
|
13219
|
-
checkFieldMismatch('sourceDomain', message.sourceDomain, params.source.chain.cctp.domain.toString(), errors);
|
|
13220
|
-
checkFieldMismatch('destinationDomain', message.destinationDomain, params.destination.chain.cctp.domain.toString(), errors);
|
|
13221
|
-
checkFieldMismatch('minFinalityThreshold', message.minFinalityThreshold, CCTPv2MinFinalityThreshold[params.config.transferSpeed ?? 'FAST'].toString(), errors);
|
|
13222
|
-
checkFieldMismatch('sender', params.source.chain.type === 'evm' ? messageBody.messageSender.toLowerCase() : messageBody.messageSender, params.source.chain.type === 'evm' ? sender?.toLowerCase() : sender, errors);
|
|
13223
|
-
checkFieldMismatch('recipient', params.destination.chain.type === 'evm' ? messageBody.mintRecipient.toLowerCase() : messageBody.mintRecipient, params.destination.chain.type === 'evm' ? mintRecipient.toLowerCase() : mintRecipient, errors);
|
|
13224
|
-
checkFieldMismatch('amount', messageBody.amount, params.amount.toString(), errors);
|
|
13225
|
-
checkFieldMismatch('burnToken', messageBody.burnToken.toLowerCase(), params.source.chain.usdcAddress.toLowerCase(), errors);
|
|
13226
|
-
if (errors.length > 0) {
|
|
13227
|
-
const errorMessage = 'Attestation validation failed: received attestation does not match expected transfer parameters';
|
|
13228
|
-
const firstError = errors[0] ?? '';
|
|
13254
|
+
* const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
|
|
13255
|
+
* const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
|
|
13256
|
+
* // updatedStep.state is now 'success' or 'error'
|
|
13257
|
+
* ```
|
|
13258
|
+
*/ async function waitForPendingTransaction(pendingStep, adapter, chain) {
|
|
13259
|
+
if (!pendingStep.txHash) {
|
|
13229
13260
|
throw new KitError({
|
|
13230
13261
|
...InputError.VALIDATION_FAILED,
|
|
13231
13262
|
recoverability: 'FATAL',
|
|
13232
|
-
message:
|
|
13233
|
-
cause: {
|
|
13234
|
-
trace: {
|
|
13235
|
-
validationErrors: errors
|
|
13236
|
-
}
|
|
13237
|
-
}
|
|
13263
|
+
message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
|
|
13238
13264
|
});
|
|
13239
13265
|
}
|
|
13266
|
+
const txHash = pendingStep.txHash;
|
|
13267
|
+
const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
|
|
13268
|
+
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
13269
|
+
chain: chain.name,
|
|
13270
|
+
txHash
|
|
13271
|
+
}))
|
|
13272
|
+
});
|
|
13273
|
+
const outcome = evaluateTransactionOutcome(txReceipt, txHash);
|
|
13274
|
+
return {
|
|
13275
|
+
...pendingStep,
|
|
13276
|
+
state: outcome.state,
|
|
13277
|
+
data: txReceipt,
|
|
13278
|
+
explorerUrl: buildExplorerUrl(chain, txHash),
|
|
13279
|
+
...outcome.errorMessage ? {
|
|
13280
|
+
errorMessage: outcome.errorMessage
|
|
13281
|
+
} : {}
|
|
13282
|
+
};
|
|
13240
13283
|
}
|
|
13241
|
-
|
|
13242
13284
|
/**
|
|
13243
|
-
*
|
|
13285
|
+
* Wait for a pending step to complete.
|
|
13244
13286
|
*
|
|
13245
|
-
*
|
|
13246
|
-
*
|
|
13247
|
-
* inputs always produce typed `KitError` validation failures.
|
|
13287
|
+
* For transaction steps: waits for the transaction to be confirmed.
|
|
13288
|
+
* For attestation: re-executes the attestation fetch.
|
|
13248
13289
|
*
|
|
13249
|
-
*
|
|
13250
|
-
* -
|
|
13251
|
-
* -
|
|
13252
|
-
*
|
|
13253
|
-
*
|
|
13254
|
-
*
|
|
13255
|
-
* -
|
|
13256
|
-
*
|
|
13257
|
-
*
|
|
13258
|
-
* - `feeToken` — valid EVM address (`0x` + 40 hex chars)
|
|
13259
|
-
* - `claim.signedQuote` — valid `0x`-prefixed hex string
|
|
13260
|
-
* - `claim.refundAddress` — valid EVM address
|
|
13261
|
-
* - `hookData` — valid `0x`-prefixed hex string when present
|
|
13290
|
+
* @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
|
|
13291
|
+
* @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
|
|
13292
|
+
* @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
|
|
13293
|
+
* @param adapter - The adapter to use.
|
|
13294
|
+
* @param chain - The chain where the step is executing.
|
|
13295
|
+
* @param context - The retry context.
|
|
13296
|
+
* @param result - The bridge result.
|
|
13297
|
+
* @param provider - The CCTP v2 bridging provider.
|
|
13298
|
+
* @returns The resolved step object with updated state.
|
|
13262
13299
|
*
|
|
13263
|
-
* @
|
|
13264
|
-
* @throws {KitError} If any field is missing or invalid.
|
|
13300
|
+
* @throws KitError when fetching attestation but burn transaction hash is not found.
|
|
13265
13301
|
*
|
|
13266
13302
|
* @example
|
|
13267
13303
|
* ```typescript
|
|
13268
|
-
*
|
|
13269
|
-
*
|
|
13270
|
-
* const {
|
|
13304
|
+
* import { waitForStepToComplete } from './bridgeStepUtils'
|
|
13305
|
+
*
|
|
13306
|
+
* const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
|
|
13307
|
+
* const updatedStep = await waitForStepToComplete(
|
|
13308
|
+
* pendingStep,
|
|
13309
|
+
* adapter,
|
|
13310
|
+
* chain,
|
|
13311
|
+
* context,
|
|
13312
|
+
* result,
|
|
13313
|
+
* provider,
|
|
13314
|
+
* )
|
|
13315
|
+
* // updatedStep.state is now 'success' or 'error'
|
|
13271
13316
|
* ```
|
|
13272
|
-
*/ function
|
|
13273
|
-
if (
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
if (source.chain.name === dest.name) {
|
|
13295
|
-
throw createUnsupportedRouteError(source.chain.name, dest.name);
|
|
13296
|
-
}
|
|
13297
|
-
// executor
|
|
13298
|
-
const executor = p['executor'];
|
|
13299
|
-
if (typeof executor !== 'string' || executor === '') {
|
|
13300
|
-
throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
|
|
13301
|
-
}
|
|
13302
|
-
// amount
|
|
13303
|
-
const rawAmount = p['amount'];
|
|
13304
|
-
if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
|
|
13305
|
-
throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
|
|
13317
|
+
*/ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
|
|
13318
|
+
if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
|
|
13319
|
+
// For attestation, re-run the fetch (it has built-in polling)
|
|
13320
|
+
const burnTxHash = getBurnTxHash(result);
|
|
13321
|
+
if (!burnTxHash) {
|
|
13322
|
+
throw new KitError({
|
|
13323
|
+
...InputError.VALIDATION_FAILED,
|
|
13324
|
+
recoverability: 'FATAL',
|
|
13325
|
+
message: 'Cannot fetch attestation: burn transaction hash not found'
|
|
13326
|
+
});
|
|
13327
|
+
}
|
|
13328
|
+
const sourceAddress = result.source.address;
|
|
13329
|
+
const attestation = await provider.fetchAttestation({
|
|
13330
|
+
chain: result.source.chain,
|
|
13331
|
+
adapter: context.from,
|
|
13332
|
+
address: sourceAddress
|
|
13333
|
+
}, burnTxHash);
|
|
13334
|
+
return {
|
|
13335
|
+
...pendingStep,
|
|
13336
|
+
state: 'success',
|
|
13337
|
+
data: attestation
|
|
13338
|
+
};
|
|
13306
13339
|
}
|
|
13340
|
+
// For transaction steps, wait for the transaction to complete
|
|
13341
|
+
return waitForPendingTransaction(pendingStep, adapter, chain);
|
|
13342
|
+
}
|
|
13343
|
+
|
|
13344
|
+
/**
|
|
13345
|
+
* Multiplier applied to a successful gas estimate before it is submitted.
|
|
13346
|
+
*
|
|
13347
|
+
* Estimates are exact, not padded: Sei returns 109_739 for an approve that
|
|
13348
|
+
* consumes 107_717 (1.9% headroom). Chains that price storage in large steps
|
|
13349
|
+
* can exceed the estimate if state changes between estimation and inclusion,
|
|
13350
|
+
* so the estimate is padded before use.
|
|
13351
|
+
*
|
|
13352
|
+
* @remarks
|
|
13353
|
+
* This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
|
|
13354
|
+
* scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
|
|
13355
|
+
* covers a slot that exists at estimation time and is consumed before
|
|
13356
|
+
* inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
|
|
13357
|
+
* the estimate covers it. For burn the buffer does cover a step (25% of
|
|
13358
|
+
* ~300_000 exceeds 51_500).
|
|
13359
|
+
*/ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
|
|
13360
|
+
/**
|
|
13361
|
+
* Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
|
|
13362
|
+
*
|
|
13363
|
+
* Estimates first so chains whose real cost exceeds the floor are covered by
|
|
13364
|
+
* their own measurement, and falls back to the floor whenever estimation is
|
|
13365
|
+
* unavailable or under-reports. Estimation failure is never fatal here: before
|
|
13366
|
+
* floors existed these requests were submitted with a pinned limit and no
|
|
13367
|
+
* estimate at all, so degrading to the floor is never worse than the previous
|
|
13368
|
+
* behaviour.
|
|
13369
|
+
*
|
|
13370
|
+
* @param request - The prepared EVM request to size a gas limit for
|
|
13371
|
+
* @param gasFloor - The minimum gas limit to submit, in gas units
|
|
13372
|
+
* @returns The gas limit to submit, in gas units
|
|
13373
|
+
* @throws Never — estimation failures degrade to `gasFloor`
|
|
13374
|
+
*
|
|
13375
|
+
* @example
|
|
13376
|
+
* ```typescript
|
|
13377
|
+
* const gasLimit = await resolveGasLimit(request, 150_000)
|
|
13378
|
+
* ```
|
|
13379
|
+
*/ const resolveGasLimit = async (request, gasFloor)=>{
|
|
13307
13380
|
try {
|
|
13308
|
-
|
|
13381
|
+
// Deliberately called without a `fallback`: both the viem and ethers
|
|
13382
|
+
// adapters *return* the supplied fallback object when estimation reverts
|
|
13383
|
+
// rather than throwing, which would set the estimate to the floor and then
|
|
13384
|
+
// multiply it by the buffer below. Omitting it routes reverts through the
|
|
13385
|
+
// catch, so a failed estimate degrades to exactly the floor.
|
|
13386
|
+
const estimate = await request.estimate();
|
|
13387
|
+
// The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
|
|
13388
|
+
// typed `bigint`, but adapters are a public extension point and may be
|
|
13389
|
+
// implemented in plain JS, so a non-bigint `gas` would throw here
|
|
13390
|
+
// ("Cannot mix BigInt and other types"). Guarding it keeps the documented
|
|
13391
|
+
// contract — estimation never aborts a step, it degrades to the floor.
|
|
13392
|
+
const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
|
|
13393
|
+
// Convert before comparing: Math.max throws on BigInt operands, and gas
|
|
13394
|
+
// units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
|
|
13395
|
+
return Math.max(Number(buffered), gasFloor);
|
|
13309
13396
|
} catch {
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
// feeTotalAmount
|
|
13313
|
-
const rawFee = p['feeTotalAmount'];
|
|
13314
|
-
if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
|
|
13315
|
-
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
|
|
13397
|
+
// Estimation is best-effort; the floor is the known-safe value.
|
|
13398
|
+
return gasFloor;
|
|
13316
13399
|
}
|
|
13400
|
+
};
|
|
13401
|
+
/**
|
|
13402
|
+
* Executes a prepared chain request and returns the result as a bridge step.
|
|
13403
|
+
*
|
|
13404
|
+
* This function takes a prepared chain request (containing transaction data) and executes
|
|
13405
|
+
* it using the appropriate adapter. It handles the execution details and formats
|
|
13406
|
+
* the result as a standardized bridge step with transaction details and explorer URLs.
|
|
13407
|
+
*
|
|
13408
|
+
* @param params - The execution parameters containing:
|
|
13409
|
+
* - `name`: The name of the step
|
|
13410
|
+
* - `request`: The prepared chain request containing transaction data
|
|
13411
|
+
* - `adapter`: The adapter that will execute the transaction
|
|
13412
|
+
* - `confirmations`: The number of confirmations to wait for (defaults to 1)
|
|
13413
|
+
* - `timeout`: The timeout for the request in milliseconds
|
|
13414
|
+
* - `gasFloor`: Optional minimum gas limit (number); the request is submitted
|
|
13415
|
+
* with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
|
|
13416
|
+
* @returns The bridge step with the transaction details and explorer URL
|
|
13417
|
+
* @throws If the transaction execution fails
|
|
13418
|
+
*
|
|
13419
|
+
* @example
|
|
13420
|
+
* ```typescript
|
|
13421
|
+
* const step = await executePreparedChainRequest({
|
|
13422
|
+
* name: 'approve',
|
|
13423
|
+
* request: preparedRequest,
|
|
13424
|
+
* adapter: adapter,
|
|
13425
|
+
* confirmations: 2,
|
|
13426
|
+
* timeout: 30000
|
|
13427
|
+
* })
|
|
13428
|
+
* console.log('Transaction hash:', step.txHash)
|
|
13429
|
+
* ```
|
|
13430
|
+
*/ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
|
|
13431
|
+
const step = {
|
|
13432
|
+
name,
|
|
13433
|
+
state: 'pending'
|
|
13434
|
+
};
|
|
13317
13435
|
try {
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13436
|
+
/**
|
|
13437
|
+
* No-op requests are not executed.
|
|
13438
|
+
* We return a noop step instead.
|
|
13439
|
+
*/ if (request.type === 'noop') {
|
|
13440
|
+
step.state = 'noop';
|
|
13441
|
+
return step;
|
|
13442
|
+
}
|
|
13443
|
+
const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
|
|
13444
|
+
gasLimit: await resolveGasLimit(request, gasFloor)
|
|
13445
|
+
}) : await request.execute();
|
|
13446
|
+
step.txHash = txHash;
|
|
13447
|
+
const retryOptions = {
|
|
13448
|
+
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
13449
|
+
chain: chain.name,
|
|
13450
|
+
txHash
|
|
13451
|
+
}))
|
|
13452
|
+
};
|
|
13453
|
+
if (timeout !== undefined) {
|
|
13454
|
+
retryOptions.deadlineMs = Date.now() + timeout;
|
|
13455
|
+
}
|
|
13456
|
+
const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
|
|
13457
|
+
confirmations,
|
|
13458
|
+
timeout
|
|
13459
|
+
}, chain), retryOptions);
|
|
13460
|
+
const outcome = evaluateTransactionOutcome(transaction, txHash);
|
|
13461
|
+
step.state = outcome.state;
|
|
13462
|
+
step.data = transaction;
|
|
13463
|
+
// Generate explorer URL for the step
|
|
13464
|
+
step.explorerUrl = buildExplorerUrl(chain, txHash);
|
|
13465
|
+
if (outcome.errorMessage) {
|
|
13466
|
+
step.errorMessage = outcome.errorMessage;
|
|
13467
|
+
// Transaction was mined but reverted on-chain.
|
|
13468
|
+
step.errorCategory = 'chain_revert';
|
|
13469
|
+
}
|
|
13470
|
+
} catch (err) {
|
|
13471
|
+
step.state = 'error';
|
|
13472
|
+
step.error = err;
|
|
13473
|
+
// Sequential path does not yet attempt fine-grained classification of
|
|
13474
|
+
// pre-submission errors (user_rejected, capability errors, etc.). Mark
|
|
13475
|
+
// as `unknown` so consumers can at least detect the category is
|
|
13476
|
+
// populated uniformly across batched and sequential flows.
|
|
13477
|
+
step.errorCategory = 'unknown';
|
|
13478
|
+
// Optionally parse for common blockchain error formats
|
|
13479
|
+
if (err instanceof Error) {
|
|
13480
|
+
step.errorMessage = err.message;
|
|
13481
|
+
} else if (typeof err === 'object' && err != null && 'message' in err) {
|
|
13482
|
+
step.errorMessage = String(err.message);
|
|
13483
|
+
} else {
|
|
13484
|
+
step.errorMessage = `Unknown error occurred during ${name} step.`;
|
|
13485
|
+
}
|
|
13325
13486
|
}
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13487
|
+
return step;
|
|
13488
|
+
}
|
|
13489
|
+
|
|
13490
|
+
/**
|
|
13491
|
+
* Default configuration values for the attestation fetcher.
|
|
13492
|
+
* @internal
|
|
13493
|
+
*/ const DEFAULT_CONFIG = {
|
|
13494
|
+
timeout: 2_000,
|
|
13495
|
+
maxRetries: 30 * 20,
|
|
13496
|
+
retryDelay: 2_000,
|
|
13497
|
+
headers: {
|
|
13498
|
+
'Content-Type': 'application/json'
|
|
13330
13499
|
}
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13500
|
+
};
|
|
13501
|
+
/**
|
|
13502
|
+
* Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
|
|
13503
|
+
*
|
|
13504
|
+
* Headers are merged independently so caller-supplied headers augment the
|
|
13505
|
+
* defaults (such as `Content-Type`) rather than replacing them wholesale.
|
|
13506
|
+
*
|
|
13507
|
+
* @param config - Caller-provided polling configuration overrides
|
|
13508
|
+
* @param internalDefaults - Internal defaults applied before `config` (for example a
|
|
13509
|
+
* reduced `maxRetries` for one-shot requests); `config` still wins on conflict
|
|
13510
|
+
* @returns The effective polling configuration
|
|
13511
|
+
* @internal
|
|
13512
|
+
*/ const mergeAttestationConfig = (config, internalDefaults = {})=>({
|
|
13513
|
+
...DEFAULT_CONFIG,
|
|
13514
|
+
...internalDefaults,
|
|
13515
|
+
...config,
|
|
13516
|
+
headers: {
|
|
13517
|
+
...DEFAULT_CONFIG.headers,
|
|
13518
|
+
...internalDefaults.headers,
|
|
13519
|
+
...config.headers
|
|
13520
|
+
}
|
|
13521
|
+
});
|
|
13522
|
+
/**
|
|
13523
|
+
* Type guard that verifies if an unknown value matches the AttestationMessage shape
|
|
13524
|
+
* and has all required properties.
|
|
13525
|
+
*
|
|
13526
|
+
* @param obj - The value to check, typically an element from the messages array
|
|
13527
|
+
* @returns True if the object matches the AttestationMessage shape, false otherwise
|
|
13528
|
+
* @internal
|
|
13529
|
+
*/ const isValidAttestationMessage = (obj)=>{
|
|
13530
|
+
return typeof obj === 'object' && obj !== null && 'message' in obj && 'eventNonce' in obj && 'attestation' in obj && 'decodedMessage' in obj && 'cctpVersion' in obj && 'status' in obj && typeof obj.status === 'string';
|
|
13531
|
+
};
|
|
13532
|
+
/**
|
|
13533
|
+
* Type guard that verifies if an attestation message is complete.
|
|
13534
|
+
*
|
|
13535
|
+
* @param message - The attestation message to check
|
|
13536
|
+
* @returns True if the message status is 'complete', false otherwise
|
|
13537
|
+
* @internal
|
|
13538
|
+
*/ const isCompleteAttestation = (message)=>{
|
|
13539
|
+
return message.status === 'complete';
|
|
13540
|
+
};
|
|
13541
|
+
/**
|
|
13542
|
+
* Type guard that verifies if an unknown value has the correct structure
|
|
13543
|
+
* for an AttestationResponse, regardless of attestation completion status.
|
|
13544
|
+
*
|
|
13545
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
13546
|
+
* @returns True if the object matches the AttestationResponse shape
|
|
13547
|
+
* @internal
|
|
13548
|
+
*/ const hasValidAttestationStructure = (obj)=>{
|
|
13549
|
+
if (typeof obj !== 'object' || obj === null || !('messages' in obj) || !Array.isArray(obj.messages)) {
|
|
13550
|
+
return false;
|
|
13334
13551
|
}
|
|
13335
|
-
|
|
13336
|
-
|
|
13552
|
+
const messages = obj.messages;
|
|
13553
|
+
// Validate all messages have the correct shape
|
|
13554
|
+
return messages.every(isValidAttestationMessage);
|
|
13555
|
+
};
|
|
13556
|
+
/**
|
|
13557
|
+
* Type guard that verifies if an unknown value matches the AttestationResponse shape
|
|
13558
|
+
* and contains a complete attestation.
|
|
13559
|
+
*
|
|
13560
|
+
* This function performs runtime validation to ensure that the provided value
|
|
13561
|
+
* conforms to the expected structure of an AttestationResponse and has at least
|
|
13562
|
+
* one complete attestation. It checks that:
|
|
13563
|
+
* 1. The value has valid AttestationResponse structure
|
|
13564
|
+
* 2. At least one message has status 'complete'
|
|
13565
|
+
*
|
|
13566
|
+
* @remarks
|
|
13567
|
+
* This type guard is used internally by the attestation fetcher to validate
|
|
13568
|
+
* responses from the IRIS API before processing them. It provides runtime
|
|
13569
|
+
* type safety for data coming from the network and ensures we have a complete
|
|
13570
|
+
* attestation before proceeding.
|
|
13571
|
+
*
|
|
13572
|
+
* If the response has valid structure but no complete attestation yet,
|
|
13573
|
+
* it throws a retryable error. If the response structure is invalid,
|
|
13574
|
+
* it throws a non-retryable validation error.
|
|
13575
|
+
*
|
|
13576
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
13577
|
+
* @returns True if the object matches the AttestationResponse shape and has a complete attestation
|
|
13578
|
+
* @throws {Error} With "Invalid attestation response structure" if structure is invalid (non-retryable)
|
|
13579
|
+
* @throws {Error} With "Attestation not ready" if no complete attestation yet (retryable)
|
|
13580
|
+
*
|
|
13581
|
+
* @example
|
|
13582
|
+
* ```typescript
|
|
13583
|
+
* const response = await fetch('https://iris-api.circle.com/...')
|
|
13584
|
+
* const data = await response.json()
|
|
13585
|
+
*
|
|
13586
|
+
* if (isAttestationResponse(data)) {
|
|
13587
|
+
* // TypeScript now knows data is AttestationResponse with at least one complete attestation
|
|
13588
|
+
* const completeMessage = data.messages.find(msg => msg.status === 'complete')
|
|
13589
|
+
* console.log('Found complete attestation:', completeMessage.attestation)
|
|
13590
|
+
* }
|
|
13591
|
+
* ```
|
|
13592
|
+
*/ const isAttestationResponse = (obj)=>{
|
|
13593
|
+
// First check if the structure is valid
|
|
13594
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
13595
|
+
// If structure is invalid, this is a permanent failure - don't retry
|
|
13596
|
+
throw new Error('Invalid attestation response structure');
|
|
13337
13597
|
}
|
|
13338
|
-
//
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
throw
|
|
13598
|
+
// Then check if at least one message is complete
|
|
13599
|
+
if (!obj.messages.some(isCompleteAttestation)) {
|
|
13600
|
+
// If no complete message, this is a temporary state - allow retry
|
|
13601
|
+
throw new Error('Attestation not ready');
|
|
13342
13602
|
}
|
|
13343
|
-
|
|
13344
|
-
|
|
13603
|
+
return true;
|
|
13604
|
+
};
|
|
13345
13605
|
/**
|
|
13346
|
-
*
|
|
13606
|
+
* Builds the IRIS API URL for fetching attestation data from Circle's CCTP service.
|
|
13347
13607
|
*
|
|
13348
|
-
*
|
|
13349
|
-
*
|
|
13350
|
-
*
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13608
|
+
* Constructs a properly formatted URL for the IRIS API v2 endpoint that provides
|
|
13609
|
+
* attestation messages for cross-chain transfers. The URL includes both the source
|
|
13610
|
+
* domain identifier and the transaction hash as query parameters. The base URL
|
|
13611
|
+
* is selected based on whether the operation is for testnet or mainnet.
|
|
13612
|
+
*
|
|
13613
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain (numeric or string)
|
|
13614
|
+
* @param transactionHash - The transaction hash of the burn operation to fetch attestation for
|
|
13615
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
13616
|
+
* @returns A fully qualified URL string for the IRIS API endpoint
|
|
13617
|
+
*
|
|
13618
|
+
* @example
|
|
13619
|
+
* ```typescript
|
|
13620
|
+
* // Mainnet URL
|
|
13621
|
+
* const mainnetUrl = buildIrisUrl(1, '0xabc...', false)
|
|
13622
|
+
* // => 'https://iris-api.circle.com/v2/messages/1?transactionHash=0xabc...'
|
|
13623
|
+
*
|
|
13624
|
+
* // Testnet URL
|
|
13625
|
+
* const testnetUrl = buildIrisUrl(1, '0xdef...', true)
|
|
13626
|
+
* // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
|
|
13627
|
+
* ```
|
|
13628
|
+
*/ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
|
|
13629
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
13630
|
+
const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
|
|
13631
|
+
url.searchParams.set('transactionHash', transactionHash);
|
|
13632
|
+
return url.toString();
|
|
13357
13633
|
};
|
|
13358
13634
|
/**
|
|
13359
|
-
*
|
|
13635
|
+
* Fetches attestation data from the IRIS API with retry and timeout handling.
|
|
13360
13636
|
*
|
|
13361
|
-
*
|
|
13362
|
-
*
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
|
|
13393
|
-
|
|
13394
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
13395
|
-
nextStep: CCTPv2StepName.approve,
|
|
13396
|
-
reason: 'Continue pending approval',
|
|
13397
|
-
isActionable: false
|
|
13398
|
-
}
|
|
13399
|
-
],
|
|
13400
|
-
// After Burn step
|
|
13401
|
-
[CCTPv2StepName.burn]: [
|
|
13402
|
-
{
|
|
13403
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
13404
|
-
nextStep: CCTPv2StepName.fetchAttestation,
|
|
13405
|
-
reason: 'Burn successful, fetch attestation',
|
|
13406
|
-
isActionable: true
|
|
13407
|
-
},
|
|
13408
|
-
{
|
|
13409
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
13410
|
-
nextStep: CCTPv2StepName.burn,
|
|
13411
|
-
reason: 'Retry failed burn',
|
|
13412
|
-
isActionable: true
|
|
13413
|
-
},
|
|
13414
|
-
{
|
|
13415
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
13416
|
-
nextStep: CCTPv2StepName.burn,
|
|
13417
|
-
reason: 'Continue pending burn',
|
|
13418
|
-
isActionable: false
|
|
13419
|
-
}
|
|
13420
|
-
],
|
|
13421
|
-
// After FetchAttestation step
|
|
13422
|
-
[CCTPv2StepName.fetchAttestation]: [
|
|
13423
|
-
{
|
|
13424
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
13425
|
-
nextStep: CCTPv2StepName.mint,
|
|
13426
|
-
reason: 'Attestation fetched, proceed to mint',
|
|
13427
|
-
isActionable: true
|
|
13428
|
-
},
|
|
13429
|
-
{
|
|
13430
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
13431
|
-
nextStep: CCTPv2StepName.fetchAttestation,
|
|
13432
|
-
reason: 'Retry fetching attestation',
|
|
13433
|
-
isActionable: true
|
|
13434
|
-
},
|
|
13435
|
-
{
|
|
13436
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
13437
|
-
nextStep: CCTPv2StepName.fetchAttestation,
|
|
13438
|
-
reason: 'Continue pending attestation fetch',
|
|
13439
|
-
isActionable: false
|
|
13440
|
-
}
|
|
13441
|
-
],
|
|
13442
|
-
// After Mint step
|
|
13443
|
-
[CCTPv2StepName.mint]: [
|
|
13444
|
-
{
|
|
13445
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
13446
|
-
nextStep: null,
|
|
13447
|
-
reason: 'Bridge completed successfully',
|
|
13448
|
-
isActionable: false
|
|
13449
|
-
},
|
|
13450
|
-
{
|
|
13451
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
13452
|
-
nextStep: CCTPv2StepName.mint,
|
|
13453
|
-
reason: 'Retry failed mint',
|
|
13454
|
-
isActionable: true
|
|
13455
|
-
},
|
|
13456
|
-
{
|
|
13457
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
13458
|
-
nextStep: CCTPv2StepName.mint,
|
|
13459
|
-
reason: 'Continue pending mint',
|
|
13460
|
-
isActionable: false
|
|
13461
|
-
}
|
|
13462
|
-
],
|
|
13463
|
-
// After ReAttest step
|
|
13464
|
-
[CCTPv2StepName.reAttest]: [
|
|
13465
|
-
{
|
|
13466
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
13467
|
-
nextStep: CCTPv2StepName.mint,
|
|
13468
|
-
reason: 'Re-attestation successful, proceed to mint',
|
|
13469
|
-
isActionable: true
|
|
13470
|
-
},
|
|
13471
|
-
{
|
|
13472
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
13473
|
-
nextStep: CCTPv2StepName.mint,
|
|
13474
|
-
reason: 'Re-attestation failed, retry mint to re-initiate recovery',
|
|
13475
|
-
isActionable: true
|
|
13476
|
-
},
|
|
13477
|
-
{
|
|
13478
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
13479
|
-
nextStep: CCTPv2StepName.mint,
|
|
13480
|
-
reason: 'Re-attestation pending, retry mint to re-initiate recovery',
|
|
13481
|
-
isActionable: true
|
|
13482
|
-
}
|
|
13483
|
-
]
|
|
13637
|
+
* Polls the IRIS API until a complete attestation is available. The default
|
|
13638
|
+
* window is sized for slow source chains where finality may take many
|
|
13639
|
+
* confirmations.
|
|
13640
|
+
*
|
|
13641
|
+
* Defaults (see `DEFAULT_CONFIG`):
|
|
13642
|
+
* - Per-attempt timeout: 2 000 ms (each HTTP request aborts after 2 s)
|
|
13643
|
+
* - Retry delay: 2 000 ms between attempts
|
|
13644
|
+
* - Max retries: 600 (30 × 20)
|
|
13645
|
+
* - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
|
|
13646
|
+
*
|
|
13647
|
+
* @param sourceDomainId - The CCTP domain ID.
|
|
13648
|
+
* @param transactionHash - The transaction hash to fetch attestation for.
|
|
13649
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
13650
|
+
* @param config - Optional configuration overrides for the attestation fetcher
|
|
13651
|
+
* @returns The attestation response data.
|
|
13652
|
+
* @throws If the request fails, times out, or returns invalid data.
|
|
13653
|
+
*
|
|
13654
|
+
* @example
|
|
13655
|
+
* ```typescript
|
|
13656
|
+
* // Fetch attestation for mainnet transaction
|
|
13657
|
+
* const response = await fetchAttestation(1, '0xabc...', false)
|
|
13658
|
+
* console.log(`Found ${response.messages.length} attestation messages`)
|
|
13659
|
+
*
|
|
13660
|
+
* // Fetch with custom timeout
|
|
13661
|
+
* const response2 = await fetchAttestation(1, '0xdef...', true, {
|
|
13662
|
+
* timeout: 5000,
|
|
13663
|
+
* maxRetries: 5
|
|
13664
|
+
* })
|
|
13665
|
+
* ```
|
|
13666
|
+
*/ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
13667
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
13668
|
+
const effectiveConfig = mergeAttestationConfig(config);
|
|
13669
|
+
return await pollApiGet(url, isAttestationResponse, effectiveConfig);
|
|
13484
13670
|
};
|
|
13485
13671
|
/**
|
|
13486
|
-
*
|
|
13672
|
+
* Type guard that validates attestation response structure without requiring completion status.
|
|
13487
13673
|
*
|
|
13488
|
-
* This
|
|
13489
|
-
*
|
|
13490
|
-
*
|
|
13674
|
+
* This is used by `fetchAttestationWithoutStatusCheck` to extract the nonce from an existing
|
|
13675
|
+
* attestation, even if the attestation is expired or pending. Unlike `isAttestationResponse`,
|
|
13676
|
+
* this function does not throw if no complete attestation is found.
|
|
13491
13677
|
*
|
|
13492
|
-
* The
|
|
13493
|
-
*
|
|
13678
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
13679
|
+
* @returns True if the object has valid attestation structure
|
|
13680
|
+
* @throws {Error} With "Invalid attestation response structure" if structure is invalid
|
|
13681
|
+
* @internal
|
|
13682
|
+
*/ const isAttestationResponseWithoutStatusCheck = (obj)=>{
|
|
13683
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
13684
|
+
throw new Error('Invalid attestation response structure');
|
|
13685
|
+
}
|
|
13686
|
+
return true;
|
|
13687
|
+
};
|
|
13688
|
+
/**
|
|
13689
|
+
* Fetches attestation data without requiring the attestation to be complete.
|
|
13494
13690
|
*
|
|
13495
|
-
*
|
|
13496
|
-
*
|
|
13497
|
-
*
|
|
13498
|
-
* - Actionable logic: Distinguishes between steps requiring user action vs waiting
|
|
13499
|
-
* - Terminal states: Properly handles completion and non-actionable states
|
|
13691
|
+
* This function is useful for retrieving attestation data (particularly the nonce)
|
|
13692
|
+
* from an existing transaction, even if the attestation has expired or is pending.
|
|
13693
|
+
* It uses minimal retries since we're fetching existing data, not waiting for completion.
|
|
13500
13694
|
*
|
|
13501
|
-
* @param
|
|
13502
|
-
* @
|
|
13503
|
-
* @
|
|
13695
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
13696
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
13697
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
13698
|
+
* @param config - Optional configuration overrides
|
|
13699
|
+
* @returns The attestation response data (may contain incomplete/expired attestations)
|
|
13700
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
13504
13701
|
*
|
|
13505
13702
|
* @example
|
|
13506
13703
|
* ```typescript
|
|
13507
|
-
*
|
|
13704
|
+
* // Fetch existing attestation to extract nonce for re-attestation
|
|
13705
|
+
* const response = await fetchAttestationWithoutStatusCheck(1, '0xabc...', true)
|
|
13706
|
+
* const nonce = response.messages[0]?.eventNonce
|
|
13707
|
+
* ```
|
|
13708
|
+
*/ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
13709
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
13710
|
+
// Use minimal retries since we're just fetching existing data
|
|
13711
|
+
const effectiveConfig = mergeAttestationConfig(config, {
|
|
13712
|
+
maxRetries: 3
|
|
13713
|
+
});
|
|
13714
|
+
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
13715
|
+
};
|
|
13716
|
+
/**
|
|
13717
|
+
* Type guard that validates attestation response has expirationBlock === '0'.
|
|
13508
13718
|
*
|
|
13509
|
-
*
|
|
13510
|
-
*
|
|
13511
|
-
*
|
|
13512
|
-
*
|
|
13513
|
-
* ]
|
|
13514
|
-
* }
|
|
13719
|
+
* This is used after requestReAttestation() to poll until the attestation
|
|
13720
|
+
* is fully re-processed and has a zero expiration block (never expires).
|
|
13721
|
+
* The expiration block transitions from non-zero to zero when Circle
|
|
13722
|
+
* completes processing the re-attestation request.
|
|
13515
13723
|
*
|
|
13516
|
-
*
|
|
13517
|
-
*
|
|
13518
|
-
*
|
|
13519
|
-
* ```
|
|
13724
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
13725
|
+
* @returns True if the attestation has expirationBlock === '0'
|
|
13726
|
+
* @throws {Error} With "Re-attestation not yet complete" if expirationBlock is not '0'
|
|
13520
13727
|
*
|
|
13521
13728
|
* @example
|
|
13522
13729
|
* ```typescript
|
|
13523
|
-
* //
|
|
13524
|
-
* const
|
|
13525
|
-
*
|
|
13526
|
-
* { name: 'Approve', state: 'pending' }
|
|
13527
|
-
* ]
|
|
13528
|
-
* }
|
|
13529
|
-
*
|
|
13530
|
-
* const analysis = analyzeSteps(bridgeResult)
|
|
13531
|
-
* // Result: { continuationStep: 'Approve', isRetryable: false,
|
|
13532
|
-
* // reason: 'Continue pending approval' }
|
|
13730
|
+
* // After requesting re-attestation, use this to validate the response
|
|
13731
|
+
* const response = await pollApiGet(url, isReAttestedAttestationResponse, config)
|
|
13732
|
+
* // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
|
|
13533
13733
|
* ```
|
|
13534
13734
|
*
|
|
13735
|
+
* @internal
|
|
13736
|
+
*/ const isReAttestedAttestationResponse = (obj)=>{
|
|
13737
|
+
// First validate the basic structure and completion status
|
|
13738
|
+
// This will throw appropriate errors for invalid structure or incomplete attestation
|
|
13739
|
+
if (!isAttestationResponse(obj)) ;
|
|
13740
|
+
// Check if the first message has expirationBlock === '0'
|
|
13741
|
+
const expirationBlock = obj.messages[0]?.decodedMessage?.decodedMessageBody?.expirationBlock;
|
|
13742
|
+
if (expirationBlock !== '0') {
|
|
13743
|
+
// Re-attestation not yet complete - allow retry via polling
|
|
13744
|
+
throw new Error('Re-attestation not yet complete: waiting for expirationBlock to become 0');
|
|
13745
|
+
}
|
|
13746
|
+
return true;
|
|
13747
|
+
};
|
|
13748
|
+
/**
|
|
13749
|
+
* Fetches attestation data and polls until expirationBlock === '0'.
|
|
13750
|
+
*
|
|
13751
|
+
* This function is used after calling requestReAttestation() to wait until
|
|
13752
|
+
* the attestation is fully re-processed. The expirationBlock transitions
|
|
13753
|
+
* from non-zero to zero when Circle completes the re-attestation.
|
|
13754
|
+
*
|
|
13755
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
13756
|
+
* @param transactionHash - The transaction hash to fetch attestation for
|
|
13757
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
13758
|
+
* @param config - Optional configuration overrides
|
|
13759
|
+
* @returns The re-attested attestation response with expirationBlock === '0'
|
|
13760
|
+
* @throws If the request fails, times out, or expirationBlock never becomes 0
|
|
13761
|
+
*
|
|
13535
13762
|
* @example
|
|
13536
13763
|
* ```typescript
|
|
13537
|
-
* //
|
|
13538
|
-
*
|
|
13539
|
-
* steps: [
|
|
13540
|
-
* { name: 'Approve', state: 'success' },
|
|
13541
|
-
* { name: 'Burn', state: 'success' },
|
|
13542
|
-
* { name: 'FetchAttestation', state: 'success' },
|
|
13543
|
-
* { name: 'Mint', state: 'success' }
|
|
13544
|
-
* ]
|
|
13545
|
-
* }
|
|
13764
|
+
* // After requesting re-attestation
|
|
13765
|
+
* await requestReAttestation(nonce, isTestnet)
|
|
13546
13766
|
*
|
|
13547
|
-
*
|
|
13548
|
-
*
|
|
13549
|
-
* //
|
|
13767
|
+
* // Poll until expirationBlock becomes 0
|
|
13768
|
+
* const response = await fetchReAttestedAttestation(domainId, txHash, isTestnet)
|
|
13769
|
+
* // response.messages[0].decodedMessage.decodedMessageBody.expirationBlock === '0'
|
|
13550
13770
|
* ```
|
|
13551
|
-
*/ const
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
}
|
|
13556
|
-
const { steps } = bridgeResult;
|
|
13557
|
-
// Build execution context from step history
|
|
13558
|
-
const context = buildFlowContext(steps);
|
|
13559
|
-
// Determine continuation logic using rule engine
|
|
13560
|
-
const continuation = determineContinuationFromRules(context);
|
|
13561
|
-
return {
|
|
13562
|
-
continuationStep: continuation.nextStep,
|
|
13563
|
-
isActionable: continuation.isActionable,
|
|
13564
|
-
completedSteps: Array.from(context.completedSteps),
|
|
13565
|
-
failedSteps: Array.from(context.failedSteps),
|
|
13566
|
-
reason: continuation.reason
|
|
13567
|
-
};
|
|
13771
|
+
*/ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
13772
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
13773
|
+
const effectiveConfig = mergeAttestationConfig(config);
|
|
13774
|
+
return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
|
|
13568
13775
|
};
|
|
13569
13776
|
/**
|
|
13570
|
-
*
|
|
13571
|
-
*
|
|
13572
|
-
* @param steps - Array of executed bridge steps.
|
|
13573
|
-
* @returns Flow context with execution state and history.
|
|
13574
|
-
*/ function buildFlowContext(steps) {
|
|
13575
|
-
const completedSteps = new Set();
|
|
13576
|
-
const failedSteps = new Set();
|
|
13577
|
-
let lastStep;
|
|
13578
|
-
// Process step history to build context
|
|
13579
|
-
for (const step of steps){
|
|
13580
|
-
if (step.state === 'success' || step.state === 'noop') {
|
|
13581
|
-
completedSteps.add(step.name);
|
|
13582
|
-
} else if (step.state === 'error') {
|
|
13583
|
-
failedSteps.add(step.name);
|
|
13584
|
-
}
|
|
13585
|
-
// Track the last step for continuation logic
|
|
13586
|
-
lastStep = {
|
|
13587
|
-
name: step.name,
|
|
13588
|
-
state: step.state
|
|
13589
|
-
};
|
|
13590
|
-
}
|
|
13591
|
-
return {
|
|
13592
|
-
completedSteps,
|
|
13593
|
-
failedSteps,
|
|
13594
|
-
...lastStep && {
|
|
13595
|
-
lastStep
|
|
13596
|
-
}
|
|
13597
|
-
};
|
|
13598
|
-
}
|
|
13599
|
-
/**
|
|
13600
|
-
* Determine continuation step using the rule engine.
|
|
13777
|
+
* Builds the IRIS API URL for re-attestation requests.
|
|
13601
13778
|
*
|
|
13602
|
-
*
|
|
13603
|
-
*
|
|
13604
|
-
*/ function determineContinuationFromRules(context) {
|
|
13605
|
-
const lastStepName = context.lastStep?.name;
|
|
13606
|
-
// Handle initial state when no steps have been executed
|
|
13607
|
-
if (lastStepName === undefined) {
|
|
13608
|
-
const rules = STEP_TRANSITION_RULES[''];
|
|
13609
|
-
const matchingRule = rules?.find((rule)=>rule.condition(context));
|
|
13610
|
-
if (!matchingRule) {
|
|
13611
|
-
return {
|
|
13612
|
-
nextStep: null,
|
|
13613
|
-
isActionable: false,
|
|
13614
|
-
reason: 'No initial state rule found'
|
|
13615
|
-
};
|
|
13616
|
-
}
|
|
13617
|
-
return {
|
|
13618
|
-
nextStep: matchingRule.nextStep,
|
|
13619
|
-
isActionable: matchingRule.isActionable,
|
|
13620
|
-
reason: matchingRule.reason
|
|
13621
|
-
};
|
|
13622
|
-
}
|
|
13623
|
-
// A step with an empty name is ambiguous and should be treated as an unrecoverable state.
|
|
13624
|
-
if (lastStepName === '') {
|
|
13625
|
-
return {
|
|
13626
|
-
nextStep: null,
|
|
13627
|
-
isActionable: false,
|
|
13628
|
-
reason: 'No transition rules defined for step with empty name'
|
|
13629
|
-
};
|
|
13630
|
-
}
|
|
13631
|
-
const rules = STEP_TRANSITION_RULES[lastStepName];
|
|
13632
|
-
if (!rules) {
|
|
13633
|
-
return {
|
|
13634
|
-
nextStep: null,
|
|
13635
|
-
isActionable: false,
|
|
13636
|
-
reason: `No transition rules defined for step: ${lastStepName}`
|
|
13637
|
-
};
|
|
13638
|
-
}
|
|
13639
|
-
// Find the first matching rule
|
|
13640
|
-
const matchingRule = rules.find((rule)=>rule.condition(context));
|
|
13641
|
-
if (!matchingRule) {
|
|
13642
|
-
return {
|
|
13643
|
-
nextStep: null,
|
|
13644
|
-
isActionable: false,
|
|
13645
|
-
reason: `No matching transition rule for current context`
|
|
13646
|
-
};
|
|
13647
|
-
}
|
|
13648
|
-
return {
|
|
13649
|
-
nextStep: matchingRule.nextStep,
|
|
13650
|
-
isActionable: matchingRule.isActionable,
|
|
13651
|
-
reason: matchingRule.reason
|
|
13652
|
-
};
|
|
13653
|
-
}
|
|
13654
|
-
|
|
13655
|
-
/**
|
|
13656
|
-
* Find a step by name in the bridge result.
|
|
13779
|
+
* Constructs the URL for Circle's re-attestation endpoint that allows
|
|
13780
|
+
* requesting a fresh attestation for an expired nonce.
|
|
13657
13781
|
*
|
|
13658
|
-
* @param
|
|
13659
|
-
* @param
|
|
13660
|
-
* @returns
|
|
13782
|
+
* @param nonce - The nonce from the original attestation
|
|
13783
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
13784
|
+
* @returns A fully qualified URL string for the re-attestation endpoint
|
|
13661
13785
|
*
|
|
13662
13786
|
* @example
|
|
13663
13787
|
* ```typescript
|
|
13664
|
-
*
|
|
13788
|
+
* // Mainnet URL
|
|
13789
|
+
* const mainnetUrl = buildReAttestUrl('0xabc', false)
|
|
13790
|
+
* // => 'https://iris-api.circle.com/v2/reattest/0xabc'
|
|
13665
13791
|
*
|
|
13666
|
-
*
|
|
13667
|
-
*
|
|
13668
|
-
*
|
|
13669
|
-
* }
|
|
13792
|
+
* // Testnet URL
|
|
13793
|
+
* const testnetUrl = buildReAttestUrl('0xabc', true)
|
|
13794
|
+
* // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
|
|
13670
13795
|
* ```
|
|
13671
|
-
*/
|
|
13672
|
-
|
|
13673
|
-
}
|
|
13796
|
+
*/ const buildReAttestUrl = (nonce, isTestnet)=>{
|
|
13797
|
+
const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
|
|
13798
|
+
const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
|
|
13799
|
+
return url.toString();
|
|
13800
|
+
};
|
|
13674
13801
|
/**
|
|
13675
|
-
*
|
|
13802
|
+
* Type guard that validates the re-attestation API response structure.
|
|
13676
13803
|
*
|
|
13677
|
-
*
|
|
13804
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
13805
|
+
* @returns True if the object matches the ReAttestationResponse shape
|
|
13806
|
+
* @throws {Error} With "Invalid re-attestation response structure" if structure is invalid
|
|
13807
|
+
* @internal
|
|
13808
|
+
*/ const isReAttestationResponse = (obj)=>{
|
|
13809
|
+
if (typeof obj !== 'object' || obj === null || !('message' in obj) || !('nonce' in obj) || typeof obj.message !== 'string' || typeof obj.nonce !== 'string') {
|
|
13810
|
+
throw new Error('Invalid re-attestation response structure');
|
|
13811
|
+
}
|
|
13812
|
+
return true;
|
|
13813
|
+
};
|
|
13814
|
+
/**
|
|
13815
|
+
* Requests re-attestation for an expired attestation nonce.
|
|
13678
13816
|
*
|
|
13679
|
-
*
|
|
13680
|
-
*
|
|
13681
|
-
*
|
|
13682
|
-
*
|
|
13817
|
+
* This function calls Circle's re-attestation API endpoint to request a fresh
|
|
13818
|
+
* attestation for a previously issued nonce. After calling this function,
|
|
13819
|
+
* you should poll `fetchAttestation` to retrieve the new attestation.
|
|
13820
|
+
*
|
|
13821
|
+
* @param nonce - The nonce from the original (expired) attestation
|
|
13822
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet chain (false)
|
|
13823
|
+
* @param config - Optional configuration overrides for the request
|
|
13824
|
+
* @returns The re-attestation response confirming the request was accepted
|
|
13825
|
+
* @throws If the request fails, times out, or returns invalid data
|
|
13683
13826
|
*
|
|
13684
13827
|
* @example
|
|
13685
13828
|
* ```typescript
|
|
13686
|
-
*
|
|
13829
|
+
* // Request re-attestation for an expired nonce
|
|
13830
|
+
* const response = await requestReAttestation('0xabc', true)
|
|
13831
|
+
* console.log(response.message) // "Re-attestation successfully requested for nonce."
|
|
13687
13832
|
*
|
|
13688
|
-
*
|
|
13689
|
-
*
|
|
13833
|
+
* // After requesting re-attestation, poll for the new attestation
|
|
13834
|
+
* const attestation = await fetchAttestation(domainId, txHash, true)
|
|
13690
13835
|
* ```
|
|
13691
|
-
*/
|
|
13692
|
-
const
|
|
13693
|
-
|
|
13836
|
+
*/ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
|
|
13837
|
+
const url = buildReAttestUrl(nonce, isTestnet);
|
|
13838
|
+
// Use minimal retries since we're just submitting a request, not polling for state
|
|
13839
|
+
const effectiveConfig = mergeAttestationConfig(config, {
|
|
13840
|
+
maxRetries: 3
|
|
13841
|
+
});
|
|
13842
|
+
return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
|
|
13843
|
+
};
|
|
13844
|
+
|
|
13845
|
+
/**
|
|
13846
|
+
* Type guard that checks if the relayer has confirmed the mint transaction.
|
|
13847
|
+
*
|
|
13848
|
+
* This function validates that:
|
|
13849
|
+
* 1. The response has valid AttestationResponse structure
|
|
13850
|
+
* 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
|
|
13851
|
+
*
|
|
13852
|
+
* If forwardState is 'FAILED', throws a non-retryable KitError.
|
|
13853
|
+
* If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
|
|
13854
|
+
*
|
|
13855
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
13856
|
+
* @returns True if the relayer has confirmed the mint
|
|
13857
|
+
* @throws {KitError} With FATAL recoverability if structure is invalid
|
|
13858
|
+
* @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
|
|
13859
|
+
* @throws {KitError} With RETRYABLE recoverability if still pending
|
|
13860
|
+
* @internal
|
|
13861
|
+
*/ const isRelayerMintConfirmed = (obj)=>{
|
|
13862
|
+
// First check if the structure is valid
|
|
13863
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
13694
13864
|
throw new KitError({
|
|
13695
13865
|
...InputError.VALIDATION_FAILED,
|
|
13696
13866
|
recoverability: 'FATAL',
|
|
13697
|
-
message:
|
|
13867
|
+
message: 'Invalid attestation response structure from IRIS API.'
|
|
13698
13868
|
});
|
|
13699
13869
|
}
|
|
13700
|
-
|
|
13701
|
-
|
|
13870
|
+
// Find the first message (typically there's only one)
|
|
13871
|
+
const message = obj.messages[0];
|
|
13872
|
+
if (!message) {
|
|
13702
13873
|
throw new KitError({
|
|
13703
13874
|
...InputError.VALIDATION_FAILED,
|
|
13704
13875
|
recoverability: 'FATAL',
|
|
13705
|
-
message: '
|
|
13876
|
+
message: 'No attestation messages found in IRIS API response.'
|
|
13706
13877
|
});
|
|
13707
13878
|
}
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13712
|
-
|
|
13879
|
+
// Check for FAILED state - this is a permanent failure
|
|
13880
|
+
if (message.forwardState === 'FAILED') {
|
|
13881
|
+
throw new KitError({
|
|
13882
|
+
...NetworkError.RELAYER_FORWARD_FAILED,
|
|
13883
|
+
recoverability: 'RESUMABLE',
|
|
13884
|
+
message: 'Circle relayer failed to forward the mint transaction. The mint may still have succeeded if another party submitted it. Check the recipient wallet balance before retrying. If the mint did not occur, you can manually submit it using the attestation data in the error cause.',
|
|
13885
|
+
cause: {
|
|
13886
|
+
trace: {
|
|
13887
|
+
eventNonce: message.eventNonce,
|
|
13888
|
+
attestation: message.attestation,
|
|
13889
|
+
message: message.message
|
|
13890
|
+
}
|
|
13891
|
+
}
|
|
13892
|
+
});
|
|
13893
|
+
}
|
|
13894
|
+
// Check if mint is confirmed (or complete) with a valid transaction hash
|
|
13895
|
+
// We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
|
|
13896
|
+
if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
|
|
13897
|
+
return true;
|
|
13898
|
+
}
|
|
13899
|
+
// Still pending or not yet processed - throw RETRYABLE error to continue polling
|
|
13900
|
+
throw new KitError({
|
|
13901
|
+
...NetworkError.RELAYER_PENDING,
|
|
13902
|
+
recoverability: 'RETRYABLE',
|
|
13903
|
+
message: 'Relayer mint not ready. Waiting for confirmation.'
|
|
13904
|
+
});
|
|
13905
|
+
};
|
|
13713
13906
|
/**
|
|
13714
|
-
*
|
|
13907
|
+
* Polls the attestation API until the relayer's mint transaction is confirmed.
|
|
13715
13908
|
*
|
|
13716
|
-
*
|
|
13717
|
-
*
|
|
13909
|
+
* This function is used when `useForwarder` is enabled. Instead of the user
|
|
13910
|
+
* submitting the mint transaction, Circle's Orbit relayer handles it automatically.
|
|
13911
|
+
* This function polls until the relayer has submitted and confirmed the mint transaction.
|
|
13912
|
+
*
|
|
13913
|
+
* @remarks
|
|
13914
|
+
* - Uses a 20-minute timeout by default (600 retries × 2 seconds)
|
|
13915
|
+
* - Throws immediately if `forwardState` is 'FAILED'
|
|
13916
|
+
* - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
|
|
13917
|
+
* - Returns the attestation message with `forwardTxHash` populated
|
|
13918
|
+
*
|
|
13919
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
13920
|
+
* @param transactionHash - The transaction hash of the burn operation
|
|
13921
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
|
|
13922
|
+
* @param config - Optional configuration overrides for polling behavior
|
|
13923
|
+
* @returns The attestation message with confirmed forwardTxHash
|
|
13924
|
+
* @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
|
|
13925
|
+
* @throws {KitError} If timeout is reached while still pending
|
|
13718
13926
|
*
|
|
13719
13927
|
* @example
|
|
13720
13928
|
* ```typescript
|
|
13721
|
-
*
|
|
13722
|
-
*
|
|
13723
|
-
* const burnTxHash = getBurnTxHash(result)
|
|
13724
|
-
* if (burnTxHash) {
|
|
13725
|
-
* console.log('Burn tx hash:', burnTxHash)
|
|
13726
|
-
* }
|
|
13929
|
+
* const attestation = await fetchRelayerMint(0, '0xabc...', false)
|
|
13930
|
+
* console.log('Relayer mint tx:', attestation.forwardTxHash)
|
|
13727
13931
|
* ```
|
|
13728
|
-
*/
|
|
13729
|
-
|
|
13730
|
-
|
|
13932
|
+
*/ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
13933
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
13934
|
+
const effectiveConfig = mergeAttestationConfig(config);
|
|
13935
|
+
let response;
|
|
13936
|
+
try {
|
|
13937
|
+
response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
|
|
13938
|
+
} catch (error) {
|
|
13939
|
+
// Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
|
|
13940
|
+
if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
|
|
13941
|
+
throw new KitError({
|
|
13942
|
+
...NetworkError.RELAYER_FORWARD_FAILED,
|
|
13943
|
+
recoverability: error.recoverability,
|
|
13944
|
+
message: error.message,
|
|
13945
|
+
cause: {
|
|
13946
|
+
...error.cause,
|
|
13947
|
+
trace: {
|
|
13948
|
+
...error.cause?.trace,
|
|
13949
|
+
burnTxHash: transactionHash
|
|
13950
|
+
}
|
|
13951
|
+
}
|
|
13952
|
+
});
|
|
13953
|
+
}
|
|
13954
|
+
throw error;
|
|
13955
|
+
}
|
|
13956
|
+
// Return the first message (which should have forwardTxHash)
|
|
13957
|
+
// Note: This check is needed for TypeScript type safety even though
|
|
13958
|
+
// isRelayerMintConfirmed validates messages[0] exists. The type guard
|
|
13959
|
+
// narrows the type at the call site, but TypeScript can't infer that
|
|
13960
|
+
// the array still has elements after pollApiGet returns.
|
|
13961
|
+
const message = response.messages[0];
|
|
13962
|
+
if (!message) {
|
|
13963
|
+
throw new KitError({
|
|
13964
|
+
...InputError.VALIDATION_FAILED,
|
|
13965
|
+
recoverability: 'FATAL',
|
|
13966
|
+
message: 'No attestation messages found in response after polling.'
|
|
13967
|
+
});
|
|
13968
|
+
}
|
|
13969
|
+
return message;
|
|
13970
|
+
};
|
|
13971
|
+
|
|
13972
|
+
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
13731
13973
|
/**
|
|
13732
|
-
*
|
|
13974
|
+
* Asserts that the provided parameters match the CCTPv2 wallet context interface.
|
|
13975
|
+
* The validation includes:
|
|
13976
|
+
* - Basic wallet context validation (adapter, address, chain)
|
|
13977
|
+
* - CCTPv2-specific chain validation (must be an EVM chain)
|
|
13733
13978
|
*
|
|
13734
|
-
* @param
|
|
13735
|
-
* @
|
|
13979
|
+
* @param params - The parameters to validate
|
|
13980
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
13736
13981
|
*
|
|
13737
13982
|
* @example
|
|
13738
13983
|
* ```typescript
|
|
13739
|
-
* import {
|
|
13984
|
+
* import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
|
|
13985
|
+
* import { Ethereum } from '@core/chains'
|
|
13740
13986
|
*
|
|
13741
|
-
*
|
|
13742
|
-
*
|
|
13743
|
-
*
|
|
13987
|
+
* // Prepare wallet context
|
|
13988
|
+
* const context = {
|
|
13989
|
+
* adapter: {
|
|
13990
|
+
* prepare: async () => ({ data: 'prepared transaction' }),
|
|
13991
|
+
* waitForTransaction: async () => ({ status: 'confirmed' })
|
|
13992
|
+
* },
|
|
13993
|
+
* address: '0x1234567890123456789012345678901234567890',
|
|
13994
|
+
* chain: {
|
|
13995
|
+
* ...Ethereum,
|
|
13996
|
+
* usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
13997
|
+
* cctp: {
|
|
13998
|
+
* domain: 1,
|
|
13999
|
+
* contracts: {
|
|
14000
|
+
* v2: {
|
|
14001
|
+
* tokenMessenger: '0xTokenMessenger',
|
|
14002
|
+
* messageTransmitter: '0xMessageTransmitter'
|
|
14003
|
+
* }
|
|
14004
|
+
* }
|
|
14005
|
+
* }
|
|
14006
|
+
* }
|
|
13744
14007
|
* }
|
|
14008
|
+
*
|
|
14009
|
+
* // This will throw if validation fails
|
|
14010
|
+
* assertCCTPv2WalletContext(context)
|
|
14011
|
+
*
|
|
14012
|
+
* // If we get here, context is guaranteed to be valid
|
|
14013
|
+
* console.log('CCTPv2 wallet context is valid')
|
|
13745
14014
|
* ```
|
|
13746
|
-
*/ function
|
|
13747
|
-
//
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
14015
|
+
*/ function assertCCTPv2WalletContext(params) {
|
|
14016
|
+
// First validate basic wallet context
|
|
14017
|
+
validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
|
|
14018
|
+
// After validation, we know params is WalletContext
|
|
14019
|
+
const context = params;
|
|
14020
|
+
// Validate USDC support
|
|
14021
|
+
if (context.chain.usdcAddress === null) {
|
|
14022
|
+
throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
|
|
14023
|
+
}
|
|
14024
|
+
// Validate CCTPv2 support
|
|
14025
|
+
if (!isCCTPV2Supported(context.chain)) {
|
|
14026
|
+
throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
|
|
13751
14027
|
}
|
|
13752
|
-
// Fall back to fetchAttestation step
|
|
13753
|
-
const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
|
|
13754
|
-
return fetchStep?.data;
|
|
13755
14028
|
}
|
|
13756
14029
|
|
|
14030
|
+
const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
13757
14031
|
/**
|
|
13758
|
-
*
|
|
13759
|
-
*
|
|
13760
|
-
*
|
|
13761
|
-
*
|
|
13762
|
-
*
|
|
14032
|
+
* Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
|
|
14033
|
+
* The validation includes:
|
|
14034
|
+
* - Basic parameter structure and types
|
|
14035
|
+
* - Amount validation (non-empty numeric string \> 0)
|
|
14036
|
+
* - Wallet address format validation (must be valid Ethereum address)
|
|
14037
|
+
* - Chain definition validation (must be a valid chain with required properties)
|
|
14038
|
+
* - Adapter validation (must implement required methods)
|
|
14039
|
+
* - Optional config validation (transfer speed and max fee)
|
|
14040
|
+
* - Network compatibility (source and destination chains must both be testnet or both mainnet)
|
|
14041
|
+
* - CCTPv2-specific wallet context validations
|
|
13763
14042
|
*
|
|
13764
|
-
* @param
|
|
13765
|
-
* @
|
|
13766
|
-
* @returns True if there is a pending step that we should wait for.
|
|
14043
|
+
* @param params - The parameters to validate
|
|
14044
|
+
* @throws {KitError} If validation fails, with details about which properties failed
|
|
13767
14045
|
*
|
|
13768
14046
|
* @example
|
|
13769
14047
|
* ```typescript
|
|
13770
|
-
* import {
|
|
13771
|
-
* import {
|
|
14048
|
+
* import { assertCCTPv2BridgeParams } from '@circle-fin/provider-cctp-v2'
|
|
14049
|
+
* import { Ethereum, Base } from '@core/chains'
|
|
13772
14050
|
*
|
|
13773
|
-
*
|
|
13774
|
-
*
|
|
13775
|
-
*
|
|
14051
|
+
* // Prepare transfer parameters
|
|
14052
|
+
* const params = {
|
|
14053
|
+
* amount: '100.50',
|
|
14054
|
+
* source: {
|
|
14055
|
+
* adapter: sourceAdapter,
|
|
14056
|
+
* address: '0xSourceAddress',
|
|
14057
|
+
* chain: {
|
|
14058
|
+
* ...Ethereum,
|
|
14059
|
+
* cctp: {
|
|
14060
|
+
* domain: 1,
|
|
14061
|
+
* contracts: {
|
|
14062
|
+
* v2: {
|
|
14063
|
+
* tokenMessenger: '0xTokenMessenger',
|
|
14064
|
+
* messageTransmitter: '0xMessageTransmitter'
|
|
14065
|
+
* }
|
|
14066
|
+
* }
|
|
14067
|
+
* }
|
|
14068
|
+
* }
|
|
14069
|
+
* },
|
|
14070
|
+
* destination: {
|
|
14071
|
+
* adapter: destAdapter,
|
|
14072
|
+
* address: '0xDestAddress',
|
|
14073
|
+
* chain: {
|
|
14074
|
+
* ...Base,
|
|
14075
|
+
* cctp: {
|
|
14076
|
+
* domain: 2,
|
|
14077
|
+
* contracts: {
|
|
14078
|
+
* v2: {
|
|
14079
|
+
* tokenMessenger: '0xTokenMessenger',
|
|
14080
|
+
* messageTransmitter: '0xMessageTransmitter'
|
|
14081
|
+
* }
|
|
14082
|
+
* }
|
|
14083
|
+
* }
|
|
14084
|
+
* }
|
|
14085
|
+
* },
|
|
14086
|
+
* token: 'USDC',
|
|
14087
|
+
* config: {
|
|
14088
|
+
* transferSpeed: 'FAST',
|
|
14089
|
+
* maxFee: '1000000'
|
|
14090
|
+
* }
|
|
13776
14091
|
* }
|
|
13777
|
-
* ```
|
|
13778
|
-
*/ /**
|
|
13779
|
-
* Evaluate a transaction receipt and return the corresponding step state
|
|
13780
|
-
* and error message. Centralises the success/revert/unconfirmed logic so
|
|
13781
|
-
* every call-site behaves identically.
|
|
13782
14092
|
*
|
|
13783
|
-
*
|
|
13784
|
-
*
|
|
13785
|
-
* @returns An object with `state` and an optional `errorMessage`.
|
|
14093
|
+
* // This will throw if validation fails
|
|
14094
|
+
* assertCCTPv2BridgeParams(params)
|
|
13786
14095
|
*
|
|
13787
|
-
*
|
|
13788
|
-
*
|
|
13789
|
-
* const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
|
|
13790
|
-
* step.state = outcome.state
|
|
13791
|
-
* if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
|
|
14096
|
+
* // If we get here, params is guaranteed to be valid
|
|
14097
|
+
* console.log('CCTPv2 transfer parameters are valid')
|
|
13792
14098
|
* ```
|
|
13793
|
-
*/ function
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
14099
|
+
*/ function assertCCTPv2BridgeParams(params) {
|
|
14100
|
+
// First validate basic bridge params
|
|
14101
|
+
validateWithStateTracking(params, bridgeParamsSchema, 'CCTPv2 bridge parameters', assertCCTPv2BridgeParamsSymbol);
|
|
14102
|
+
// After validation, we know params is CCTPV2BridgeParams
|
|
14103
|
+
const bridgeParams = params;
|
|
14104
|
+
// Enforce that source and destination chains are either testnet or mainnet
|
|
14105
|
+
if (bridgeParams.source.chain.isTestnet !== bridgeParams.destination.chain.isTestnet) {
|
|
14106
|
+
throw createNetworkMismatchError(bridgeParams.source.chain, bridgeParams.destination.chain);
|
|
13798
14107
|
}
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
function
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
14108
|
+
assertCCTPV2Support(bridgeParams.source.chain, bridgeParams.destination.chain);
|
|
14109
|
+
// Validate that the destination chain supports forwarding when forwarder is enabled
|
|
14110
|
+
assertForwarderRouteSupport(bridgeParams.source.chain, bridgeParams.destination.chain, bridgeParams.destination.useForwarder);
|
|
14111
|
+
/**
|
|
14112
|
+
* Enforce that if fee is defined then feeRecipient must be defined.
|
|
14113
|
+
* We do not do this in the validation function itself because we want to allow
|
|
14114
|
+
* optional properties when calling `provider.bridge()` due to the custom fee
|
|
14115
|
+
* configuration being possible at the kit level as well.
|
|
14116
|
+
*/ if (bridgeParams.config?.customFee?.value !== undefined && bridgeParams.config?.customFee?.recipientAddress === undefined) {
|
|
14117
|
+
throw createValidationFailedError$1('recipientAddress', bridgeParams.config.customFee.value, 'Custom fee is defined but fee recipient is not. Please provide a fee recipient.');
|
|
14118
|
+
}
|
|
14119
|
+
// Check if this is a forwarder-only destination (no adapter, requires useForwarder: true)
|
|
14120
|
+
const isForwarderOnly = bridgeParams.destination.useForwarder === true && !('adapter' in bridgeParams.destination && bridgeParams.destination.adapter);
|
|
14121
|
+
// Forwarder-only destinations require recipientAddress
|
|
14122
|
+
if (isForwarderOnly) {
|
|
14123
|
+
if (!bridgeParams.destination.recipientAddress?.trim()) {
|
|
14124
|
+
throw createValidationFailedError$1('recipientAddress', bridgeParams.destination.recipientAddress, 'recipientAddress is required when using forwarder without a destination adapter.');
|
|
14125
|
+
}
|
|
14126
|
+
}
|
|
14127
|
+
// Validate CCTP v2 specific requirements for source wallet
|
|
14128
|
+
assertCCTPv2WalletContext(bridgeParams.source);
|
|
14129
|
+
// Validate that source adapter supports the chain (defense-in-depth)
|
|
14130
|
+
bridgeParams.source.adapter.validateChainSupport(bridgeParams.source.chain);
|
|
14131
|
+
// Only validate destination wallet context and adapter if not forwarder-only
|
|
14132
|
+
if (!isForwarderOnly) {
|
|
14133
|
+
assertCCTPv2WalletContext(bridgeParams.destination);
|
|
14134
|
+
// Validate that destination adapter supports the chain (defense-in-depth)
|
|
14135
|
+
bridgeParams.destination.adapter.validateChainSupport(bridgeParams.destination.chain);
|
|
13808
14136
|
}
|
|
13809
|
-
// Verify that the continuation step actually exists and is in pending state
|
|
13810
|
-
const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
|
|
13811
|
-
return pendingStep !== undefined;
|
|
13812
14137
|
}
|
|
13813
14138
|
/**
|
|
13814
|
-
*
|
|
13815
|
-
|
|
13816
|
-
*
|
|
13817
|
-
* @param stepNames - The ordered list of step names in the execution flow.
|
|
13818
|
-
* @returns True if this is the last step in the flow.
|
|
13819
|
-
*
|
|
13820
|
-
* @example
|
|
13821
|
-
* ```typescript
|
|
13822
|
-
* import { isLastStep } from './stepUtils'
|
|
14139
|
+
* Validate CCTP v2 support on both chains
|
|
14140
|
+
*/ /**
|
|
14141
|
+
* Throws a KitError if the given chain does not support CCTP v2.
|
|
13823
14142
|
*
|
|
13824
|
-
*
|
|
13825
|
-
*
|
|
13826
|
-
*
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
14143
|
+
* @param chain - The chain to check for CCTP v2 support
|
|
14144
|
+
* @param otherChain - The other chain in the route (for error context)
|
|
14145
|
+
* @param isSource - Whether this is the source chain (for error context)
|
|
14146
|
+
*/ function assertCCTPV2Support(source, destination) {
|
|
14147
|
+
if (!isCCTPV2Supported(source) || !isCCTPV2Supported(destination)) {
|
|
14148
|
+
throw createUnsupportedRouteError(source.name, destination.name);
|
|
14149
|
+
}
|
|
13831
14150
|
}
|
|
13832
14151
|
/**
|
|
13833
|
-
*
|
|
13834
|
-
*
|
|
13835
|
-
* Poll the adapter until the transaction is confirmed on-chain and return
|
|
13836
|
-
* the updated step with success or error state based on the receipt.
|
|
14152
|
+
* Validates that the forwarder (relaying) feature is compatible with the route.
|
|
13837
14153
|
*
|
|
13838
|
-
*
|
|
13839
|
-
*
|
|
13840
|
-
* @param chain - The chain where the transaction was submitted.
|
|
13841
|
-
* @returns The updated step object with success or error state.
|
|
14154
|
+
* Checks the destination chain's `cctp.forwarderSupported.destination` property
|
|
14155
|
+
* to determine whether the chain supports receiving forwarded transfers.
|
|
13842
14156
|
*
|
|
13843
|
-
* @
|
|
14157
|
+
* @param source - The source chain definition
|
|
14158
|
+
* @param destination - The destination chain definition
|
|
14159
|
+
* @param useForwarder - Whether the forwarder is enabled on the destination
|
|
14160
|
+
* @throws {KitError} If the forwarder is enabled and the destination chain does not support forwarding
|
|
14161
|
+
*/ function assertForwarderRouteSupport(source, destination, useForwarder) {
|
|
14162
|
+
if (useForwarder === true && !destination.cctp?.forwarderSupported.destination) {
|
|
14163
|
+
throw new KitError({
|
|
14164
|
+
...InputError.UNSUPPORTED_ROUTE,
|
|
14165
|
+
recoverability: 'FATAL',
|
|
14166
|
+
message: `Route from ${source.name} to ${destination.name} with forwarder is not supported (destination chain does not support forwarding).`,
|
|
14167
|
+
cause: {
|
|
14168
|
+
trace: {
|
|
14169
|
+
source: source.name,
|
|
14170
|
+
destination: destination.name
|
|
14171
|
+
}
|
|
14172
|
+
}
|
|
14173
|
+
});
|
|
14174
|
+
}
|
|
14175
|
+
}
|
|
14176
|
+
|
|
14177
|
+
/**
|
|
14178
|
+
* Checks if a decoded attestation field matches the corresponding transfer parameter.
|
|
14179
|
+
* If the values do not match, appends a descriptive error message to the errors array.
|
|
13844
14180
|
*
|
|
13845
|
-
* @
|
|
13846
|
-
*
|
|
13847
|
-
*
|
|
14181
|
+
* @param field - The name of the field being compared (for error reporting)
|
|
14182
|
+
* @param decoded - The value decoded from the attestation message
|
|
14183
|
+
* @param param - The expected value from the transfer parameters
|
|
14184
|
+
* @param errors - The array to which error messages will be appended if a mismatch is found
|
|
14185
|
+
*/ function checkFieldMismatch(field, decoded, param, errors) {
|
|
14186
|
+
if (decoded !== param) {
|
|
14187
|
+
errors.push(`${field} mismatch: decoded=${String(decoded)}, params=${String(param)}`);
|
|
14188
|
+
}
|
|
14189
|
+
}
|
|
14190
|
+
/**
|
|
14191
|
+
* Asserts that the decoded message from attestation matches the provided transfer params.
|
|
14192
|
+
* Throws KitError if any field mismatches, with clear error messages.
|
|
13848
14193
|
*
|
|
13849
|
-
*
|
|
13850
|
-
*
|
|
13851
|
-
*
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
14194
|
+
* @param attestation - The attestation message containing the decoded message
|
|
14195
|
+
* @param params - The transfer parameters to validate against
|
|
14196
|
+
* @throws {@link KitError} If any field mismatches
|
|
14197
|
+
*/ async function assertCCTPv2AttestationParams(attestation, params) {
|
|
14198
|
+
const errors = [];
|
|
14199
|
+
const message = attestation.decodedMessage;
|
|
14200
|
+
const messageBody = message.decodedMessageBody;
|
|
14201
|
+
// Use recipientAddress if provided, otherwise use destination.address
|
|
14202
|
+
const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
|
|
14203
|
+
const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
|
|
14204
|
+
let sender;
|
|
14205
|
+
if (hasCustomContractSupport(params.source.chain, 'bridge')) {
|
|
14206
|
+
if (params.source.chain.type === 'solana') {
|
|
14207
|
+
// Solana: User → Bridge contract → CCTP (user remains sender)
|
|
14208
|
+
sender = params.source.address;
|
|
14209
|
+
} else {
|
|
14210
|
+
// Other chains (like EVM): Bridge contract → CCTP (bridge contract becomes sender)
|
|
14211
|
+
sender = params.source.chain.kitContracts?.bridge;
|
|
14212
|
+
}
|
|
14213
|
+
} else {
|
|
14214
|
+
sender = params.source.address;
|
|
14215
|
+
}
|
|
14216
|
+
checkFieldMismatch('sourceDomain', message.sourceDomain, params.source.chain.cctp.domain.toString(), errors);
|
|
14217
|
+
checkFieldMismatch('destinationDomain', message.destinationDomain, params.destination.chain.cctp.domain.toString(), errors);
|
|
14218
|
+
checkFieldMismatch('minFinalityThreshold', message.minFinalityThreshold, CCTPv2MinFinalityThreshold[params.config.transferSpeed ?? 'FAST'].toString(), errors);
|
|
14219
|
+
checkFieldMismatch('sender', params.source.chain.type === 'evm' ? messageBody.messageSender.toLowerCase() : messageBody.messageSender, params.source.chain.type === 'evm' ? sender?.toLowerCase() : sender, errors);
|
|
14220
|
+
checkFieldMismatch('recipient', params.destination.chain.type === 'evm' ? messageBody.mintRecipient.toLowerCase() : messageBody.mintRecipient, params.destination.chain.type === 'evm' ? mintRecipient.toLowerCase() : mintRecipient, errors);
|
|
14221
|
+
checkFieldMismatch('amount', messageBody.amount, params.amount.toString(), errors);
|
|
14222
|
+
checkFieldMismatch('burnToken', messageBody.burnToken.toLowerCase(), params.source.chain.usdcAddress.toLowerCase(), errors);
|
|
14223
|
+
if (errors.length > 0) {
|
|
14224
|
+
const errorMessage = 'Attestation validation failed: received attestation does not match expected transfer parameters';
|
|
14225
|
+
const firstError = errors[0] ?? '';
|
|
13855
14226
|
throw new KitError({
|
|
13856
14227
|
...InputError.VALIDATION_FAILED,
|
|
13857
14228
|
recoverability: 'FATAL',
|
|
13858
|
-
message:
|
|
14229
|
+
message: `${errorMessage}: ${firstError}`,
|
|
14230
|
+
cause: {
|
|
14231
|
+
trace: {
|
|
14232
|
+
validationErrors: errors
|
|
14233
|
+
}
|
|
14234
|
+
}
|
|
13859
14235
|
});
|
|
13860
14236
|
}
|
|
13861
|
-
const txHash = pendingStep.txHash;
|
|
13862
|
-
const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
|
|
13863
|
-
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
13864
|
-
chain: chain.name,
|
|
13865
|
-
txHash
|
|
13866
|
-
}))
|
|
13867
|
-
});
|
|
13868
|
-
const outcome = evaluateTransactionOutcome(txReceipt, txHash);
|
|
13869
|
-
return {
|
|
13870
|
-
...pendingStep,
|
|
13871
|
-
state: outcome.state,
|
|
13872
|
-
data: txReceipt,
|
|
13873
|
-
explorerUrl: buildExplorerUrl(chain, txHash),
|
|
13874
|
-
...outcome.errorMessage ? {
|
|
13875
|
-
errorMessage: outcome.errorMessage
|
|
13876
|
-
} : {}
|
|
13877
|
-
};
|
|
13878
14237
|
}
|
|
14238
|
+
|
|
13879
14239
|
/**
|
|
13880
|
-
*
|
|
14240
|
+
* Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
|
|
13881
14241
|
*
|
|
13882
|
-
*
|
|
13883
|
-
*
|
|
14242
|
+
* Validates the full public-boundary input before any field destructuring,
|
|
14243
|
+
* `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
|
|
14244
|
+
* inputs always produce typed `KitError` validation failures.
|
|
13884
14245
|
*
|
|
13885
|
-
*
|
|
13886
|
-
*
|
|
13887
|
-
*
|
|
13888
|
-
*
|
|
13889
|
-
*
|
|
13890
|
-
*
|
|
13891
|
-
*
|
|
13892
|
-
*
|
|
13893
|
-
*
|
|
14246
|
+
* Checks performed (in order):
|
|
14247
|
+
* - `params` must be a non-null plain object
|
|
14248
|
+
* - `source` — valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
|
|
14249
|
+
* - `destinationChain` — present and supports CCTP v2
|
|
14250
|
+
* - source and destination chains must both be testnet or both mainnet
|
|
14251
|
+
* - source and destination chains must differ
|
|
14252
|
+
* - `executor` — non-empty string
|
|
14253
|
+
* - `amount` — bigint or non-empty string coercible to bigint
|
|
14254
|
+
* - `feeTotalAmount` — bigint or non-empty string coercible to bigint
|
|
14255
|
+
* - `feeToken` — valid EVM address (`0x` + 40 hex chars)
|
|
14256
|
+
* - `claim.signedQuote` — valid `0x`-prefixed hex string
|
|
14257
|
+
* - `claim.refundAddress` — valid EVM address
|
|
14258
|
+
* - `hookData` — valid `0x`-prefixed hex string when present
|
|
13894
14259
|
*
|
|
13895
|
-
* @
|
|
14260
|
+
* @param params - The value to validate.
|
|
14261
|
+
* @throws {KitError} If any field is missing or invalid.
|
|
13896
14262
|
*
|
|
13897
14263
|
* @example
|
|
13898
14264
|
* ```typescript
|
|
13899
|
-
*
|
|
13900
|
-
*
|
|
13901
|
-
* const
|
|
13902
|
-
* const updatedStep = await waitForStepToComplete(
|
|
13903
|
-
* pendingStep,
|
|
13904
|
-
* adapter,
|
|
13905
|
-
* chain,
|
|
13906
|
-
* context,
|
|
13907
|
-
* result,
|
|
13908
|
-
* provider,
|
|
13909
|
-
* )
|
|
13910
|
-
* // updatedStep.state is now 'success' or 'error'
|
|
14265
|
+
* assertBurnWithFeesParams(params)
|
|
14266
|
+
* // params is now typed as BurnWithFeesParams and safe to use
|
|
14267
|
+
* const { source, destinationChain, amount } = params
|
|
13911
14268
|
* ```
|
|
13912
|
-
*/
|
|
13913
|
-
if (
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
14269
|
+
*/ function assertBurnWithFeesParams(params) {
|
|
14270
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
14271
|
+
throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
|
|
14272
|
+
}
|
|
14273
|
+
const p = params;
|
|
14274
|
+
// Source wallet context
|
|
14275
|
+
assertCCTPv2WalletContext(p['source']);
|
|
14276
|
+
const source = p['source'];
|
|
14277
|
+
// destinationChain
|
|
14278
|
+
const destinationChain = p['destinationChain'];
|
|
14279
|
+
if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
|
|
14280
|
+
throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
|
|
14281
|
+
}
|
|
14282
|
+
if (!isCCTPV2Supported(destinationChain)) {
|
|
14283
|
+
throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
|
|
14284
|
+
}
|
|
14285
|
+
const dest = destinationChain;
|
|
14286
|
+
// Testnet / mainnet mismatch
|
|
14287
|
+
if (source.chain.isTestnet !== dest.isTestnet) {
|
|
14288
|
+
throw createNetworkMismatchError(source.chain, dest);
|
|
14289
|
+
}
|
|
14290
|
+
// Same-chain guard
|
|
14291
|
+
if (source.chain.name === dest.name) {
|
|
14292
|
+
throw createUnsupportedRouteError(source.chain.name, dest.name);
|
|
14293
|
+
}
|
|
14294
|
+
// executor
|
|
14295
|
+
const executor = p['executor'];
|
|
14296
|
+
if (typeof executor !== 'string' || executor === '') {
|
|
14297
|
+
throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
|
|
14298
|
+
}
|
|
14299
|
+
// amount
|
|
14300
|
+
const rawAmount = p['amount'];
|
|
14301
|
+
if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
|
|
14302
|
+
throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
|
|
13934
14303
|
}
|
|
13935
|
-
// For transaction steps, wait for the transaction to complete
|
|
13936
|
-
return waitForPendingTransaction(pendingStep, adapter, chain);
|
|
13937
|
-
}
|
|
13938
|
-
|
|
13939
|
-
/**
|
|
13940
|
-
* Executes a prepared chain request and returns the result as a bridge step.
|
|
13941
|
-
*
|
|
13942
|
-
* This function takes a prepared chain request (containing transaction data) and executes
|
|
13943
|
-
* it using the appropriate adapter. It handles the execution details and formats
|
|
13944
|
-
* the result as a standardized bridge step with transaction details and explorer URLs.
|
|
13945
|
-
*
|
|
13946
|
-
* @param params - The execution parameters containing:
|
|
13947
|
-
* - `name`: The name of the step
|
|
13948
|
-
* - `request`: The prepared chain request containing transaction data
|
|
13949
|
-
* - `adapter`: The adapter that will execute the transaction
|
|
13950
|
-
* - `confirmations`: The number of confirmations to wait for (defaults to 1)
|
|
13951
|
-
* - `timeout`: The timeout for the request in milliseconds
|
|
13952
|
-
* - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
|
|
13953
|
-
* bypassing `eth_estimateGas`; ignored for non-EVM requests
|
|
13954
|
-
* @returns The bridge step with the transaction details and explorer URL
|
|
13955
|
-
* @throws If the transaction execution fails
|
|
13956
|
-
*
|
|
13957
|
-
* @example
|
|
13958
|
-
* ```typescript
|
|
13959
|
-
* const step = await executePreparedChainRequest({
|
|
13960
|
-
* name: 'approve',
|
|
13961
|
-
* request: preparedRequest,
|
|
13962
|
-
* adapter: adapter,
|
|
13963
|
-
* confirmations: 2,
|
|
13964
|
-
* timeout: 30000
|
|
13965
|
-
* })
|
|
13966
|
-
* console.log('Transaction hash:', step.txHash)
|
|
13967
|
-
* ```
|
|
13968
|
-
*/ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
|
|
13969
|
-
const step = {
|
|
13970
|
-
name,
|
|
13971
|
-
state: 'pending'
|
|
13972
|
-
};
|
|
13973
14304
|
try {
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
14008
|
-
} catch (err) {
|
|
14009
|
-
step.state = 'error';
|
|
14010
|
-
step.error = err;
|
|
14011
|
-
// Sequential path does not yet attempt fine-grained classification of
|
|
14012
|
-
// pre-submission errors (user_rejected, capability errors, etc.). Mark
|
|
14013
|
-
// as `unknown` so consumers can at least detect the category is
|
|
14014
|
-
// populated uniformly across batched and sequential flows.
|
|
14015
|
-
step.errorCategory = 'unknown';
|
|
14016
|
-
// Optionally parse for common blockchain error formats
|
|
14017
|
-
if (err instanceof Error) {
|
|
14018
|
-
step.errorMessage = err.message;
|
|
14019
|
-
} else if (typeof err === 'object' && err != null && 'message' in err) {
|
|
14020
|
-
step.errorMessage = String(err.message);
|
|
14021
|
-
} else {
|
|
14022
|
-
step.errorMessage = `Unknown error occurred during ${name} step.`;
|
|
14023
|
-
}
|
|
14305
|
+
BigInt(rawAmount);
|
|
14306
|
+
} catch {
|
|
14307
|
+
throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
|
|
14308
|
+
}
|
|
14309
|
+
// feeTotalAmount
|
|
14310
|
+
const rawFee = p['feeTotalAmount'];
|
|
14311
|
+
if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
|
|
14312
|
+
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
|
|
14313
|
+
}
|
|
14314
|
+
try {
|
|
14315
|
+
BigInt(rawFee);
|
|
14316
|
+
} catch {
|
|
14317
|
+
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
|
|
14318
|
+
}
|
|
14319
|
+
// feeToken
|
|
14320
|
+
if (!evmAddressSchema.safeParse(p['feeToken']).success) {
|
|
14321
|
+
throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
|
|
14322
|
+
}
|
|
14323
|
+
// claim
|
|
14324
|
+
const rawClaim = p['claim'];
|
|
14325
|
+
if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
|
|
14326
|
+
throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
|
|
14327
|
+
}
|
|
14328
|
+
const claim = rawClaim;
|
|
14329
|
+
if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
|
|
14330
|
+
throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
|
|
14331
|
+
}
|
|
14332
|
+
if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
|
|
14333
|
+
throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
|
|
14334
|
+
}
|
|
14335
|
+
// hookData (optional)
|
|
14336
|
+
const hookData = p['hookData'];
|
|
14337
|
+
if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
|
|
14338
|
+
throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
|
|
14024
14339
|
}
|
|
14025
|
-
return step;
|
|
14026
14340
|
}
|
|
14027
14341
|
|
|
14028
14342
|
/**
|
|
@@ -14053,7 +14367,7 @@ function hasPendingState(analysis, result) {
|
|
|
14053
14367
|
adapter: params.source.adapter,
|
|
14054
14368
|
chain: params.source.chain,
|
|
14055
14369
|
request: await provider.approve(params.source, approvalAmount),
|
|
14056
|
-
|
|
14370
|
+
gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.approve)
|
|
14057
14371
|
});
|
|
14058
14372
|
}
|
|
14059
14373
|
|
|
@@ -14081,7 +14395,7 @@ function hasPendingState(analysis, result) {
|
|
|
14081
14395
|
adapter: params.source.adapter,
|
|
14082
14396
|
chain: params.source.chain,
|
|
14083
14397
|
request: await provider.burn(params),
|
|
14084
|
-
|
|
14398
|
+
gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.burn)
|
|
14085
14399
|
});
|
|
14086
14400
|
}
|
|
14087
14401
|
|
|
@@ -14175,10 +14489,9 @@ function hasPendingState(analysis, result) {
|
|
|
14175
14489
|
request: mintRequest,
|
|
14176
14490
|
// Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
|
|
14177
14491
|
// eth_estimateGas does not account for, returning a below-floor value
|
|
14178
|
-
// without reverting.
|
|
14179
|
-
//
|
|
14180
|
-
|
|
14181
|
-
gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
|
|
14492
|
+
// without reverting. The floor covers those; chains that cost more than the
|
|
14493
|
+
// floor are covered by their own estimate.
|
|
14494
|
+
gasFloor: Number(BRIDGE_STEP_GAS_FLOORS_EVM.mint)
|
|
14182
14495
|
});
|
|
14183
14496
|
// Add forwarded: false for non-relayer mints
|
|
14184
14497
|
return {
|
|
@@ -14701,7 +15014,7 @@ const mockAttestationMessage = {
|
|
|
14701
15014
|
return step;
|
|
14702
15015
|
}
|
|
14703
15016
|
|
|
14704
|
-
var version$2 = "1.
|
|
15017
|
+
var version$2 = "1.11.0";
|
|
14705
15018
|
var pkg$2 = {
|
|
14706
15019
|
version: version$2};
|
|
14707
15020
|
|
|
@@ -15660,10 +15973,39 @@ function assertCCTPV2Config(config) {
|
|
|
15660
15973
|
// CCTP-specific transfer params validation (includes base validation)
|
|
15661
15974
|
assertCCTPv2BridgeParams(params);
|
|
15662
15975
|
const { source, destination, amount } = params;
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
|
|
15976
|
+
/**
|
|
15977
|
+
* Price the gas a step will RESERVE on-chain, not what it will spend.
|
|
15978
|
+
*
|
|
15979
|
+
* A transaction is only admitted when the sender holds
|
|
15980
|
+
* `gasLimit * gasPrice`, and `executePreparedChainRequest` submits
|
|
15981
|
+
* `max(estimate * buffer, floor)`. Quoting a bare `estimate()` therefore
|
|
15982
|
+
* under-reports what a wallet must hold to send at all: the floor governs
|
|
15983
|
+
* on virtually every chain, which put the burn quote ~2.5x below the real
|
|
15984
|
+
* requirement and left anyone funding from it unable to submit.
|
|
15985
|
+
*
|
|
15986
|
+
* Delegates to the same `resolveGasLimit` the execute path calls, so a
|
|
15987
|
+
* quote and the limit later submitted for that step cannot drift apart.
|
|
15988
|
+
* That also means an estimation failure degrades to the floor here exactly
|
|
15989
|
+
* as it does on execution, rather than surfacing as a failed quote — the
|
|
15990
|
+
* floor is what would be submitted, so it is the honest number to quote.
|
|
15991
|
+
*
|
|
15992
|
+
* Non-EVM requests are unchanged: `executePreparedChainRequest` only
|
|
15993
|
+
* applies a floor when `request.type === 'evm'`, so there is no reserved
|
|
15994
|
+
* limit to quote on other chains.
|
|
15995
|
+
*/ const quoteReservedGas = async (request, gasFloor, priceGas, // Ignored on EVM — only evaluated and used on non-EVM paths.
|
|
15996
|
+
nonEvmFallbackGasEstimate)=>{
|
|
15997
|
+
if (request.type !== 'evm') {
|
|
15998
|
+
return nonEvmFallbackGasEstimate === undefined ? await request.estimate() : await request.estimate(undefined, await priceGas(nonEvmFallbackGasEstimate()));
|
|
15999
|
+
}
|
|
16000
|
+
// resolveGasLimit returns number; gas units are well below Number.MAX_SAFE_INTEGER,
|
|
16001
|
+
// so the Number() → resolveGasLimit → BigInt() round-trip is lossless.
|
|
16002
|
+
return await priceGas(BigInt(await resolveGasLimit(request, Number(gasFloor))));
|
|
15666
16003
|
};
|
|
16004
|
+
const priceGasFor = (ctx)=>async (gasUnits)=>await ctx.adapter.calculateTransactionFee(gasUnits, undefined, ctx.chain);
|
|
16005
|
+
const priceSourceGas = priceGasFor(source);
|
|
16006
|
+
const priceDestinationGas = priceGasFor(destination);
|
|
16007
|
+
const estimateApprove = async ()=>await quoteReservedGas(await this.approve(source, amount), BRIDGE_STEP_GAS_FLOORS_EVM.approve, priceSourceGas);
|
|
16008
|
+
const estimateBurn = async ()=>await quoteReservedGas(await this.burn(params), BRIDGE_STEP_GAS_FLOORS_EVM.burn, priceSourceGas, ()=>hasCustomContractSupport(source.chain, 'bridge') ? CUSTOM_BURN_GAS_ESTIMATE_EVM : DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM);
|
|
15667
16009
|
// Only estimate Mint gas when not using forwarder (user pays gas)
|
|
15668
16010
|
// When useForwarder=true, Circle's Orbit relayer handles and pays for the mint
|
|
15669
16011
|
const useForwarder = destination.useForwarder === true;
|
|
@@ -15672,12 +16014,11 @@ function assertCCTPV2Config(config) {
|
|
|
15672
16014
|
return null // Skip mint estimation when forwarder handles it
|
|
15673
16015
|
;
|
|
15674
16016
|
}
|
|
15675
|
-
|
|
15676
|
-
return await mint.estimate(undefined, await destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, undefined, destination.chain));
|
|
16017
|
+
return await quoteReservedGas(await this.mint(source, destination, mockAttestationMessage), BRIDGE_STEP_GAS_FLOORS_EVM.mint, priceDestinationGas, ()=>RECEIVE_MESSAGE_GAS_ESTIMATE_EVM);
|
|
15677
16018
|
};
|
|
15678
16019
|
// Parallelize all independent async operations
|
|
15679
16020
|
const [approveEstimate, depositForBurnFee, receiveMessageFee, feeEstimates] = await Promise.allSettled([
|
|
15680
|
-
|
|
16021
|
+
estimateApprove(),
|
|
15681
16022
|
estimateBurn(),
|
|
15682
16023
|
estimateMint(),
|
|
15683
16024
|
this.getMaxFee(params)
|
|
@@ -17299,7 +17640,9 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
|
|
|
17299
17640
|
headers: context.headers
|
|
17300
17641
|
}
|
|
17301
17642
|
});
|
|
17302
|
-
if (
|
|
17643
|
+
if (context.customFeePolicy?.bridge != null) {
|
|
17644
|
+
kit.setCustomFeePolicy(context.customFeePolicy.bridge);
|
|
17645
|
+
} else if (hasBoth) {
|
|
17303
17646
|
kit.setCustomFeePolicy({
|
|
17304
17647
|
calculateFee: async (params)=>{
|
|
17305
17648
|
const feeStr = await getFee('bridge', params);
|
|
@@ -17315,7 +17658,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
|
|
|
17315
17658
|
};
|
|
17316
17659
|
|
|
17317
17660
|
var name$1 = "@circle-fin/swap-kit";
|
|
17318
|
-
var version$1 = "1.5.
|
|
17661
|
+
var version$1 = "1.5.2";
|
|
17319
17662
|
var pkg$1 = {
|
|
17320
17663
|
name: name$1,
|
|
17321
17664
|
version: version$1};
|
|
@@ -19258,7 +19601,7 @@ new Set(Object.values(Blockchain));
|
|
|
19258
19601
|
registerKit(`${pkg$1.name}/${pkg$1.version}`);
|
|
19259
19602
|
|
|
19260
19603
|
var name = "@circle-fin/earn-kit";
|
|
19261
|
-
var version = "1.
|
|
19604
|
+
var version = "1.5.1";
|
|
19262
19605
|
var pkg = {
|
|
19263
19606
|
name: name,
|
|
19264
19607
|
version: version};
|
|
@@ -20269,11 +20612,16 @@ const sourceAdapterContextSchema = zod.z.object({
|
|
|
20269
20612
|
*
|
|
20270
20613
|
* Validate the optional Kit Key field using the standard `apiKeySchema`
|
|
20271
20614
|
* format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
|
|
20272
|
-
* operates in permissionless mode.
|
|
20615
|
+
* operates in permissionless mode. `baseUrl` overrides the Earn Service
|
|
20616
|
+
* endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
|
|
20617
|
+
* batched execution. Both are forwarded to the provider, so this `.strict()`
|
|
20618
|
+
* schema must accept them or a valid config object is rejected.
|
|
20273
20619
|
*
|
|
20274
20620
|
* @internal
|
|
20275
20621
|
*/ const earnConfigSchema = zod.z.object({
|
|
20276
|
-
kitKey: apiKeySchema.optional()
|
|
20622
|
+
kitKey: apiKeySchema.optional(),
|
|
20623
|
+
baseUrl: zod.z.string().optional(),
|
|
20624
|
+
batchTransactions: zod.z.boolean().optional()
|
|
20277
20625
|
}).strict();
|
|
20278
20626
|
/**
|
|
20279
20627
|
* Canonical decimal form: a leading digit with no leading zeros (a single
|