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