@circle-fin/provider-cctp-v2 1.8.5 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +5 -5
- package/index.cjs +2488 -1832
- package/index.d.cts +889 -220
- package/index.d.mts +889 -220
- package/index.d.ts +889 -220
- package/index.mjs +2484 -1833
- package/package.json +3 -1
package/index.mjs
CHANGED
|
@@ -28,10 +28,11 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
import { z } from 'zod';
|
|
31
|
-
import pino from 'pino';
|
|
32
|
-
import { formatUnits as formatUnits$1 } from '@ethersproject/units';
|
|
33
31
|
import { hexlify, hexZeroPad } from '@ethersproject/bytes';
|
|
32
|
+
import '@ethersproject/abi';
|
|
34
33
|
import { getAddress } from '@ethersproject/address';
|
|
34
|
+
import pino from 'pino';
|
|
35
|
+
import { formatUnits as formatUnits$1 } from '@ethersproject/units';
|
|
35
36
|
import bs58 from 'bs58';
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -77,6 +78,8 @@ import bs58 from 'bs58';
|
|
|
77
78
|
Blockchain["Celo_Alfajores_Testnet"] = "Celo_Alfajores_Testnet";
|
|
78
79
|
Blockchain["Codex"] = "Codex";
|
|
79
80
|
Blockchain["Codex_Testnet"] = "Codex_Testnet";
|
|
81
|
+
Blockchain["Cronos"] = "Cronos";
|
|
82
|
+
Blockchain["Cronos_Testnet"] = "Cronos_Testnet";
|
|
80
83
|
Blockchain["Edge"] = "Edge";
|
|
81
84
|
Blockchain["Edge_Testnet"] = "Edge_Testnet";
|
|
82
85
|
Blockchain["Ethereum"] = "Ethereum";
|
|
@@ -159,6 +162,7 @@ var BridgeChain;
|
|
|
159
162
|
BridgeChain["Avalanche"] = "Avalanche";
|
|
160
163
|
BridgeChain["Base"] = "Base";
|
|
161
164
|
BridgeChain["Codex"] = "Codex";
|
|
165
|
+
BridgeChain["Cronos"] = "Cronos";
|
|
162
166
|
BridgeChain["Edge"] = "Edge";
|
|
163
167
|
BridgeChain["Ethereum"] = "Ethereum";
|
|
164
168
|
BridgeChain["HyperEVM"] = "HyperEVM";
|
|
@@ -183,6 +187,7 @@ var BridgeChain;
|
|
|
183
187
|
BridgeChain["Avalanche_Fuji"] = "Avalanche_Fuji";
|
|
184
188
|
BridgeChain["Base_Sepolia"] = "Base_Sepolia";
|
|
185
189
|
BridgeChain["Codex_Testnet"] = "Codex_Testnet";
|
|
190
|
+
BridgeChain["Cronos_Testnet"] = "Cronos_Testnet";
|
|
186
191
|
BridgeChain["Edge_Testnet"] = "Edge_Testnet";
|
|
187
192
|
BridgeChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
|
|
188
193
|
BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
|
|
@@ -694,7 +699,10 @@ var EarnChain;
|
|
|
694
699
|
contracts: {
|
|
695
700
|
v1: {
|
|
696
701
|
wallet: GATEWAY_WALLET_EVM_TESTNET,
|
|
697
|
-
minter: GATEWAY_MINTER_EVM_TESTNET
|
|
702
|
+
minter: GATEWAY_MINTER_EVM_TESTNET,
|
|
703
|
+
// DepositForHandler the GenericExecutor calls to run a fast cross-chain
|
|
704
|
+
// deposit into the GatewayWallet above.
|
|
705
|
+
depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
|
|
698
706
|
}
|
|
699
707
|
},
|
|
700
708
|
forwarderSupported: {
|
|
@@ -1228,6 +1236,96 @@ var EarnChain;
|
|
|
1228
1236
|
}
|
|
1229
1237
|
});
|
|
1230
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Cronos Mainnet chain definition
|
|
1241
|
+
* @remarks
|
|
1242
|
+
* This represents the official production network for the Cronos blockchain.
|
|
1243
|
+
* Cronos is an EVM-compatible blockchain.
|
|
1244
|
+
*/ const Cronos = defineChain({
|
|
1245
|
+
type: 'evm',
|
|
1246
|
+
chain: Blockchain.Cronos,
|
|
1247
|
+
name: 'Cronos',
|
|
1248
|
+
title: 'Cronos Mainnet',
|
|
1249
|
+
nativeCurrency: {
|
|
1250
|
+
name: 'Cronos',
|
|
1251
|
+
symbol: 'CRO',
|
|
1252
|
+
decimals: 18
|
|
1253
|
+
},
|
|
1254
|
+
chainId: 25,
|
|
1255
|
+
isTestnet: false,
|
|
1256
|
+
explorerUrl: 'https://cronoscan.com/tx/{hash}',
|
|
1257
|
+
rpcEndpoints: [
|
|
1258
|
+
'https://evm.cronos.org'
|
|
1259
|
+
],
|
|
1260
|
+
eurcAddress: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
|
|
1261
|
+
usdcAddress: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
|
|
1262
|
+
usdtAddress: null,
|
|
1263
|
+
cctp: {
|
|
1264
|
+
domain: 32,
|
|
1265
|
+
contracts: {
|
|
1266
|
+
v2: {
|
|
1267
|
+
type: 'split',
|
|
1268
|
+
tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
|
|
1269
|
+
messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
|
|
1270
|
+
confirmations: 1,
|
|
1271
|
+
fastConfirmations: 1
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
forwarderSupported: {
|
|
1275
|
+
source: false,
|
|
1276
|
+
destination: false
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
kitContracts: {
|
|
1280
|
+
bridge: BRIDGE_CONTRACT_EVM_MAINNET
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Cronos Testnet chain definition
|
|
1286
|
+
* @remarks
|
|
1287
|
+
* This represents the official test network for the Cronos blockchain.
|
|
1288
|
+
* Cronos is an EVM-compatible blockchain.
|
|
1289
|
+
*/ const CronosTestnet = defineChain({
|
|
1290
|
+
type: 'evm',
|
|
1291
|
+
chain: Blockchain.Cronos_Testnet,
|
|
1292
|
+
name: 'Cronos Testnet',
|
|
1293
|
+
title: 'Cronos Testnet',
|
|
1294
|
+
nativeCurrency: {
|
|
1295
|
+
name: 'CRO',
|
|
1296
|
+
symbol: 'tCRO',
|
|
1297
|
+
decimals: 18
|
|
1298
|
+
},
|
|
1299
|
+
chainId: 338,
|
|
1300
|
+
isTestnet: true,
|
|
1301
|
+
explorerUrl: 'https://explorer.cronos.org/testnet/tx/{hash}',
|
|
1302
|
+
rpcEndpoints: [
|
|
1303
|
+
'https://evm-t3.cronos.org'
|
|
1304
|
+
],
|
|
1305
|
+
eurcAddress: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
|
|
1306
|
+
usdcAddress: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
|
|
1307
|
+
usdtAddress: null,
|
|
1308
|
+
cctp: {
|
|
1309
|
+
domain: 32,
|
|
1310
|
+
contracts: {
|
|
1311
|
+
v2: {
|
|
1312
|
+
type: 'split',
|
|
1313
|
+
tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
|
|
1314
|
+
messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
|
|
1315
|
+
confirmations: 1,
|
|
1316
|
+
fastConfirmations: 1
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
forwarderSupported: {
|
|
1320
|
+
source: false,
|
|
1321
|
+
destination: false
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
kitContracts: {
|
|
1325
|
+
bridge: BRIDGE_CONTRACT_EVM_TESTNET
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1231
1329
|
/**
|
|
1232
1330
|
* Edge Mainnet chain definition
|
|
1233
1331
|
* @remarks
|
|
@@ -3579,6 +3677,8 @@ var Chains = /*#__PURE__*/Object.freeze({
|
|
|
3579
3677
|
CeloAlfajoresTestnet: CeloAlfajoresTestnet,
|
|
3580
3678
|
Codex: Codex,
|
|
3581
3679
|
CodexTestnet: CodexTestnet,
|
|
3680
|
+
Cronos: Cronos,
|
|
3681
|
+
CronosTestnet: CronosTestnet,
|
|
3582
3682
|
Edge: Edge,
|
|
3583
3683
|
EdgeTestnet: EdgeTestnet,
|
|
3584
3684
|
Ethereum: Ethereum,
|
|
@@ -3720,7 +3820,10 @@ var Chains = /*#__PURE__*/Object.freeze({
|
|
|
3720
3820
|
minter: z.string({
|
|
3721
3821
|
required_error: 'Gateway minter address is required. Please provide a valid contract address.',
|
|
3722
3822
|
invalid_type_error: 'Gateway minter address must be a string.'
|
|
3723
|
-
}).min(1, 'Gateway minter address cannot be empty.')
|
|
3823
|
+
}).min(1, 'Gateway minter address cannot be empty.'),
|
|
3824
|
+
depositForHandler: z.string({
|
|
3825
|
+
invalid_type_error: 'Gateway depositForHandler address must be a string.'
|
|
3826
|
+
}).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
|
|
3724
3827
|
}).strict() // Reject any additional properties not defined in the schema
|
|
3725
3828
|
;
|
|
3726
3829
|
/**
|
|
@@ -4190,21 +4293,31 @@ const swapTokenEnumSchema = z.enum([
|
|
|
4190
4293
|
* returning the appropriate address based on the requested contract type.
|
|
4191
4294
|
*
|
|
4192
4295
|
* @param chain - The chain definition to resolve the contract address for
|
|
4193
|
-
* @param contractType - The type of contract address to resolve ('tokenMessenger' or '
|
|
4296
|
+
* @param contractType - The type of contract address to resolve ('tokenMessenger', 'messageTransmitter', or 'tokenMessengerWithFees')
|
|
4194
4297
|
* @returns The contract address for the specified contract type
|
|
4195
4298
|
* @throws Error when chain does not support CCTP v2 or has unsupported contract configuration
|
|
4299
|
+
* @throws Error when 'tokenMessengerWithFees' is requested but not configured on the chain
|
|
4196
4300
|
*/ const resolveCCTPV2ContractAddress = (chain, contractType)=>{
|
|
4197
4301
|
// Handle custom bridge contract for tokenMessenger (burn transaction)
|
|
4198
|
-
if (hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
|
|
4302
|
+
if (contractType === 'tokenMessenger' && hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
|
|
4199
4303
|
return chain.kitContracts.bridge;
|
|
4200
4304
|
}
|
|
4201
4305
|
// At this point we know CCTP v2 is supported, so contracts exist
|
|
4202
4306
|
const cctpConfig = chain.cctp;
|
|
4203
4307
|
const contracts = cctpConfig.contracts.v2;
|
|
4308
|
+
// The `TokenMessengerWithFees` wrapper (prepaid FORWARD path) is an optional
|
|
4309
|
+
// deployment carried alongside both split and merged configurations.
|
|
4310
|
+
if (contractType === 'tokenMessengerWithFees') {
|
|
4311
|
+
const wrapper = contracts.tokenMessengerWithFees;
|
|
4312
|
+
if (wrapper === undefined || wrapper === '') {
|
|
4313
|
+
throw new Error(`TokenMessengerWithFees is not configured on chain ${chain.name}. The prepaid FORWARD path is unavailable on this chain.`);
|
|
4314
|
+
}
|
|
4315
|
+
return wrapper;
|
|
4316
|
+
}
|
|
4204
4317
|
// Handle different contract types with explicit type checking
|
|
4205
4318
|
switch(contracts.type){
|
|
4206
4319
|
case 'split':
|
|
4207
|
-
return contracts.tokenMessenger ;
|
|
4320
|
+
return contractType === 'tokenMessenger' ? contracts.tokenMessenger : contracts.messageTransmitter;
|
|
4208
4321
|
case 'merged':
|
|
4209
4322
|
return contracts.contract;
|
|
4210
4323
|
default:
|
|
@@ -7353,6 +7466,7 @@ class KitError extends Error {
|
|
|
7353
7466
|
[Blockchain.Base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
7354
7467
|
[Blockchain.Celo]: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
|
|
7355
7468
|
[Blockchain.Codex]: '0xd996633a415985DBd7D6D12f4A4343E31f5037cf',
|
|
7469
|
+
[Blockchain.Cronos]: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
|
|
7356
7470
|
[Blockchain.Edge]: '0x98d2919b9A214E6Fa5384AC81E6864bA686Ad74c',
|
|
7357
7471
|
[Blockchain.Ethereum]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
7358
7472
|
[Blockchain.Hedera]: '0.0.456858',
|
|
@@ -7386,6 +7500,7 @@ class KitError extends Error {
|
|
|
7386
7500
|
[Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
|
|
7387
7501
|
[Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
7388
7502
|
[Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
|
|
7503
|
+
[Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
|
|
7389
7504
|
[Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
|
|
7390
7505
|
[Blockchain.Ethereum_Sepolia]: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
|
|
7391
7506
|
[Blockchain.Hedera_Testnet]: '0.0.429274',
|
|
@@ -7458,6 +7573,7 @@ class KitError extends Error {
|
|
|
7458
7573
|
// =========================================================================
|
|
7459
7574
|
[Blockchain.Avalanche]: '0xc891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
|
|
7460
7575
|
[Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
|
|
7576
|
+
[Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
|
|
7461
7577
|
[Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
|
|
7462
7578
|
[Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
|
|
7463
7579
|
[Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
|
|
@@ -7466,6 +7582,7 @@ class KitError extends Error {
|
|
|
7466
7582
|
// =========================================================================
|
|
7467
7583
|
[Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
|
|
7468
7584
|
[Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
|
|
7585
|
+
[Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
|
|
7469
7586
|
[Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
|
|
7470
7587
|
}
|
|
7471
7588
|
};
|
|
@@ -9019,6 +9136,198 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
9019
9136
|
return false;
|
|
9020
9137
|
};
|
|
9021
9138
|
|
|
9139
|
+
/**
|
|
9140
|
+
* The zero address, denoting a native-currency fee in a signed quote.
|
|
9141
|
+
*/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
9142
|
+
/**
|
|
9143
|
+
* Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
|
|
9144
|
+
*
|
|
9145
|
+
* Determines the native `msg.value` and the ERC-20 approvals required, honouring
|
|
9146
|
+
* the quote's `feeToken`:
|
|
9147
|
+
* - Native fee (`feeToken` is the zero address): attach exactly `feeTotalAmount`
|
|
9148
|
+
* as `msg.value`; approve only the burn amount.
|
|
9149
|
+
* - ERC-20 fee equal to the burn token (both USDC — the `isBurnTokenFee` case):
|
|
9150
|
+
* approve a single combined `amount + feeTotalAmount` and skip the redundant
|
|
9151
|
+
* second approval.
|
|
9152
|
+
* - ERC-20 fee different from the burn token: approve the burn amount and the fee
|
|
9153
|
+
* amount separately.
|
|
9154
|
+
*
|
|
9155
|
+
* This encodes only balance/allowance intent; it does not fetch balances. The
|
|
9156
|
+
* caller is responsible for a balance preflight against the fresh quote.
|
|
9157
|
+
*
|
|
9158
|
+
* @param params - The fee token, burn token, burn amount, and total fee amount.
|
|
9159
|
+
* @returns The resolved fee payment plan.
|
|
9160
|
+
* @throws KitError if `amount` or `feeTotalAmount` is negative.
|
|
9161
|
+
*
|
|
9162
|
+
* @example
|
|
9163
|
+
* ```typescript
|
|
9164
|
+
* // Native fee
|
|
9165
|
+
* resolveFeePayment({
|
|
9166
|
+
* feeToken: '0x0000000000000000000000000000000000000000',
|
|
9167
|
+
* burnToken: '0xUSDC...',
|
|
9168
|
+
* amount: 1_000_000n,
|
|
9169
|
+
* feeTotalAmount: 3_500_000n,
|
|
9170
|
+
* })
|
|
9171
|
+
* // → { isNativeFee: true, isBurnTokenFee: false, nativeValue: 3_500_000n,
|
|
9172
|
+
* // approvals: [{ token: '0xUSDC...', amount: 1_000_000n }] }
|
|
9173
|
+
* ```
|
|
9174
|
+
*/ const resolveFeePayment = (params)=>{
|
|
9175
|
+
const { feeToken, burnToken, amount, feeTotalAmount } = params;
|
|
9176
|
+
if (typeof amount !== 'bigint' || amount < 0n) {
|
|
9177
|
+
throw createValidationFailedError('amount', amount, 'Must be a non-negative bigint');
|
|
9178
|
+
}
|
|
9179
|
+
if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
|
|
9180
|
+
throw createValidationFailedError('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
|
|
9181
|
+
}
|
|
9182
|
+
const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS;
|
|
9183
|
+
const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
|
|
9184
|
+
if (isNativeFee) {
|
|
9185
|
+
return {
|
|
9186
|
+
isNativeFee: true,
|
|
9187
|
+
isBurnTokenFee: false,
|
|
9188
|
+
nativeValue: feeTotalAmount,
|
|
9189
|
+
approvals: [
|
|
9190
|
+
{
|
|
9191
|
+
token: burnToken,
|
|
9192
|
+
amount
|
|
9193
|
+
}
|
|
9194
|
+
]
|
|
9195
|
+
};
|
|
9196
|
+
}
|
|
9197
|
+
if (isBurnTokenFee) {
|
|
9198
|
+
// Fee and burn draw on the same token — a single combined approval covers
|
|
9199
|
+
// both; the redundant second approval is skipped.
|
|
9200
|
+
return {
|
|
9201
|
+
isNativeFee: false,
|
|
9202
|
+
isBurnTokenFee: true,
|
|
9203
|
+
nativeValue: 0n,
|
|
9204
|
+
approvals: [
|
|
9205
|
+
{
|
|
9206
|
+
token: burnToken,
|
|
9207
|
+
amount: amount + feeTotalAmount
|
|
9208
|
+
}
|
|
9209
|
+
]
|
|
9210
|
+
};
|
|
9211
|
+
}
|
|
9212
|
+
return {
|
|
9213
|
+
isNativeFee: false,
|
|
9214
|
+
isBurnTokenFee: false,
|
|
9215
|
+
nativeValue: 0n,
|
|
9216
|
+
approvals: [
|
|
9217
|
+
{
|
|
9218
|
+
token: burnToken,
|
|
9219
|
+
amount
|
|
9220
|
+
},
|
|
9221
|
+
{
|
|
9222
|
+
token: feeToken,
|
|
9223
|
+
amount: feeTotalAmount
|
|
9224
|
+
}
|
|
9225
|
+
]
|
|
9226
|
+
};
|
|
9227
|
+
};
|
|
9228
|
+
|
|
9229
|
+
/**
|
|
9230
|
+
* Custom errors reverted by the `TokenMessengerWithFees` wrapper on the prepaid
|
|
9231
|
+
* FORWARD path.
|
|
9232
|
+
*
|
|
9233
|
+
* - `IncorrectNativePayment` — `msg.value` did not equal the quoted native fee.
|
|
9234
|
+
* - `ForwardFeeWithoutHook` — the quote has a FORWARD item but the hookData lacks
|
|
9235
|
+
* a `cctp-forward` frame.
|
|
9236
|
+
* - `ForwardHookWithoutFee` — the hookData has a `cctp-forward` frame but the quote
|
|
9237
|
+
* omits a FORWARD item.
|
|
9238
|
+
* - `QuoteArgsMismatch` — the signed quote's `argsHash` does not match the on-chain
|
|
9239
|
+
* call arguments (wrong hookData/destinationCaller/amount/etc.).
|
|
9240
|
+
* - `InvalidQuoteSigner` — the signed quote was not signed by a recognized signer.
|
|
9241
|
+
*/ const PREPAID_FORWARD_REVERT_NAMES = [
|
|
9242
|
+
'IncorrectNativePayment',
|
|
9243
|
+
'ForwardFeeWithoutHook',
|
|
9244
|
+
'ForwardHookWithoutFee',
|
|
9245
|
+
'QuoteArgsMismatch',
|
|
9246
|
+
'InvalidQuoteSigner'
|
|
9247
|
+
];
|
|
9248
|
+
/**
|
|
9249
|
+
* The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
|
|
9250
|
+
* hookData must start with.
|
|
9251
|
+
*/ const CCTP_FORWARD_MAGIC_HEX = Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
|
|
9252
|
+
/**
|
|
9253
|
+
* Determine whether a hookData blob begins with the `cctp-forward` envelope.
|
|
9254
|
+
*
|
|
9255
|
+
* The prepaid FORWARD path requires the GenericExecutor blob to be wrapped in a
|
|
9256
|
+
* `cctp-forward` frame; without it the wrapper reverts `ForwardFeeWithoutHook`.
|
|
9257
|
+
*
|
|
9258
|
+
* @param hookData - The 0x-prefixed hookData hex string.
|
|
9259
|
+
* @returns True when the hookData starts with the `cctp-forward` magic.
|
|
9260
|
+
*
|
|
9261
|
+
* @example
|
|
9262
|
+
* ```typescript
|
|
9263
|
+
* hasForwardHook('0x636374702d666f7277617264...') // true
|
|
9264
|
+
* hasForwardHook('0xdeadbeef') // false
|
|
9265
|
+
* ```
|
|
9266
|
+
*/ const hasForwardHook = (hookData)=>{
|
|
9267
|
+
if (typeof hookData !== 'string') {
|
|
9268
|
+
return false;
|
|
9269
|
+
}
|
|
9270
|
+
const normalized = (hookData.startsWith('0x') ? hookData.slice(2) : hookData).toLowerCase();
|
|
9271
|
+
return normalized.startsWith(CCTP_FORWARD_MAGIC_HEX);
|
|
9272
|
+
};
|
|
9273
|
+
/**
|
|
9274
|
+
* Assert that a hookData blob is forward-friendly for the prepaid FORWARD path.
|
|
9275
|
+
*
|
|
9276
|
+
* The prepaid FORWARD path always requests a FORWARD fee item, so the wrapper
|
|
9277
|
+
* requires the hookData to start with a `cctp-forward` frame. Validating this
|
|
9278
|
+
* before the burn surfaces the guaranteed `ForwardFeeWithoutHook` revert as a
|
|
9279
|
+
* typed input error instead of an on-chain failure.
|
|
9280
|
+
*
|
|
9281
|
+
* @param hookData - The 0x-prefixed hookData hex string.
|
|
9282
|
+
* @throws KitError (`INPUT_VALIDATION_FAILED`) if the hookData is missing or lacks
|
|
9283
|
+
* the `cctp-forward` frame.
|
|
9284
|
+
*
|
|
9285
|
+
* @example
|
|
9286
|
+
* ```typescript
|
|
9287
|
+
* assertForwardHookData(geForwardHookData) // ok
|
|
9288
|
+
* assertForwardHookData('0xdeadbeef') // throws — would revert ForwardFeeWithoutHook
|
|
9289
|
+
* ```
|
|
9290
|
+
*/ const assertForwardHookData = (hookData)=>{
|
|
9291
|
+
if (!hasForwardHook(hookData)) {
|
|
9292
|
+
throw createValidationFailedError('hookData', hookData, 'Prepaid FORWARD burns require a cctp-forward-wrapped hookData; without it the TokenMessengerWithFees wrapper reverts ForwardFeeWithoutHook');
|
|
9293
|
+
}
|
|
9294
|
+
};
|
|
9295
|
+
/**
|
|
9296
|
+
* Map a raw error thrown while submitting a prepaid-FORWARD burn to a typed error.
|
|
9297
|
+
*
|
|
9298
|
+
* Inspects the error for a known `TokenMessengerWithFees` custom revert name and,
|
|
9299
|
+
* when found, returns a structured {@link KitError} describing it. Returns
|
|
9300
|
+
* `undefined` when the error is not a recognized wrapper revert so the caller can
|
|
9301
|
+
* fall through to generic blockchain-error parsing.
|
|
9302
|
+
*
|
|
9303
|
+
* @param error - The error thrown by transaction simulation or execution.
|
|
9304
|
+
* @param chain - The source chain name, for the error message.
|
|
9305
|
+
* @returns A typed {@link KitError} for a recognized wrapper revert, else `undefined`.
|
|
9306
|
+
*
|
|
9307
|
+
* @example
|
|
9308
|
+
* ```typescript
|
|
9309
|
+
* try {
|
|
9310
|
+
* await prepared.execute()
|
|
9311
|
+
* } catch (error) {
|
|
9312
|
+
* throw mapPrepaidForwardError(error, 'Ethereum') ?? parseBlockchainError(error, { chain: 'Ethereum' })
|
|
9313
|
+
* }
|
|
9314
|
+
* ```
|
|
9315
|
+
*/ const mapPrepaidForwardError = (error, chain)=>{
|
|
9316
|
+
let message = '';
|
|
9317
|
+
if (error instanceof Error) {
|
|
9318
|
+
message = error.message;
|
|
9319
|
+
} else if (typeof error === 'string') {
|
|
9320
|
+
message = error;
|
|
9321
|
+
}
|
|
9322
|
+
const matched = PREPAID_FORWARD_REVERT_NAMES.find((name)=>message.includes(name));
|
|
9323
|
+
if (matched === undefined) {
|
|
9324
|
+
return undefined;
|
|
9325
|
+
}
|
|
9326
|
+
return createTransactionRevertedError(chain, matched, {
|
|
9327
|
+
revert: matched
|
|
9328
|
+
});
|
|
9329
|
+
};
|
|
9330
|
+
|
|
9022
9331
|
/**
|
|
9023
9332
|
* Type guard to validate the forwardFee object structure.
|
|
9024
9333
|
*
|
|
@@ -9175,6 +9484,15 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
|
|
|
9175
9484
|
;
|
|
9176
9485
|
const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
|
|
9177
9486
|
;
|
|
9487
|
+
// Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
|
|
9488
|
+
// chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
|
|
9489
|
+
// Kept separate from the fee-estimate averages above.
|
|
9490
|
+
const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
|
|
9491
|
+
;
|
|
9492
|
+
const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
|
|
9493
|
+
;
|
|
9494
|
+
const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
|
|
9495
|
+
;
|
|
9178
9496
|
/**
|
|
9179
9497
|
* The minimum finality threshold for CCTPv2 transfers.
|
|
9180
9498
|
*
|
|
@@ -9203,6 +9521,27 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
|
|
|
9203
9521
|
'Content-Type': 'application/json'
|
|
9204
9522
|
}
|
|
9205
9523
|
};
|
|
9524
|
+
/**
|
|
9525
|
+
* Merges caller-provided polling overrides on top of {@link DEFAULT_CONFIG}.
|
|
9526
|
+
*
|
|
9527
|
+
* Headers are merged independently so caller-supplied headers augment the
|
|
9528
|
+
* defaults (such as `Content-Type`) rather than replacing them wholesale.
|
|
9529
|
+
*
|
|
9530
|
+
* @param config - Caller-provided polling configuration overrides
|
|
9531
|
+
* @param internalDefaults - Internal defaults applied before `config` (for example a
|
|
9532
|
+
* reduced `maxRetries` for one-shot requests); `config` still wins on conflict
|
|
9533
|
+
* @returns The effective polling configuration
|
|
9534
|
+
* @internal
|
|
9535
|
+
*/ const mergeAttestationConfig = (config, internalDefaults = {})=>({
|
|
9536
|
+
...DEFAULT_CONFIG,
|
|
9537
|
+
...internalDefaults,
|
|
9538
|
+
...config,
|
|
9539
|
+
headers: {
|
|
9540
|
+
...DEFAULT_CONFIG.headers,
|
|
9541
|
+
...internalDefaults.headers,
|
|
9542
|
+
...config.headers
|
|
9543
|
+
}
|
|
9544
|
+
});
|
|
9206
9545
|
/**
|
|
9207
9546
|
* Type guard that verifies if an unknown value matches the AttestationMessage shape
|
|
9208
9547
|
* and has all required properties.
|
|
@@ -9349,10 +9688,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
|
|
|
9349
9688
|
* ```
|
|
9350
9689
|
*/ const fetchAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
9351
9690
|
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
9352
|
-
const effectiveConfig =
|
|
9353
|
-
...DEFAULT_CONFIG,
|
|
9354
|
-
...config
|
|
9355
|
-
};
|
|
9691
|
+
const effectiveConfig = mergeAttestationConfig(config);
|
|
9356
9692
|
return await pollApiGet(url, isAttestationResponse, effectiveConfig);
|
|
9357
9693
|
};
|
|
9358
9694
|
/**
|
|
@@ -9395,11 +9731,9 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
|
|
|
9395
9731
|
*/ const fetchAttestationWithoutStatusCheck = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
9396
9732
|
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
9397
9733
|
// Use minimal retries since we're just fetching existing data
|
|
9398
|
-
const effectiveConfig = {
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
...config
|
|
9402
|
-
};
|
|
9734
|
+
const effectiveConfig = mergeAttestationConfig(config, {
|
|
9735
|
+
maxRetries: 3
|
|
9736
|
+
});
|
|
9403
9737
|
return await pollApiGet(url, isAttestationResponseWithoutStatusCheck, effectiveConfig);
|
|
9404
9738
|
};
|
|
9405
9739
|
/**
|
|
@@ -9459,10 +9793,7 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
|
|
|
9459
9793
|
* ```
|
|
9460
9794
|
*/ const fetchReAttestedAttestation = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
9461
9795
|
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
9462
|
-
const effectiveConfig =
|
|
9463
|
-
...DEFAULT_CONFIG,
|
|
9464
|
-
...config
|
|
9465
|
-
};
|
|
9796
|
+
const effectiveConfig = mergeAttestationConfig(config);
|
|
9466
9797
|
return await pollApiGet(url, isReAttestedAttestationResponse, effectiveConfig);
|
|
9467
9798
|
};
|
|
9468
9799
|
/**
|
|
@@ -9528,85 +9859,210 @@ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_8
|
|
|
9528
9859
|
*/ const requestReAttestation = async (nonce, isTestnet, config = {})=>{
|
|
9529
9860
|
const url = buildReAttestUrl(nonce, isTestnet);
|
|
9530
9861
|
// Use minimal retries since we're just submitting a request, not polling for state
|
|
9531
|
-
const effectiveConfig = {
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
...config
|
|
9535
|
-
};
|
|
9862
|
+
const effectiveConfig = mergeAttestationConfig(config, {
|
|
9863
|
+
maxRetries: 3
|
|
9864
|
+
});
|
|
9536
9865
|
return await pollApiPost(url, {}, isReAttestationResponse, effectiveConfig);
|
|
9537
9866
|
};
|
|
9538
9867
|
|
|
9539
|
-
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
9540
9868
|
/**
|
|
9541
|
-
*
|
|
9542
|
-
* The validation includes:
|
|
9543
|
-
* - Basic wallet context validation (adapter, address, chain)
|
|
9544
|
-
* - CCTPv2-specific chain validation (must be an EVM chain)
|
|
9545
|
-
*
|
|
9546
|
-
* @param params - The parameters to validate
|
|
9547
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
9548
|
-
*
|
|
9549
|
-
* @example
|
|
9550
|
-
* ```typescript
|
|
9551
|
-
* import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
|
|
9552
|
-
* import { Ethereum } from '@core/chains'
|
|
9869
|
+
* Type guard that checks if the relayer has confirmed the mint transaction.
|
|
9553
9870
|
*
|
|
9554
|
-
*
|
|
9555
|
-
*
|
|
9556
|
-
*
|
|
9557
|
-
* prepare: async () => ({ data: 'prepared transaction' }),
|
|
9558
|
-
* waitForTransaction: async () => ({ status: 'confirmed' })
|
|
9559
|
-
* },
|
|
9560
|
-
* address: '0x1234567890123456789012345678901234567890',
|
|
9561
|
-
* chain: {
|
|
9562
|
-
* ...Ethereum,
|
|
9563
|
-
* usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
9564
|
-
* cctp: {
|
|
9565
|
-
* domain: 1,
|
|
9566
|
-
* contracts: {
|
|
9567
|
-
* v2: {
|
|
9568
|
-
* tokenMessenger: '0xTokenMessenger',
|
|
9569
|
-
* messageTransmitter: '0xMessageTransmitter'
|
|
9570
|
-
* }
|
|
9571
|
-
* }
|
|
9572
|
-
* }
|
|
9573
|
-
* }
|
|
9574
|
-
* }
|
|
9871
|
+
* This function validates that:
|
|
9872
|
+
* 1. The response has valid AttestationResponse structure
|
|
9873
|
+
* 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
|
|
9575
9874
|
*
|
|
9576
|
-
*
|
|
9577
|
-
*
|
|
9875
|
+
* If forwardState is 'FAILED', throws a non-retryable KitError.
|
|
9876
|
+
* If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
|
|
9578
9877
|
*
|
|
9579
|
-
*
|
|
9580
|
-
*
|
|
9581
|
-
*
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9878
|
+
* @param obj - The value to check, typically a parsed JSON response
|
|
9879
|
+
* @returns True if the relayer has confirmed the mint
|
|
9880
|
+
* @throws {KitError} With FATAL recoverability if structure is invalid
|
|
9881
|
+
* @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
|
|
9882
|
+
* @throws {KitError} With RETRYABLE recoverability if still pending
|
|
9883
|
+
* @internal
|
|
9884
|
+
*/ const isRelayerMintConfirmed = (obj)=>{
|
|
9885
|
+
// First check if the structure is valid
|
|
9886
|
+
if (!hasValidAttestationStructure(obj)) {
|
|
9887
|
+
throw new KitError({
|
|
9888
|
+
...InputError.VALIDATION_FAILED,
|
|
9889
|
+
recoverability: 'FATAL',
|
|
9890
|
+
message: 'Invalid attestation response structure from IRIS API.'
|
|
9891
|
+
});
|
|
9590
9892
|
}
|
|
9591
|
-
//
|
|
9592
|
-
|
|
9593
|
-
|
|
9893
|
+
// Find the first message (typically there's only one)
|
|
9894
|
+
const message = obj.messages[0];
|
|
9895
|
+
if (!message) {
|
|
9896
|
+
throw new KitError({
|
|
9897
|
+
...InputError.VALIDATION_FAILED,
|
|
9898
|
+
recoverability: 'FATAL',
|
|
9899
|
+
message: 'No attestation messages found in IRIS API response.'
|
|
9900
|
+
});
|
|
9594
9901
|
}
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9902
|
+
// Check for FAILED state - this is a permanent failure
|
|
9903
|
+
if (message.forwardState === 'FAILED') {
|
|
9904
|
+
throw new KitError({
|
|
9905
|
+
...NetworkError.RELAYER_FORWARD_FAILED,
|
|
9906
|
+
recoverability: 'RESUMABLE',
|
|
9907
|
+
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.',
|
|
9908
|
+
cause: {
|
|
9909
|
+
trace: {
|
|
9910
|
+
eventNonce: message.eventNonce,
|
|
9911
|
+
attestation: message.attestation,
|
|
9912
|
+
message: message.message
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
});
|
|
9916
|
+
}
|
|
9917
|
+
// Check if mint is confirmed (or complete) with a valid transaction hash
|
|
9918
|
+
// We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
|
|
9919
|
+
if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
|
|
9920
|
+
return true;
|
|
9921
|
+
}
|
|
9922
|
+
// Still pending or not yet processed - throw RETRYABLE error to continue polling
|
|
9923
|
+
throw new KitError({
|
|
9924
|
+
...NetworkError.RELAYER_PENDING,
|
|
9925
|
+
recoverability: 'RETRYABLE',
|
|
9926
|
+
message: 'Relayer mint not ready. Waiting for confirmation.'
|
|
9927
|
+
});
|
|
9928
|
+
};
|
|
9929
|
+
/**
|
|
9930
|
+
* Polls the attestation API until the relayer's mint transaction is confirmed.
|
|
9931
|
+
*
|
|
9932
|
+
* This function is used when `useForwarder` is enabled. Instead of the user
|
|
9933
|
+
* submitting the mint transaction, Circle's Orbit relayer handles it automatically.
|
|
9934
|
+
* This function polls until the relayer has submitted and confirmed the mint transaction.
|
|
9935
|
+
*
|
|
9936
|
+
* @remarks
|
|
9937
|
+
* - Uses a 20-minute timeout by default (600 retries × 2 seconds)
|
|
9938
|
+
* - Throws immediately if `forwardState` is 'FAILED'
|
|
9939
|
+
* - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
|
|
9940
|
+
* - Returns the attestation message with `forwardTxHash` populated
|
|
9941
|
+
*
|
|
9942
|
+
* @param sourceDomainId - The CCTP domain ID of the source chain
|
|
9943
|
+
* @param transactionHash - The transaction hash of the burn operation
|
|
9944
|
+
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
|
|
9945
|
+
* @param config - Optional configuration overrides for polling behavior
|
|
9946
|
+
* @returns The attestation message with confirmed forwardTxHash
|
|
9947
|
+
* @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
|
|
9948
|
+
* @throws {KitError} If timeout is reached while still pending
|
|
9949
|
+
*
|
|
9950
|
+
* @example
|
|
9951
|
+
* ```typescript
|
|
9952
|
+
* const attestation = await fetchRelayerMint(0, '0xabc...', false)
|
|
9953
|
+
* console.log('Relayer mint tx:', attestation.forwardTxHash)
|
|
9954
|
+
* ```
|
|
9955
|
+
*/ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
|
|
9956
|
+
const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
|
|
9957
|
+
const effectiveConfig = mergeAttestationConfig(config);
|
|
9958
|
+
let response;
|
|
9959
|
+
try {
|
|
9960
|
+
response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
|
|
9961
|
+
} catch (error) {
|
|
9962
|
+
// Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
|
|
9963
|
+
if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
|
|
9964
|
+
throw new KitError({
|
|
9965
|
+
...NetworkError.RELAYER_FORWARD_FAILED,
|
|
9966
|
+
recoverability: error.recoverability,
|
|
9967
|
+
message: error.message,
|
|
9968
|
+
cause: {
|
|
9969
|
+
...error.cause,
|
|
9970
|
+
trace: {
|
|
9971
|
+
...error.cause?.trace,
|
|
9972
|
+
burnTxHash: transactionHash
|
|
9973
|
+
}
|
|
9974
|
+
}
|
|
9975
|
+
});
|
|
9976
|
+
}
|
|
9977
|
+
throw error;
|
|
9978
|
+
}
|
|
9979
|
+
// Return the first message (which should have forwardTxHash)
|
|
9980
|
+
// Note: This check is needed for TypeScript type safety even though
|
|
9981
|
+
// isRelayerMintConfirmed validates messages[0] exists. The type guard
|
|
9982
|
+
// narrows the type at the call site, but TypeScript can't infer that
|
|
9983
|
+
// the array still has elements after pollApiGet returns.
|
|
9984
|
+
const message = response.messages[0];
|
|
9985
|
+
if (!message) {
|
|
9986
|
+
throw new KitError({
|
|
9987
|
+
...InputError.VALIDATION_FAILED,
|
|
9988
|
+
recoverability: 'FATAL',
|
|
9989
|
+
message: 'No attestation messages found in response after polling.'
|
|
9990
|
+
});
|
|
9991
|
+
}
|
|
9992
|
+
return message;
|
|
9993
|
+
};
|
|
9994
|
+
|
|
9995
|
+
const assertCCTPv2WalletContextSymbol = Symbol('assertCCTPv2WalletContext');
|
|
9996
|
+
/**
|
|
9997
|
+
* Asserts that the provided parameters match the CCTPv2 wallet context interface.
|
|
9998
|
+
* The validation includes:
|
|
9999
|
+
* - Basic wallet context validation (adapter, address, chain)
|
|
10000
|
+
* - CCTPv2-specific chain validation (must be an EVM chain)
|
|
10001
|
+
*
|
|
10002
|
+
* @param params - The parameters to validate
|
|
10003
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
10004
|
+
*
|
|
10005
|
+
* @example
|
|
10006
|
+
* ```typescript
|
|
10007
|
+
* import { assertCCTPv2WalletContext } from '@circle-fin/provider-cctp-v2'
|
|
10008
|
+
* import { Ethereum } from '@core/chains'
|
|
10009
|
+
*
|
|
10010
|
+
* // Prepare wallet context
|
|
10011
|
+
* const context = {
|
|
10012
|
+
* adapter: {
|
|
10013
|
+
* prepare: async () => ({ data: 'prepared transaction' }),
|
|
10014
|
+
* waitForTransaction: async () => ({ status: 'confirmed' })
|
|
10015
|
+
* },
|
|
10016
|
+
* address: '0x1234567890123456789012345678901234567890',
|
|
10017
|
+
* chain: {
|
|
10018
|
+
* ...Ethereum,
|
|
10019
|
+
* usdcAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
10020
|
+
* cctp: {
|
|
10021
|
+
* domain: 1,
|
|
10022
|
+
* contracts: {
|
|
10023
|
+
* v2: {
|
|
10024
|
+
* tokenMessenger: '0xTokenMessenger',
|
|
10025
|
+
* messageTransmitter: '0xMessageTransmitter'
|
|
10026
|
+
* }
|
|
10027
|
+
* }
|
|
10028
|
+
* }
|
|
10029
|
+
* }
|
|
10030
|
+
* }
|
|
10031
|
+
*
|
|
10032
|
+
* // This will throw if validation fails
|
|
10033
|
+
* assertCCTPv2WalletContext(context)
|
|
10034
|
+
*
|
|
10035
|
+
* // If we get here, context is guaranteed to be valid
|
|
10036
|
+
* console.log('CCTPv2 wallet context is valid')
|
|
10037
|
+
* ```
|
|
10038
|
+
*/ function assertCCTPv2WalletContext(params) {
|
|
10039
|
+
// First validate basic wallet context
|
|
10040
|
+
validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
|
|
10041
|
+
// After validation, we know params is WalletContext
|
|
10042
|
+
const context = params;
|
|
10043
|
+
// Validate USDC support
|
|
10044
|
+
if (context.chain.usdcAddress === null) {
|
|
10045
|
+
throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
|
|
10046
|
+
}
|
|
10047
|
+
// Validate CCTPv2 support
|
|
10048
|
+
if (!isCCTPV2Supported(context.chain)) {
|
|
10049
|
+
throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
|
|
10050
|
+
}
|
|
10051
|
+
}
|
|
10052
|
+
|
|
10053
|
+
const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
10054
|
+
/**
|
|
10055
|
+
* Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
|
|
10056
|
+
* The validation includes:
|
|
10057
|
+
* - Basic parameter structure and types
|
|
10058
|
+
* - Amount validation (non-empty numeric string \> 0)
|
|
10059
|
+
* - Wallet address format validation (must be valid Ethereum address)
|
|
10060
|
+
* - Chain definition validation (must be a valid chain with required properties)
|
|
10061
|
+
* - Adapter validation (must implement required methods)
|
|
10062
|
+
* - Optional config validation (transfer speed and max fee)
|
|
10063
|
+
* - Network compatibility (source and destination chains must both be testnet or both mainnet)
|
|
10064
|
+
* - CCTPv2-specific wallet context validations
|
|
10065
|
+
*
|
|
9610
10066
|
* @param params - The parameters to validate
|
|
9611
10067
|
* @throws {KitError} If validation fails, with details about which properties failed
|
|
9612
10068
|
*
|
|
@@ -9804,1422 +10260,1735 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
9804
10260
|
}
|
|
9805
10261
|
|
|
9806
10262
|
/**
|
|
9807
|
-
*
|
|
10263
|
+
* Resolves an operation context into concrete chain and address values.
|
|
9808
10264
|
*
|
|
9809
|
-
* This
|
|
9810
|
-
*
|
|
9811
|
-
*
|
|
9812
|
-
|
|
9813
|
-
approve: 'approve',
|
|
9814
|
-
burn: 'burn',
|
|
9815
|
-
fetchAttestation: 'fetchAttestation',
|
|
9816
|
-
mint: 'mint',
|
|
9817
|
-
reAttest: 'reAttest'
|
|
9818
|
-
};
|
|
9819
|
-
/**
|
|
9820
|
-
* Conditional step transition rules for CCTP bridge flow.
|
|
10265
|
+
* This function ensures that action handlers always receive defined chain and address
|
|
10266
|
+
* values by applying validation and resolution logic based on the adapter's capabilities.
|
|
10267
|
+
* It enforces compile-time and runtime address requirements based on the adapter's
|
|
10268
|
+
* address control model.
|
|
9821
10269
|
*
|
|
9822
|
-
*
|
|
9823
|
-
*
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
10270
|
+
* **Resolution logic**:
|
|
10271
|
+
* - **Chain**: Uses provided chain identifier and resolves to ChainDefinition
|
|
10272
|
+
* - **Address**:
|
|
10273
|
+
* - For user-controlled adapters: Retrieves current address from adapter (context address ignored)
|
|
10274
|
+
* - For developer-controlled adapters: Uses address provided in context (required)
|
|
10275
|
+
*
|
|
10276
|
+
* @typeParam TAdapterCapabilities - The adapter capabilities type for compile-time validation
|
|
10277
|
+
* @param adapter - The typed adapter instance with capabilities defined
|
|
10278
|
+
* @param ctx - Operation context with compile-time validated address requirements
|
|
10279
|
+
* @returns Promise resolving to concrete chain and address values
|
|
10280
|
+
* @throws Error when adapter capabilities are not defined
|
|
10281
|
+
* @throws Error when operation context is not provided
|
|
10282
|
+
* @throws Error when address is required but not provided (developer-controlled)
|
|
10283
|
+
* @throws Error when adapter.getAddress() fails (user-controlled)
|
|
10284
|
+
*
|
|
10285
|
+
* @example
|
|
10286
|
+
* ```typescript
|
|
10287
|
+
* import { resolveOperationContext } from '@core/adapter'
|
|
10288
|
+
*
|
|
10289
|
+
* // User-controlled adapter - address forbidden in context
|
|
10290
|
+
* const userAdapter: Adapter<{ addressContext: 'user-controlled', supportedChains: [] }>
|
|
10291
|
+
* const resolved = await resolveOperationContext(userAdapter, {
|
|
10292
|
+
* chain: 'Base' // address will be resolved from wallet
|
|
10293
|
+
* })
|
|
10294
|
+
* console.log(resolved.chain) // ChainDefinition - always defined
|
|
10295
|
+
* console.log(resolved.address) // string - resolved from adapter
|
|
10296
|
+
*
|
|
10297
|
+
* // Developer-controlled adapter - address required in context
|
|
10298
|
+
* const devAdapter: Adapter<{ addressContext: 'developer-controlled', supportedChains: [] }>
|
|
10299
|
+
* const resolved = await resolveOperationContext(devAdapter, {
|
|
10300
|
+
* chain: 'Base',
|
|
10301
|
+
* address: '0x123...' // Required and enforced at compile time
|
|
10302
|
+
* })
|
|
10303
|
+
* console.log(resolved.address) // '0x123...' - from context
|
|
10304
|
+
* ```
|
|
10305
|
+
*/ async function resolveOperationContext(adapter, ctx) {
|
|
10306
|
+
// Adapter must have capabilities defined
|
|
10307
|
+
if (adapter.capabilities === undefined) {
|
|
10308
|
+
throw new Error('Adapter capabilities must be defined. Please ensure the adapter implements the capabilities property.');
|
|
10309
|
+
}
|
|
10310
|
+
// Operation context is required for new typed adapters
|
|
10311
|
+
if (ctx === undefined) {
|
|
10312
|
+
throw new Error('Operation context is required. Please provide a context with the required chain and address information.');
|
|
10313
|
+
}
|
|
10314
|
+
// Resolve chain from context (required)
|
|
10315
|
+
const resolvedChain = resolveChainIdentifier(ctx.chain);
|
|
10316
|
+
// Resolve address based on adapter capabilities
|
|
10317
|
+
let resolvedAddress;
|
|
10318
|
+
if (adapter.capabilities.addressContext === 'developer-controlled') {
|
|
10319
|
+
// Developer-controlled: address must be explicitly provided
|
|
10320
|
+
if (!ctx.address) {
|
|
10321
|
+
throw new Error('Address is required for developer-controlled adapters. Please provide an address in the operation context.');
|
|
9832
10322
|
}
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
{
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
9844
|
-
nextStep: CCTPv2StepName.approve,
|
|
9845
|
-
reason: 'Retry failed approval',
|
|
9846
|
-
isActionable: true
|
|
9847
|
-
},
|
|
9848
|
-
{
|
|
9849
|
-
condition: (ctx)=>ctx.lastStep?.state === 'noop',
|
|
9850
|
-
nextStep: CCTPv2StepName.burn,
|
|
9851
|
-
reason: 'No approval needed, proceed to burn',
|
|
9852
|
-
isActionable: true
|
|
9853
|
-
},
|
|
9854
|
-
{
|
|
9855
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
9856
|
-
nextStep: CCTPv2StepName.approve,
|
|
9857
|
-
reason: 'Continue pending approval',
|
|
9858
|
-
isActionable: false
|
|
9859
|
-
}
|
|
9860
|
-
],
|
|
9861
|
-
// After Burn step
|
|
9862
|
-
[CCTPv2StepName.burn]: [
|
|
9863
|
-
{
|
|
9864
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
9865
|
-
nextStep: CCTPv2StepName.fetchAttestation,
|
|
9866
|
-
reason: 'Burn successful, fetch attestation',
|
|
9867
|
-
isActionable: true
|
|
9868
|
-
},
|
|
9869
|
-
{
|
|
9870
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
9871
|
-
nextStep: CCTPv2StepName.burn,
|
|
9872
|
-
reason: 'Retry failed burn',
|
|
9873
|
-
isActionable: true
|
|
9874
|
-
},
|
|
9875
|
-
{
|
|
9876
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
9877
|
-
nextStep: CCTPv2StepName.burn,
|
|
9878
|
-
reason: 'Continue pending burn',
|
|
9879
|
-
isActionable: false
|
|
9880
|
-
}
|
|
9881
|
-
],
|
|
9882
|
-
// After FetchAttestation step
|
|
9883
|
-
[CCTPv2StepName.fetchAttestation]: [
|
|
9884
|
-
{
|
|
9885
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
9886
|
-
nextStep: CCTPv2StepName.mint,
|
|
9887
|
-
reason: 'Attestation fetched, proceed to mint',
|
|
9888
|
-
isActionable: true
|
|
9889
|
-
},
|
|
9890
|
-
{
|
|
9891
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
9892
|
-
nextStep: CCTPv2StepName.fetchAttestation,
|
|
9893
|
-
reason: 'Retry fetching attestation',
|
|
9894
|
-
isActionable: true
|
|
9895
|
-
},
|
|
9896
|
-
{
|
|
9897
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
9898
|
-
nextStep: CCTPv2StepName.fetchAttestation,
|
|
9899
|
-
reason: 'Continue pending attestation fetch',
|
|
9900
|
-
isActionable: false
|
|
9901
|
-
}
|
|
9902
|
-
],
|
|
9903
|
-
// After Mint step
|
|
9904
|
-
[CCTPv2StepName.mint]: [
|
|
9905
|
-
{
|
|
9906
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
9907
|
-
nextStep: null,
|
|
9908
|
-
reason: 'Bridge completed successfully',
|
|
9909
|
-
isActionable: false
|
|
9910
|
-
},
|
|
9911
|
-
{
|
|
9912
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
9913
|
-
nextStep: CCTPv2StepName.mint,
|
|
9914
|
-
reason: 'Retry failed mint',
|
|
9915
|
-
isActionable: true
|
|
9916
|
-
},
|
|
9917
|
-
{
|
|
9918
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
9919
|
-
nextStep: CCTPv2StepName.mint,
|
|
9920
|
-
reason: 'Continue pending mint',
|
|
9921
|
-
isActionable: false
|
|
9922
|
-
}
|
|
9923
|
-
],
|
|
9924
|
-
// After ReAttest step
|
|
9925
|
-
[CCTPv2StepName.reAttest]: [
|
|
9926
|
-
{
|
|
9927
|
-
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
9928
|
-
nextStep: CCTPv2StepName.mint,
|
|
9929
|
-
reason: 'Re-attestation successful, proceed to mint',
|
|
9930
|
-
isActionable: true
|
|
9931
|
-
},
|
|
9932
|
-
{
|
|
9933
|
-
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
9934
|
-
nextStep: CCTPv2StepName.mint,
|
|
9935
|
-
reason: 'Re-attestation failed, retry mint to re-initiate recovery',
|
|
9936
|
-
isActionable: true
|
|
9937
|
-
},
|
|
9938
|
-
{
|
|
9939
|
-
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
9940
|
-
nextStep: CCTPv2StepName.mint,
|
|
9941
|
-
reason: 'Re-attestation pending, retry mint to re-initiate recovery',
|
|
9942
|
-
isActionable: true
|
|
10323
|
+
resolvedAddress = ctx.address;
|
|
10324
|
+
} else {
|
|
10325
|
+
// User-controlled: get current address from adapter
|
|
10326
|
+
try {
|
|
10327
|
+
// Pass resolved chain to getAddress for adapters that support it (like ViemAdapter)
|
|
10328
|
+
// The chain parameter is optional in implementations, so this is safe
|
|
10329
|
+
resolvedAddress = await adapter.getAddress(resolvedChain);
|
|
10330
|
+
} catch (error) {
|
|
10331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10332
|
+
throw new Error(`Failed to resolve address from user-controlled adapter: ${message}`);
|
|
9943
10333
|
}
|
|
9944
|
-
|
|
9945
|
-
|
|
10334
|
+
}
|
|
10335
|
+
return {
|
|
10336
|
+
chain: resolvedChain,
|
|
10337
|
+
address: resolvedAddress
|
|
10338
|
+
};
|
|
10339
|
+
}
|
|
10340
|
+
|
|
9946
10341
|
/**
|
|
9947
|
-
*
|
|
10342
|
+
* Schema for validating hexadecimal strings with '0x' prefix.
|
|
9948
10343
|
*
|
|
9949
|
-
* This
|
|
9950
|
-
*
|
|
9951
|
-
*
|
|
10344
|
+
* This schema validates that a string:
|
|
10345
|
+
* - Is a string type
|
|
10346
|
+
* - Is not empty after trimming
|
|
10347
|
+
* - Starts with '0x'
|
|
10348
|
+
* - Contains only valid hexadecimal characters (0-9, a-f, A-F) after '0x'
|
|
9952
10349
|
*
|
|
9953
|
-
*
|
|
9954
|
-
*
|
|
10350
|
+
* @remarks
|
|
10351
|
+
* This schema does not validate length, making it suitable for various hex string types
|
|
10352
|
+
* like addresses, transaction hashes, and other hex-encoded data.
|
|
9955
10353
|
*
|
|
9956
|
-
*
|
|
9957
|
-
* - Rule-based transitions: Easy to extend with new step types and logic
|
|
9958
|
-
* - Context-aware decisions: Considers execution history and step states
|
|
9959
|
-
* - Actionable logic: Distinguishes between steps requiring user action vs waiting
|
|
9960
|
-
* - Terminal states: Properly handles completion and non-actionable states
|
|
10354
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
9961
10355
|
*
|
|
9962
|
-
* @
|
|
9963
|
-
*
|
|
9964
|
-
*
|
|
10356
|
+
* @example
|
|
10357
|
+
* ```typescript
|
|
10358
|
+
* import { hexStringSchema } from '@core/adapter'
|
|
10359
|
+
*
|
|
10360
|
+
* const validAddress = '0x1234567890123456789012345678901234567890'
|
|
10361
|
+
* const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
10362
|
+
*
|
|
10363
|
+
* const addressResult = hexStringSchema.safeParse(validAddress)
|
|
10364
|
+
* const txHashResult = hexStringSchema.safeParse(validTxHash)
|
|
10365
|
+
* console.log(addressResult.success) // true
|
|
10366
|
+
* console.log(txHashResult.success) // true
|
|
10367
|
+
* ```
|
|
10368
|
+
*/ const hexStringSchema = z.string().min(1, 'Hex string is required').refine((value)=>value.trim().length > 0, 'Hex string cannot be empty').refine((value)=>value.startsWith('0x'), 'Hex string must start with 0x prefix').refine((value)=>{
|
|
10369
|
+
const hexPattern = /^0x[0-9a-fA-F]+$/;
|
|
10370
|
+
return hexPattern.test(value);
|
|
10371
|
+
}, 'Hex string contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x');
|
|
10372
|
+
/**
|
|
10373
|
+
* Schema for validating EVM addresses.
|
|
10374
|
+
*
|
|
10375
|
+
* This schema validates that a string is a properly formatted EVM address:
|
|
10376
|
+
* - Must be a valid hex string with '0x' prefix
|
|
10377
|
+
* - Must be exactly 42 characters long (0x + 40 hex characters)
|
|
10378
|
+
*
|
|
10379
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
9965
10380
|
*
|
|
9966
10381
|
* @example
|
|
9967
10382
|
* ```typescript
|
|
9968
|
-
* import {
|
|
10383
|
+
* import { evmAddressSchema } from '@core/adapter'
|
|
9969
10384
|
*
|
|
9970
|
-
*
|
|
9971
|
-
* const bridgeResult = {
|
|
9972
|
-
* steps: [
|
|
9973
|
-
* { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
|
|
9974
|
-
* ]
|
|
9975
|
-
* }
|
|
10385
|
+
* const validAddress = '0x1234567890123456789012345678901234567890'
|
|
9976
10386
|
*
|
|
9977
|
-
* const
|
|
9978
|
-
* //
|
|
9979
|
-
* // reason: 'Retry failed approval' }
|
|
10387
|
+
* const result = evmAddressSchema.safeParse(validAddress)
|
|
10388
|
+
* console.log(result.success) // true
|
|
9980
10389
|
* ```
|
|
10390
|
+
*/ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
|
|
10391
|
+
/**
|
|
10392
|
+
* Schema for validating transaction hashes.
|
|
10393
|
+
*
|
|
10394
|
+
* This schema validates that a string is a properly formatted transaction hash:
|
|
10395
|
+
* - Must be a valid hex string with '0x' prefix
|
|
10396
|
+
* - Must be exactly 66 characters long (0x + 64 hex characters)
|
|
10397
|
+
*
|
|
10398
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
9981
10399
|
*
|
|
9982
10400
|
* @example
|
|
9983
10401
|
* ```typescript
|
|
9984
|
-
*
|
|
9985
|
-
* const bridgeResult = {
|
|
9986
|
-
* steps: [
|
|
9987
|
-
* { name: 'Approve', state: 'pending' }
|
|
9988
|
-
* ]
|
|
9989
|
-
* }
|
|
10402
|
+
* import { evmTransactionHashSchema } from '@core/adapter'
|
|
9990
10403
|
*
|
|
9991
|
-
* const
|
|
9992
|
-
*
|
|
9993
|
-
*
|
|
10404
|
+
* const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
10405
|
+
*
|
|
10406
|
+
* const result = evmTransactionHashSchema.safeParse(validTxHash)
|
|
10407
|
+
* console.log(result.success) // true
|
|
9994
10408
|
* ```
|
|
10409
|
+
*/ hexStringSchema.refine((value)=>value.length === 66, 'Transaction hash must be exactly 66 characters long (0x + 64 hex characters)');
|
|
10410
|
+
/**
|
|
10411
|
+
* Schema for validating EVM signatures.
|
|
10412
|
+
*
|
|
10413
|
+
* This schema validates that a string is a properly formatted 65-byte EVM
|
|
10414
|
+
* signature:
|
|
10415
|
+
* - Must be a valid hex string with '0x' prefix
|
|
10416
|
+
* - Must be exactly 132 characters long (0x + 130 hex characters)
|
|
10417
|
+
*
|
|
10418
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
9995
10419
|
*
|
|
9996
10420
|
* @example
|
|
9997
10421
|
* ```typescript
|
|
9998
|
-
*
|
|
9999
|
-
* const bridgeResult = {
|
|
10000
|
-
* steps: [
|
|
10001
|
-
* { name: 'Approve', state: 'success' },
|
|
10002
|
-
* { name: 'Burn', state: 'success' },
|
|
10003
|
-
* { name: 'FetchAttestation', state: 'success' },
|
|
10004
|
-
* { name: 'Mint', state: 'success' }
|
|
10005
|
-
* ]
|
|
10006
|
-
* }
|
|
10422
|
+
* import { evmSignatureSchema } from '@core/adapter'
|
|
10007
10423
|
*
|
|
10008
|
-
* const
|
|
10009
|
-
*
|
|
10010
|
-
*
|
|
10424
|
+
* const validSignature = `0x${'ab'.repeat(65)}`
|
|
10425
|
+
*
|
|
10426
|
+
* const result = evmSignatureSchema.safeParse(validSignature)
|
|
10427
|
+
* console.log(result.success) // true
|
|
10011
10428
|
* ```
|
|
10012
|
-
*/
|
|
10013
|
-
// Input validation
|
|
10014
|
-
if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
|
|
10015
|
-
throw new Error('Invalid bridgeResult: must contain a steps array');
|
|
10016
|
-
}
|
|
10017
|
-
const { steps } = bridgeResult;
|
|
10018
|
-
// Build execution context from step history
|
|
10019
|
-
const context = buildFlowContext(steps);
|
|
10020
|
-
// Determine continuation logic using rule engine
|
|
10021
|
-
const continuation = determineContinuationFromRules(context);
|
|
10022
|
-
return {
|
|
10023
|
-
continuationStep: continuation.nextStep,
|
|
10024
|
-
isActionable: continuation.isActionable,
|
|
10025
|
-
completedSteps: Array.from(context.completedSteps),
|
|
10026
|
-
failedSteps: Array.from(context.failedSteps),
|
|
10027
|
-
reason: continuation.reason
|
|
10028
|
-
};
|
|
10029
|
-
};
|
|
10429
|
+
*/ hexStringSchema.refine((value)=>value.length === 132, 'EVM signature must be exactly 132 characters long (0x + 130 hex characters)').transform((value)=>value);
|
|
10030
10430
|
/**
|
|
10031
|
-
*
|
|
10431
|
+
* Schema for validating base58-encoded strings.
|
|
10032
10432
|
*
|
|
10033
|
-
*
|
|
10034
|
-
*
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
let lastStep;
|
|
10039
|
-
// Process step history to build context
|
|
10040
|
-
for (const step of steps){
|
|
10041
|
-
if (step.state === 'success' || step.state === 'noop') {
|
|
10042
|
-
completedSteps.add(step.name);
|
|
10043
|
-
} else if (step.state === 'error') {
|
|
10044
|
-
failedSteps.add(step.name);
|
|
10045
|
-
}
|
|
10046
|
-
// Track the last step for continuation logic
|
|
10047
|
-
lastStep = {
|
|
10048
|
-
name: step.name,
|
|
10049
|
-
state: step.state
|
|
10050
|
-
};
|
|
10051
|
-
}
|
|
10052
|
-
return {
|
|
10053
|
-
completedSteps,
|
|
10054
|
-
failedSteps,
|
|
10055
|
-
...lastStep && {
|
|
10056
|
-
lastStep
|
|
10057
|
-
}
|
|
10058
|
-
};
|
|
10059
|
-
}
|
|
10060
|
-
/**
|
|
10061
|
-
* Determine continuation step using the rule engine.
|
|
10433
|
+
* This schema validates that a string:
|
|
10434
|
+
* - Is a string type
|
|
10435
|
+
* - Is not empty after trimming
|
|
10436
|
+
* - Contains only valid base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z)
|
|
10437
|
+
* - Does not contain commonly confused characters (0, O, I, l)
|
|
10062
10438
|
*
|
|
10063
|
-
* @
|
|
10064
|
-
*
|
|
10065
|
-
|
|
10066
|
-
const lastStepName = context.lastStep?.name;
|
|
10067
|
-
// Handle initial state when no steps have been executed
|
|
10068
|
-
if (lastStepName === undefined) {
|
|
10069
|
-
const rules = STEP_TRANSITION_RULES[''];
|
|
10070
|
-
const matchingRule = rules?.find((rule)=>rule.condition(context));
|
|
10071
|
-
if (!matchingRule) {
|
|
10072
|
-
return {
|
|
10073
|
-
nextStep: null,
|
|
10074
|
-
isActionable: false,
|
|
10075
|
-
reason: 'No initial state rule found'
|
|
10076
|
-
};
|
|
10077
|
-
}
|
|
10078
|
-
return {
|
|
10079
|
-
nextStep: matchingRule.nextStep,
|
|
10080
|
-
isActionable: matchingRule.isActionable,
|
|
10081
|
-
reason: matchingRule.reason
|
|
10082
|
-
};
|
|
10083
|
-
}
|
|
10084
|
-
// A step with an empty name is ambiguous and should be treated as an unrecoverable state.
|
|
10085
|
-
if (lastStepName === '') {
|
|
10086
|
-
return {
|
|
10087
|
-
nextStep: null,
|
|
10088
|
-
isActionable: false,
|
|
10089
|
-
reason: 'No transition rules defined for step with empty name'
|
|
10090
|
-
};
|
|
10091
|
-
}
|
|
10092
|
-
const rules = STEP_TRANSITION_RULES[lastStepName];
|
|
10093
|
-
if (!rules) {
|
|
10094
|
-
return {
|
|
10095
|
-
nextStep: null,
|
|
10096
|
-
isActionable: false,
|
|
10097
|
-
reason: `No transition rules defined for step: ${lastStepName}`
|
|
10098
|
-
};
|
|
10099
|
-
}
|
|
10100
|
-
// Find the first matching rule
|
|
10101
|
-
const matchingRule = rules.find((rule)=>rule.condition(context));
|
|
10102
|
-
if (!matchingRule) {
|
|
10103
|
-
return {
|
|
10104
|
-
nextStep: null,
|
|
10105
|
-
isActionable: false,
|
|
10106
|
-
reason: `No matching transition rule for current context`
|
|
10107
|
-
};
|
|
10108
|
-
}
|
|
10109
|
-
return {
|
|
10110
|
-
nextStep: matchingRule.nextStep,
|
|
10111
|
-
isActionable: matchingRule.isActionable,
|
|
10112
|
-
reason: matchingRule.reason
|
|
10113
|
-
};
|
|
10114
|
-
}
|
|
10115
|
-
|
|
10116
|
-
/**
|
|
10117
|
-
* Find a step by name in the bridge result.
|
|
10439
|
+
* @remarks
|
|
10440
|
+
* This schema does not validate length, making it suitable for various base58-encoded data
|
|
10441
|
+
* like Solana addresses, transaction signatures, and other base58-encoded data.
|
|
10118
10442
|
*
|
|
10119
|
-
* @
|
|
10120
|
-
* @param stepName - The name of the step to find.
|
|
10121
|
-
* @returns The step if found, undefined otherwise.
|
|
10443
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
10122
10444
|
*
|
|
10123
10445
|
* @example
|
|
10124
10446
|
* ```typescript
|
|
10125
|
-
* import {
|
|
10447
|
+
* import { base58StringSchema } from '@core/adapter'
|
|
10126
10448
|
*
|
|
10127
|
-
* const
|
|
10128
|
-
*
|
|
10129
|
-
*
|
|
10130
|
-
*
|
|
10449
|
+
* const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
|
|
10450
|
+
* const validTxHash = '3Jf8k2L5mN9pQ7rS1tV4wX6yZ8aB2cD4eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5'
|
|
10451
|
+
*
|
|
10452
|
+
* const addressResult = base58StringSchema.safeParse(validAddress)
|
|
10453
|
+
* const txHashResult = base58StringSchema.safeParse(validTxHash)
|
|
10454
|
+
* console.log(addressResult.success) // true
|
|
10455
|
+
* console.log(txHashResult.success) // true
|
|
10131
10456
|
* ```
|
|
10132
|
-
*/
|
|
10133
|
-
|
|
10134
|
-
|
|
10457
|
+
*/ const base58StringSchema = z.string().min(1, 'Base58 string is required').refine((value)=>value.trim().length > 0, 'Base58 string cannot be empty').refine((value)=>{
|
|
10458
|
+
// Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
|
|
10459
|
+
// Excludes: 0, O, I, l to avoid confusion
|
|
10460
|
+
const base58Pattern = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
|
|
10461
|
+
return base58Pattern.test(value);
|
|
10462
|
+
}, 'Base58 string contains invalid characters. Only base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z) are allowed');
|
|
10135
10463
|
/**
|
|
10136
|
-
*
|
|
10464
|
+
* Schema for validating Solana addresses.
|
|
10137
10465
|
*
|
|
10138
|
-
*
|
|
10466
|
+
* This schema validates that a string is a properly formatted Solana address:
|
|
10467
|
+
* - Must be a valid base58-encoded string
|
|
10468
|
+
* - Must be between 32-44 characters long (typical length for base58-encoded 32-byte addresses)
|
|
10139
10469
|
*
|
|
10140
|
-
* @
|
|
10141
|
-
* @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
|
|
10142
|
-
* @returns An object containing the step and its index in the steps array.
|
|
10143
|
-
* @throws KitError if the specified pending step is not found.
|
|
10470
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
10144
10471
|
*
|
|
10145
10472
|
* @example
|
|
10146
10473
|
* ```typescript
|
|
10147
|
-
* import {
|
|
10474
|
+
* import { solanaAddressSchema } from '@core/adapter'
|
|
10148
10475
|
*
|
|
10149
|
-
* const
|
|
10150
|
-
*
|
|
10476
|
+
* const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
|
|
10477
|
+
*
|
|
10478
|
+
* const result = solanaAddressSchema.safeParse(validAddress)
|
|
10479
|
+
* console.log(result.success) // true
|
|
10151
10480
|
* ```
|
|
10152
|
-
*/
|
|
10153
|
-
const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
|
|
10154
|
-
if (index === -1) {
|
|
10155
|
-
throw new KitError({
|
|
10156
|
-
...InputError.VALIDATION_FAILED,
|
|
10157
|
-
recoverability: 'FATAL',
|
|
10158
|
-
message: `Pending step "${stepName}" not found in result`
|
|
10159
|
-
});
|
|
10160
|
-
}
|
|
10161
|
-
const step = result.steps[index];
|
|
10162
|
-
if (!step) {
|
|
10163
|
-
throw new KitError({
|
|
10164
|
-
...InputError.VALIDATION_FAILED,
|
|
10165
|
-
recoverability: 'FATAL',
|
|
10166
|
-
message: 'Pending step is undefined'
|
|
10167
|
-
});
|
|
10168
|
-
}
|
|
10169
|
-
return {
|
|
10170
|
-
step,
|
|
10171
|
-
index
|
|
10172
|
-
};
|
|
10173
|
-
}
|
|
10481
|
+
*/ base58StringSchema.refine((value)=>value.length >= 32 && value.length <= 44, 'Solana address must be between 32-44 characters long (base58-encoded 32-byte address)');
|
|
10174
10482
|
/**
|
|
10175
|
-
*
|
|
10483
|
+
* Schema for validating Solana transaction hashes.
|
|
10176
10484
|
*
|
|
10177
|
-
*
|
|
10178
|
-
*
|
|
10485
|
+
* This schema validates that a string is a properly formatted Solana transaction hash:
|
|
10486
|
+
* - Must be a valid base58-encoded string
|
|
10487
|
+
* - Must be between 86-88 characters long (typical length for base58-encoded 64-byte signatures)
|
|
10488
|
+
*
|
|
10489
|
+
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
10179
10490
|
*
|
|
10180
10491
|
* @example
|
|
10181
10492
|
* ```typescript
|
|
10182
|
-
* import {
|
|
10493
|
+
* import { solanaTransactionHashSchema } from '@core/adapter'
|
|
10183
10494
|
*
|
|
10184
|
-
* const
|
|
10185
|
-
*
|
|
10186
|
-
*
|
|
10187
|
-
*
|
|
10495
|
+
* const validTxHash = '5VfYmGBjvQKe3xgLtTQPSMEUdpEVHrJwLK7pKBJWKzYpNBE2g3kJrq7RSe9M8DqzQJ5J2aZPTjHLvd4WgxPpJKS'
|
|
10496
|
+
*
|
|
10497
|
+
* const result = solanaTransactionHashSchema.safeParse(validTxHash)
|
|
10498
|
+
* console.log(result.success) // true
|
|
10188
10499
|
* ```
|
|
10189
|
-
*/
|
|
10190
|
-
return findStepByName(result, CCTPv2StepName.burn)?.txHash;
|
|
10191
|
-
}
|
|
10500
|
+
*/ base58StringSchema.refine((value)=>value.length >= 86 && value.length <= 88, 'Solana transaction hash must be between 86-88 characters long (base58-encoded 64-byte signature)');
|
|
10192
10501
|
/**
|
|
10193
|
-
*
|
|
10502
|
+
* Schema for validating Adapter objects.
|
|
10503
|
+
* Checks for the required methods that define an Adapter.
|
|
10504
|
+
*/ z.object({
|
|
10505
|
+
prepare: z.function(),
|
|
10506
|
+
waitForTransaction: z.function(),
|
|
10507
|
+
getAddress: z.function()
|
|
10508
|
+
});
|
|
10509
|
+
|
|
10510
|
+
/**
|
|
10511
|
+
* Validate that the adapter has sufficient token balance for a transaction.
|
|
10194
10512
|
*
|
|
10195
|
-
*
|
|
10196
|
-
*
|
|
10513
|
+
* This function checks if the adapter's current token balance is greater than or equal
|
|
10514
|
+
* to the requested transaction amount. It throws a KitError with code 9001
|
|
10515
|
+
* (BALANCE_INSUFFICIENT_TOKEN) if the balance is insufficient, providing detailed
|
|
10516
|
+
* information about the shortfall.
|
|
10517
|
+
*
|
|
10518
|
+
* @param params - The validation parameters containing adapter, amount, token, and token address.
|
|
10519
|
+
* @returns A promise that resolves to void if validation passes.
|
|
10520
|
+
* @throws {KitError} When the adapter's balance is less than the required amount (code: 9001).
|
|
10197
10521
|
*
|
|
10198
10522
|
* @example
|
|
10199
10523
|
* ```typescript
|
|
10200
|
-
* import {
|
|
10524
|
+
* import { validateBalanceForTransaction } from '@core/adapter'
|
|
10525
|
+
* import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
10526
|
+
* import { isKitError, ERROR_TYPES } from '@core/errors'
|
|
10201
10527
|
*
|
|
10202
|
-
* const
|
|
10203
|
-
*
|
|
10204
|
-
*
|
|
10528
|
+
* const adapter = createViemAdapterFromPrivateKey({
|
|
10529
|
+
* privateKey: '0x...',
|
|
10530
|
+
* chain: 'Ethereum',
|
|
10531
|
+
* })
|
|
10532
|
+
*
|
|
10533
|
+
* try {
|
|
10534
|
+
* await validateBalanceForTransaction({
|
|
10535
|
+
* adapter,
|
|
10536
|
+
* amount: '1000000', // 1 USDC (6 decimals)
|
|
10537
|
+
* token: 'USDC',
|
|
10538
|
+
* tokenAddress: '0xA0b86a33E6441c8C1c7C16e4c5e3e5b5e4c5e3e5b5e4c5e',
|
|
10539
|
+
* operationContext: { chain: 'Ethereum' },
|
|
10540
|
+
* })
|
|
10541
|
+
* console.log('Balance validation passed')
|
|
10542
|
+
* } catch (error) {
|
|
10543
|
+
* if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
|
|
10544
|
+
* console.error('Insufficient funds:', error.message)
|
|
10545
|
+
* }
|
|
10205
10546
|
* }
|
|
10206
10547
|
* ```
|
|
10207
|
-
*/
|
|
10208
|
-
|
|
10209
|
-
const
|
|
10210
|
-
|
|
10211
|
-
|
|
10548
|
+
*/ const validateBalanceForTransaction = async (params)=>{
|
|
10549
|
+
const { amount, adapter, token, tokenAddress, operationContext } = params;
|
|
10550
|
+
const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
|
|
10551
|
+
walletAddress: operationContext.address
|
|
10552
|
+
}, operationContext);
|
|
10553
|
+
const balance = await balancePrepared.execute();
|
|
10554
|
+
if (BigInt(balance) < BigInt(amount)) {
|
|
10555
|
+
// Extract chain name from operationContext
|
|
10556
|
+
const chainName = extractChainInfo(operationContext.chain).name;
|
|
10557
|
+
// Create KitError with rich context in trace
|
|
10558
|
+
throw createInsufficientTokenBalanceError(chainName, token, {
|
|
10559
|
+
balance: balance.toString(),
|
|
10560
|
+
amount,
|
|
10561
|
+
tokenAddress,
|
|
10562
|
+
walletAddress: operationContext.address
|
|
10563
|
+
});
|
|
10212
10564
|
}
|
|
10213
|
-
|
|
10214
|
-
const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
|
|
10215
|
-
return fetchStep?.data;
|
|
10216
|
-
}
|
|
10565
|
+
};
|
|
10217
10566
|
|
|
10218
10567
|
/**
|
|
10219
|
-
*
|
|
10568
|
+
* Permit signature standards for gasless token approvals.
|
|
10220
10569
|
*
|
|
10221
|
-
*
|
|
10222
|
-
*
|
|
10223
|
-
* wait for an ongoing operation to complete.
|
|
10570
|
+
* Defines the permit types that can be used to approve token spending
|
|
10571
|
+
* without requiring a separate approval transaction.
|
|
10224
10572
|
*
|
|
10225
|
-
* @
|
|
10226
|
-
*
|
|
10227
|
-
*
|
|
10573
|
+
* @remarks
|
|
10574
|
+
* - NONE: No permit, tokens must be pre-approved via separate transaction
|
|
10575
|
+
* - EIP2612: Standard ERC-20 permit (USDC, DAI v2, and most modern tokens)
|
|
10576
|
+
*/ var PermitType;
|
|
10577
|
+
(function(PermitType) {
|
|
10578
|
+
/** No permit required - tokens must be pre-approved */ PermitType[PermitType["NONE"] = 0] = "NONE";
|
|
10579
|
+
/** EIP-2612 standard permit */ PermitType[PermitType["EIP2612"] = 1] = "EIP2612";
|
|
10580
|
+
})(PermitType || (PermitType = {}));
|
|
10581
|
+
|
|
10582
|
+
/**
|
|
10583
|
+
* Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
|
|
10228
10584
|
*
|
|
10229
|
-
*
|
|
10230
|
-
*
|
|
10231
|
-
*
|
|
10232
|
-
* import { analyzeSteps } from '../analyzeSteps'
|
|
10585
|
+
* Validates the full public-boundary input before any field destructuring,
|
|
10586
|
+
* `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
|
|
10587
|
+
* inputs always produce typed `KitError` validation failures.
|
|
10233
10588
|
*
|
|
10234
|
-
*
|
|
10235
|
-
*
|
|
10236
|
-
*
|
|
10237
|
-
*
|
|
10238
|
-
*
|
|
10239
|
-
|
|
10240
|
-
*
|
|
10241
|
-
*
|
|
10242
|
-
*
|
|
10589
|
+
* Checks performed (in order):
|
|
10590
|
+
* - `params` must be a non-null plain object
|
|
10591
|
+
* - `source` — valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
|
|
10592
|
+
* - `destinationChain` — present and supports CCTP v2
|
|
10593
|
+
* - source and destination chains must both be testnet or both mainnet
|
|
10594
|
+
* - source and destination chains must differ
|
|
10595
|
+
* - `executor` — non-empty string
|
|
10596
|
+
* - `amount` — bigint or non-empty string coercible to bigint
|
|
10597
|
+
* - `feeTotalAmount` — bigint or non-empty string coercible to bigint
|
|
10598
|
+
* - `feeToken` — valid EVM address (`0x` + 40 hex chars)
|
|
10599
|
+
* - `claim.signedQuote` — valid `0x`-prefixed hex string
|
|
10600
|
+
* - `claim.refundAddress` — valid EVM address
|
|
10601
|
+
* - `hookData` — valid `0x`-prefixed hex string when present
|
|
10243
10602
|
*
|
|
10244
|
-
* @param
|
|
10245
|
-
* @
|
|
10246
|
-
* @returns An object with `state` and an optional `errorMessage`.
|
|
10603
|
+
* @param params - The value to validate.
|
|
10604
|
+
* @throws {KitError} If any field is missing or invalid.
|
|
10247
10605
|
*
|
|
10248
10606
|
* @example
|
|
10249
10607
|
* ```typescript
|
|
10250
|
-
*
|
|
10251
|
-
*
|
|
10252
|
-
*
|
|
10608
|
+
* assertBurnWithFeesParams(params)
|
|
10609
|
+
* // params is now typed as BurnWithFeesParams and safe to use
|
|
10610
|
+
* const { source, destinationChain, amount } = params
|
|
10253
10611
|
* ```
|
|
10254
|
-
*/ function
|
|
10255
|
-
if (
|
|
10256
|
-
|
|
10257
|
-
state: 'success'
|
|
10258
|
-
};
|
|
10612
|
+
*/ function assertBurnWithFeesParams(params) {
|
|
10613
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
10614
|
+
throw createValidationFailedError('params', params, 'Must be a non-null plain object');
|
|
10259
10615
|
}
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10616
|
+
const p = params;
|
|
10617
|
+
// Source wallet context
|
|
10618
|
+
assertCCTPv2WalletContext(p['source']);
|
|
10619
|
+
const source = p['source'];
|
|
10620
|
+
// destinationChain
|
|
10621
|
+
const destinationChain = p['destinationChain'];
|
|
10622
|
+
if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
|
|
10623
|
+
throw createValidationFailedError('destinationChain', destinationChain, 'Must be a chain definition object');
|
|
10624
|
+
}
|
|
10625
|
+
if (!isCCTPV2Supported(destinationChain)) {
|
|
10626
|
+
throw createValidationFailedError('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
|
|
10627
|
+
}
|
|
10628
|
+
const dest = destinationChain;
|
|
10629
|
+
// Testnet / mainnet mismatch
|
|
10630
|
+
if (source.chain.isTestnet !== dest.isTestnet) {
|
|
10631
|
+
throw createNetworkMismatchError(source.chain, dest);
|
|
10632
|
+
}
|
|
10633
|
+
// Same-chain guard
|
|
10634
|
+
if (source.chain.name === dest.name) {
|
|
10635
|
+
throw createUnsupportedRouteError(source.chain.name, dest.name);
|
|
10636
|
+
}
|
|
10637
|
+
// executor
|
|
10638
|
+
const executor = p['executor'];
|
|
10639
|
+
if (typeof executor !== 'string' || executor === '') {
|
|
10640
|
+
throw createValidationFailedError('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
|
|
10641
|
+
}
|
|
10642
|
+
// amount
|
|
10643
|
+
const rawAmount = p['amount'];
|
|
10644
|
+
if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
|
|
10645
|
+
throw createValidationFailedError('amount', rawAmount, 'Must be a bigint or a numeric string');
|
|
10646
|
+
}
|
|
10647
|
+
try {
|
|
10648
|
+
BigInt(rawAmount);
|
|
10649
|
+
} catch {
|
|
10650
|
+
throw createValidationFailedError('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
|
|
10651
|
+
}
|
|
10652
|
+
// feeTotalAmount
|
|
10653
|
+
const rawFee = p['feeTotalAmount'];
|
|
10654
|
+
if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
|
|
10655
|
+
throw createValidationFailedError('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
|
|
10656
|
+
}
|
|
10657
|
+
try {
|
|
10658
|
+
BigInt(rawFee);
|
|
10659
|
+
} catch {
|
|
10660
|
+
throw createValidationFailedError('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
|
|
10661
|
+
}
|
|
10662
|
+
// feeToken
|
|
10663
|
+
if (!evmAddressSchema.safeParse(p['feeToken']).success) {
|
|
10664
|
+
throw createValidationFailedError('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
|
|
10665
|
+
}
|
|
10666
|
+
// claim
|
|
10667
|
+
const rawClaim = p['claim'];
|
|
10668
|
+
if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
|
|
10669
|
+
throw createValidationFailedError('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
|
|
10670
|
+
}
|
|
10671
|
+
const claim = rawClaim;
|
|
10672
|
+
if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
|
|
10673
|
+
throw createValidationFailedError('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
|
|
10674
|
+
}
|
|
10675
|
+
if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
|
|
10676
|
+
throw createValidationFailedError('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
|
|
10677
|
+
}
|
|
10678
|
+
// hookData (optional)
|
|
10679
|
+
const hookData = p['hookData'];
|
|
10680
|
+
if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
|
|
10681
|
+
throw createValidationFailedError('hookData', hookData, 'Must be a valid hex string starting with 0x');
|
|
10269
10682
|
}
|
|
10270
|
-
// Verify that the continuation step actually exists and is in pending state
|
|
10271
|
-
const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
|
|
10272
|
-
return pendingStep !== undefined;
|
|
10273
10683
|
}
|
|
10684
|
+
|
|
10274
10685
|
/**
|
|
10275
|
-
*
|
|
10276
|
-
*
|
|
10277
|
-
* @param step - The step object to check.
|
|
10278
|
-
* @param stepNames - The ordered list of step names in the execution flow.
|
|
10279
|
-
* @returns True if this is the last step in the flow.
|
|
10686
|
+
* CCTP bridge step names that can occur in the bridging flow.
|
|
10280
10687
|
*
|
|
10281
|
-
*
|
|
10282
|
-
*
|
|
10283
|
-
*
|
|
10688
|
+
* This object provides type safety for step names and represents all possible
|
|
10689
|
+
* steps that can be executed during a CCTP bridge operation. Using const assertions
|
|
10690
|
+
* makes this tree-shakable and follows modern TypeScript best practices.
|
|
10691
|
+
*/ const CCTPv2StepName = {
|
|
10692
|
+
approve: 'approve',
|
|
10693
|
+
burn: 'burn',
|
|
10694
|
+
fetchAttestation: 'fetchAttestation',
|
|
10695
|
+
mint: 'mint',
|
|
10696
|
+
reAttest: 'reAttest'
|
|
10697
|
+
};
|
|
10698
|
+
/**
|
|
10699
|
+
* Conditional step transition rules for CCTP bridge flow.
|
|
10284
10700
|
*
|
|
10285
|
-
*
|
|
10286
|
-
*
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10701
|
+
* Rules are evaluated in order - the first matching condition determines the next step.
|
|
10702
|
+
* This approach supports flexible flow logic and makes it easy to extend with new patterns.
|
|
10703
|
+
*/ const STEP_TRANSITION_RULES = {
|
|
10704
|
+
// Starting state - no steps executed yet
|
|
10705
|
+
'': [
|
|
10706
|
+
{
|
|
10707
|
+
condition: ()=>true,
|
|
10708
|
+
nextStep: CCTPv2StepName.approve,
|
|
10709
|
+
reason: 'Start with approval step',
|
|
10710
|
+
isActionable: true
|
|
10711
|
+
}
|
|
10712
|
+
],
|
|
10713
|
+
// After Approve step
|
|
10714
|
+
[CCTPv2StepName.approve]: [
|
|
10715
|
+
{
|
|
10716
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
10717
|
+
nextStep: CCTPv2StepName.burn,
|
|
10718
|
+
reason: 'Approval successful, proceed to burn',
|
|
10719
|
+
isActionable: true
|
|
10720
|
+
},
|
|
10721
|
+
{
|
|
10722
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
10723
|
+
nextStep: CCTPv2StepName.approve,
|
|
10724
|
+
reason: 'Retry failed approval',
|
|
10725
|
+
isActionable: true
|
|
10726
|
+
},
|
|
10727
|
+
{
|
|
10728
|
+
condition: (ctx)=>ctx.lastStep?.state === 'noop',
|
|
10729
|
+
nextStep: CCTPv2StepName.burn,
|
|
10730
|
+
reason: 'No approval needed, proceed to burn',
|
|
10731
|
+
isActionable: true
|
|
10732
|
+
},
|
|
10733
|
+
{
|
|
10734
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
10735
|
+
nextStep: CCTPv2StepName.approve,
|
|
10736
|
+
reason: 'Continue pending approval',
|
|
10737
|
+
isActionable: false
|
|
10738
|
+
}
|
|
10739
|
+
],
|
|
10740
|
+
// After Burn step
|
|
10741
|
+
[CCTPv2StepName.burn]: [
|
|
10742
|
+
{
|
|
10743
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
10744
|
+
nextStep: CCTPv2StepName.fetchAttestation,
|
|
10745
|
+
reason: 'Burn successful, fetch attestation',
|
|
10746
|
+
isActionable: true
|
|
10747
|
+
},
|
|
10748
|
+
{
|
|
10749
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
10750
|
+
nextStep: CCTPv2StepName.burn,
|
|
10751
|
+
reason: 'Retry failed burn',
|
|
10752
|
+
isActionable: true
|
|
10753
|
+
},
|
|
10754
|
+
{
|
|
10755
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
10756
|
+
nextStep: CCTPv2StepName.burn,
|
|
10757
|
+
reason: 'Continue pending burn',
|
|
10758
|
+
isActionable: false
|
|
10759
|
+
}
|
|
10760
|
+
],
|
|
10761
|
+
// After FetchAttestation step
|
|
10762
|
+
[CCTPv2StepName.fetchAttestation]: [
|
|
10763
|
+
{
|
|
10764
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
10765
|
+
nextStep: CCTPv2StepName.mint,
|
|
10766
|
+
reason: 'Attestation fetched, proceed to mint',
|
|
10767
|
+
isActionable: true
|
|
10768
|
+
},
|
|
10769
|
+
{
|
|
10770
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
10771
|
+
nextStep: CCTPv2StepName.fetchAttestation,
|
|
10772
|
+
reason: 'Retry fetching attestation',
|
|
10773
|
+
isActionable: true
|
|
10774
|
+
},
|
|
10775
|
+
{
|
|
10776
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
10777
|
+
nextStep: CCTPv2StepName.fetchAttestation,
|
|
10778
|
+
reason: 'Continue pending attestation fetch',
|
|
10779
|
+
isActionable: false
|
|
10780
|
+
}
|
|
10781
|
+
],
|
|
10782
|
+
// After Mint step
|
|
10783
|
+
[CCTPv2StepName.mint]: [
|
|
10784
|
+
{
|
|
10785
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
10786
|
+
nextStep: null,
|
|
10787
|
+
reason: 'Bridge completed successfully',
|
|
10788
|
+
isActionable: false
|
|
10789
|
+
},
|
|
10790
|
+
{
|
|
10791
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
10792
|
+
nextStep: CCTPv2StepName.mint,
|
|
10793
|
+
reason: 'Retry failed mint',
|
|
10794
|
+
isActionable: true
|
|
10795
|
+
},
|
|
10796
|
+
{
|
|
10797
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
10798
|
+
nextStep: CCTPv2StepName.mint,
|
|
10799
|
+
reason: 'Continue pending mint',
|
|
10800
|
+
isActionable: false
|
|
10801
|
+
}
|
|
10802
|
+
],
|
|
10803
|
+
// After ReAttest step
|
|
10804
|
+
[CCTPv2StepName.reAttest]: [
|
|
10805
|
+
{
|
|
10806
|
+
condition: (ctx)=>ctx.lastStep?.state === 'success',
|
|
10807
|
+
nextStep: CCTPv2StepName.mint,
|
|
10808
|
+
reason: 'Re-attestation successful, proceed to mint',
|
|
10809
|
+
isActionable: true
|
|
10810
|
+
},
|
|
10811
|
+
{
|
|
10812
|
+
condition: (ctx)=>ctx.lastStep?.state === 'error',
|
|
10813
|
+
nextStep: CCTPv2StepName.mint,
|
|
10814
|
+
reason: 'Re-attestation failed, retry mint to re-initiate recovery',
|
|
10815
|
+
isActionable: true
|
|
10816
|
+
},
|
|
10817
|
+
{
|
|
10818
|
+
condition: (ctx)=>ctx.lastStep?.state === 'pending',
|
|
10819
|
+
nextStep: CCTPv2StepName.mint,
|
|
10820
|
+
reason: 'Re-attestation pending, retry mint to re-initiate recovery',
|
|
10821
|
+
isActionable: true
|
|
10822
|
+
}
|
|
10823
|
+
]
|
|
10824
|
+
};
|
|
10293
10825
|
/**
|
|
10294
|
-
*
|
|
10826
|
+
* Analyze bridge steps to determine retry feasibility and continuation point.
|
|
10295
10827
|
*
|
|
10296
|
-
*
|
|
10297
|
-
*
|
|
10828
|
+
* This function examines the current state of bridge steps to determine the optimal
|
|
10829
|
+
* continuation strategy. It uses a rule-based approach that makes it easy to extend
|
|
10830
|
+
* with new flow patterns and step types in the future.
|
|
10298
10831
|
*
|
|
10299
|
-
*
|
|
10300
|
-
*
|
|
10301
|
-
* @param chain - The chain where the transaction was submitted.
|
|
10302
|
-
* @returns The updated step object with success or error state.
|
|
10832
|
+
* The current analysis supports the standard CCTP flow:
|
|
10833
|
+
* **Traditional flow**: Approve → Burn → FetchAttestation → Mint
|
|
10303
10834
|
*
|
|
10304
|
-
*
|
|
10835
|
+
* Key features:
|
|
10836
|
+
* - Rule-based transitions: Easy to extend with new step types and logic
|
|
10837
|
+
* - Context-aware decisions: Considers execution history and step states
|
|
10838
|
+
* - Actionable logic: Distinguishes between steps requiring user action vs waiting
|
|
10839
|
+
* - Terminal states: Properly handles completion and non-actionable states
|
|
10840
|
+
*
|
|
10841
|
+
* @param bridgeResult - The bridge result containing step execution history.
|
|
10842
|
+
* @returns Analysis result with continuation step and actionability information.
|
|
10843
|
+
* @throws Error when bridgeResult is invalid or contains no steps array.
|
|
10305
10844
|
*
|
|
10306
10845
|
* @example
|
|
10307
10846
|
* ```typescript
|
|
10308
|
-
* import {
|
|
10847
|
+
* import { analyzeSteps } from './analyzeSteps'
|
|
10309
10848
|
*
|
|
10310
|
-
*
|
|
10311
|
-
* const
|
|
10312
|
-
*
|
|
10849
|
+
* // Failed approval step (requires user action)
|
|
10850
|
+
* const bridgeResult = {
|
|
10851
|
+
* steps: [
|
|
10852
|
+
* { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
|
|
10853
|
+
* ]
|
|
10854
|
+
* }
|
|
10855
|
+
*
|
|
10856
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
10857
|
+
* // Result: { continuationStep: 'Approve', isRetryable: true,
|
|
10858
|
+
* // reason: 'Retry failed approval' }
|
|
10313
10859
|
* ```
|
|
10314
|
-
*/ async function waitForPendingTransaction(pendingStep, adapter, chain) {
|
|
10315
|
-
if (!pendingStep.txHash) {
|
|
10316
|
-
throw new KitError({
|
|
10317
|
-
...InputError.VALIDATION_FAILED,
|
|
10318
|
-
recoverability: 'FATAL',
|
|
10319
|
-
message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
|
|
10320
|
-
});
|
|
10321
|
-
}
|
|
10322
|
-
const txHash = pendingStep.txHash;
|
|
10323
|
-
const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
|
|
10324
|
-
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
10325
|
-
chain: chain.name,
|
|
10326
|
-
txHash
|
|
10327
|
-
}))
|
|
10328
|
-
});
|
|
10329
|
-
const outcome = evaluateTransactionOutcome(txReceipt, txHash);
|
|
10330
|
-
return {
|
|
10331
|
-
...pendingStep,
|
|
10332
|
-
state: outcome.state,
|
|
10333
|
-
data: txReceipt,
|
|
10334
|
-
explorerUrl: buildExplorerUrl(chain, txHash),
|
|
10335
|
-
...outcome.errorMessage ? {
|
|
10336
|
-
errorMessage: outcome.errorMessage
|
|
10337
|
-
} : {}
|
|
10338
|
-
};
|
|
10339
|
-
}
|
|
10340
|
-
/**
|
|
10341
|
-
* Wait for a pending step to complete.
|
|
10342
10860
|
*
|
|
10343
|
-
*
|
|
10344
|
-
*
|
|
10345
|
-
*
|
|
10346
|
-
*
|
|
10347
|
-
*
|
|
10348
|
-
*
|
|
10349
|
-
*
|
|
10350
|
-
*
|
|
10351
|
-
* @param context - The retry context.
|
|
10352
|
-
* @param result - The bridge result.
|
|
10353
|
-
* @param provider - The CCTP v2 bridging provider.
|
|
10354
|
-
* @returns The resolved step object with updated state.
|
|
10861
|
+
* @example
|
|
10862
|
+
* ```typescript
|
|
10863
|
+
* // Pending transaction (requires waiting, not actionable)
|
|
10864
|
+
* const bridgeResult = {
|
|
10865
|
+
* steps: [
|
|
10866
|
+
* { name: 'Approve', state: 'pending' }
|
|
10867
|
+
* ]
|
|
10868
|
+
* }
|
|
10355
10869
|
*
|
|
10356
|
-
*
|
|
10870
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
10871
|
+
* // Result: { continuationStep: 'Approve', isRetryable: false,
|
|
10872
|
+
* // reason: 'Continue pending approval' }
|
|
10873
|
+
* ```
|
|
10357
10874
|
*
|
|
10358
10875
|
* @example
|
|
10359
10876
|
* ```typescript
|
|
10360
|
-
*
|
|
10877
|
+
* // Completed bridge (nothing to do)
|
|
10878
|
+
* const bridgeResult = {
|
|
10879
|
+
* steps: [
|
|
10880
|
+
* { name: 'Approve', state: 'success' },
|
|
10881
|
+
* { name: 'Burn', state: 'success' },
|
|
10882
|
+
* { name: 'FetchAttestation', state: 'success' },
|
|
10883
|
+
* { name: 'Mint', state: 'success' }
|
|
10884
|
+
* ]
|
|
10885
|
+
* }
|
|
10361
10886
|
*
|
|
10362
|
-
* const
|
|
10363
|
-
*
|
|
10364
|
-
*
|
|
10365
|
-
* adapter,
|
|
10366
|
-
* chain,
|
|
10367
|
-
* context,
|
|
10368
|
-
* result,
|
|
10369
|
-
* provider,
|
|
10370
|
-
* )
|
|
10371
|
-
* // updatedStep.state is now 'success' or 'error'
|
|
10887
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
10888
|
+
* // Result: { continuationStep: null, isRetryable: false,
|
|
10889
|
+
* // reason: 'Bridge completed successfully' }
|
|
10372
10890
|
* ```
|
|
10373
|
-
*/
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10891
|
+
*/ const analyzeSteps = (bridgeResult)=>{
|
|
10892
|
+
// Input validation
|
|
10893
|
+
if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
|
|
10894
|
+
throw new Error('Invalid bridgeResult: must contain a steps array');
|
|
10895
|
+
}
|
|
10896
|
+
const { steps } = bridgeResult;
|
|
10897
|
+
// Build execution context from step history
|
|
10898
|
+
const context = buildFlowContext(steps);
|
|
10899
|
+
// Determine continuation logic using rule engine
|
|
10900
|
+
const continuation = determineContinuationFromRules(context);
|
|
10901
|
+
return {
|
|
10902
|
+
continuationStep: continuation.nextStep,
|
|
10903
|
+
isActionable: continuation.isActionable,
|
|
10904
|
+
completedSteps: Array.from(context.completedSteps),
|
|
10905
|
+
failedSteps: Array.from(context.failedSteps),
|
|
10906
|
+
reason: continuation.reason
|
|
10907
|
+
};
|
|
10908
|
+
};
|
|
10909
|
+
/**
|
|
10910
|
+
* Build flow context from the execution history.
|
|
10911
|
+
*
|
|
10912
|
+
* @param steps - Array of executed bridge steps.
|
|
10913
|
+
* @returns Flow context with execution state and history.
|
|
10914
|
+
*/ function buildFlowContext(steps) {
|
|
10915
|
+
const completedSteps = new Set();
|
|
10916
|
+
const failedSteps = new Set();
|
|
10917
|
+
let lastStep;
|
|
10918
|
+
// Process step history to build context
|
|
10919
|
+
for (const step of steps){
|
|
10920
|
+
if (step.state === 'success' || step.state === 'noop') {
|
|
10921
|
+
completedSteps.add(step.name);
|
|
10922
|
+
} else if (step.state === 'error') {
|
|
10923
|
+
failedSteps.add(step.name);
|
|
10924
|
+
}
|
|
10925
|
+
// Track the last step for continuation logic
|
|
10926
|
+
lastStep = {
|
|
10927
|
+
name: step.name,
|
|
10928
|
+
state: step.state
|
|
10929
|
+
};
|
|
10930
|
+
}
|
|
10931
|
+
return {
|
|
10932
|
+
completedSteps,
|
|
10933
|
+
failedSteps,
|
|
10934
|
+
...lastStep && {
|
|
10935
|
+
lastStep
|
|
10936
|
+
}
|
|
10937
|
+
};
|
|
10938
|
+
}
|
|
10939
|
+
/**
|
|
10940
|
+
* Determine continuation step using the rule engine.
|
|
10941
|
+
*
|
|
10942
|
+
* @param context - The flow context with execution history.
|
|
10943
|
+
* @returns Continuation decision with next step and actionability information.
|
|
10944
|
+
*/ function determineContinuationFromRules(context) {
|
|
10945
|
+
const lastStepName = context.lastStep?.name;
|
|
10946
|
+
// Handle initial state when no steps have been executed
|
|
10947
|
+
if (lastStepName === undefined) {
|
|
10948
|
+
const rules = STEP_TRANSITION_RULES[''];
|
|
10949
|
+
const matchingRule = rules?.find((rule)=>rule.condition(context));
|
|
10950
|
+
if (!matchingRule) {
|
|
10951
|
+
return {
|
|
10952
|
+
nextStep: null,
|
|
10953
|
+
isActionable: false,
|
|
10954
|
+
reason: 'No initial state rule found'
|
|
10955
|
+
};
|
|
10383
10956
|
}
|
|
10384
|
-
const sourceAddress = result.source.address;
|
|
10385
|
-
const attestation = await provider.fetchAttestation({
|
|
10386
|
-
chain: result.source.chain,
|
|
10387
|
-
adapter: context.from,
|
|
10388
|
-
address: sourceAddress
|
|
10389
|
-
}, burnTxHash);
|
|
10390
10957
|
return {
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10958
|
+
nextStep: matchingRule.nextStep,
|
|
10959
|
+
isActionable: matchingRule.isActionable,
|
|
10960
|
+
reason: matchingRule.reason
|
|
10394
10961
|
};
|
|
10395
10962
|
}
|
|
10396
|
-
//
|
|
10397
|
-
|
|
10963
|
+
// A step with an empty name is ambiguous and should be treated as an unrecoverable state.
|
|
10964
|
+
if (lastStepName === '') {
|
|
10965
|
+
return {
|
|
10966
|
+
nextStep: null,
|
|
10967
|
+
isActionable: false,
|
|
10968
|
+
reason: 'No transition rules defined for step with empty name'
|
|
10969
|
+
};
|
|
10970
|
+
}
|
|
10971
|
+
const rules = STEP_TRANSITION_RULES[lastStepName];
|
|
10972
|
+
if (!rules) {
|
|
10973
|
+
return {
|
|
10974
|
+
nextStep: null,
|
|
10975
|
+
isActionable: false,
|
|
10976
|
+
reason: `No transition rules defined for step: ${lastStepName}`
|
|
10977
|
+
};
|
|
10978
|
+
}
|
|
10979
|
+
// Find the first matching rule
|
|
10980
|
+
const matchingRule = rules.find((rule)=>rule.condition(context));
|
|
10981
|
+
if (!matchingRule) {
|
|
10982
|
+
return {
|
|
10983
|
+
nextStep: null,
|
|
10984
|
+
isActionable: false,
|
|
10985
|
+
reason: `No matching transition rule for current context`
|
|
10986
|
+
};
|
|
10987
|
+
}
|
|
10988
|
+
return {
|
|
10989
|
+
nextStep: matchingRule.nextStep,
|
|
10990
|
+
isActionable: matchingRule.isActionable,
|
|
10991
|
+
reason: matchingRule.reason
|
|
10992
|
+
};
|
|
10398
10993
|
}
|
|
10399
10994
|
|
|
10400
10995
|
/**
|
|
10401
|
-
*
|
|
10996
|
+
* Find a step by name in the bridge result.
|
|
10402
10997
|
*
|
|
10403
|
-
*
|
|
10404
|
-
*
|
|
10405
|
-
*
|
|
10998
|
+
* @param result - The bridge result to search.
|
|
10999
|
+
* @param stepName - The name of the step to find.
|
|
11000
|
+
* @returns The step if found, undefined otherwise.
|
|
10406
11001
|
*
|
|
10407
|
-
* @
|
|
10408
|
-
*
|
|
10409
|
-
*
|
|
10410
|
-
*
|
|
10411
|
-
*
|
|
10412
|
-
*
|
|
10413
|
-
*
|
|
10414
|
-
*
|
|
11002
|
+
* @example
|
|
11003
|
+
* ```typescript
|
|
11004
|
+
* import { findStepByName } from './findStep'
|
|
11005
|
+
*
|
|
11006
|
+
* const burnStep = findStepByName(result, 'burn')
|
|
11007
|
+
* if (burnStep) {
|
|
11008
|
+
* console.log('Burn tx:', burnStep.txHash)
|
|
11009
|
+
* }
|
|
11010
|
+
* ```
|
|
11011
|
+
*/ function findStepByName(result, stepName) {
|
|
11012
|
+
return result.steps.find((step)=>step.name === stepName);
|
|
11013
|
+
}
|
|
11014
|
+
/**
|
|
11015
|
+
* Find a pending step by name and return it with its index.
|
|
11016
|
+
*
|
|
11017
|
+
* Searches for a step that matches both the step name and has a pending state.
|
|
11018
|
+
*
|
|
11019
|
+
* @param result - The bridge result containing steps to search through.
|
|
11020
|
+
* @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
|
|
11021
|
+
* @returns An object containing the step and its index in the steps array.
|
|
11022
|
+
* @throws KitError if the specified pending step is not found.
|
|
10415
11023
|
*
|
|
10416
11024
|
* @example
|
|
10417
11025
|
* ```typescript
|
|
10418
|
-
*
|
|
10419
|
-
*
|
|
10420
|
-
*
|
|
10421
|
-
*
|
|
10422
|
-
* confirmations: 2,
|
|
10423
|
-
* timeout: 30000
|
|
10424
|
-
* })
|
|
10425
|
-
* console.log('Transaction hash:', step.txHash)
|
|
11026
|
+
* import { findPendingStep } from './findStep'
|
|
11027
|
+
*
|
|
11028
|
+
* const { step, index } = findPendingStep(result, 'burn')
|
|
11029
|
+
* console.log('Pending step:', step.name, 'at index:', index)
|
|
10426
11030
|
* ```
|
|
10427
|
-
*/
|
|
10428
|
-
const
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
* We return a noop step instead.
|
|
10436
|
-
*/ if (request.type === 'noop') {
|
|
10437
|
-
step.state = 'noop';
|
|
10438
|
-
return step;
|
|
10439
|
-
}
|
|
10440
|
-
const txHash = await request.execute();
|
|
10441
|
-
step.txHash = txHash;
|
|
10442
|
-
const retryOptions = {
|
|
10443
|
-
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
10444
|
-
chain: chain.name,
|
|
10445
|
-
txHash
|
|
10446
|
-
}))
|
|
10447
|
-
};
|
|
10448
|
-
if (timeout !== undefined) {
|
|
10449
|
-
retryOptions.deadlineMs = Date.now() + timeout;
|
|
10450
|
-
}
|
|
10451
|
-
const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
|
|
10452
|
-
confirmations,
|
|
10453
|
-
timeout
|
|
10454
|
-
}, chain), retryOptions);
|
|
10455
|
-
const outcome = evaluateTransactionOutcome(transaction, txHash);
|
|
10456
|
-
step.state = outcome.state;
|
|
10457
|
-
step.data = transaction;
|
|
10458
|
-
// Generate explorer URL for the step
|
|
10459
|
-
step.explorerUrl = buildExplorerUrl(chain, txHash);
|
|
10460
|
-
if (outcome.errorMessage) {
|
|
10461
|
-
step.errorMessage = outcome.errorMessage;
|
|
10462
|
-
// Transaction was mined but reverted on-chain.
|
|
10463
|
-
step.errorCategory = 'chain_revert';
|
|
10464
|
-
}
|
|
10465
|
-
} catch (err) {
|
|
10466
|
-
step.state = 'error';
|
|
10467
|
-
step.error = err;
|
|
10468
|
-
// Sequential path does not yet attempt fine-grained classification of
|
|
10469
|
-
// pre-submission errors (user_rejected, capability errors, etc.). Mark
|
|
10470
|
-
// as `unknown` so consumers can at least detect the category is
|
|
10471
|
-
// populated uniformly across batched and sequential flows.
|
|
10472
|
-
step.errorCategory = 'unknown';
|
|
10473
|
-
// Optionally parse for common blockchain error formats
|
|
10474
|
-
if (err instanceof Error) {
|
|
10475
|
-
step.errorMessage = err.message;
|
|
10476
|
-
} else if (typeof err === 'object' && err != null && 'message' in err) {
|
|
10477
|
-
step.errorMessage = String(err.message);
|
|
10478
|
-
} else {
|
|
10479
|
-
step.errorMessage = `Unknown error occurred during ${name} step.`;
|
|
10480
|
-
}
|
|
11031
|
+
*/ function findPendingStep(result, stepName) {
|
|
11032
|
+
const index = result.steps.findIndex((step)=>step.name === stepName && step.state === 'pending');
|
|
11033
|
+
if (index === -1) {
|
|
11034
|
+
throw new KitError({
|
|
11035
|
+
...InputError.VALIDATION_FAILED,
|
|
11036
|
+
recoverability: 'FATAL',
|
|
11037
|
+
message: `Pending step "${stepName}" not found in result`
|
|
11038
|
+
});
|
|
10481
11039
|
}
|
|
10482
|
-
|
|
11040
|
+
const step = result.steps[index];
|
|
11041
|
+
if (!step) {
|
|
11042
|
+
throw new KitError({
|
|
11043
|
+
...InputError.VALIDATION_FAILED,
|
|
11044
|
+
recoverability: 'FATAL',
|
|
11045
|
+
message: 'Pending step is undefined'
|
|
11046
|
+
});
|
|
11047
|
+
}
|
|
11048
|
+
return {
|
|
11049
|
+
step,
|
|
11050
|
+
index
|
|
11051
|
+
};
|
|
10483
11052
|
}
|
|
10484
|
-
|
|
10485
11053
|
/**
|
|
10486
|
-
*
|
|
10487
|
-
*
|
|
10488
|
-
* This function handles the approval step of the CCTP v2 bridge process, allowing
|
|
10489
|
-
* the TokenMessenger contract to spend the specified amount of USDC tokens on behalf
|
|
10490
|
-
* of the user. This is a prerequisite for the subsequent burn operation.
|
|
11054
|
+
* Get the burn transaction hash from bridge result.
|
|
10491
11055
|
*
|
|
10492
|
-
* @param
|
|
10493
|
-
* @
|
|
10494
|
-
* @returns Promise resolving to the bridge step with transaction details
|
|
10495
|
-
* @throws {KitError} If the parameters are invalid
|
|
10496
|
-
* @throws {BridgeError} If the approval transaction fails
|
|
11056
|
+
* @param result - The bridge result.
|
|
11057
|
+
* @returns The burn transaction hash, or undefined if not found.
|
|
10497
11058
|
*
|
|
10498
11059
|
* @example
|
|
10499
11060
|
* ```typescript
|
|
10500
|
-
*
|
|
10501
|
-
*
|
|
11061
|
+
* import { getBurnTxHash } from './findStep'
|
|
11062
|
+
*
|
|
11063
|
+
* const burnTxHash = getBurnTxHash(result)
|
|
11064
|
+
* if (burnTxHash) {
|
|
11065
|
+
* console.log('Burn tx hash:', burnTxHash)
|
|
11066
|
+
* }
|
|
10502
11067
|
* ```
|
|
10503
|
-
*/
|
|
10504
|
-
|
|
10505
|
-
const customFee = BigInt(params.config?.customFee?.value ?? '0');
|
|
10506
|
-
const amountBigInt = BigInt(params.amount);
|
|
10507
|
-
const approvalAmount = (amountBigInt + customFee).toString();
|
|
10508
|
-
return await executePreparedChainRequest({
|
|
10509
|
-
name: 'approve',
|
|
10510
|
-
adapter: params.source.adapter,
|
|
10511
|
-
chain: params.source.chain,
|
|
10512
|
-
request: await provider.approve(params.source, approvalAmount)
|
|
10513
|
-
});
|
|
11068
|
+
*/ function getBurnTxHash(result) {
|
|
11069
|
+
return findStepByName(result, CCTPv2StepName.burn)?.txHash;
|
|
10514
11070
|
}
|
|
10515
|
-
|
|
10516
11071
|
/**
|
|
10517
|
-
*
|
|
10518
|
-
*
|
|
10519
|
-
* This function handles the burning step of the CCTP v2 bridge process, where USDC tokens
|
|
10520
|
-
* are burned on the source chain to create a message that can be used to mint equivalent
|
|
10521
|
-
* tokens on the destination chain.
|
|
11072
|
+
* Get the attestation data from bridge result.
|
|
10522
11073
|
*
|
|
10523
|
-
* @param
|
|
10524
|
-
* @
|
|
10525
|
-
* @returns Promise resolving to the bridge step with transaction details
|
|
10526
|
-
* @throws {KitError} If the parameters are invalid
|
|
10527
|
-
* @throws \{BridgeError\} If the burn transaction fails
|
|
11074
|
+
* @param result - The bridge result.
|
|
11075
|
+
* @returns The attestation data, or undefined if not found.
|
|
10528
11076
|
*
|
|
10529
11077
|
* @example
|
|
10530
11078
|
* ```typescript
|
|
10531
|
-
*
|
|
10532
|
-
*
|
|
11079
|
+
* import { getAttestationData } from './findStep'
|
|
11080
|
+
*
|
|
11081
|
+
* const attestation = getAttestationData(result)
|
|
11082
|
+
* if (attestation) {
|
|
11083
|
+
* console.log('Attestation:', attestation.message)
|
|
11084
|
+
* }
|
|
10533
11085
|
* ```
|
|
10534
|
-
*/
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
11086
|
+
*/ function getAttestationData(result) {
|
|
11087
|
+
// Prefer reAttest data (most recent attestation after expiry)
|
|
11088
|
+
const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
|
|
11089
|
+
if (reAttestStep?.state === 'success' && reAttestStep.data) {
|
|
11090
|
+
return reAttestStep.data;
|
|
11091
|
+
}
|
|
11092
|
+
// Fall back to fetchAttestation step
|
|
11093
|
+
const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
|
|
11094
|
+
return fetchStep?.data;
|
|
10541
11095
|
}
|
|
10542
11096
|
|
|
10543
11097
|
/**
|
|
10544
|
-
*
|
|
11098
|
+
* Check if the analysis indicates a non-actionable pending state.
|
|
10545
11099
|
*
|
|
10546
|
-
*
|
|
10547
|
-
*
|
|
10548
|
-
*
|
|
11100
|
+
* A pending state is non-actionable when there's a continuation step but
|
|
11101
|
+
* the analysis marks it as not actionable, typically because we need to
|
|
11102
|
+
* wait for an ongoing operation to complete.
|
|
10549
11103
|
*
|
|
10550
|
-
* @param
|
|
10551
|
-
* @param
|
|
10552
|
-
* @returns
|
|
10553
|
-
* @throws {KitError} If the parameters are invalid
|
|
10554
|
-
* @throws {BridgeError} If the attestation fetch fails
|
|
11104
|
+
* @param analysis - The step analysis result from analyzeSteps.
|
|
11105
|
+
* @param result - The bridge result to check for pending steps.
|
|
11106
|
+
* @returns True if there is a pending step that we should wait for.
|
|
10555
11107
|
*
|
|
10556
11108
|
* @example
|
|
10557
11109
|
* ```typescript
|
|
10558
|
-
*
|
|
10559
|
-
*
|
|
10560
|
-
*
|
|
10561
|
-
* )
|
|
10562
|
-
*
|
|
11110
|
+
* import { hasPendingState } from './stepUtils'
|
|
11111
|
+
* import { analyzeSteps } from '../analyzeSteps'
|
|
11112
|
+
*
|
|
11113
|
+
* const analysis = analyzeSteps(bridgeResult)
|
|
11114
|
+
* if (hasPendingState(analysis, bridgeResult)) {
|
|
11115
|
+
* // Wait for the pending operation to complete
|
|
11116
|
+
* }
|
|
10563
11117
|
* ```
|
|
10564
|
-
*/
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
};
|
|
10584
|
-
} catch (err) {
|
|
10585
|
-
let errorMessage = 'Unknown attestation error';
|
|
10586
|
-
if (err instanceof Error) {
|
|
10587
|
-
errorMessage = err.message;
|
|
10588
|
-
} else if (typeof err === 'string') {
|
|
10589
|
-
errorMessage = err;
|
|
10590
|
-
}
|
|
10591
|
-
step = {
|
|
10592
|
-
...step,
|
|
10593
|
-
state: 'error',
|
|
10594
|
-
error: err,
|
|
10595
|
-
errorMessage,
|
|
10596
|
-
data: undefined
|
|
11118
|
+
*/ /**
|
|
11119
|
+
* Evaluate a transaction receipt and return the corresponding step state
|
|
11120
|
+
* and error message. Centralises the success/revert/unconfirmed logic so
|
|
11121
|
+
* every call-site behaves identically.
|
|
11122
|
+
*
|
|
11123
|
+
* @param receipt - The transaction receipt containing status and block info.
|
|
11124
|
+
* @param txHash - The transaction hash used in error messages.
|
|
11125
|
+
* @returns An object with `state` and an optional `errorMessage`.
|
|
11126
|
+
*
|
|
11127
|
+
* @example
|
|
11128
|
+
* ```typescript
|
|
11129
|
+
* const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
|
|
11130
|
+
* step.state = outcome.state
|
|
11131
|
+
* if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
|
|
11132
|
+
* ```
|
|
11133
|
+
*/ function evaluateTransactionOutcome(receipt, txHash) {
|
|
11134
|
+
if (receipt.status === 'success' && receipt.blockNumber) {
|
|
11135
|
+
return {
|
|
11136
|
+
state: 'success'
|
|
10597
11137
|
};
|
|
10598
11138
|
}
|
|
10599
|
-
return
|
|
11139
|
+
return {
|
|
11140
|
+
state: 'error',
|
|
11141
|
+
errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
|
|
11142
|
+
};
|
|
11143
|
+
}
|
|
11144
|
+
function hasPendingState(analysis, result) {
|
|
11145
|
+
// Check if there's a continuation step that's marked as non-actionable
|
|
11146
|
+
if (analysis.continuationStep === null || analysis.isActionable) {
|
|
11147
|
+
return false;
|
|
11148
|
+
}
|
|
11149
|
+
// Verify that the continuation step actually exists and is in pending state
|
|
11150
|
+
const pendingStep = result.steps.find((step)=>step.name === analysis.continuationStep && step.state === 'pending');
|
|
11151
|
+
return pendingStep !== undefined;
|
|
10600
11152
|
}
|
|
10601
|
-
|
|
10602
11153
|
/**
|
|
10603
|
-
*
|
|
10604
|
-
*
|
|
10605
|
-
* This function handles the minting step of the CCTP v2 bridge process, where USDC tokens
|
|
10606
|
-
* are minted on the destination chain using the attestation and message from the source chain.
|
|
10607
|
-
* This is the final step that completes the cross-chain bridge operation.
|
|
11154
|
+
* Check if the step is the last one in the execution flow.
|
|
10608
11155
|
*
|
|
10609
|
-
* @param
|
|
10610
|
-
* @param
|
|
10611
|
-
* @
|
|
10612
|
-
* @param message - The message from the source chain burn operation
|
|
10613
|
-
* @returns Promise resolving to the bridge step with transaction details
|
|
10614
|
-
* @throws {KitError} If the parameters are invalid
|
|
10615
|
-
* @throws {BridgeError} If the mint transaction fails
|
|
11156
|
+
* @param step - The step object to check.
|
|
11157
|
+
* @param stepNames - The ordered list of step names in the execution flow.
|
|
11158
|
+
* @returns True if this is the last step in the flow.
|
|
10616
11159
|
*
|
|
10617
11160
|
* @example
|
|
10618
11161
|
* ```typescript
|
|
10619
|
-
*
|
|
10620
|
-
*
|
|
11162
|
+
* import { isLastStep } from './stepUtils'
|
|
11163
|
+
*
|
|
11164
|
+
* const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
|
|
11165
|
+
* isLastStep({ name: 'mint' }, stepNames) // true
|
|
11166
|
+
* isLastStep({ name: 'burn' }, stepNames) // false
|
|
10621
11167
|
* ```
|
|
10622
|
-
*/
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
const step = await executePreparedChainRequest({
|
|
10626
|
-
name: 'mint',
|
|
10627
|
-
adapter: params.destination.adapter,
|
|
10628
|
-
chain: params.destination.chain,
|
|
10629
|
-
request: await provider.mint(params.source, params.destination, attestation)
|
|
10630
|
-
});
|
|
10631
|
-
// Add forwarded: false for non-relayer mints
|
|
10632
|
-
return {
|
|
10633
|
-
...step,
|
|
10634
|
-
forwarded: false
|
|
10635
|
-
};
|
|
11168
|
+
*/ function isLastStep(step, stepNames) {
|
|
11169
|
+
const stepIndex = stepNames.indexOf(step.name);
|
|
11170
|
+
return stepIndex === -1 || stepIndex >= stepNames.length - 1;
|
|
10636
11171
|
}
|
|
10637
|
-
const mockAttestationMessage = {
|
|
10638
|
-
attestation: '0x8bd9b9e63eb05128eb2896b63e3e1df39bfd6bbfb893b69dc53c39252aeb85df1ded70263b07da17abf88e6e1b77f16ebcdb4eb1c6b4ea4c625215e5cb9dddb81bffe0b9d75e2094f05e48f18f70d0b254999bde90bab26f5c0da29b2d7e00feca1cfed119cba0d2a0e887648a40e803e0ca34aa8fd94ce068515eeaef72b520aa1b',
|
|
10639
|
-
message: '0x000000010000000000000006f446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f30000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000003e8000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
|
|
10640
|
-
eventNonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
|
|
10641
|
-
cctpVersion: 2,
|
|
10642
|
-
status: 'complete',
|
|
10643
|
-
decodedMessage: {
|
|
10644
|
-
sourceDomain: '0',
|
|
10645
|
-
destinationDomain: '6',
|
|
10646
|
-
nonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
|
|
10647
|
-
sender: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
10648
|
-
recipient: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
10649
|
-
destinationCaller: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
10650
|
-
minFinalityThreshold: '1000',
|
|
10651
|
-
finalityThresholdExecuted: '1000',
|
|
10652
|
-
messageBody: '0x000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
|
|
10653
|
-
decodedMessageBody: {
|
|
10654
|
-
burnToken: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238',
|
|
10655
|
-
mintRecipient: '0x23f9a5bea7b92a0638520607407bc7f0310aeed4',
|
|
10656
|
-
amount: '100000',
|
|
10657
|
-
messageSender: '0xc5567a5e3370d4dbfb0540025078e283e36a363d',
|
|
10658
|
-
maxFee: '11',
|
|
10659
|
-
feeExecuted: '10',
|
|
10660
|
-
expirationBlock: '32944472',
|
|
10661
|
-
hookData: null
|
|
10662
|
-
}
|
|
10663
|
-
},
|
|
10664
|
-
delayReason: null
|
|
10665
|
-
};
|
|
10666
|
-
|
|
10667
11172
|
/**
|
|
10668
|
-
*
|
|
11173
|
+
* Wait for a pending transaction to complete.
|
|
10669
11174
|
*
|
|
10670
|
-
*
|
|
10671
|
-
*
|
|
10672
|
-
* 2. At least one message has forwardState === 'CONFIRMED' (or 'COMPLETE') and a valid forwardTxHash
|
|
11175
|
+
* Poll the adapter until the transaction is confirmed on-chain and return
|
|
11176
|
+
* the updated step with success or error state based on the receipt.
|
|
10673
11177
|
*
|
|
10674
|
-
*
|
|
10675
|
-
*
|
|
11178
|
+
* @param pendingStep - The full step object containing the transaction hash.
|
|
11179
|
+
* @param adapter - The adapter to use for waiting.
|
|
11180
|
+
* @param chain - The chain where the transaction was submitted.
|
|
11181
|
+
* @returns The updated step object with success or error state.
|
|
10676
11182
|
*
|
|
10677
|
-
* @
|
|
10678
|
-
*
|
|
10679
|
-
* @
|
|
10680
|
-
*
|
|
10681
|
-
*
|
|
10682
|
-
*
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
message: 'Invalid attestation response structure from IRIS API.'
|
|
10690
|
-
});
|
|
10691
|
-
}
|
|
10692
|
-
// Find the first message (typically there's only one)
|
|
10693
|
-
const message = obj.messages[0];
|
|
10694
|
-
if (!message) {
|
|
11183
|
+
* @throws KitError when the pending step has no transaction hash.
|
|
11184
|
+
*
|
|
11185
|
+
* @example
|
|
11186
|
+
* ```typescript
|
|
11187
|
+
* import { waitForPendingTransaction } from './bridgeStepUtils'
|
|
11188
|
+
*
|
|
11189
|
+
* const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
|
|
11190
|
+
* const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
|
|
11191
|
+
* // updatedStep.state is now 'success' or 'error'
|
|
11192
|
+
* ```
|
|
11193
|
+
*/ async function waitForPendingTransaction(pendingStep, adapter, chain) {
|
|
11194
|
+
if (!pendingStep.txHash) {
|
|
10695
11195
|
throw new KitError({
|
|
10696
11196
|
...InputError.VALIDATION_FAILED,
|
|
10697
11197
|
recoverability: 'FATAL',
|
|
10698
|
-
message:
|
|
10699
|
-
});
|
|
10700
|
-
}
|
|
10701
|
-
// Check for FAILED state - this is a permanent failure
|
|
10702
|
-
if (message.forwardState === 'FAILED') {
|
|
10703
|
-
throw new KitError({
|
|
10704
|
-
...NetworkError.RELAYER_FORWARD_FAILED,
|
|
10705
|
-
recoverability: 'RESUMABLE',
|
|
10706
|
-
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.',
|
|
10707
|
-
cause: {
|
|
10708
|
-
trace: {
|
|
10709
|
-
eventNonce: message.eventNonce,
|
|
10710
|
-
attestation: message.attestation,
|
|
10711
|
-
message: message.message
|
|
10712
|
-
}
|
|
10713
|
-
}
|
|
11198
|
+
message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`
|
|
10714
11199
|
});
|
|
10715
11200
|
}
|
|
10716
|
-
|
|
10717
|
-
|
|
10718
|
-
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
throw new KitError({
|
|
10723
|
-
...NetworkError.RELAYER_PENDING,
|
|
10724
|
-
recoverability: 'RETRYABLE',
|
|
10725
|
-
message: 'Relayer mint not ready. Waiting for confirmation.'
|
|
11201
|
+
const txHash = pendingStep.txHash;
|
|
11202
|
+
const txReceipt = await retryAsync(async ()=>adapter.waitForTransaction(txHash, undefined, chain), {
|
|
11203
|
+
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
11204
|
+
chain: chain.name,
|
|
11205
|
+
txHash
|
|
11206
|
+
}))
|
|
10726
11207
|
});
|
|
10727
|
-
|
|
11208
|
+
const outcome = evaluateTransactionOutcome(txReceipt, txHash);
|
|
11209
|
+
return {
|
|
11210
|
+
...pendingStep,
|
|
11211
|
+
state: outcome.state,
|
|
11212
|
+
data: txReceipt,
|
|
11213
|
+
explorerUrl: buildExplorerUrl(chain, txHash),
|
|
11214
|
+
...outcome.errorMessage ? {
|
|
11215
|
+
errorMessage: outcome.errorMessage
|
|
11216
|
+
} : {}
|
|
11217
|
+
};
|
|
11218
|
+
}
|
|
10728
11219
|
/**
|
|
10729
|
-
*
|
|
11220
|
+
* Wait for a pending step to complete.
|
|
10730
11221
|
*
|
|
10731
|
-
*
|
|
10732
|
-
*
|
|
10733
|
-
* This function polls until the relayer has submitted and confirmed the mint transaction.
|
|
11222
|
+
* For transaction steps: waits for the transaction to be confirmed.
|
|
11223
|
+
* For attestation: re-executes the attestation fetch.
|
|
10734
11224
|
*
|
|
10735
|
-
* @
|
|
10736
|
-
*
|
|
10737
|
-
* -
|
|
10738
|
-
* -
|
|
10739
|
-
* -
|
|
11225
|
+
* @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
|
|
11226
|
+
* @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
|
|
11227
|
+
* @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
|
|
11228
|
+
* @param adapter - The adapter to use.
|
|
11229
|
+
* @param chain - The chain where the step is executing.
|
|
11230
|
+
* @param context - The retry context.
|
|
11231
|
+
* @param result - The bridge result.
|
|
11232
|
+
* @param provider - The CCTP v2 bridging provider.
|
|
11233
|
+
* @returns The resolved step object with updated state.
|
|
10740
11234
|
*
|
|
10741
|
-
* @
|
|
10742
|
-
* @param transactionHash - The transaction hash of the burn operation
|
|
10743
|
-
* @param isTestnet - Whether this is for a testnet chain (true) or mainnet (false)
|
|
10744
|
-
* @param config - Optional configuration overrides for polling behavior
|
|
10745
|
-
* @returns The attestation message with confirmed forwardTxHash
|
|
10746
|
-
* @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
|
|
10747
|
-
* @throws {KitError} If timeout is reached while still pending
|
|
11235
|
+
* @throws KitError when fetching attestation but burn transaction hash is not found.
|
|
10748
11236
|
*
|
|
10749
11237
|
* @example
|
|
10750
11238
|
* ```typescript
|
|
10751
|
-
*
|
|
10752
|
-
*
|
|
11239
|
+
* import { waitForStepToComplete } from './bridgeStepUtils'
|
|
11240
|
+
*
|
|
11241
|
+
* const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
|
|
11242
|
+
* const updatedStep = await waitForStepToComplete(
|
|
11243
|
+
* pendingStep,
|
|
11244
|
+
* adapter,
|
|
11245
|
+
* chain,
|
|
11246
|
+
* context,
|
|
11247
|
+
* result,
|
|
11248
|
+
* provider,
|
|
11249
|
+
* )
|
|
11250
|
+
* // updatedStep.state is now 'success' or 'error'
|
|
10753
11251
|
* ```
|
|
10754
|
-
*/
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
};
|
|
10760
|
-
let response;
|
|
10761
|
-
try {
|
|
10762
|
-
response = await pollApiGet(url, isRelayerMintConfirmed, effectiveConfig);
|
|
10763
|
-
} catch (error) {
|
|
10764
|
-
// Enrich RELAYER_FORWARD_FAILED errors with the burn transaction hash
|
|
10765
|
-
if (error instanceof KitError && error.name === 'NETWORK_RELAYER_FORWARD_FAILED') {
|
|
11252
|
+
*/ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
|
|
11253
|
+
if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
|
|
11254
|
+
// For attestation, re-run the fetch (it has built-in polling)
|
|
11255
|
+
const burnTxHash = getBurnTxHash(result);
|
|
11256
|
+
if (!burnTxHash) {
|
|
10766
11257
|
throw new KitError({
|
|
10767
|
-
...
|
|
10768
|
-
recoverability:
|
|
10769
|
-
message:
|
|
10770
|
-
cause: {
|
|
10771
|
-
...error.cause,
|
|
10772
|
-
trace: {
|
|
10773
|
-
...error.cause?.trace,
|
|
10774
|
-
burnTxHash: transactionHash
|
|
10775
|
-
}
|
|
10776
|
-
}
|
|
11258
|
+
...InputError.VALIDATION_FAILED,
|
|
11259
|
+
recoverability: 'FATAL',
|
|
11260
|
+
message: 'Cannot fetch attestation: burn transaction hash not found'
|
|
10777
11261
|
});
|
|
10778
11262
|
}
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
recoverability: 'FATAL',
|
|
10791
|
-
message: 'No attestation messages found in response after polling.'
|
|
10792
|
-
});
|
|
11263
|
+
const sourceAddress = result.source.address;
|
|
11264
|
+
const attestation = await provider.fetchAttestation({
|
|
11265
|
+
chain: result.source.chain,
|
|
11266
|
+
adapter: context.from,
|
|
11267
|
+
address: sourceAddress
|
|
11268
|
+
}, burnTxHash);
|
|
11269
|
+
return {
|
|
11270
|
+
...pendingStep,
|
|
11271
|
+
state: 'success',
|
|
11272
|
+
data: attestation
|
|
11273
|
+
};
|
|
10793
11274
|
}
|
|
10794
|
-
|
|
10795
|
-
|
|
11275
|
+
// For transaction steps, wait for the transaction to complete
|
|
11276
|
+
return waitForPendingTransaction(pendingStep, adapter, chain);
|
|
11277
|
+
}
|
|
10796
11278
|
|
|
10797
11279
|
/**
|
|
10798
|
-
* Executes
|
|
11280
|
+
* Executes a prepared chain request and returns the result as a bridge step.
|
|
10799
11281
|
*
|
|
10800
|
-
*
|
|
10801
|
-
*
|
|
10802
|
-
*
|
|
10803
|
-
* 3. Returns the mint step with the relayer's transaction details
|
|
11282
|
+
* This function takes a prepared chain request (containing transaction data) and executes
|
|
11283
|
+
* it using the appropriate adapter. It handles the execution details and formats
|
|
11284
|
+
* the result as a standardized bridge step with transaction details and explorer URLs.
|
|
10804
11285
|
*
|
|
10805
|
-
*
|
|
10806
|
-
*
|
|
10807
|
-
*
|
|
10808
|
-
*
|
|
10809
|
-
*
|
|
10810
|
-
*
|
|
10811
|
-
*
|
|
10812
|
-
*
|
|
10813
|
-
* @
|
|
10814
|
-
* @throws
|
|
11286
|
+
* @param params - The execution parameters containing:
|
|
11287
|
+
* - `name`: The name of the step
|
|
11288
|
+
* - `request`: The prepared chain request containing transaction data
|
|
11289
|
+
* - `adapter`: The adapter that will execute the transaction
|
|
11290
|
+
* - `confirmations`: The number of confirmations to wait for (defaults to 1)
|
|
11291
|
+
* - `timeout`: The timeout for the request in milliseconds
|
|
11292
|
+
* - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
|
|
11293
|
+
* bypassing `eth_estimateGas`; ignored for non-EVM requests
|
|
11294
|
+
* @returns The bridge step with the transaction details and explorer URL
|
|
11295
|
+
* @throws If the transaction execution fails
|
|
10815
11296
|
*
|
|
10816
11297
|
* @example
|
|
10817
11298
|
* ```typescript
|
|
10818
|
-
* const step = await
|
|
10819
|
-
*
|
|
11299
|
+
* const step = await executePreparedChainRequest({
|
|
11300
|
+
* name: 'approve',
|
|
11301
|
+
* request: preparedRequest,
|
|
11302
|
+
* adapter: adapter,
|
|
11303
|
+
* confirmations: 2,
|
|
11304
|
+
* timeout: 30000
|
|
11305
|
+
* })
|
|
11306
|
+
* console.log('Transaction hash:', step.txHash)
|
|
10820
11307
|
* ```
|
|
10821
|
-
*/ async function
|
|
11308
|
+
*/ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
|
|
10822
11309
|
const step = {
|
|
10823
|
-
name
|
|
10824
|
-
state: 'pending'
|
|
10825
|
-
forwarded: true
|
|
11310
|
+
name,
|
|
11311
|
+
state: 'pending'
|
|
10826
11312
|
};
|
|
10827
|
-
// Validate context has burnTxHash
|
|
10828
|
-
if (!context?.burnTxHash) {
|
|
10829
|
-
throw new KitError({
|
|
10830
|
-
...InputError.VALIDATION_FAILED,
|
|
10831
|
-
recoverability: 'FATAL',
|
|
10832
|
-
message: 'Burn transaction hash not available for relayer mint. Ensure the burn step completed successfully.'
|
|
10833
|
-
});
|
|
10834
|
-
}
|
|
10835
11313
|
try {
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
11314
|
+
/**
|
|
11315
|
+
* No-op requests are not executed.
|
|
11316
|
+
* We return a noop step instead.
|
|
11317
|
+
*/ if (request.type === 'noop') {
|
|
11318
|
+
step.state = 'noop';
|
|
11319
|
+
return step;
|
|
11320
|
+
}
|
|
11321
|
+
const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
|
|
11322
|
+
gasLimit
|
|
11323
|
+
}) : await request.execute();
|
|
11324
|
+
step.txHash = txHash;
|
|
11325
|
+
const retryOptions = {
|
|
11326
|
+
isRetryable: (err)=>isRetryableError$1(parseBlockchainError(err, {
|
|
11327
|
+
chain: chain.name,
|
|
11328
|
+
txHash
|
|
11329
|
+
}))
|
|
11330
|
+
};
|
|
11331
|
+
if (timeout !== undefined) {
|
|
11332
|
+
retryOptions.deadlineMs = Date.now() + timeout;
|
|
11333
|
+
}
|
|
11334
|
+
const transaction = await retryAsync(async ()=>adapter.waitForTransaction(txHash, {
|
|
11335
|
+
confirmations,
|
|
11336
|
+
timeout
|
|
11337
|
+
}, chain), retryOptions);
|
|
11338
|
+
const outcome = evaluateTransactionOutcome(transaction, txHash);
|
|
11339
|
+
step.state = outcome.state;
|
|
11340
|
+
step.data = transaction;
|
|
11341
|
+
// Generate explorer URL for the step
|
|
11342
|
+
step.explorerUrl = buildExplorerUrl(chain, txHash);
|
|
11343
|
+
if (outcome.errorMessage) {
|
|
11344
|
+
step.errorMessage = outcome.errorMessage;
|
|
11345
|
+
// Transaction was mined but reverted on-chain.
|
|
11346
|
+
step.errorCategory = 'chain_revert';
|
|
10846
11347
|
}
|
|
10847
|
-
step.txHash = forwardTxHash;
|
|
10848
|
-
step.explorerUrl = buildExplorerUrl(params.destination.chain, forwardTxHash);
|
|
10849
|
-
step.state = 'success';
|
|
10850
11348
|
} catch (err) {
|
|
10851
11349
|
step.state = 'error';
|
|
10852
11350
|
step.error = err;
|
|
10853
|
-
|
|
11351
|
+
// Sequential path does not yet attempt fine-grained classification of
|
|
11352
|
+
// pre-submission errors (user_rejected, capability errors, etc.). Mark
|
|
11353
|
+
// as `unknown` so consumers can at least detect the category is
|
|
11354
|
+
// populated uniformly across batched and sequential flows.
|
|
11355
|
+
step.errorCategory = 'unknown';
|
|
11356
|
+
// Optionally parse for common blockchain error formats
|
|
11357
|
+
if (err instanceof Error) {
|
|
11358
|
+
step.errorMessage = err.message;
|
|
11359
|
+
} else if (typeof err === 'object' && err != null && 'message' in err) {
|
|
11360
|
+
step.errorMessage = String(err.message);
|
|
11361
|
+
} else {
|
|
11362
|
+
step.errorMessage = `Unknown error occurred during ${name} step.`;
|
|
11363
|
+
}
|
|
10854
11364
|
}
|
|
10855
11365
|
return step;
|
|
10856
11366
|
}
|
|
10857
11367
|
|
|
10858
11368
|
/**
|
|
10859
|
-
*
|
|
11369
|
+
* Approves the TokenMessenger contract to spend USDC tokens for a bridge operation.
|
|
10860
11370
|
*
|
|
10861
|
-
*
|
|
10862
|
-
*
|
|
10863
|
-
*
|
|
11371
|
+
* This function handles the approval step of the CCTP v2 bridge process, allowing
|
|
11372
|
+
* the TokenMessenger contract to spend the specified amount of USDC tokens on behalf
|
|
11373
|
+
* of the user. This is a prerequisite for the subsequent burn operation.
|
|
10864
11374
|
*
|
|
10865
|
-
*
|
|
10866
|
-
*
|
|
10867
|
-
*
|
|
11375
|
+
* @param params - The bridge parameters containing source, destination, amount and optional config
|
|
11376
|
+
* @param sourceChain - The source chain definition where the approval will occur
|
|
11377
|
+
* @returns Promise resolving to the bridge step with transaction details
|
|
11378
|
+
* @throws {KitError} If the parameters are invalid
|
|
11379
|
+
* @throws {BridgeError} If the approval transaction fails
|
|
10868
11380
|
*
|
|
10869
|
-
* @
|
|
10870
|
-
*
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
updateContext: (step)=>{
|
|
10887
|
-
if (!step.txHash) {
|
|
10888
|
-
throw new KitError({
|
|
10889
|
-
...InputError.VALIDATION_FAILED,
|
|
10890
|
-
recoverability: 'FATAL',
|
|
10891
|
-
message: 'Burn step completed but no transaction hash was returned'
|
|
10892
|
-
});
|
|
10893
|
-
}
|
|
10894
|
-
return {
|
|
10895
|
-
burnTxHash: step.txHash
|
|
10896
|
-
};
|
|
10897
|
-
}
|
|
10898
|
-
},
|
|
10899
|
-
{
|
|
10900
|
-
name: 'fetchAttestation',
|
|
10901
|
-
executor: async (params, provider, context)=>{
|
|
10902
|
-
if (!context?.burnTxHash) {
|
|
10903
|
-
throw new KitError({
|
|
10904
|
-
...InputError.VALIDATION_FAILED,
|
|
10905
|
-
recoverability: 'FATAL',
|
|
10906
|
-
message: 'Burn transaction hash not available for attestation'
|
|
10907
|
-
});
|
|
10908
|
-
}
|
|
10909
|
-
return bridgeFetchAttestation({
|
|
10910
|
-
params,
|
|
10911
|
-
provider
|
|
10912
|
-
}, context.burnTxHash);
|
|
10913
|
-
},
|
|
10914
|
-
updateContext: (step)=>{
|
|
10915
|
-
if (!step.data) {
|
|
10916
|
-
throw new KitError({
|
|
10917
|
-
...InputError.VALIDATION_FAILED,
|
|
10918
|
-
recoverability: 'FATAL',
|
|
10919
|
-
message: 'Attestation step completed but no data was returned'
|
|
10920
|
-
});
|
|
10921
|
-
}
|
|
10922
|
-
return {
|
|
10923
|
-
attestationData: step.data
|
|
10924
|
-
};
|
|
10925
|
-
}
|
|
10926
|
-
},
|
|
10927
|
-
{
|
|
10928
|
-
name: 'mint',
|
|
10929
|
-
// bridgeRelayerMint validates context.burnTxHash internally with a KitError
|
|
10930
|
-
// bridgeMint requires attestationData which is validated here
|
|
10931
|
-
executor: useForwarder ? async (params, _provider, context)=>{
|
|
10932
|
-
if (!context) {
|
|
10933
|
-
throw new KitError({
|
|
10934
|
-
...InputError.VALIDATION_FAILED,
|
|
10935
|
-
recoverability: 'FATAL',
|
|
10936
|
-
message: 'Step context is required for relayer mint'
|
|
10937
|
-
});
|
|
10938
|
-
}
|
|
10939
|
-
return bridgeRelayerMint(params, context);
|
|
10940
|
-
} : async (params, provider, context)=>{
|
|
10941
|
-
if (!context?.attestationData) {
|
|
10942
|
-
throw new KitError({
|
|
10943
|
-
...InputError.VALIDATION_FAILED,
|
|
10944
|
-
recoverability: 'FATAL',
|
|
10945
|
-
message: 'Attestation data not available for minting'
|
|
10946
|
-
});
|
|
10947
|
-
}
|
|
10948
|
-
return bridgeMint({
|
|
10949
|
-
params,
|
|
10950
|
-
provider
|
|
10951
|
-
}, context.attestationData);
|
|
10952
|
-
}
|
|
10953
|
-
}
|
|
10954
|
-
];
|
|
11381
|
+
* @example
|
|
11382
|
+
* ```typescript
|
|
11383
|
+
* const approveStep = await approve(params, sourceChain)
|
|
11384
|
+
* console.log('Approval tx:', approveStep.transactionHash)
|
|
11385
|
+
* ```
|
|
11386
|
+
*/ async function bridgeApproval({ params, provider }) {
|
|
11387
|
+
// Calculate the approval amount
|
|
11388
|
+
const customFee = BigInt(params.config?.customFee?.value ?? '0');
|
|
11389
|
+
const amountBigInt = BigInt(params.amount);
|
|
11390
|
+
const approvalAmount = (amountBigInt + customFee).toString();
|
|
11391
|
+
return await executePreparedChainRequest({
|
|
11392
|
+
name: 'approve',
|
|
11393
|
+
adapter: params.source.adapter,
|
|
11394
|
+
chain: params.source.chain,
|
|
11395
|
+
request: await provider.approve(params.source, approvalAmount),
|
|
11396
|
+
gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
|
|
11397
|
+
});
|
|
10955
11398
|
}
|
|
11399
|
+
|
|
10956
11400
|
/**
|
|
10957
|
-
*
|
|
11401
|
+
* Executes a deposit-for-burn operation on the source chain to initiate a bridge.
|
|
10958
11402
|
*
|
|
10959
|
-
*
|
|
10960
|
-
*
|
|
10961
|
-
*
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
11403
|
+
* This function handles the burning step of the CCTP v2 bridge process, where USDC tokens
|
|
11404
|
+
* are burned on the source chain to create a message that can be used to mint equivalent
|
|
11405
|
+
* tokens on the destination chain.
|
|
11406
|
+
*
|
|
11407
|
+
* @param params - The bridge parameters containing source, destination, amount and optional config
|
|
11408
|
+
* @param sourceChain - The source chain definition where the burn will occur
|
|
11409
|
+
* @returns Promise resolving to the bridge step with transaction details
|
|
11410
|
+
* @throws {KitError} If the parameters are invalid
|
|
11411
|
+
* @throws \{BridgeError\} If the burn transaction fails
|
|
11412
|
+
*
|
|
11413
|
+
* @example
|
|
11414
|
+
* ```typescript
|
|
11415
|
+
* const burnStep = await burn(params, sourceChain)
|
|
11416
|
+
* console.log('Burn tx:', burnStep.transactionHash)
|
|
11417
|
+
* ```
|
|
11418
|
+
*/ async function bridgeBurn({ params, provider }) {
|
|
11419
|
+
return await executePreparedChainRequest({
|
|
11420
|
+
name: 'burn',
|
|
11421
|
+
adapter: params.source.adapter,
|
|
11422
|
+
chain: params.source.chain,
|
|
11423
|
+
request: await provider.burn(params),
|
|
11424
|
+
gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
|
|
10969
11425
|
});
|
|
10970
11426
|
}
|
|
10971
11427
|
|
|
10972
11428
|
/**
|
|
10973
|
-
*
|
|
11429
|
+
* Fetches the attestation from Circle's API for a completed burn operation.
|
|
10974
11430
|
*
|
|
10975
|
-
*
|
|
10976
|
-
*
|
|
10977
|
-
*
|
|
11431
|
+
* This function retrieves the attestation data required to mint tokens on the destination
|
|
11432
|
+
* chain after a successful burn operation. The attestation proves that the burn occurred
|
|
11433
|
+
* and authorizes the minting of equivalent tokens.
|
|
10978
11434
|
*
|
|
10979
|
-
* @param
|
|
10980
|
-
* @param
|
|
10981
|
-
* @
|
|
10982
|
-
* @
|
|
11435
|
+
* @param params - The bridge parameters containing source, destination, amount and optional config
|
|
11436
|
+
* @param txHash - The hash of the burn transaction
|
|
11437
|
+
* @returns Promise resolving to the bridge step with attestation data
|
|
11438
|
+
* @throws {KitError} If the parameters are invalid
|
|
11439
|
+
* @throws {BridgeError} If the attestation fetch fails
|
|
10983
11440
|
*
|
|
10984
11441
|
* @example
|
|
10985
11442
|
* ```typescript
|
|
10986
|
-
* const
|
|
10987
|
-
*
|
|
10988
|
-
*
|
|
10989
|
-
*
|
|
10990
|
-
*
|
|
10991
|
-
*
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
if (!provider.actionDispatcher) {
|
|
10997
|
-
return;
|
|
10998
|
-
}
|
|
10999
|
-
// Extract traceId from invocation context if provided
|
|
11000
|
-
const traceId = invocation?.traceId;
|
|
11001
|
-
const actionValues = {
|
|
11002
|
-
protocol: 'cctp',
|
|
11003
|
-
version: 'v2',
|
|
11004
|
-
...traceId !== undefined && {
|
|
11005
|
-
traceId
|
|
11006
|
-
},
|
|
11007
|
-
values: step
|
|
11443
|
+
* const attestationStep = await bridgeFetchAttestation(
|
|
11444
|
+
* { params, provider },
|
|
11445
|
+
* burnTxHash
|
|
11446
|
+
* )
|
|
11447
|
+
* console.log('Attestation data:', attestationStep.data)
|
|
11448
|
+
* ```
|
|
11449
|
+
*/ async function bridgeFetchAttestation({ params, provider }, txHash) {
|
|
11450
|
+
let step = {
|
|
11451
|
+
name: 'fetchAttestation',
|
|
11452
|
+
state: 'pending'
|
|
11008
11453
|
};
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11018
|
-
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11454
|
+
try {
|
|
11455
|
+
/**
|
|
11456
|
+
* Give slow transfers a longer retry delay of 30 seconds to account for the longer time it takes to get the
|
|
11457
|
+
* attestation so that we do not hit the rate limit of the IRIS API or the retry limit of the API polling utility.
|
|
11458
|
+
* We only do this for slow transfers on source chains that have greater than 20 confirmations (5 minutes of blocktime
|
|
11459
|
+
* on the slowest blocktime chains) so that we do not slow down the attestation polling for lower confirmation chains.
|
|
11460
|
+
*/ const apiPollingConfig = params.config?.transferSpeed === TransferSpeed.SLOW && params.source.chain.cctp.contracts.v2.confirmations > 20 ? {
|
|
11461
|
+
retryDelay: 30_000
|
|
11462
|
+
} : undefined;
|
|
11463
|
+
const attestation = await provider.fetchAttestation(params.source, txHash, apiPollingConfig);
|
|
11464
|
+
step = {
|
|
11465
|
+
...step,
|
|
11466
|
+
state: 'success',
|
|
11467
|
+
data: attestation
|
|
11468
|
+
};
|
|
11469
|
+
} catch (err) {
|
|
11470
|
+
let errorMessage = 'Unknown attestation error';
|
|
11471
|
+
if (err instanceof Error) {
|
|
11472
|
+
errorMessage = err.message;
|
|
11473
|
+
} else if (typeof err === 'string') {
|
|
11474
|
+
errorMessage = err;
|
|
11475
|
+
}
|
|
11476
|
+
step = {
|
|
11477
|
+
...step,
|
|
11478
|
+
state: 'error',
|
|
11479
|
+
error: err,
|
|
11480
|
+
errorMessage,
|
|
11481
|
+
data: undefined
|
|
11482
|
+
};
|
|
11026
11483
|
}
|
|
11484
|
+
return step;
|
|
11027
11485
|
}
|
|
11028
11486
|
|
|
11029
11487
|
/**
|
|
11030
|
-
*
|
|
11488
|
+
* Executes a mint operation on the destination chain to complete a bridge.
|
|
11031
11489
|
*
|
|
11032
|
-
*
|
|
11033
|
-
|
|
11034
|
-
*
|
|
11490
|
+
* This function handles the minting step of the CCTP v2 bridge process, where USDC tokens
|
|
11491
|
+
* are minted on the destination chain using the attestation and message from the source chain.
|
|
11492
|
+
* This is the final step that completes the cross-chain bridge operation.
|
|
11035
11493
|
*
|
|
11036
|
-
* @
|
|
11037
|
-
*
|
|
11038
|
-
*
|
|
11494
|
+
* @param params - The bridge parameters containing source, destination, amount and optional config
|
|
11495
|
+
* @param destinationChain - The destination chain definition where the mint will occur
|
|
11496
|
+
* @param attestation - The attestation data from Circle's API
|
|
11497
|
+
* @param message - The message from the source chain burn operation
|
|
11498
|
+
* @returns Promise resolving to the bridge step with transaction details
|
|
11499
|
+
* @throws {KitError} If the parameters are invalid
|
|
11500
|
+
* @throws {BridgeError} If the mint transaction fails
|
|
11039
11501
|
*
|
|
11040
11502
|
* @example
|
|
11041
11503
|
* ```typescript
|
|
11042
|
-
*
|
|
11043
|
-
*
|
|
11044
|
-
* const start = defaultClock.now()
|
|
11045
|
-
* // ... do work ...
|
|
11046
|
-
* const elapsed = defaultClock.since(start)
|
|
11504
|
+
* const mintStep = await mint(params, destinationChain, attestation, message)
|
|
11505
|
+
* console.log('Mint tx:', mintStep.transactionHash)
|
|
11047
11506
|
* ```
|
|
11048
|
-
*/
|
|
11049
|
-
|
|
11050
|
-
|
|
11507
|
+
*/ async function bridgeMint({ params, provider }, attestation) {
|
|
11508
|
+
// Validate attestation message matches transfer params
|
|
11509
|
+
await assertCCTPv2AttestationParams(attestation, params);
|
|
11510
|
+
const mintRequest = await provider.mint(params.source, params.destination, attestation);
|
|
11511
|
+
const step = await executePreparedChainRequest({
|
|
11512
|
+
name: 'mint',
|
|
11513
|
+
adapter: params.destination.adapter,
|
|
11514
|
+
chain: params.destination.chain,
|
|
11515
|
+
request: mintRequest,
|
|
11516
|
+
// Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
|
|
11517
|
+
// eth_estimateGas does not account for, returning a below-floor value
|
|
11518
|
+
// without reverting. Pinning to a value above the observed execution max
|
|
11519
|
+
// (310_839) bypasses re-estimation and guarantees we clear both the floor
|
|
11520
|
+
// and the actual execution cost.
|
|
11521
|
+
gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
|
|
11522
|
+
});
|
|
11523
|
+
// Add forwarded: false for non-relayer mints
|
|
11524
|
+
return {
|
|
11525
|
+
...step,
|
|
11526
|
+
forwarded: false
|
|
11527
|
+
};
|
|
11528
|
+
}
|
|
11529
|
+
const mockAttestationMessage = {
|
|
11530
|
+
attestation: '0x8bd9b9e63eb05128eb2896b63e3e1df39bfd6bbfb893b69dc53c39252aeb85df1ded70263b07da17abf88e6e1b77f16ebcdb4eb1c6b4ea4c625215e5cb9dddb81bffe0b9d75e2094f05e48f18f70d0b254999bde90bab26f5c0da29b2d7e00feca1cfed119cba0d2a0e887648a40e803e0ca34aa8fd94ce068515eeaef72b520aa1b',
|
|
11531
|
+
message: '0x000000010000000000000006f446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f30000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000003e8000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
|
|
11532
|
+
eventNonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
|
|
11533
|
+
cctpVersion: 2,
|
|
11534
|
+
status: 'complete',
|
|
11535
|
+
decodedMessage: {
|
|
11536
|
+
sourceDomain: '0',
|
|
11537
|
+
destinationDomain: '6',
|
|
11538
|
+
nonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
|
|
11539
|
+
sender: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
11540
|
+
recipient: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
|
|
11541
|
+
destinationCaller: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
11542
|
+
minFinalityThreshold: '1000',
|
|
11543
|
+
finalityThresholdExecuted: '1000',
|
|
11544
|
+
messageBody: '0x000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
|
|
11545
|
+
decodedMessageBody: {
|
|
11546
|
+
burnToken: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238',
|
|
11547
|
+
mintRecipient: '0x23f9a5bea7b92a0638520607407bc7f0310aeed4',
|
|
11548
|
+
amount: '100000',
|
|
11549
|
+
messageSender: '0xc5567a5e3370d4dbfb0540025078e283e36a363d',
|
|
11550
|
+
maxFee: '11',
|
|
11551
|
+
feeExecuted: '10',
|
|
11552
|
+
expirationBlock: '32944472',
|
|
11553
|
+
hookData: null
|
|
11554
|
+
}
|
|
11555
|
+
},
|
|
11556
|
+
delayReason: null
|
|
11051
11557
|
};
|
|
11052
11558
|
|
|
11053
|
-
// ============================================================================
|
|
11054
|
-
// Crypto Utilities (Internal)
|
|
11055
|
-
// ============================================================================
|
|
11056
|
-
/** @internal */ function hasGetRandomValues() {
|
|
11057
|
-
return typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function';
|
|
11058
|
-
}
|
|
11059
11559
|
/**
|
|
11060
|
-
*
|
|
11560
|
+
* Executes the mint step for forwarding mode where Circle's relayer handles the mint.
|
|
11061
11561
|
*
|
|
11062
|
-
*
|
|
11562
|
+
* Instead of the user signing and submitting a mint transaction, this function:
|
|
11563
|
+
* 1. Polls the attestation API until the relayer has confirmed the mint (forwardState === 'CONFIRMED')
|
|
11564
|
+
* 2. Extracts the relayer's mint transaction hash (forwardTxHash)
|
|
11565
|
+
* 3. Returns the mint step with the relayer's transaction details
|
|
11063
11566
|
*
|
|
11064
|
-
*
|
|
11065
|
-
*
|
|
11066
|
-
*
|
|
11567
|
+
* Since Circle's relayer handles the mint transaction, we don't wait for on-chain
|
|
11568
|
+
* confirmation - the IRIS API confirmation is sufficient. The step completes
|
|
11569
|
+
* immediately after the API confirms the mint.
|
|
11570
|
+
*
|
|
11571
|
+
* @param params - The bridge parameters containing source, destination, amount and config
|
|
11572
|
+
* @param provider - The CCTP v2 bridging provider
|
|
11573
|
+
* @param context - The step context containing burnTxHash from the burn step
|
|
11574
|
+
* @returns Promise resolving to the bridge step with transaction details
|
|
11575
|
+
* @throws {KitError} If burnTxHash is not available in context
|
|
11576
|
+
* @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
|
|
11577
|
+
* @throws {KitError} If timeout is reached while waiting for relayer
|
|
11067
11578
|
*
|
|
11068
11579
|
* @example
|
|
11069
11580
|
* ```typescript
|
|
11070
|
-
* const
|
|
11581
|
+
* const step = await bridgeRelayerMint(
|
|
11582
|
+
* { params, provider },
|
|
11583
|
+
* { burnTxHash: '0x...' }
|
|
11584
|
+
* )
|
|
11585
|
+
* console.log('Relayer mint tx:', step.txHash)
|
|
11071
11586
|
* ```
|
|
11072
|
-
*/ function
|
|
11073
|
-
const
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11587
|
+
*/ async function bridgeRelayerMint({ params, provider }, context) {
|
|
11588
|
+
const step = {
|
|
11589
|
+
name: 'mint',
|
|
11590
|
+
state: 'pending',
|
|
11591
|
+
forwarded: true
|
|
11592
|
+
};
|
|
11593
|
+
// Validate context has burnTxHash
|
|
11594
|
+
if (!context?.burnTxHash) {
|
|
11595
|
+
throw new KitError({
|
|
11596
|
+
...InputError.VALIDATION_FAILED,
|
|
11597
|
+
recoverability: 'FATAL',
|
|
11598
|
+
message: 'Burn transaction hash not available for relayer mint. Ensure the burn step completed successfully.'
|
|
11599
|
+
});
|
|
11600
|
+
}
|
|
11601
|
+
try {
|
|
11602
|
+
// Poll attestation API until relayer confirms the mint
|
|
11603
|
+
const attestation = await provider.fetchRelayerMint(params.source, context.burnTxHash);
|
|
11604
|
+
// Extract the relayer's mint transaction hash
|
|
11605
|
+
const forwardTxHash = attestation.forwardTxHash;
|
|
11606
|
+
if (!forwardTxHash) {
|
|
11607
|
+
throw new KitError({
|
|
11608
|
+
...InputError.VALIDATION_FAILED,
|
|
11609
|
+
recoverability: 'FATAL',
|
|
11610
|
+
message: 'Relayer mint transaction hash not found in attestation response.'
|
|
11611
|
+
});
|
|
11080
11612
|
}
|
|
11613
|
+
step.txHash = forwardTxHash;
|
|
11614
|
+
step.explorerUrl = buildExplorerUrl(params.destination.chain, forwardTxHash);
|
|
11615
|
+
step.state = 'success';
|
|
11616
|
+
} catch (err) {
|
|
11617
|
+
step.state = 'error';
|
|
11618
|
+
step.error = err;
|
|
11619
|
+
step.errorMessage = getErrorMessage(err);
|
|
11081
11620
|
}
|
|
11082
|
-
return
|
|
11621
|
+
return step;
|
|
11083
11622
|
}
|
|
11084
11623
|
|
|
11085
11624
|
/**
|
|
11086
|
-
*
|
|
11087
|
-
*
|
|
11088
|
-
* @param tags - Tags object, may contain undefined values. Accepts `undefined` or `null`.
|
|
11089
|
-
* @returns A new object with undefined values removed, or empty object if input is nullish.
|
|
11090
|
-
*
|
|
11091
|
-
* @remarks
|
|
11092
|
-
* This helper ensures tags are safe for serialization and logging.
|
|
11093
|
-
* Similar to the internal `cleanUndefined` in the logger module, but
|
|
11094
|
-
* typed specifically for {@link Tags}.
|
|
11095
|
-
*
|
|
11096
|
-
* @example
|
|
11097
|
-
* ```typescript
|
|
11098
|
-
* import { cleanTags } from '@core/runtime'
|
|
11099
|
-
*
|
|
11100
|
-
* const tags = { chain: 'Ethereum', amount: 100, extra: undefined }
|
|
11101
|
-
* const cleaned = cleanTags(tags)
|
|
11102
|
-
* // { chain: 'Ethereum', amount: 100 }
|
|
11625
|
+
* Creates the ordered sequence of CCTP v2 bridge step executors.
|
|
11103
11626
|
*
|
|
11104
|
-
*
|
|
11105
|
-
*
|
|
11106
|
-
*
|
|
11107
|
-
*/ function cleanTags(tags) {
|
|
11108
|
-
if (tags == null) return {};
|
|
11109
|
-
return Object.fromEntries(Object.entries(tags).filter(([, value])=>value !== undefined));
|
|
11110
|
-
}
|
|
11111
|
-
|
|
11112
|
-
/**
|
|
11113
|
-
* Check if a pattern is valid.
|
|
11627
|
+
* Defines the complete bridge flow: approve → burn → fetchAttestation → mint.
|
|
11628
|
+
* Each step includes an executor function and optional context updater for
|
|
11629
|
+
* passing data between steps.
|
|
11114
11630
|
*
|
|
11115
|
-
*
|
|
11116
|
-
*
|
|
11117
|
-
*
|
|
11118
|
-
* - `**` not at end (e.g., `a.**.b`)
|
|
11631
|
+
* When `useForwarder` is true, the mint step uses the relayer mint executor
|
|
11632
|
+
* which polls for Circle's relayer to submit the mint transaction instead of
|
|
11633
|
+
* requiring the user to sign and submit it.
|
|
11119
11634
|
*
|
|
11120
|
-
* @param
|
|
11121
|
-
* @returns
|
|
11122
|
-
*/ function
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11635
|
+
* @param useForwarder - When true, uses relayer mint instead of user mint
|
|
11636
|
+
* @returns Array of step executor configurations
|
|
11637
|
+
*/ function createStepExecutors(useForwarder = false) {
|
|
11638
|
+
return [
|
|
11639
|
+
{
|
|
11640
|
+
name: 'approve',
|
|
11641
|
+
executor: async (params, provider)=>bridgeApproval({
|
|
11642
|
+
params,
|
|
11643
|
+
provider
|
|
11644
|
+
})
|
|
11645
|
+
},
|
|
11646
|
+
{
|
|
11647
|
+
name: 'burn',
|
|
11648
|
+
executor: async (params, provider)=>bridgeBurn({
|
|
11649
|
+
params,
|
|
11650
|
+
provider
|
|
11651
|
+
}),
|
|
11652
|
+
updateContext: (step)=>{
|
|
11653
|
+
if (!step.txHash) {
|
|
11654
|
+
throw new KitError({
|
|
11655
|
+
...InputError.VALIDATION_FAILED,
|
|
11656
|
+
recoverability: 'FATAL',
|
|
11657
|
+
message: 'Burn step completed but no transaction hash was returned'
|
|
11658
|
+
});
|
|
11659
|
+
}
|
|
11660
|
+
return {
|
|
11661
|
+
burnTxHash: step.txHash
|
|
11662
|
+
};
|
|
11663
|
+
}
|
|
11664
|
+
},
|
|
11665
|
+
{
|
|
11666
|
+
name: 'fetchAttestation',
|
|
11667
|
+
executor: async (params, provider, context)=>{
|
|
11668
|
+
if (!context?.burnTxHash) {
|
|
11669
|
+
throw new KitError({
|
|
11670
|
+
...InputError.VALIDATION_FAILED,
|
|
11671
|
+
recoverability: 'FATAL',
|
|
11672
|
+
message: 'Burn transaction hash not available for attestation'
|
|
11673
|
+
});
|
|
11674
|
+
}
|
|
11675
|
+
return bridgeFetchAttestation({
|
|
11676
|
+
params,
|
|
11677
|
+
provider
|
|
11678
|
+
}, context.burnTxHash);
|
|
11679
|
+
},
|
|
11680
|
+
updateContext: (step)=>{
|
|
11681
|
+
if (!step.data) {
|
|
11682
|
+
throw new KitError({
|
|
11683
|
+
...InputError.VALIDATION_FAILED,
|
|
11684
|
+
recoverability: 'FATAL',
|
|
11685
|
+
message: 'Attestation step completed but no data was returned'
|
|
11686
|
+
});
|
|
11687
|
+
}
|
|
11688
|
+
return {
|
|
11689
|
+
attestationData: step.data
|
|
11690
|
+
};
|
|
11691
|
+
}
|
|
11692
|
+
},
|
|
11693
|
+
{
|
|
11694
|
+
name: 'mint',
|
|
11695
|
+
// bridgeRelayerMint validates context.burnTxHash internally with a KitError
|
|
11696
|
+
// bridgeMint requires attestationData which is validated here
|
|
11697
|
+
executor: useForwarder ? async (params, provider, context)=>{
|
|
11698
|
+
if (!context) {
|
|
11699
|
+
throw new KitError({
|
|
11700
|
+
...InputError.VALIDATION_FAILED,
|
|
11701
|
+
recoverability: 'FATAL',
|
|
11702
|
+
message: 'Step context is required for relayer mint'
|
|
11703
|
+
});
|
|
11704
|
+
}
|
|
11705
|
+
return bridgeRelayerMint({
|
|
11706
|
+
params,
|
|
11707
|
+
provider
|
|
11708
|
+
}, context);
|
|
11709
|
+
} : async (params, provider, context)=>{
|
|
11710
|
+
if (!context?.attestationData) {
|
|
11711
|
+
throw new KitError({
|
|
11712
|
+
...InputError.VALIDATION_FAILED,
|
|
11713
|
+
recoverability: 'FATAL',
|
|
11714
|
+
message: 'Attestation data not available for minting'
|
|
11715
|
+
});
|
|
11716
|
+
}
|
|
11717
|
+
return bridgeMint({
|
|
11718
|
+
params,
|
|
11719
|
+
provider
|
|
11720
|
+
}, context.attestationData);
|
|
11721
|
+
}
|
|
11722
|
+
}
|
|
11723
|
+
];
|
|
11134
11724
|
}
|
|
11135
11725
|
/**
|
|
11136
|
-
*
|
|
11726
|
+
* Helper function to create error step and set result state to error.
|
|
11137
11727
|
*
|
|
11138
|
-
* @param
|
|
11139
|
-
* @
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
}).join(String.raw`\.`);
|
|
11150
|
-
// ** matches zero or more segments:
|
|
11151
|
-
// - If prefix is empty (**), match anything
|
|
11152
|
-
// - Otherwise, match prefix, then optionally (dot + more content)
|
|
11153
|
-
if (prefix === '') {
|
|
11154
|
-
return /^.*$/;
|
|
11155
|
-
}
|
|
11156
|
-
return new RegExp(String.raw`^${prefix}(?:\..*)?$`);
|
|
11157
|
-
}
|
|
11158
|
-
// No ** - just convert segments normally
|
|
11159
|
-
const escaped = segments.map((segment)=>{
|
|
11160
|
-
if (segment === '*') return '[^.]+';
|
|
11161
|
-
return segment.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
11162
|
-
}).join(String.raw`\.`);
|
|
11163
|
-
return new RegExp(String.raw`^${escaped}$`);
|
|
11728
|
+
* @param stepName - The name of the step that failed
|
|
11729
|
+
* @param error - The error that occurred
|
|
11730
|
+
* @param result - The bridge result object to update
|
|
11731
|
+
*/ function handleStepError(stepName, error, result) {
|
|
11732
|
+
result.state = 'error';
|
|
11733
|
+
result.steps.push({
|
|
11734
|
+
name: stepName,
|
|
11735
|
+
state: 'error',
|
|
11736
|
+
error,
|
|
11737
|
+
errorMessage: getErrorMessage(error)
|
|
11738
|
+
});
|
|
11164
11739
|
}
|
|
11740
|
+
|
|
11165
11741
|
/**
|
|
11166
|
-
*
|
|
11742
|
+
* Dispatch a bridge step event through the provider's action dispatcher.
|
|
11167
11743
|
*
|
|
11168
|
-
*
|
|
11169
|
-
*
|
|
11170
|
-
*
|
|
11744
|
+
* Constructs the appropriate action payload and dispatches it to any registered
|
|
11745
|
+
* event listeners. Handles type-safe dispatching for different step types.
|
|
11746
|
+
* When provided, traceId from the invocation context is included for end-to-end correlation.
|
|
11171
11747
|
*
|
|
11172
|
-
* @
|
|
11173
|
-
* -
|
|
11174
|
-
* -
|
|
11175
|
-
* -
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11748
|
+
* @param name - The step name (approve, burn, fetchAttestation, or mint).
|
|
11749
|
+
* @param step - The completed bridge step containing transaction details and explorerUrl.
|
|
11750
|
+
* @param provider - The CCTP v2 provider with action dispatcher.
|
|
11751
|
+
* @param invocation - Optional invocation context containing traceId for correlation.
|
|
11752
|
+
*
|
|
11753
|
+
* @example
|
|
11754
|
+
* ```typescript
|
|
11755
|
+
* const step: BridgeStep = {
|
|
11756
|
+
* name: 'burn',
|
|
11757
|
+
* state: 'success',
|
|
11758
|
+
* txHash: '0xabc...',
|
|
11759
|
+
* explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
|
|
11760
|
+
* data: { ... }
|
|
11761
|
+
* }
|
|
11762
|
+
* dispatchStepEvent('burn', step, provider, invocationContext)
|
|
11763
|
+
* ```
|
|
11764
|
+
*/ function dispatchStepEvent(name, step, provider, invocation) {
|
|
11765
|
+
if (!provider.actionDispatcher) {
|
|
11766
|
+
return;
|
|
11767
|
+
}
|
|
11768
|
+
// Extract traceId from invocation context if provided
|
|
11769
|
+
const traceId = invocation?.traceId;
|
|
11770
|
+
const actionValues = {
|
|
11771
|
+
protocol: 'cctp',
|
|
11772
|
+
version: 'v2',
|
|
11773
|
+
...traceId !== undefined && {
|
|
11774
|
+
traceId
|
|
11775
|
+
},
|
|
11776
|
+
values: step
|
|
11777
|
+
};
|
|
11778
|
+
switch(name){
|
|
11779
|
+
case 'approve':
|
|
11780
|
+
case 'burn':
|
|
11781
|
+
case 'mint':
|
|
11782
|
+
provider.actionDispatcher.dispatch(name, {
|
|
11783
|
+
...actionValues,
|
|
11784
|
+
method: name
|
|
11186
11785
|
});
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11786
|
+
break;
|
|
11787
|
+
case 'fetchAttestation':
|
|
11788
|
+
case 'reAttest':
|
|
11789
|
+
provider.actionDispatcher.dispatch(name, {
|
|
11790
|
+
...actionValues,
|
|
11791
|
+
method: name,
|
|
11792
|
+
values: step
|
|
11793
|
+
});
|
|
11794
|
+
break;
|
|
11194
11795
|
}
|
|
11195
11796
|
}
|
|
11797
|
+
|
|
11196
11798
|
/**
|
|
11197
|
-
*
|
|
11799
|
+
* Default clock implementation.
|
|
11198
11800
|
*
|
|
11199
|
-
* @
|
|
11200
|
-
|
|
11801
|
+
* @packageDocumentation
|
|
11802
|
+
*/ /**
|
|
11803
|
+
* Default clock implementation using `Date.now()`.
|
|
11804
|
+
*
|
|
11805
|
+
* @remarks
|
|
11806
|
+
* Use this in production code. For testing, inject a mock clock
|
|
11807
|
+
* that returns controlled timestamps.
|
|
11201
11808
|
*
|
|
11202
11809
|
* @example
|
|
11203
11810
|
* ```typescript
|
|
11204
|
-
* import {
|
|
11811
|
+
* import { defaultClock } from '@core/runtime'
|
|
11205
11812
|
*
|
|
11206
|
-
* const
|
|
11813
|
+
* const start = defaultClock.now()
|
|
11814
|
+
* // ... do work ...
|
|
11815
|
+
* const elapsed = defaultClock.since(start)
|
|
11816
|
+
* ```
|
|
11817
|
+
*/ const defaultClock = {
|
|
11818
|
+
now: ()=>Date.now(),
|
|
11819
|
+
since: (start)=>Date.now() - start
|
|
11820
|
+
};
|
|
11821
|
+
|
|
11822
|
+
// ============================================================================
|
|
11823
|
+
// Crypto Utilities (Internal)
|
|
11824
|
+
// ============================================================================
|
|
11825
|
+
/** @internal */ function hasGetRandomValues() {
|
|
11826
|
+
return typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function';
|
|
11827
|
+
}
|
|
11828
|
+
/**
|
|
11829
|
+
* Create a W3C/OpenTelemetry-compatible trace ID.
|
|
11207
11830
|
*
|
|
11208
|
-
*
|
|
11209
|
-
* const unsubscribe = bus.on('tx.*', (event) => {
|
|
11210
|
-
* console.log('Transaction event:', event.name)
|
|
11211
|
-
* })
|
|
11831
|
+
* @returns 32-character lowercase hex string (128-bit).
|
|
11212
11832
|
*
|
|
11213
|
-
*
|
|
11214
|
-
*
|
|
11833
|
+
* @remarks
|
|
11834
|
+
* **Standard function for generating `traceId` values.** Compatible with
|
|
11835
|
+
* OpenTelemetry, Jaeger, Zipkin, and AWS X-Ray.
|
|
11215
11836
|
*
|
|
11216
|
-
*
|
|
11217
|
-
*
|
|
11837
|
+
* @example
|
|
11838
|
+
* ```typescript
|
|
11839
|
+
* const traceId = createTraceId() // "a1b2c3d4e5f6789012345678abcdef00"
|
|
11218
11840
|
* ```
|
|
11219
|
-
*/ function
|
|
11220
|
-
const
|
|
11221
|
-
|
|
11222
|
-
|
|
11841
|
+
*/ function createTraceId() {
|
|
11842
|
+
const bytes = new Uint8Array(16);
|
|
11843
|
+
if (hasGetRandomValues()) {
|
|
11844
|
+
crypto.getRandomValues(bytes);
|
|
11845
|
+
} else {
|
|
11846
|
+
for(let i = 0; i < 16; i++){
|
|
11847
|
+
bytes[i] = Math.floor(Math.random() * 256) // NOSONAR:
|
|
11848
|
+
;
|
|
11849
|
+
}
|
|
11850
|
+
}
|
|
11851
|
+
return Array.from(bytes, (b)=>b.toString(16).padStart(2, '0')).join('');
|
|
11852
|
+
}
|
|
11853
|
+
|
|
11854
|
+
/**
|
|
11855
|
+
* Clean tags by removing keys with undefined values.
|
|
11856
|
+
*
|
|
11857
|
+
* @param tags - Tags object, may contain undefined values. Accepts `undefined` or `null`.
|
|
11858
|
+
* @returns A new object with undefined values removed, or empty object if input is nullish.
|
|
11859
|
+
*
|
|
11860
|
+
* @remarks
|
|
11861
|
+
* This helper ensures tags are safe for serialization and logging.
|
|
11862
|
+
* Similar to the internal `cleanUndefined` in the logger module, but
|
|
11863
|
+
* typed specifically for {@link Tags}.
|
|
11864
|
+
*
|
|
11865
|
+
* @example
|
|
11866
|
+
* ```typescript
|
|
11867
|
+
* import { cleanTags } from '@core/runtime'
|
|
11868
|
+
*
|
|
11869
|
+
* const tags = { chain: 'Ethereum', amount: 100, extra: undefined }
|
|
11870
|
+
* const cleaned = cleanTags(tags)
|
|
11871
|
+
* // { chain: 'Ethereum', amount: 100 }
|
|
11872
|
+
*
|
|
11873
|
+
* const empty = cleanTags(undefined)
|
|
11874
|
+
* // {}
|
|
11875
|
+
* ```
|
|
11876
|
+
*/ function cleanTags(tags) {
|
|
11877
|
+
if (tags == null) return {};
|
|
11878
|
+
return Object.fromEntries(Object.entries(tags).filter(([, value])=>value !== undefined));
|
|
11879
|
+
}
|
|
11880
|
+
|
|
11881
|
+
/**
|
|
11882
|
+
* Check if a pattern is valid.
|
|
11883
|
+
*
|
|
11884
|
+
* @remarks
|
|
11885
|
+
* Invalid patterns:
|
|
11886
|
+
* - Empty segments (e.g., `a..b`, `.a`, `a.`)
|
|
11887
|
+
* - `**` not at end (e.g., `a.**.b`)
|
|
11888
|
+
*
|
|
11889
|
+
* @param pattern - The pattern to validate.
|
|
11890
|
+
* @returns True if valid, false otherwise.
|
|
11891
|
+
*/ function isValidPattern(pattern) {
|
|
11892
|
+
// Empty pattern is valid (matches empty topic)
|
|
11893
|
+
if (pattern === '') return true;
|
|
11894
|
+
const segments = pattern.split('.');
|
|
11895
|
+
// Check for empty segments
|
|
11896
|
+
if (segments.includes('')) return false;
|
|
11897
|
+
// Check that ** only appears at the end
|
|
11898
|
+
const doubleStarIndex = segments.indexOf('**');
|
|
11899
|
+
if (doubleStarIndex !== -1 && doubleStarIndex !== segments.length - 1) {
|
|
11900
|
+
return false;
|
|
11901
|
+
}
|
|
11902
|
+
return true;
|
|
11903
|
+
}
|
|
11904
|
+
/**
|
|
11905
|
+
* Convert a pattern to a regular expression.
|
|
11906
|
+
*
|
|
11907
|
+
* @param pattern - The pattern to convert.
|
|
11908
|
+
* @returns A RegExp that matches topics according to the pattern.
|
|
11909
|
+
*/ function patternToRegex(pattern) {
|
|
11910
|
+
const segments = pattern.split('.');
|
|
11911
|
+
// Handle ** at end specially to match zero or more segments
|
|
11912
|
+
const lastSegment = segments.at(-1);
|
|
11913
|
+
if (lastSegment === '**') {
|
|
11914
|
+
// Everything before ** must match, then optionally more segments
|
|
11915
|
+
const prefix = segments.slice(0, -1).map((seg)=>{
|
|
11916
|
+
if (seg === '*') return '[^.]+';
|
|
11917
|
+
return seg.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
11918
|
+
}).join(String.raw`\.`);
|
|
11919
|
+
// ** matches zero or more segments:
|
|
11920
|
+
// - If prefix is empty (**), match anything
|
|
11921
|
+
// - Otherwise, match prefix, then optionally (dot + more content)
|
|
11922
|
+
if (prefix === '') {
|
|
11923
|
+
return /^.*$/;
|
|
11924
|
+
}
|
|
11925
|
+
return new RegExp(String.raw`^${prefix}(?:\..*)?$`);
|
|
11926
|
+
}
|
|
11927
|
+
// No ** - just convert segments normally
|
|
11928
|
+
const escaped = segments.map((segment)=>{
|
|
11929
|
+
if (segment === '*') return '[^.]+';
|
|
11930
|
+
return segment.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
11931
|
+
}).join(String.raw`\.`);
|
|
11932
|
+
return new RegExp(String.raw`^${escaped}$`);
|
|
11933
|
+
}
|
|
11934
|
+
/**
|
|
11935
|
+
* Execute an event handler with error isolation.
|
|
11936
|
+
*
|
|
11937
|
+
* @param handler - The handler function to execute.
|
|
11938
|
+
* @param event - The event to pass to the handler.
|
|
11939
|
+
* @param logger - Optional logger for error reporting.
|
|
11940
|
+
*
|
|
11941
|
+
* @remarks
|
|
11942
|
+
* - Synchronous errors are caught and logged
|
|
11943
|
+
* - Promise rejections are caught without awaiting
|
|
11944
|
+
* - Handler errors never propagate to caller
|
|
11945
|
+
*/ function executeHandler(handler, event, logger) {
|
|
11946
|
+
try {
|
|
11947
|
+
const result = handler(event);
|
|
11948
|
+
// Handle promise rejection without awaiting
|
|
11949
|
+
if (result instanceof Promise) {
|
|
11950
|
+
result.catch((err)=>{
|
|
11951
|
+
logger?.error('Event handler promise rejected', {
|
|
11952
|
+
event: event.name,
|
|
11953
|
+
error: extractErrorInfo(err)
|
|
11954
|
+
});
|
|
11955
|
+
});
|
|
11956
|
+
}
|
|
11957
|
+
} catch (err) {
|
|
11958
|
+
// Isolate handler errors
|
|
11959
|
+
logger?.error('Event handler threw', {
|
|
11960
|
+
event: event.name,
|
|
11961
|
+
error: extractErrorInfo(err)
|
|
11962
|
+
});
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
/**
|
|
11966
|
+
* Create an event bus.
|
|
11967
|
+
*
|
|
11968
|
+
* @param opts - Options for the event bus.
|
|
11969
|
+
* @returns An EventBus instance.
|
|
11970
|
+
*
|
|
11971
|
+
* @example
|
|
11972
|
+
* ```typescript
|
|
11973
|
+
* import { createEventBus } from '@core/runtime'
|
|
11974
|
+
*
|
|
11975
|
+
* const bus = createEventBus({ logger })
|
|
11976
|
+
*
|
|
11977
|
+
* // Subscribe to events
|
|
11978
|
+
* const unsubscribe = bus.on('tx.*', (event) => {
|
|
11979
|
+
* console.log('Transaction event:', event.name)
|
|
11980
|
+
* })
|
|
11981
|
+
*
|
|
11982
|
+
* // Emit events
|
|
11983
|
+
* bus.emit({ name: 'tx.sent', data: { txHash: '0x123' } })
|
|
11984
|
+
*
|
|
11985
|
+
* // Unsubscribe
|
|
11986
|
+
* unsubscribe()
|
|
11987
|
+
* ```
|
|
11988
|
+
*/ function createEventBus(opts) {
|
|
11989
|
+
const logger = opts?.logger;
|
|
11990
|
+
const subscriptions = new Set();
|
|
11991
|
+
function createBus(baseTags) {
|
|
11223
11992
|
return {
|
|
11224
11993
|
emit (event) {
|
|
11225
11994
|
// Invalid topic names silently don't match any subscriptions
|
|
@@ -11999,10 +12768,15 @@ const mockAttestationMessage = {
|
|
|
11999
12768
|
const burnCallData = burnRequest.getCallData();
|
|
12000
12769
|
// batchExecute may throw before submission (wallet declined) but never
|
|
12001
12770
|
// after — post-submission errors are returned as empty receipts.
|
|
12771
|
+
// The sender is threaded for adapters whose execution is routed through a
|
|
12772
|
+
// signing strategy (which has no wallet account to read it from); the
|
|
12773
|
+
// wallet-client path ignores it.
|
|
12002
12774
|
const batchResult = await adapter.batchExecute([
|
|
12003
12775
|
approveCallData,
|
|
12004
12776
|
burnCallData
|
|
12005
|
-
], chain
|
|
12777
|
+
], chain, {
|
|
12778
|
+
fromAddress: params.source.address
|
|
12779
|
+
});
|
|
12006
12780
|
const approveReceipt = batchResult.receipts[0];
|
|
12007
12781
|
const burnReceipt = batchResult.receipts[1];
|
|
12008
12782
|
const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
|
|
@@ -12141,595 +12915,275 @@ const mockAttestationMessage = {
|
|
|
12141
12915
|
step.error = err;
|
|
12142
12916
|
step.errorMessage = err instanceof Error ? err.message : 'Unknown error during confirmation.';
|
|
12143
12917
|
step.errorCategory = 'unknown';
|
|
12144
|
-
}
|
|
12145
|
-
return step;
|
|
12146
|
-
}
|
|
12147
|
-
|
|
12148
|
-
var version = "1.8.5";
|
|
12149
|
-
var pkg = {
|
|
12150
|
-
version: version};
|
|
12151
|
-
|
|
12152
|
-
/**
|
|
12153
|
-
* Provider caller component for bridge operations.
|
|
12154
|
-
*/ const BRIDGE_CALLER = {
|
|
12155
|
-
type: 'provider',
|
|
12156
|
-
name: 'CCTPV2BridgingProvider.bridge',
|
|
12157
|
-
version: pkg.version
|
|
12158
|
-
};
|
|
12159
|
-
/**
|
|
12160
|
-
* Resolve invocation context for bridge operations.
|
|
12161
|
-
*
|
|
12162
|
-
* Creates or extends invocation context with the bridge caller.
|
|
12163
|
-
*
|
|
12164
|
-
* @param invocationMeta - Optional invocation metadata provided by caller.
|
|
12165
|
-
* @returns Resolved invocation context with bridge caller.
|
|
12166
|
-
*
|
|
12167
|
-
* @internal
|
|
12168
|
-
*/ function resolveBridgeInvocation(invocationMeta) {
|
|
12169
|
-
const defaults = {
|
|
12170
|
-
runtime: createRuntime(),
|
|
12171
|
-
tokens: createTokenRegistry()
|
|
12172
|
-
};
|
|
12173
|
-
return extendInvocationContext(resolveInvocationContext(invocationMeta, defaults), BRIDGE_CALLER);
|
|
12174
|
-
}
|
|
12175
|
-
/**
|
|
12176
|
-
* Execute the batched approve + burn path via EIP-5792.
|
|
12177
|
-
*
|
|
12178
|
-
* Mutate `result` with step data and error state as needed. Return the
|
|
12179
|
-
* batch context on success, or `undefined` when the batch failed (in
|
|
12180
|
-
* which case `result.state` is set to `'error'`).
|
|
12181
|
-
*
|
|
12182
|
-
* @internal
|
|
12183
|
-
* @param params - Bridge parameters (read-only).
|
|
12184
|
-
* @param provider - The CCTP v2 bridging provider (read-only).
|
|
12185
|
-
* @param result - Bridge result object — **mutated in place** with step
|
|
12186
|
-
* data and, on failure, `state: 'error'` plus an `error` payload.
|
|
12187
|
-
* @param invocation - Invocation context for telemetry (read-only).
|
|
12188
|
-
* @returns The step context on success, or `undefined` when the batch failed.
|
|
12189
|
-
*/ async function executeBatchedPath(params, provider, result, invocation) {
|
|
12190
|
-
// IMPORTANT: once executeBatchedApproveAndBurn is called, we NEVER
|
|
12191
|
-
// fall back to sequential. The wallet may have already signed &
|
|
12192
|
-
// submitted the batch; retrying as individual txs would double-spend.
|
|
12193
|
-
try {
|
|
12194
|
-
const { approveStep, burnStep, context: batchContext } = await executeBatchedApproveAndBurn(params, provider);
|
|
12195
|
-
for (const step of [
|
|
12196
|
-
approveStep,
|
|
12197
|
-
burnStep
|
|
12198
|
-
]){
|
|
12199
|
-
const stepName = step.name;
|
|
12200
|
-
if (step.state === 'error') {
|
|
12201
|
-
ensureStepErrorMessage(step.name, step);
|
|
12202
|
-
result.steps.push(step);
|
|
12203
|
-
result.state = 'error';
|
|
12204
|
-
dispatchStepEvent(stepName, step, provider, invocation);
|
|
12205
|
-
return undefined;
|
|
12206
|
-
}
|
|
12207
|
-
dispatchStepEvent(stepName, step, provider, invocation);
|
|
12208
|
-
result.steps.push(step);
|
|
12209
|
-
}
|
|
12210
|
-
return batchContext;
|
|
12211
|
-
} catch (error_) {
|
|
12212
|
-
// Only handles pre-submission failures (prepare rejected, wallet
|
|
12213
|
-
// declined, etc.). batchExecute never throws after sendCalls succeeds.
|
|
12214
|
-
result.state = 'error';
|
|
12215
|
-
result.steps.push({
|
|
12216
|
-
name: 'batch',
|
|
12217
|
-
state: 'error',
|
|
12218
|
-
batched: true,
|
|
12219
|
-
error: error_,
|
|
12220
|
-
errorMessage: error_ instanceof Error ? error_.message : 'Batched approve + burn failed.',
|
|
12221
|
-
errorCategory: classifyPreSubmissionError(error_)
|
|
12222
|
-
});
|
|
12223
|
-
return undefined;
|
|
12224
|
-
}
|
|
12225
|
-
}
|
|
12226
|
-
/**
|
|
12227
|
-
* Ensure `step.errorMessage` is populated when an error object exists.
|
|
12228
|
-
*
|
|
12229
|
-
* @internal
|
|
12230
|
-
*/ function ensureStepErrorMessage(name, step) {
|
|
12231
|
-
if (!step.errorMessage && step.error) {
|
|
12232
|
-
step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
|
|
12233
|
-
}
|
|
12234
|
-
}
|
|
12235
|
-
/**
|
|
12236
|
-
* Coerce a raw JSON-RPC `code` to a number.
|
|
12237
|
-
*
|
|
12238
|
-
* Some wallet SDKs serialize the JSON-RPC `code` as a string ("4001")
|
|
12239
|
-
* after round-tripping through JSON; accept both shapes so strict `===`
|
|
12240
|
-
* comparisons downstream still classify 5720/5730/5740 correctly — those
|
|
12241
|
-
* codes have no message-pattern fallback.
|
|
12242
|
-
*
|
|
12243
|
-
* @param rawCode - The raw `code` extracted from the error object.
|
|
12244
|
-
* @returns The numeric code, or `undefined` if the value cannot be parsed.
|
|
12245
|
-
*
|
|
12246
|
-
* @internal
|
|
12247
|
-
*/ function coerceRpcCode(rawCode) {
|
|
12248
|
-
if (typeof rawCode === 'number') {
|
|
12249
|
-
return rawCode;
|
|
12250
|
-
}
|
|
12251
|
-
if (typeof rawCode === 'string') {
|
|
12252
|
-
return Number.parseInt(rawCode, 10);
|
|
12253
|
-
}
|
|
12254
|
-
return undefined;
|
|
12255
|
-
}
|
|
12256
|
-
/**
|
|
12257
|
-
* Classify a pre-submission error thrown during `wallet_sendCalls`.
|
|
12258
|
-
*
|
|
12259
|
-
* Inspect the error's JSON-RPC `code` (falling back to message pattern
|
|
12260
|
-
* matching for wrapper errors like viem's `ChainMismatchError`) and map
|
|
12261
|
-
* it to a {@link BridgeStepErrorCategory}. This lets downstream consumers
|
|
12262
|
-
* distinguish user rejections, wallet capability gaps, and unknown
|
|
12263
|
-
* failures without parsing error messages.
|
|
12264
|
-
*
|
|
12265
|
-
* @remarks
|
|
12266
|
-
* Does NOT alter control flow — the SDK continues to surface a
|
|
12267
|
-
* `state: 'error'` step. Auto-fallback to sequential execution is
|
|
12268
|
-
* intentionally out of scope for this helper.
|
|
12269
|
-
*
|
|
12270
|
-
* @param err - The error thrown by `wallet_sendCalls`.
|
|
12271
|
-
* @returns The derived error category, or `'unknown'` if no match.
|
|
12272
|
-
*
|
|
12273
|
-
* @internal
|
|
12274
|
-
*/ function classifyPreSubmissionError(err) {
|
|
12275
|
-
// Cross-realm-safe duck typing: `instanceof Error` returns false for
|
|
12276
|
-
// errors thrown in a different JavaScript realm (e.g., a wallet
|
|
12277
|
-
// provider running inside an iframe, which is common with WalletConnect
|
|
12278
|
-
// and the Coinbase Wallet SDK).
|
|
12279
|
-
if (typeof err !== 'object' || err === null || !('message' in err)) {
|
|
12280
|
-
return 'unknown';
|
|
12281
|
-
}
|
|
12282
|
-
const code = coerceRpcCode(err.code);
|
|
12283
|
-
const message = String(err.message);
|
|
12284
|
-
// Numeric JSON-RPC codes are authoritative; check them before falling
|
|
12285
|
-
// back to message-pattern matching. Order matters: an error carrying
|
|
12286
|
-
// `code === 5750` with a message like "user rejected the upgrade"
|
|
12287
|
-
// is a capability problem, not a plain user rejection.
|
|
12288
|
-
if (code === 4001) {
|
|
12289
|
-
return 'user_rejected';
|
|
12290
|
-
}
|
|
12291
|
-
if (code === 5700 || code === 5710 || code === 5750) {
|
|
12292
|
-
return 'atomic_unsupported';
|
|
12293
|
-
}
|
|
12294
|
-
if (code === 5720) {
|
|
12295
|
-
return 'duplicate_batch_id';
|
|
12296
|
-
}
|
|
12297
|
-
if (code === 5730) {
|
|
12298
|
-
return 'unknown_bundle';
|
|
12299
|
-
}
|
|
12300
|
-
if (code === 5740) {
|
|
12301
|
-
return 'batch_too_large';
|
|
12302
|
-
}
|
|
12303
|
-
// Fall back to message patterns when no specific code is available —
|
|
12304
|
-
// viem (and other wrapper layers) sometimes strip the numeric code
|
|
12305
|
-
// while preserving the original wallet message in `Details:`.
|
|
12306
|
-
if (/EIP-7702 not supported/i.test(message) || /does not support the requested chain/i.test(message) || /rejected the upgrade/i.test(message)) {
|
|
12307
|
-
return 'atomic_unsupported';
|
|
12308
|
-
}
|
|
12309
|
-
if (/user rejected/i.test(message)) {
|
|
12310
|
-
return 'user_rejected';
|
|
12311
|
-
}
|
|
12312
|
-
return 'unknown';
|
|
12313
|
-
}
|
|
12314
|
-
/**
|
|
12315
|
-
* Execute a cross-chain USDC bridge using the CCTP v2 protocol.
|
|
12316
|
-
*
|
|
12317
|
-
* @remarks
|
|
12318
|
-
* This function performs the full CCTP v2 bridge flow: token approval, burning on the source chain,
|
|
12319
|
-
* attestation fetching, and minting on the destination chain. Each step is validated and dispatched
|
|
12320
|
-
* through the provider's action dispatcher. If any step fails, the bridge result will have state 'error'.
|
|
12321
|
-
* Each step includes transaction details and explorer URLs for easy verification on block explorers.
|
|
12322
|
-
*
|
|
12323
|
-
* @param params - The bridge parameters containing source, destination, amount, and optional config.
|
|
12324
|
-
* @param provider - The CCTP v2 bridging provider instance.
|
|
12325
|
-
* @returns A promise resolving to the bridge result, including transaction details, step states, and explorer URLs.
|
|
12326
|
-
*
|
|
12327
|
-
* @throws Will propagate errors thrown by underlying step functions (approval, burn, attestation, mint) if they throw unexpectedly.
|
|
12328
|
-
*
|
|
12329
|
-
* @example
|
|
12330
|
-
* ```typescript
|
|
12331
|
-
* import { bridge } from '@circle-fin/provider-cctp-v2/provider/bridge/bridge'
|
|
12332
|
-
* // ...setup params and provider...
|
|
12333
|
-
* const result = await bridge(params, provider)
|
|
12334
|
-
* if (result.state === 'success') {
|
|
12335
|
-
* console.log('Bridge completed!')
|
|
12336
|
-
* result.steps.forEach(step => {
|
|
12337
|
-
* if (step.explorerUrl) {
|
|
12338
|
-
* console.log(`${step.name}: ${step.explorerUrl}`)
|
|
12339
|
-
* }
|
|
12340
|
-
* })
|
|
12341
|
-
* } else {
|
|
12342
|
-
* console.error('Bridge failed at step:', result.steps.find(s => s.state === 'error'))
|
|
12343
|
-
* }
|
|
12344
|
-
* ```
|
|
12345
|
-
*/ async function bridge(params, provider) {
|
|
12346
|
-
const useForwarder = params.destination.useForwarder === true;
|
|
12347
|
-
const invocation = resolveBridgeInvocation(params.invocationMeta);
|
|
12348
|
-
const result = {
|
|
12349
|
-
state: 'pending',
|
|
12350
|
-
amount: params.amount,
|
|
12351
|
-
token: params.token,
|
|
12352
|
-
source: {
|
|
12353
|
-
address: params.source.address,
|
|
12354
|
-
chain: params.source.chain
|
|
12355
|
-
},
|
|
12356
|
-
destination: {
|
|
12357
|
-
address: params.destination.address,
|
|
12358
|
-
chain: params.destination.chain,
|
|
12359
|
-
...params.destination.recipientAddress && {
|
|
12360
|
-
recipientAddress: params.destination.recipientAddress
|
|
12361
|
-
},
|
|
12362
|
-
...useForwarder && {
|
|
12363
|
-
useForwarder: true
|
|
12364
|
-
}
|
|
12365
|
-
},
|
|
12366
|
-
steps: [],
|
|
12367
|
-
config: params.config,
|
|
12368
|
-
provider: provider.name
|
|
12369
|
-
};
|
|
12370
|
-
let context = undefined;
|
|
12371
|
-
let useBatched = false;
|
|
12372
|
-
try {
|
|
12373
|
-
useBatched = await shouldUseBatchedExecution(params);
|
|
12374
|
-
} catch {
|
|
12375
|
-
// Silently fall back to sequential
|
|
12376
|
-
}
|
|
12377
|
-
const executors = createStepExecutors(useForwarder);
|
|
12378
|
-
if (useBatched) {
|
|
12379
|
-
const batchContext = await executeBatchedPath(params, provider, result, invocation);
|
|
12380
|
-
if (result.state === 'error') {
|
|
12381
|
-
return result;
|
|
12382
|
-
}
|
|
12383
|
-
context = batchContext;
|
|
12384
|
-
}
|
|
12385
|
-
const stepsToRun = useBatched ? executors.filter(({ name })=>name !== 'approve' && name !== 'burn') : executors;
|
|
12386
|
-
for (const { name, executor, updateContext } of stepsToRun){
|
|
12387
|
-
try {
|
|
12388
|
-
const step = await executor(params, provider, context);
|
|
12389
|
-
if (step.state === 'error') {
|
|
12390
|
-
ensureStepErrorMessage(name, step);
|
|
12391
|
-
result.steps.push(step);
|
|
12392
|
-
result.state = 'error';
|
|
12393
|
-
dispatchStepEvent(name, step, provider, invocation);
|
|
12394
|
-
return result;
|
|
12395
|
-
}
|
|
12396
|
-
const newContext = updateContext?.(step);
|
|
12397
|
-
if (newContext) {
|
|
12398
|
-
context = {
|
|
12399
|
-
...context,
|
|
12400
|
-
...newContext
|
|
12401
|
-
};
|
|
12402
|
-
}
|
|
12403
|
-
dispatchStepEvent(name, step, provider, invocation);
|
|
12404
|
-
result.steps.push(step);
|
|
12405
|
-
} catch (error) {
|
|
12406
|
-
handleStepError(name, error, result);
|
|
12407
|
-
return result;
|
|
12408
|
-
}
|
|
12409
|
-
}
|
|
12410
|
-
result.state = 'success';
|
|
12411
|
-
return result;
|
|
12412
|
-
}
|
|
12413
|
-
|
|
12414
|
-
/**
|
|
12415
|
-
* Resolves an operation context into concrete chain and address values.
|
|
12416
|
-
*
|
|
12417
|
-
* This function ensures that action handlers always receive defined chain and address
|
|
12418
|
-
* values by applying validation and resolution logic based on the adapter's capabilities.
|
|
12419
|
-
* It enforces compile-time and runtime address requirements based on the adapter's
|
|
12420
|
-
* address control model.
|
|
12421
|
-
*
|
|
12422
|
-
* **Resolution logic**:
|
|
12423
|
-
* - **Chain**: Uses provided chain identifier and resolves to ChainDefinition
|
|
12424
|
-
* - **Address**:
|
|
12425
|
-
* - For user-controlled adapters: Retrieves current address from adapter (context address ignored)
|
|
12426
|
-
* - For developer-controlled adapters: Uses address provided in context (required)
|
|
12427
|
-
*
|
|
12428
|
-
* @typeParam TAdapterCapabilities - The adapter capabilities type for compile-time validation
|
|
12429
|
-
* @param adapter - The typed adapter instance with capabilities defined
|
|
12430
|
-
* @param ctx - Operation context with compile-time validated address requirements
|
|
12431
|
-
* @returns Promise resolving to concrete chain and address values
|
|
12432
|
-
* @throws Error when adapter capabilities are not defined
|
|
12433
|
-
* @throws Error when operation context is not provided
|
|
12434
|
-
* @throws Error when address is required but not provided (developer-controlled)
|
|
12435
|
-
* @throws Error when adapter.getAddress() fails (user-controlled)
|
|
12436
|
-
*
|
|
12437
|
-
* @example
|
|
12438
|
-
* ```typescript
|
|
12439
|
-
* import { resolveOperationContext } from '@core/adapter'
|
|
12440
|
-
*
|
|
12441
|
-
* // User-controlled adapter - address forbidden in context
|
|
12442
|
-
* const userAdapter: Adapter<{ addressContext: 'user-controlled', supportedChains: [] }>
|
|
12443
|
-
* const resolved = await resolveOperationContext(userAdapter, {
|
|
12444
|
-
* chain: 'Base' // address will be resolved from wallet
|
|
12445
|
-
* })
|
|
12446
|
-
* console.log(resolved.chain) // ChainDefinition - always defined
|
|
12447
|
-
* console.log(resolved.address) // string - resolved from adapter
|
|
12448
|
-
*
|
|
12449
|
-
* // Developer-controlled adapter - address required in context
|
|
12450
|
-
* const devAdapter: Adapter<{ addressContext: 'developer-controlled', supportedChains: [] }>
|
|
12451
|
-
* const resolved = await resolveOperationContext(devAdapter, {
|
|
12452
|
-
* chain: 'Base',
|
|
12453
|
-
* address: '0x123...' // Required and enforced at compile time
|
|
12454
|
-
* })
|
|
12455
|
-
* console.log(resolved.address) // '0x123...' - from context
|
|
12456
|
-
* ```
|
|
12457
|
-
*/ async function resolveOperationContext(adapter, ctx) {
|
|
12458
|
-
// Adapter must have capabilities defined
|
|
12459
|
-
if (adapter.capabilities === undefined) {
|
|
12460
|
-
throw new Error('Adapter capabilities must be defined. Please ensure the adapter implements the capabilities property.');
|
|
12461
|
-
}
|
|
12462
|
-
// Operation context is required for new typed adapters
|
|
12463
|
-
if (ctx === undefined) {
|
|
12464
|
-
throw new Error('Operation context is required. Please provide a context with the required chain and address information.');
|
|
12465
|
-
}
|
|
12466
|
-
// Resolve chain from context (required)
|
|
12467
|
-
const resolvedChain = resolveChainIdentifier(ctx.chain);
|
|
12468
|
-
// Resolve address based on adapter capabilities
|
|
12469
|
-
let resolvedAddress;
|
|
12470
|
-
if (adapter.capabilities.addressContext === 'developer-controlled') {
|
|
12471
|
-
// Developer-controlled: address must be explicitly provided
|
|
12472
|
-
if (!ctx.address) {
|
|
12473
|
-
throw new Error('Address is required for developer-controlled adapters. Please provide an address in the operation context.');
|
|
12474
|
-
}
|
|
12475
|
-
resolvedAddress = ctx.address;
|
|
12476
|
-
} else {
|
|
12477
|
-
// User-controlled: get current address from adapter
|
|
12478
|
-
try {
|
|
12479
|
-
// Pass resolved chain to getAddress for adapters that support it (like ViemAdapter)
|
|
12480
|
-
// The chain parameter is optional in implementations, so this is safe
|
|
12481
|
-
resolvedAddress = await adapter.getAddress(resolvedChain);
|
|
12482
|
-
} catch (error) {
|
|
12483
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
12484
|
-
throw new Error(`Failed to resolve address from user-controlled adapter: ${message}`);
|
|
12485
|
-
}
|
|
12486
|
-
}
|
|
12487
|
-
return {
|
|
12488
|
-
chain: resolvedChain,
|
|
12489
|
-
address: resolvedAddress
|
|
12490
|
-
};
|
|
12918
|
+
}
|
|
12919
|
+
return step;
|
|
12491
12920
|
}
|
|
12492
12921
|
|
|
12922
|
+
var version = "1.10.0";
|
|
12923
|
+
var pkg = {
|
|
12924
|
+
version: version};
|
|
12925
|
+
|
|
12493
12926
|
/**
|
|
12494
|
-
*
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
* - Contains only valid hexadecimal characters (0-9, a-f, A-F) after '0x'
|
|
12501
|
-
*
|
|
12502
|
-
* @remarks
|
|
12503
|
-
* This schema does not validate length, making it suitable for various hex string types
|
|
12504
|
-
* like addresses, transaction hashes, and other hex-encoded data.
|
|
12505
|
-
*
|
|
12506
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
12507
|
-
*
|
|
12508
|
-
* @example
|
|
12509
|
-
* ```typescript
|
|
12510
|
-
* import { hexStringSchema } from '@core/adapter'
|
|
12511
|
-
*
|
|
12512
|
-
* const validAddress = '0x1234567890123456789012345678901234567890'
|
|
12513
|
-
* const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
12514
|
-
*
|
|
12515
|
-
* const addressResult = hexStringSchema.safeParse(validAddress)
|
|
12516
|
-
* const txHashResult = hexStringSchema.safeParse(validTxHash)
|
|
12517
|
-
* console.log(addressResult.success) // true
|
|
12518
|
-
* console.log(txHashResult.success) // true
|
|
12519
|
-
* ```
|
|
12520
|
-
*/ const hexStringSchema = z.string().min(1, 'Hex string is required').refine((value)=>value.trim().length > 0, 'Hex string cannot be empty').refine((value)=>value.startsWith('0x'), 'Hex string must start with 0x prefix').refine((value)=>{
|
|
12521
|
-
const hexPattern = /^0x[0-9a-fA-F]+$/;
|
|
12522
|
-
return hexPattern.test(value);
|
|
12523
|
-
}, 'Hex string contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x');
|
|
12927
|
+
* Provider caller component for bridge operations.
|
|
12928
|
+
*/ const BRIDGE_CALLER = {
|
|
12929
|
+
type: 'provider',
|
|
12930
|
+
name: 'CCTPV2BridgingProvider.bridge',
|
|
12931
|
+
version: pkg.version
|
|
12932
|
+
};
|
|
12524
12933
|
/**
|
|
12525
|
-
*
|
|
12526
|
-
*
|
|
12527
|
-
* This schema validates that a string is a properly formatted EVM address:
|
|
12528
|
-
* - Must be a valid hex string with '0x' prefix
|
|
12529
|
-
* - Must be exactly 42 characters long (0x + 40 hex characters)
|
|
12530
|
-
*
|
|
12531
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
12934
|
+
* Resolve invocation context for bridge operations.
|
|
12532
12935
|
*
|
|
12533
|
-
*
|
|
12534
|
-
* ```typescript
|
|
12535
|
-
* import { evmAddressSchema } from '@core/adapter'
|
|
12936
|
+
* Creates or extends invocation context with the bridge caller.
|
|
12536
12937
|
*
|
|
12537
|
-
*
|
|
12938
|
+
* @param invocationMeta - Optional invocation metadata provided by caller.
|
|
12939
|
+
* @returns Resolved invocation context with bridge caller.
|
|
12538
12940
|
*
|
|
12539
|
-
*
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12941
|
+
* @internal
|
|
12942
|
+
*/ function resolveBridgeInvocation(invocationMeta) {
|
|
12943
|
+
const defaults = {
|
|
12944
|
+
runtime: createRuntime(),
|
|
12945
|
+
tokens: createTokenRegistry()
|
|
12946
|
+
};
|
|
12947
|
+
return extendInvocationContext(resolveInvocationContext(invocationMeta, defaults), BRIDGE_CALLER);
|
|
12948
|
+
}
|
|
12543
12949
|
/**
|
|
12544
|
-
*
|
|
12545
|
-
*
|
|
12546
|
-
* This schema validates that a string is a properly formatted transaction hash:
|
|
12547
|
-
* - Must be a valid hex string with '0x' prefix
|
|
12548
|
-
* - Must be exactly 66 characters long (0x + 64 hex characters)
|
|
12549
|
-
*
|
|
12550
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
12551
|
-
*
|
|
12552
|
-
* @example
|
|
12553
|
-
* ```typescript
|
|
12554
|
-
* import { evmTransactionHashSchema } from '@core/adapter'
|
|
12950
|
+
* Execute the batched approve + burn path via EIP-5792.
|
|
12555
12951
|
*
|
|
12556
|
-
*
|
|
12952
|
+
* Mutate `result` with step data and error state as needed. Return the
|
|
12953
|
+
* batch context on success, or `undefined` when the batch failed (in
|
|
12954
|
+
* which case `result.state` is set to `'error'`).
|
|
12557
12955
|
*
|
|
12558
|
-
*
|
|
12559
|
-
*
|
|
12560
|
-
*
|
|
12561
|
-
|
|
12956
|
+
* @internal
|
|
12957
|
+
* @param params - Bridge parameters (read-only).
|
|
12958
|
+
* @param provider - The CCTP v2 bridging provider (read-only).
|
|
12959
|
+
* @param result - Bridge result object — **mutated in place** with step
|
|
12960
|
+
* data and, on failure, `state: 'error'` plus an `error` payload.
|
|
12961
|
+
* @param invocation - Invocation context for telemetry (read-only).
|
|
12962
|
+
* @returns The step context on success, or `undefined` when the batch failed.
|
|
12963
|
+
*/ async function executeBatchedPath(params, provider, result, invocation) {
|
|
12964
|
+
// IMPORTANT: once executeBatchedApproveAndBurn is called, we NEVER
|
|
12965
|
+
// fall back to sequential. The wallet may have already signed &
|
|
12966
|
+
// submitted the batch; retrying as individual txs would double-spend.
|
|
12967
|
+
try {
|
|
12968
|
+
const { approveStep, burnStep, context: batchContext } = await executeBatchedApproveAndBurn(params, provider);
|
|
12969
|
+
for (const step of [
|
|
12970
|
+
approveStep,
|
|
12971
|
+
burnStep
|
|
12972
|
+
]){
|
|
12973
|
+
const stepName = step.name;
|
|
12974
|
+
if (step.state === 'error') {
|
|
12975
|
+
ensureStepErrorMessage(step.name, step);
|
|
12976
|
+
result.steps.push(step);
|
|
12977
|
+
result.state = 'error';
|
|
12978
|
+
dispatchStepEvent(stepName, step, provider, invocation);
|
|
12979
|
+
return undefined;
|
|
12980
|
+
}
|
|
12981
|
+
dispatchStepEvent(stepName, step, provider, invocation);
|
|
12982
|
+
result.steps.push(step);
|
|
12983
|
+
}
|
|
12984
|
+
return batchContext;
|
|
12985
|
+
} catch (error_) {
|
|
12986
|
+
// Only handles pre-submission failures (prepare rejected, wallet
|
|
12987
|
+
// declined, etc.). batchExecute never throws after sendCalls succeeds.
|
|
12988
|
+
result.state = 'error';
|
|
12989
|
+
result.steps.push({
|
|
12990
|
+
name: 'batch',
|
|
12991
|
+
state: 'error',
|
|
12992
|
+
batched: true,
|
|
12993
|
+
error: error_,
|
|
12994
|
+
errorMessage: error_ instanceof Error ? error_.message : 'Batched approve + burn failed.',
|
|
12995
|
+
errorCategory: classifyPreSubmissionError(error_)
|
|
12996
|
+
});
|
|
12997
|
+
return undefined;
|
|
12998
|
+
}
|
|
12999
|
+
}
|
|
12562
13000
|
/**
|
|
12563
|
-
*
|
|
12564
|
-
*
|
|
12565
|
-
* This schema validates that a string is a properly formatted 65-byte EVM
|
|
12566
|
-
* signature:
|
|
12567
|
-
* - Must be a valid hex string with '0x' prefix
|
|
12568
|
-
* - Must be exactly 132 characters long (0x + 130 hex characters)
|
|
12569
|
-
*
|
|
12570
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
12571
|
-
*
|
|
12572
|
-
* @example
|
|
12573
|
-
* ```typescript
|
|
12574
|
-
* import { evmSignatureSchema } from '@core/adapter'
|
|
12575
|
-
*
|
|
12576
|
-
* const validSignature = `0x${'ab'.repeat(65)}`
|
|
13001
|
+
* Ensure `step.errorMessage` is populated when an error object exists.
|
|
12577
13002
|
*
|
|
12578
|
-
*
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
13003
|
+
* @internal
|
|
13004
|
+
*/ function ensureStepErrorMessage(name, step) {
|
|
13005
|
+
if (!step.errorMessage && step.error) {
|
|
13006
|
+
step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
|
|
13007
|
+
}
|
|
13008
|
+
}
|
|
12582
13009
|
/**
|
|
12583
|
-
*
|
|
12584
|
-
*
|
|
12585
|
-
* This schema validates that a string:
|
|
12586
|
-
* - Is a string type
|
|
12587
|
-
* - Is not empty after trimming
|
|
12588
|
-
* - Contains only valid base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z)
|
|
12589
|
-
* - Does not contain commonly confused characters (0, O, I, l)
|
|
12590
|
-
*
|
|
12591
|
-
* @remarks
|
|
12592
|
-
* This schema does not validate length, making it suitable for various base58-encoded data
|
|
12593
|
-
* like Solana addresses, transaction signatures, and other base58-encoded data.
|
|
12594
|
-
*
|
|
12595
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
13010
|
+
* Coerce a raw JSON-RPC `code` to a number.
|
|
12596
13011
|
*
|
|
12597
|
-
*
|
|
12598
|
-
*
|
|
12599
|
-
*
|
|
13012
|
+
* Some wallet SDKs serialize the JSON-RPC `code` as a string ("4001")
|
|
13013
|
+
* after round-tripping through JSON; accept both shapes so strict `===`
|
|
13014
|
+
* comparisons downstream still classify 5720/5730/5740 correctly — those
|
|
13015
|
+
* codes have no message-pattern fallback.
|
|
12600
13016
|
*
|
|
12601
|
-
*
|
|
12602
|
-
*
|
|
13017
|
+
* @param rawCode - The raw `code` extracted from the error object.
|
|
13018
|
+
* @returns The numeric code, or `undefined` if the value cannot be parsed.
|
|
12603
13019
|
*
|
|
12604
|
-
*
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
12614
|
-
}, 'Base58 string contains invalid characters. Only base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z) are allowed');
|
|
13020
|
+
* @internal
|
|
13021
|
+
*/ function coerceRpcCode(rawCode) {
|
|
13022
|
+
if (typeof rawCode === 'number') {
|
|
13023
|
+
return rawCode;
|
|
13024
|
+
}
|
|
13025
|
+
if (typeof rawCode === 'string') {
|
|
13026
|
+
return Number.parseInt(rawCode, 10);
|
|
13027
|
+
}
|
|
13028
|
+
return undefined;
|
|
13029
|
+
}
|
|
12615
13030
|
/**
|
|
12616
|
-
*
|
|
12617
|
-
*
|
|
12618
|
-
* This schema validates that a string is a properly formatted Solana address:
|
|
12619
|
-
* - Must be a valid base58-encoded string
|
|
12620
|
-
* - Must be between 32-44 characters long (typical length for base58-encoded 32-byte addresses)
|
|
13031
|
+
* Classify a pre-submission error thrown during `wallet_sendCalls`.
|
|
12621
13032
|
*
|
|
12622
|
-
*
|
|
13033
|
+
* Inspect the error's JSON-RPC `code` (falling back to message pattern
|
|
13034
|
+
* matching for wrapper errors like viem's `ChainMismatchError`) and map
|
|
13035
|
+
* it to a {@link BridgeStepErrorCategory}. This lets downstream consumers
|
|
13036
|
+
* distinguish user rejections, wallet capability gaps, and unknown
|
|
13037
|
+
* failures without parsing error messages.
|
|
12623
13038
|
*
|
|
12624
|
-
* @
|
|
12625
|
-
*
|
|
12626
|
-
*
|
|
13039
|
+
* @remarks
|
|
13040
|
+
* Does NOT alter control flow — the SDK continues to surface a
|
|
13041
|
+
* `state: 'error'` step. Auto-fallback to sequential execution is
|
|
13042
|
+
* intentionally out of scope for this helper.
|
|
12627
13043
|
*
|
|
12628
|
-
*
|
|
13044
|
+
* @param err - The error thrown by `wallet_sendCalls`.
|
|
13045
|
+
* @returns The derived error category, or `'unknown'` if no match.
|
|
12629
13046
|
*
|
|
12630
|
-
*
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
13047
|
+
* @internal
|
|
13048
|
+
*/ function classifyPreSubmissionError(err) {
|
|
13049
|
+
// Cross-realm-safe duck typing: `instanceof Error` returns false for
|
|
13050
|
+
// errors thrown in a different JavaScript realm (e.g., a wallet
|
|
13051
|
+
// provider running inside an iframe, which is common with WalletConnect
|
|
13052
|
+
// and the Coinbase Wallet SDK).
|
|
13053
|
+
if (typeof err !== 'object' || err === null || !('message' in err)) {
|
|
13054
|
+
return 'unknown';
|
|
13055
|
+
}
|
|
13056
|
+
const code = coerceRpcCode(err.code);
|
|
13057
|
+
const message = String(err.message);
|
|
13058
|
+
// Numeric JSON-RPC codes are authoritative; check them before falling
|
|
13059
|
+
// back to message-pattern matching. Order matters: an error carrying
|
|
13060
|
+
// `code === 5750` with a message like "user rejected the upgrade"
|
|
13061
|
+
// is a capability problem, not a plain user rejection.
|
|
13062
|
+
if (code === 4001) {
|
|
13063
|
+
return 'user_rejected';
|
|
13064
|
+
}
|
|
13065
|
+
if (code === 5700 || code === 5710 || code === 5750) {
|
|
13066
|
+
return 'atomic_unsupported';
|
|
13067
|
+
}
|
|
13068
|
+
if (code === 5720) {
|
|
13069
|
+
return 'duplicate_batch_id';
|
|
13070
|
+
}
|
|
13071
|
+
if (code === 5730) {
|
|
13072
|
+
return 'unknown_bundle';
|
|
13073
|
+
}
|
|
13074
|
+
if (code === 5740) {
|
|
13075
|
+
return 'batch_too_large';
|
|
13076
|
+
}
|
|
13077
|
+
// Fall back to message patterns when no specific code is available —
|
|
13078
|
+
// viem (and other wrapper layers) sometimes strip the numeric code
|
|
13079
|
+
// while preserving the original wallet message in `Details:`.
|
|
13080
|
+
if (/EIP-7702 not supported/i.test(message) || /does not support the requested chain/i.test(message) || /rejected the upgrade/i.test(message)) {
|
|
13081
|
+
return 'atomic_unsupported';
|
|
13082
|
+
}
|
|
13083
|
+
if (/user rejected/i.test(message)) {
|
|
13084
|
+
return 'user_rejected';
|
|
13085
|
+
}
|
|
13086
|
+
return 'unknown';
|
|
13087
|
+
}
|
|
12634
13088
|
/**
|
|
12635
|
-
*
|
|
12636
|
-
*
|
|
12637
|
-
* This schema validates that a string is a properly formatted Solana transaction hash:
|
|
12638
|
-
* - Must be a valid base58-encoded string
|
|
12639
|
-
* - Must be between 86-88 characters long (typical length for base58-encoded 64-byte signatures)
|
|
12640
|
-
*
|
|
12641
|
-
* @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
|
|
12642
|
-
*
|
|
12643
|
-
* @example
|
|
12644
|
-
* ```typescript
|
|
12645
|
-
* import { solanaTransactionHashSchema } from '@core/adapter'
|
|
12646
|
-
*
|
|
12647
|
-
* const validTxHash = '5VfYmGBjvQKe3xgLtTQPSMEUdpEVHrJwLK7pKBJWKzYpNBE2g3kJrq7RSe9M8DqzQJ5J2aZPTjHLvd4WgxPpJKS'
|
|
13089
|
+
* Execute a cross-chain USDC bridge using the CCTP v2 protocol.
|
|
12648
13090
|
*
|
|
12649
|
-
*
|
|
12650
|
-
*
|
|
12651
|
-
*
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
* Schema for validating Adapter objects.
|
|
12655
|
-
* Checks for the required methods that define an Adapter.
|
|
12656
|
-
*/ z.object({
|
|
12657
|
-
prepare: z.function(),
|
|
12658
|
-
waitForTransaction: z.function(),
|
|
12659
|
-
getAddress: z.function()
|
|
12660
|
-
});
|
|
12661
|
-
|
|
12662
|
-
/**
|
|
12663
|
-
* Validate that the adapter has sufficient token balance for a transaction.
|
|
13091
|
+
* @remarks
|
|
13092
|
+
* This function performs the full CCTP v2 bridge flow: token approval, burning on the source chain,
|
|
13093
|
+
* attestation fetching, and minting on the destination chain. Each step is validated and dispatched
|
|
13094
|
+
* through the provider's action dispatcher. If any step fails, the bridge result will have state 'error'.
|
|
13095
|
+
* Each step includes transaction details and explorer URLs for easy verification on block explorers.
|
|
12664
13096
|
*
|
|
12665
|
-
*
|
|
12666
|
-
*
|
|
12667
|
-
*
|
|
12668
|
-
* information about the shortfall.
|
|
13097
|
+
* @param params - The bridge parameters containing source, destination, amount, and optional config.
|
|
13098
|
+
* @param provider - The CCTP v2 bridging provider instance.
|
|
13099
|
+
* @returns A promise resolving to the bridge result, including transaction details, step states, and explorer URLs.
|
|
12669
13100
|
*
|
|
12670
|
-
* @
|
|
12671
|
-
* @returns A promise that resolves to void if validation passes.
|
|
12672
|
-
* @throws {KitError} When the adapter's balance is less than the required amount (code: 9001).
|
|
13101
|
+
* @throws Will propagate errors thrown by underlying step functions (approval, burn, attestation, mint) if they throw unexpectedly.
|
|
12673
13102
|
*
|
|
12674
13103
|
* @example
|
|
12675
13104
|
* ```typescript
|
|
12676
|
-
* import {
|
|
12677
|
-
*
|
|
12678
|
-
*
|
|
12679
|
-
*
|
|
12680
|
-
*
|
|
12681
|
-
*
|
|
12682
|
-
*
|
|
12683
|
-
* })
|
|
12684
|
-
*
|
|
12685
|
-
* try {
|
|
12686
|
-
* await validateBalanceForTransaction({
|
|
12687
|
-
* adapter,
|
|
12688
|
-
* amount: '1000000', // 1 USDC (6 decimals)
|
|
12689
|
-
* token: 'USDC',
|
|
12690
|
-
* tokenAddress: '0xA0b86a33E6441c8C1c7C16e4c5e3e5b5e4c5e3e5b5e4c5e',
|
|
12691
|
-
* operationContext: { chain: 'Ethereum' },
|
|
13105
|
+
* import { bridge } from '@circle-fin/provider-cctp-v2/provider/bridge/bridge'
|
|
13106
|
+
* // ...setup params and provider...
|
|
13107
|
+
* const result = await bridge(params, provider)
|
|
13108
|
+
* if (result.state === 'success') {
|
|
13109
|
+
* console.log('Bridge completed!')
|
|
13110
|
+
* result.steps.forEach(step => {
|
|
13111
|
+
* if (step.explorerUrl) {
|
|
13112
|
+
* console.log(`${step.name}: ${step.explorerUrl}`)
|
|
13113
|
+
* }
|
|
12692
13114
|
* })
|
|
12693
|
-
*
|
|
12694
|
-
*
|
|
12695
|
-
* if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
|
|
12696
|
-
* console.error('Insufficient funds:', error.message)
|
|
12697
|
-
* }
|
|
13115
|
+
* } else {
|
|
13116
|
+
* console.error('Bridge failed at step:', result.steps.find(s => s.state === 'error'))
|
|
12698
13117
|
* }
|
|
12699
13118
|
* ```
|
|
12700
|
-
*/
|
|
12701
|
-
const
|
|
12702
|
-
const
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
13119
|
+
*/ async function bridge(params, provider) {
|
|
13120
|
+
const useForwarder = params.destination.useForwarder === true;
|
|
13121
|
+
const invocation = resolveBridgeInvocation(params.invocationMeta);
|
|
13122
|
+
const result = {
|
|
13123
|
+
state: 'pending',
|
|
13124
|
+
amount: params.amount,
|
|
13125
|
+
token: params.token,
|
|
13126
|
+
source: {
|
|
13127
|
+
address: params.source.address,
|
|
13128
|
+
chain: params.source.chain
|
|
13129
|
+
},
|
|
13130
|
+
destination: {
|
|
13131
|
+
address: params.destination.address,
|
|
13132
|
+
chain: params.destination.chain,
|
|
13133
|
+
...params.destination.recipientAddress && {
|
|
13134
|
+
recipientAddress: params.destination.recipientAddress
|
|
13135
|
+
},
|
|
13136
|
+
...useForwarder && {
|
|
13137
|
+
useForwarder: true
|
|
13138
|
+
}
|
|
13139
|
+
},
|
|
13140
|
+
steps: [],
|
|
13141
|
+
config: params.config,
|
|
13142
|
+
provider: provider.name
|
|
13143
|
+
};
|
|
13144
|
+
let context = undefined;
|
|
13145
|
+
let useBatched = false;
|
|
13146
|
+
try {
|
|
13147
|
+
useBatched = await shouldUseBatchedExecution(params);
|
|
13148
|
+
} catch {
|
|
13149
|
+
// Silently fall back to sequential
|
|
12716
13150
|
}
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
(
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
13151
|
+
const executors = createStepExecutors(useForwarder);
|
|
13152
|
+
if (useBatched) {
|
|
13153
|
+
const batchContext = await executeBatchedPath(params, provider, result, invocation);
|
|
13154
|
+
if (result.state === 'error') {
|
|
13155
|
+
return result;
|
|
13156
|
+
}
|
|
13157
|
+
context = batchContext;
|
|
13158
|
+
}
|
|
13159
|
+
const stepsToRun = useBatched ? executors.filter(({ name })=>name !== 'approve' && name !== 'burn') : executors;
|
|
13160
|
+
for (const { name, executor, updateContext } of stepsToRun){
|
|
13161
|
+
try {
|
|
13162
|
+
const step = await executor(params, provider, context);
|
|
13163
|
+
if (step.state === 'error') {
|
|
13164
|
+
ensureStepErrorMessage(name, step);
|
|
13165
|
+
result.steps.push(step);
|
|
13166
|
+
result.state = 'error';
|
|
13167
|
+
dispatchStepEvent(name, step, provider, invocation);
|
|
13168
|
+
return result;
|
|
13169
|
+
}
|
|
13170
|
+
const newContext = updateContext?.(step);
|
|
13171
|
+
if (newContext) {
|
|
13172
|
+
context = {
|
|
13173
|
+
...context,
|
|
13174
|
+
...newContext
|
|
13175
|
+
};
|
|
13176
|
+
}
|
|
13177
|
+
dispatchStepEvent(name, step, provider, invocation);
|
|
13178
|
+
result.steps.push(step);
|
|
13179
|
+
} catch (error) {
|
|
13180
|
+
handleStepError(name, error, result);
|
|
13181
|
+
return result;
|
|
13182
|
+
}
|
|
13183
|
+
}
|
|
13184
|
+
result.state = 'success';
|
|
13185
|
+
return result;
|
|
13186
|
+
}
|
|
12733
13187
|
|
|
12734
13188
|
/**
|
|
12735
13189
|
* Determine if a step executes on the source chain.
|
|
@@ -13171,6 +13625,39 @@ var pkg = {
|
|
|
13171
13625
|
}
|
|
13172
13626
|
}
|
|
13173
13627
|
|
|
13628
|
+
function isPlainObject(value) {
|
|
13629
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
13630
|
+
return false;
|
|
13631
|
+
}
|
|
13632
|
+
const prototype = Object.getPrototypeOf(value);
|
|
13633
|
+
return prototype === Object.prototype || prototype === null;
|
|
13634
|
+
}
|
|
13635
|
+
function assertHeadersConfig(headers, field) {
|
|
13636
|
+
if (headers === undefined) {
|
|
13637
|
+
return;
|
|
13638
|
+
}
|
|
13639
|
+
if (!isPlainObject(headers)) {
|
|
13640
|
+
throw createValidationFailedError(field, headers, `${field} must be a plain object with string header values when provided`);
|
|
13641
|
+
}
|
|
13642
|
+
for (const [name, value] of Object.entries(headers)){
|
|
13643
|
+
if (typeof value !== 'string') {
|
|
13644
|
+
throw createValidationFailedError(`${field}.${name}`, value, 'header values must be strings');
|
|
13645
|
+
}
|
|
13646
|
+
}
|
|
13647
|
+
}
|
|
13648
|
+
function assertCCTPV2Config(config) {
|
|
13649
|
+
if (!isPlainObject(config)) {
|
|
13650
|
+
throw createValidationFailedError('config', config, 'config must be a plain object when provided');
|
|
13651
|
+
}
|
|
13652
|
+
assertHeadersConfig(config['headers'], 'config.headers');
|
|
13653
|
+
const attestation = config['attestation'];
|
|
13654
|
+
if (attestation !== undefined) {
|
|
13655
|
+
if (!isPlainObject(attestation)) {
|
|
13656
|
+
throw createValidationFailedError('config.attestation', attestation, 'config.attestation must be a plain object when provided');
|
|
13657
|
+
}
|
|
13658
|
+
assertHeadersConfig(attestation['headers'], 'config.attestation.headers');
|
|
13659
|
+
}
|
|
13660
|
+
}
|
|
13174
13661
|
/**
|
|
13175
13662
|
* All chains that are supported by the CCTP v2 provider.
|
|
13176
13663
|
*
|
|
@@ -13239,9 +13726,38 @@ var pkg = {
|
|
|
13239
13726
|
* @param config - Optional configuration overrides for the provider
|
|
13240
13727
|
*/ constructor(config = {}){
|
|
13241
13728
|
super();
|
|
13729
|
+
assertCCTPV2Config(config);
|
|
13242
13730
|
this.config = config;
|
|
13243
13731
|
}
|
|
13244
13732
|
/**
|
|
13733
|
+
* Resolves the effective polling configuration for an attestation request.
|
|
13734
|
+
*
|
|
13735
|
+
* Precedence (lowest to highest): provider `config.attestation`, then the
|
|
13736
|
+
* per-call `config`. Headers merge independently across
|
|
13737
|
+
* `config.attestation.headers`, the provider-level `config.headers`, and any
|
|
13738
|
+
* per-call `config.headers`, so a more specific header augments rather than
|
|
13739
|
+
* replaces the broader ones. The `headers` key is omitted entirely when no
|
|
13740
|
+
* headers are configured, leaving the attestation fetchers' defaults intact.
|
|
13741
|
+
*
|
|
13742
|
+
* @param config - Optional per-call polling configuration overrides
|
|
13743
|
+
* @returns The merged polling configuration passed to the attestation fetchers
|
|
13744
|
+
*/ resolveAttestationConfig(config) {
|
|
13745
|
+
const headers = {
|
|
13746
|
+
...this.config?.attestation?.headers,
|
|
13747
|
+
...this.config?.headers,
|
|
13748
|
+
...config?.headers
|
|
13749
|
+
};
|
|
13750
|
+
// Polling fields follow normal precedence; headers are merged separately
|
|
13751
|
+
// below so narrower config layers augment rather than replace broader ones.
|
|
13752
|
+
return {
|
|
13753
|
+
...this.config?.attestation,
|
|
13754
|
+
...config,
|
|
13755
|
+
...Object.keys(headers).length > 0 ? {
|
|
13756
|
+
headers
|
|
13757
|
+
} : {}
|
|
13758
|
+
};
|
|
13759
|
+
}
|
|
13760
|
+
/**
|
|
13245
13761
|
* Execute a cross-chain USDC bridge operation using the CCTP v2 protocol.
|
|
13246
13762
|
*
|
|
13247
13763
|
* This method orchestrates the complete CCTP v2 bridge flow including validation,
|
|
@@ -13558,7 +14074,7 @@ var pkg = {
|
|
|
13558
14074
|
throw new Error(`Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
|
|
13559
14075
|
}
|
|
13560
14076
|
// Resolve spender address with proper error handling
|
|
13561
|
-
const spenderAddress = resolveCCTPV2ContractAddress(chain);
|
|
14077
|
+
const spenderAddress = resolveCCTPV2ContractAddress(chain, 'tokenMessenger');
|
|
13562
14078
|
// Prepare action parameters
|
|
13563
14079
|
const actionParams = {
|
|
13564
14080
|
amount: BigInt(amount),
|
|
@@ -13667,11 +14183,7 @@ var pkg = {
|
|
|
13667
14183
|
*/ async fetchAttestation(source, transactionHash, config) {
|
|
13668
14184
|
assertCCTPv2WalletContext(source);
|
|
13669
14185
|
try {
|
|
13670
|
-
|
|
13671
|
-
const effectiveConfig = {
|
|
13672
|
-
...this.config?.attestation,
|
|
13673
|
-
...config
|
|
13674
|
-
};
|
|
14186
|
+
const effectiveConfig = this.resolveAttestationConfig(config);
|
|
13675
14187
|
const response = await fetchAttestation(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
13676
14188
|
const message = response.messages[0];
|
|
13677
14189
|
if (!message) {
|
|
@@ -13688,6 +14200,49 @@ var pkg = {
|
|
|
13688
14200
|
}
|
|
13689
14201
|
}
|
|
13690
14202
|
/**
|
|
14203
|
+
* Polls attestation data until Circle's relayer mint transaction is confirmed.
|
|
14204
|
+
*
|
|
14205
|
+
* This method is used by forwarded transfers. It polls the same Iris
|
|
14206
|
+
* attestation endpoint as {@link CCTPV2BridgingProvider.fetchAttestation},
|
|
14207
|
+
* but waits for a completed relayer forward state and returns the attestation
|
|
14208
|
+
* message containing `forwardTxHash`.
|
|
14209
|
+
*
|
|
14210
|
+
* @typeParam TFromAdapterCapabilities - The type representing the capabilities of the source adapter
|
|
14211
|
+
* @param source - The source wallet context containing the chain definition and wallet address
|
|
14212
|
+
* @param transactionHash - The transaction hash of the burn operation
|
|
14213
|
+
* @param config - Optional polling configuration overrides for timeout, retries, delay, and headers
|
|
14214
|
+
* @returns A promise that resolves to the attestation message with `forwardTxHash`
|
|
14215
|
+
* @throws KitError If the relayer forward fails, the response is invalid, or polling times out
|
|
14216
|
+
*
|
|
14217
|
+
* @example
|
|
14218
|
+
* ```typescript
|
|
14219
|
+
* import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
14220
|
+
* import { Chains } from '@core/chains'
|
|
14221
|
+
*
|
|
14222
|
+
* const provider = new CCTPV2BridgingProvider({
|
|
14223
|
+
* headers: { 'X-Partner-UUID': '00000000-0000-0000-0000-000000000000' },
|
|
14224
|
+
* })
|
|
14225
|
+
*
|
|
14226
|
+
* const attestation = await provider.fetchRelayerMint(
|
|
14227
|
+
* {
|
|
14228
|
+
* adapter,
|
|
14229
|
+
* chain: Chains.EthereumSepolia,
|
|
14230
|
+
* address: '0x1234...',
|
|
14231
|
+
* },
|
|
14232
|
+
* '0xabc123...',
|
|
14233
|
+
* )
|
|
14234
|
+
*
|
|
14235
|
+
* console.log('Relayer mint tx:', attestation.forwardTxHash)
|
|
14236
|
+
* ```
|
|
14237
|
+
*/ async fetchRelayerMint(source, transactionHash, config) {
|
|
14238
|
+
assertCCTPv2WalletContext(source);
|
|
14239
|
+
if (typeof transactionHash !== 'string' || transactionHash.trim() === '') {
|
|
14240
|
+
throw createValidationFailedError('transactionHash', transactionHash, 'transactionHash must be a non-empty string');
|
|
14241
|
+
}
|
|
14242
|
+
const effectiveConfig = this.resolveAttestationConfig(config);
|
|
14243
|
+
return await fetchRelayerMint(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
14244
|
+
}
|
|
14245
|
+
/**
|
|
13691
14246
|
* Requests a fresh attestation for an expired attestation.
|
|
13692
14247
|
*
|
|
13693
14248
|
* This method is used when the original attestation has expired before the mint
|
|
@@ -13741,11 +14296,7 @@ var pkg = {
|
|
|
13741
14296
|
throw new Error('Failed to re-attest: Invalid transaction hash');
|
|
13742
14297
|
}
|
|
13743
14298
|
try {
|
|
13744
|
-
|
|
13745
|
-
const effectiveConfig = {
|
|
13746
|
-
...this.config?.attestation,
|
|
13747
|
-
...config
|
|
13748
|
-
};
|
|
14299
|
+
const effectiveConfig = this.resolveAttestationConfig(config);
|
|
13749
14300
|
// Step 1: Get existing attestation data to extract nonce
|
|
13750
14301
|
const existingAttestation = await fetchAttestationWithoutStatusCheck(source.chain.cctp.domain, transactionHash, source.chain.isTestnet, effectiveConfig);
|
|
13751
14302
|
const nonce = existingAttestation.messages[0]?.eventNonce;
|
|
@@ -14003,6 +14554,106 @@ var pkg = {
|
|
|
14003
14554
|
return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
|
|
14004
14555
|
}
|
|
14005
14556
|
/**
|
|
14557
|
+
* Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
|
|
14558
|
+
*
|
|
14559
|
+
* Builds the source-chain `depositForBurnWithHookAndFees` call for the
|
|
14560
|
+
* GenericExecutor FORWARD path: fees are collected up front on the source chain
|
|
14561
|
+
* against a signed quote, `mintRecipient` and `destinationCaller` are both set to
|
|
14562
|
+
* the GenericExecutor, and the GE `hookData` is passed through unchanged.
|
|
14563
|
+
*
|
|
14564
|
+
* This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
|
|
14565
|
+
* and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
|
|
14566
|
+
* `claim` are produced elsewhere and passed in here:
|
|
14567
|
+
* - `hookData`: `buildForwardingHookDataWithPayload(version,
|
|
14568
|
+
* buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
|
|
14569
|
+
* - `claim.signedQuote` / `feeToken` / `feeTotalAmount`: from `fetchFeeQuote`
|
|
14570
|
+
* (`@circle-fin/provider-fee-v1`), whose FORWARD item must be bound to the
|
|
14571
|
+
* SAME `hookData` and executor `destinationCaller` used here.
|
|
14572
|
+
*
|
|
14573
|
+
* The returned approvals and burn are NOT executed — the caller executes the
|
|
14574
|
+
* approvals first (in order) and then the burn. The fee payment channel matches
|
|
14575
|
+
* the quote's `feeToken`:
|
|
14576
|
+
* - Native fee: exactly `feeTotalAmount` is attached as the burn's `msg.value`;
|
|
14577
|
+
* only the burn amount is approved.
|
|
14578
|
+
* - USDC fee (same token as the burn): a single combined `amount + feeTotalAmount`
|
|
14579
|
+
* approval covers both; the redundant second approval is skipped.
|
|
14580
|
+
*
|
|
14581
|
+
* @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
|
|
14582
|
+
* @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
|
|
14583
|
+
* @returns The prepared approvals, the prepared burn, and the resolved fee plan.
|
|
14584
|
+
* @throws {KitError} If the wallet context is invalid, `destinationChain` does not
|
|
14585
|
+
* support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
|
|
14586
|
+
* a bigint or a numeric string coercible to bigint, the hookData lacks a
|
|
14587
|
+
* `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
|
|
14588
|
+
* context cannot be resolved.
|
|
14589
|
+
*
|
|
14590
|
+
* @example
|
|
14591
|
+
* ```typescript
|
|
14592
|
+
* const { approvals, burn } = await provider.burnWithFees({
|
|
14593
|
+
* source,
|
|
14594
|
+
* destinationChain: Arc,
|
|
14595
|
+
* amount: 1_000_000n,
|
|
14596
|
+
* executor: genericExecutorAddress,
|
|
14597
|
+
* hookData: geForwardHookData,
|
|
14598
|
+
* claim: { signedQuote: '0x01...', refundAddress: userAddress },
|
|
14599
|
+
* feeToken: '0x0000000000000000000000000000000000000000', // native
|
|
14600
|
+
* feeTotalAmount: 3_500_000n,
|
|
14601
|
+
* })
|
|
14602
|
+
* for (const approval of approvals) await approval.execute()
|
|
14603
|
+
* const txHash = await burn.execute()
|
|
14604
|
+
* ```
|
|
14605
|
+
*/ async burnWithFees(params) {
|
|
14606
|
+
assertBurnWithFeesParams(params);
|
|
14607
|
+
const { source, destinationChain, executor, hookData, claim, feeToken } = params;
|
|
14608
|
+
const amount = BigInt(params.amount);
|
|
14609
|
+
const feeTotalAmount = BigInt(params.feeTotalAmount);
|
|
14610
|
+
// Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
|
|
14611
|
+
// so the hookData must carry a cctp-forward frame; otherwise the wrapper
|
|
14612
|
+
// reverts ForwardFeeWithoutHook. Surface it as a typed input error up front.
|
|
14613
|
+
assertForwardHookData(hookData);
|
|
14614
|
+
const burnToken = source.chain.usdcAddress;
|
|
14615
|
+
const feePayment = resolveFeePayment({
|
|
14616
|
+
feeToken,
|
|
14617
|
+
burnToken,
|
|
14618
|
+
amount,
|
|
14619
|
+
feeTotalAmount
|
|
14620
|
+
});
|
|
14621
|
+
// Resolve operation context from the source wallet context.
|
|
14622
|
+
const operationContext = this.extractOperationContext(source);
|
|
14623
|
+
let resolvedContext;
|
|
14624
|
+
try {
|
|
14625
|
+
resolvedContext = await resolveOperationContext(source.adapter, operationContext);
|
|
14626
|
+
} catch (error) {
|
|
14627
|
+
throw createValidationFailedError('source.adapter', undefined, `Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
|
|
14628
|
+
}
|
|
14629
|
+
const context = resolvedContext;
|
|
14630
|
+
const wrapperAddress = resolveCCTPV2ContractAddress(source.chain, 'tokenMessengerWithFees');
|
|
14631
|
+
// Build the ERC-20 approvals to the wrapper (burn token, plus a distinct fee
|
|
14632
|
+
// token only when the fee is not paid in the burn token).
|
|
14633
|
+
const approvals = await Promise.all(feePayment.approvals.map(async (approval)=>source.adapter.prepareAction('token.approve', {
|
|
14634
|
+
tokenAddress: approval.token,
|
|
14635
|
+
delegate: wrapperAddress,
|
|
14636
|
+
amount: approval.amount
|
|
14637
|
+
}, context)));
|
|
14638
|
+
// Build the burn: mintRecipient AND destinationCaller are both the executor.
|
|
14639
|
+
const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
|
|
14640
|
+
fromChain: source.chain,
|
|
14641
|
+
toChain: destinationChain,
|
|
14642
|
+
amount,
|
|
14643
|
+
mintRecipient: executor,
|
|
14644
|
+
destinationCaller: executor,
|
|
14645
|
+
hookData,
|
|
14646
|
+
claim,
|
|
14647
|
+
feeToken,
|
|
14648
|
+
feeTotalAmount
|
|
14649
|
+
}, context);
|
|
14650
|
+
return {
|
|
14651
|
+
approvals,
|
|
14652
|
+
burn,
|
|
14653
|
+
feePayment
|
|
14654
|
+
};
|
|
14655
|
+
}
|
|
14656
|
+
/**
|
|
14006
14657
|
* Waits for a transaction to be mined and confirmed on the blockchain.
|
|
14007
14658
|
*
|
|
14008
14659
|
* This method should block until the transaction is confirmed on the blockchain.
|
|
@@ -14027,5 +14678,5 @@ var pkg = {
|
|
|
14027
14678
|
}
|
|
14028
14679
|
}
|
|
14029
14680
|
|
|
14030
|
-
export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation, validateAndConvertFee };
|
|
14681
|
+
export { CCTPV2BridgingProvider, PREPAID_FORWARD_REVERT_NAMES, assertForwardHookData, getBlocksUntilExpiry, getMintRecipientAccount, hasForwardHook, isAttestationExpired, isMintFailureRelatedToAttestation, mapPrepaidForwardError, resolveFeePayment, validateAndConvertFee };
|
|
14031
14682
|
//# sourceMappingURL=index.mjs.map
|