@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/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 'messageTransmitter')
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
- ...DEFAULT_CONFIG,
9407
- maxRetries: 3,
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
- ...DEFAULT_CONFIG,
9540
- maxRetries: 3,
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
- * Asserts that the provided parameters match the CCTPv2 wallet context interface.
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
- * // Prepare wallet context
9562
- * const context = {
9563
- * adapter: {
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
- * // This will throw if validation fails
9584
- * assertCCTPv2WalletContext(context)
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
- * // If we get here, context is guaranteed to be valid
9587
- * console.log('CCTPv2 wallet context is valid')
9588
- * ```
9589
- */ function assertCCTPv2WalletContext(params) {
9590
- // First validate basic wallet context
9591
- validateWithStateTracking(params, walletContextSchema, 'CCTPv2 wallet context', assertCCTPv2WalletContextSymbol);
9592
- // After validation, we know params is WalletContext
9593
- const context = params;
9594
- // Validate USDC support
9595
- if (context.chain.usdcAddress === null) {
9596
- throw createInvalidChainError(context.chain.name, 'Does not have USDC configured');
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
- // Validate CCTPv2 support
9599
- if (!isCCTPV2Supported(context.chain)) {
9600
- throw createInvalidChainError(context.chain.name, 'Does not support CCTPv2');
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
- const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
9605
- /**
9606
- * Asserts that the provided parameters match the CCTPv2 bridge parameters interface.
9607
- * The validation includes:
9608
- * - Basic parameter structure and types
9609
- * - Amount validation (non-empty numeric string \> 0)
9610
- * - Wallet address format validation (must be valid Ethereum address)
9611
- * - Chain definition validation (must be a valid chain with required properties)
9612
- * - Adapter validation (must implement required methods)
9613
- * - Optional config validation (transfer speed and max fee)
9614
- * - Network compatibility (source and destination chains must both be testnet or both mainnet)
9615
- * - CCTPv2-specific wallet context validations
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
- * CCTP bridge step names that can occur in the bridging flow.
10270
+ * Resolves an operation context into concrete chain and address values.
9815
10271
  *
9816
- * This object provides type safety for step names and represents all possible
9817
- * steps that can be executed during a CCTP bridge operation. Using const assertions
9818
- * makes this tree-shakable and follows modern TypeScript best practices.
9819
- */ const CCTPv2StepName = {
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
- * Rules are evaluated in order - the first matching condition determines the next step.
9830
- * This approach supports flexible flow logic and makes it easy to extend with new patterns.
9831
- */ const STEP_TRANSITION_RULES = {
9832
- // Starting state - no steps executed yet
9833
- '': [
9834
- {
9835
- condition: ()=>true,
9836
- nextStep: CCTPv2StepName.approve,
9837
- reason: 'Start with approval step',
9838
- isActionable: true
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
- // After Approve step
9842
- [CCTPv2StepName.approve]: [
9843
- {
9844
- condition: (ctx)=>ctx.lastStep?.state === 'success',
9845
- nextStep: CCTPv2StepName.burn,
9846
- reason: 'Approval successful, proceed to burn',
9847
- isActionable: true
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
- * Analyze bridge steps to determine retry feasibility and continuation point.
10349
+ * Schema for validating hexadecimal strings with '0x' prefix.
9955
10350
  *
9956
- * This function examines the current state of bridge steps to determine the optimal
9957
- * continuation strategy. It uses a rule-based approach that makes it easy to extend
9958
- * with new flow patterns and step types in the future.
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
- * The current analysis supports the standard CCTP flow:
9961
- * **Traditional flow**: Approve Burn FetchAttestation Mint
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
- * Key features:
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
- * @param bridgeResult - The bridge result containing step execution history.
9970
- * @returns Analysis result with continuation step and actionability information.
9971
- * @throws Error when bridgeResult is invalid or contains no steps array.
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 { analyzeSteps } from './analyzeSteps'
10390
+ * import { evmAddressSchema } from '@core/adapter'
9976
10391
  *
9977
- * // Failed approval step (requires user action)
9978
- * const bridgeResult = {
9979
- * steps: [
9980
- * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
9981
- * ]
9982
- * }
10392
+ * const validAddress = '0x1234567890123456789012345678901234567890'
9983
10393
  *
9984
- * const analysis = analyzeSteps(bridgeResult)
9985
- * // Result: { continuationStep: 'Approve', isRetryable: true,
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
- * // Pending transaction (requires waiting, not actionable)
9992
- * const bridgeResult = {
9993
- * steps: [
9994
- * { name: 'Approve', state: 'pending' }
9995
- * ]
9996
- * }
10409
+ * import { evmTransactionHashSchema } from '@core/adapter'
9997
10410
  *
9998
- * const analysis = analyzeSteps(bridgeResult)
9999
- * // Result: { continuationStep: 'Approve', isRetryable: false,
10000
- * // reason: 'Continue pending approval' }
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
- * // Completed bridge (nothing to do)
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 analysis = analyzeSteps(bridgeResult)
10016
- * // Result: { continuationStep: null, isRetryable: false,
10017
- * // reason: 'Bridge completed successfully' }
10431
+ * const validSignature = `0x${'ab'.repeat(65)}`
10432
+ *
10433
+ * const result = evmSignatureSchema.safeParse(validSignature)
10434
+ * console.log(result.success) // true
10018
10435
  * ```
10019
- */ const analyzeSteps = (bridgeResult)=>{
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
- * Build flow context from the execution history.
10438
+ * Schema for validating base58-encoded strings.
10039
10439
  *
10040
- * @param steps - Array of executed bridge steps.
10041
- * @returns Flow context with execution state and history.
10042
- */ function buildFlowContext(steps) {
10043
- const completedSteps = new Set();
10044
- const failedSteps = new Set();
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
- * @param context - The flow context with execution history.
10071
- * @returns Continuation decision with next step and actionability information.
10072
- */ function determineContinuationFromRules(context) {
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
- * @param result - The bridge result to search.
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 { findStepByName } from './findStep'
10454
+ * import { base58StringSchema } from '@core/adapter'
10133
10455
  *
10134
- * const burnStep = findStepByName(result, 'burn')
10135
- * if (burnStep) {
10136
- * console.log('Burn tx:', burnStep.txHash)
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
- */ function findStepByName(result, stepName) {
10140
- return result.steps.find((step)=>step.name === stepName);
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
- * Find a pending step by name and return it with its index.
10471
+ * Schema for validating Solana addresses.
10144
10472
  *
10145
- * Searches for a step that matches both the step name and has a pending state.
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
- * @param result - The bridge result containing steps to search through.
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 { findPendingStep } from './findStep'
10481
+ * import { solanaAddressSchema } from '@core/adapter'
10155
10482
  *
10156
- * const { step, index } = findPendingStep(result, 'burn')
10157
- * console.log('Pending step:', step.name, 'at index:', index)
10483
+ * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
10484
+ *
10485
+ * const result = solanaAddressSchema.safeParse(validAddress)
10486
+ * console.log(result.success) // true
10158
10487
  * ```
10159
- */ function findPendingStep(result, stepName) {
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
- * Get the burn transaction hash from bridge result.
10490
+ * Schema for validating Solana transaction hashes.
10183
10491
  *
10184
- * @param result - The bridge result.
10185
- * @returns The burn transaction hash, or undefined if not found.
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 { getBurnTxHash } from './findStep'
10500
+ * import { solanaTransactionHashSchema } from '@core/adapter'
10190
10501
  *
10191
- * const burnTxHash = getBurnTxHash(result)
10192
- * if (burnTxHash) {
10193
- * console.log('Burn tx hash:', burnTxHash)
10194
- * }
10502
+ * const validTxHash = '5VfYmGBjvQKe3xgLtTQPSMEUdpEVHrJwLK7pKBJWKzYpNBE2g3kJrq7RSe9M8DqzQJ5J2aZPTjHLvd4WgxPpJKS'
10503
+ *
10504
+ * const result = solanaTransactionHashSchema.safeParse(validTxHash)
10505
+ * console.log(result.success) // true
10195
10506
  * ```
10196
- */ function getBurnTxHash(result) {
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
- * Get the attestation data from bridge result.
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
- * @param result - The bridge result.
10203
- * @returns The attestation data, or undefined if not found.
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 { getAttestationData } from './findStep'
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 attestation = getAttestationData(result)
10210
- * if (attestation) {
10211
- * console.log('Attestation:', attestation.message)
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
- */ function getAttestationData(result) {
10215
- // Prefer reAttest data (most recent attestation after expiry)
10216
- const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
10217
- if (reAttestStep?.state === 'success' && reAttestStep.data) {
10218
- return reAttestStep.data;
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
- // Fall back to fetchAttestation step
10221
- const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
10222
- return fetchStep?.data;
10223
- }
10572
+ };
10224
10573
 
10225
10574
  /**
10226
- * Check if the analysis indicates a non-actionable pending state.
10575
+ * Permit signature standards for gasless token approvals.
10227
10576
  *
10228
- * A pending state is non-actionable when there's a continuation step but
10229
- * the analysis marks it as not actionable, typically because we need to
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
- * @param analysis - The step analysis result from analyzeSteps.
10233
- * @param result - The bridge result to check for pending steps.
10234
- * @returns True if there is a pending step that we should wait for.
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
- * @example
10237
- * ```typescript
10238
- * import { hasPendingState } from './stepUtils'
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
- * const analysis = analyzeSteps(bridgeResult)
10242
- * if (hasPendingState(analysis, bridgeResult)) {
10243
- * // Wait for the pending operation to complete
10244
- * }
10245
- * ```
10246
- */ /**
10247
- * Evaluate a transaction receipt and return the corresponding step state
10248
- * and error message. Centralises the success/revert/unconfirmed logic so
10249
- * every call-site behaves identically.
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 receipt - The transaction receipt containing status and block info.
10252
- * @param txHash - The transaction hash used in error messages.
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
- * const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
10258
- * step.state = outcome.state
10259
- * if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
10615
+ * assertBurnWithFeesParams(params)
10616
+ * // params is now typed as BurnWithFeesParams and safe to use
10617
+ * const { source, destinationChain, amount } = params
10260
10618
  * ```
10261
- */ function evaluateTransactionOutcome(receipt, txHash) {
10262
- if (receipt.status === 'success' && receipt.blockNumber) {
10263
- return {
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
- return {
10268
- state: 'error',
10269
- errorMessage: receipt.status === 'reverted' ? `Transaction ${txHash} was reverted` : 'Transaction was not confirmed on-chain'
10270
- };
10271
- }
10272
- function hasPendingState(analysis, result) {
10273
- // Check if there's a continuation step that's marked as non-actionable
10274
- if (analysis.continuationStep === null || analysis.isActionable) {
10275
- return false;
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
- * Check if the step is the last one in the execution flow.
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
- * @example
10289
- * ```typescript
10290
- * import { isLastStep } from './stepUtils'
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
- * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
10293
- * isLastStep({ name: 'mint' }, stepNames) // true
10294
- * isLastStep({ name: 'burn' }, stepNames) // false
10295
- * ```
10296
- */ function isLastStep(step, stepNames) {
10297
- const stepIndex = stepNames.indexOf(step.name);
10298
- return stepIndex === -1 || stepIndex >= stepNames.length - 1;
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
- * Wait for a pending transaction to complete.
10833
+ * Analyze bridge steps to determine retry feasibility and continuation point.
10302
10834
  *
10303
- * Poll the adapter until the transaction is confirmed on-chain and return
10304
- * the updated step with success or error state based on the receipt.
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
- * @param pendingStep - The full step object containing the transaction hash.
10307
- * @param adapter - The adapter to use for waiting.
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
- * @throws KitError when the pending step has no transaction hash.
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 { waitForPendingTransaction } from './bridgeStepUtils'
10854
+ * import { analyzeSteps } from './analyzeSteps'
10316
10855
  *
10317
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
10318
- * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
10319
- * // updatedStep.state is now 'success' or 'error'
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
- * For transaction steps: waits for the transaction to be confirmed.
10351
- * For attestation: re-executes the attestation fetch.
10352
- *
10353
- * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
10354
- * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
10355
- * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
10356
- * @param adapter - The adapter to use.
10357
- * @param chain - The chain where the step is executing.
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
- * @throws KitError when fetching attestation but burn transaction hash is not found.
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
- * import { waitForStepToComplete } from './bridgeStepUtils'
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 pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
10370
- * const updatedStep = await waitForStepToComplete(
10371
- * pendingStep,
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
- */ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
10381
- if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
10382
- // For attestation, re-run the fetch (it has built-in polling)
10383
- const burnTxHash = getBurnTxHash(result);
10384
- if (!burnTxHash) {
10385
- throw new KitError({
10386
- ...InputError.VALIDATION_FAILED,
10387
- recoverability: 'FATAL',
10388
- message: 'Cannot fetch attestation: burn transaction hash not found'
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
- ...pendingStep,
10399
- state: 'success',
10400
- data: attestation
10965
+ nextStep: matchingRule.nextStep,
10966
+ isActionable: matchingRule.isActionable,
10967
+ reason: matchingRule.reason
10401
10968
  };
10402
10969
  }
10403
- // For transaction steps, wait for the transaction to complete
10404
- return waitForPendingTransaction(pendingStep, adapter, chain);
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
- * Executes a prepared chain request and returns the result as a bridge step.
11003
+ * Find a step by name in the bridge result.
10409
11004
  *
10410
- * This function takes a prepared chain request (containing transaction data) and executes
10411
- * it using the appropriate adapter. It handles the execution details and formats
10412
- * the result as a standardized bridge step with transaction details and explorer URLs.
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
- * @param params - The execution parameters containing:
10415
- * - `name`: The name of the step
10416
- * - `request`: The prepared chain request containing transaction data
10417
- * - `adapter`: The adapter that will execute the transaction
10418
- * - `confirmations`: The number of confirmations to wait for (defaults to 1)
10419
- * - `timeout`: The timeout for the request in milliseconds
10420
- * @returns The bridge step with the transaction details and explorer URL
10421
- * @throws If the transaction execution fails
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
- * const step = await executePreparedChainRequest({
10426
- * name: 'approve',
10427
- * request: preparedRequest,
10428
- * adapter: adapter,
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
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout }) {
10435
- const step = {
10436
- name,
10437
- state: 'pending'
10438
- };
10439
- try {
10440
- /**
10441
- * No-op requests are not executed.
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
- return step;
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
- * Approves the TokenMessenger contract to spend USDC tokens for a bridge operation.
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 params - The bridge parameters containing source, destination, amount and optional config
10500
- * @param sourceChain - The source chain definition where the approval will occur
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
- * const approveStep = await approve(params, sourceChain)
10508
- * console.log('Approval tx:', approveStep.transactionHash)
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
- */ async function bridgeApproval({ params, provider }) {
10511
- // Calculate the approval amount
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
- * Executes a deposit-for-burn operation on the source chain to initiate a bridge.
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 params - The bridge parameters containing source, destination, amount and optional config
10531
- * @param sourceChain - The source chain definition where the burn will occur
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
- * const burnStep = await burn(params, sourceChain)
10539
- * console.log('Burn tx:', burnStep.transactionHash)
11086
+ * import { getAttestationData } from './findStep'
11087
+ *
11088
+ * const attestation = getAttestationData(result)
11089
+ * if (attestation) {
11090
+ * console.log('Attestation:', attestation.message)
11091
+ * }
10540
11092
  * ```
10541
- */ async function bridgeBurn({ params, provider }) {
10542
- return await executePreparedChainRequest({
10543
- name: 'burn',
10544
- adapter: params.source.adapter,
10545
- chain: params.source.chain,
10546
- request: await provider.burn(params)
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
- * Fetches the attestation from Circle's API for a completed burn operation.
11105
+ * Check if the analysis indicates a non-actionable pending state.
10552
11106
  *
10553
- * This function retrieves the attestation data required to mint tokens on the destination
10554
- * chain after a successful burn operation. The attestation proves that the burn occurred
10555
- * and authorizes the minting of equivalent tokens.
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 params - The bridge parameters containing source, destination, amount and optional config
10558
- * @param txHash - The hash of the burn transaction
10559
- * @returns Promise resolving to the bridge step with attestation data
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
- * const attestationStep = await bridgeFetchAttestation(
10566
- * { params, provider },
10567
- * burnTxHash
10568
- * )
10569
- * console.log('Attestation data:', attestationStep.data)
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
- */ async function bridgeFetchAttestation({ params, provider }, txHash) {
10572
- let step = {
10573
- name: 'fetchAttestation',
10574
- state: 'pending'
10575
- };
10576
- try {
10577
- /**
10578
- * Give slow transfers a longer retry delay of 30 seconds to account for the longer time it takes to get the
10579
- * attestation so that we do not hit the rate limit of the IRIS API or the retry limit of the API polling utility.
10580
- * We only do this for slow transfers on source chains that have greater than 20 confirmations (5 minutes of blocktime
10581
- * on the slowest blocktime chains) so that we do not slow down the attestation polling for lower confirmation chains.
10582
- */ const apiPollingConfig = params.config?.transferSpeed === TransferSpeed.SLOW && params.source.chain.cctp.contracts.v2.confirmations > 20 ? {
10583
- retryDelay: 30_000
10584
- } : undefined;
10585
- const attestation = await provider.fetchAttestation(params.source, txHash, apiPollingConfig);
10586
- step = {
10587
- ...step,
10588
- state: 'success',
10589
- data: attestation
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 step;
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
- * Executes a mint operation on the destination chain to complete a bridge.
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 params - The bridge parameters containing source, destination, amount and optional config
10617
- * @param destinationChain - The destination chain definition where the mint will occur
10618
- * @param attestation - The attestation data from Circle's API
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
- * const mintStep = await mint(params, destinationChain, attestation, message)
10627
- * console.log('Mint tx:', mintStep.transactionHash)
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
- */ async function bridgeMint({ params, provider }, attestation) {
10630
- // Validate attestation message matches transfer params
10631
- await assertCCTPv2AttestationParams(attestation, params);
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
- * Type guard that checks if the relayer has confirmed the mint transaction.
11180
+ * Wait for a pending transaction to complete.
10676
11181
  *
10677
- * This function validates that:
10678
- * 1. The response has valid AttestationResponse structure
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
- * If forwardState is 'FAILED', throws a non-retryable KitError.
10682
- * If forwardState is 'PENDING' or not present, throws a RETRYABLE KitError to continue polling.
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
- * @param obj - The value to check, typically a parsed JSON response
10685
- * @returns True if the relayer has confirmed the mint
10686
- * @throws {KitError} With FATAL recoverability if structure is invalid
10687
- * @throws {KitError} With RESUMABLE recoverability if forwardState is 'FAILED'
10688
- * @throws {KitError} With RETRYABLE recoverability if still pending
10689
- * @internal
10690
- */ const isRelayerMintConfirmed = (obj)=>{
10691
- // First check if the structure is valid
10692
- if (!hasValidAttestationStructure(obj)) {
10693
- throw new KitError({
10694
- ...InputError.VALIDATION_FAILED,
10695
- recoverability: 'FATAL',
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: 'No attestation messages found in IRIS API response.'
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
- // Check if mint is confirmed (or complete) with a valid transaction hash
10724
- // We accept both CONFIRMED and COMPLETE since COMPLETE implies CONFIRMED
10725
- if ((message.forwardState === 'CONFIRMED' || message.forwardState === 'COMPLETE') && typeof message.forwardTxHash === 'string' && message.forwardTxHash.trim().length > 0) {
10726
- return true;
10727
- }
10728
- // Still pending or not yet processed - throw RETRYABLE error to continue polling
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
- * Polls the attestation API until the relayer's mint transaction is confirmed.
11227
+ * Wait for a pending step to complete.
10737
11228
  *
10738
- * This function is used when `useForwarder` is enabled. Instead of the user
10739
- * submitting the mint transaction, Circle's Orbit relayer handles it automatically.
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
- * @remarks
10743
- * - Uses a 20-minute timeout by default (600 retries × 2 seconds)
10744
- * - Throws immediately if `forwardState` is 'FAILED'
10745
- * - Waits for `forwardState` to be 'CONFIRMED' or 'COMPLETE' (COMPLETE implies CONFIRMED)
10746
- * - Returns the attestation message with `forwardTxHash` populated
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
- * @param sourceDomainId - The CCTP domain ID of the source chain
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
- * const attestation = await fetchRelayerMint(0, '0xabc...', false)
10759
- * console.log('Relayer mint tx:', attestation.forwardTxHash)
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
- */ const fetchRelayerMint = async (sourceDomainId, transactionHash, isTestnet, config = {})=>{
10762
- const url = buildIrisUrl(sourceDomainId, transactionHash, isTestnet);
10763
- const effectiveConfig = {
10764
- ...DEFAULT_CONFIG,
10765
- ...config
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
- ...NetworkError.RELAYER_FORWARD_FAILED,
10775
- recoverability: error.recoverability,
10776
- message: error.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
- throw error;
10787
- }
10788
- // Return the first message (which should have forwardTxHash)
10789
- // Note: This check is needed for TypeScript type safety even though
10790
- // isRelayerMintConfirmed validates messages[0] exists. The type guard
10791
- // narrows the type at the call site, but TypeScript can't infer that
10792
- // the array still has elements after pollApiGet returns.
10793
- const message = response.messages[0];
10794
- if (!message) {
10795
- throw new KitError({
10796
- ...InputError.VALIDATION_FAILED,
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
- return message;
10802
- };
11282
+ // For transaction steps, wait for the transaction to complete
11283
+ return waitForPendingTransaction(pendingStep, adapter, chain);
11284
+ }
10803
11285
 
10804
11286
  /**
10805
- * Executes the mint step for forwarding mode where Circle's relayer handles the mint.
11287
+ * Executes a prepared chain request and returns the result as a bridge step.
10806
11288
  *
10807
- * Instead of the user signing and submitting a mint transaction, this function:
10808
- * 1. Polls the attestation API until the relayer has confirmed the mint (forwardState === 'CONFIRMED')
10809
- * 2. Extracts the relayer's mint transaction hash (forwardTxHash)
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
- * Since Circle's relayer handles the mint transaction, we don't wait for on-chain
10813
- * confirmation - the IRIS API confirmation is sufficient. The step completes
10814
- * immediately after the API confirms the mint.
10815
- *
10816
- * @param params - The bridge parameters containing source, destination, amount and config
10817
- * @param context - The step context containing burnTxHash from the burn step
10818
- * @returns Promise resolving to the bridge step with transaction details
10819
- * @throws {KitError} If burnTxHash is not available in context
10820
- * @throws {KitError} With code 'NETWORK_RELAYER_FORWARD_FAILED' if relayer failed
10821
- * @throws {KitError} If timeout is reached while waiting for relayer
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 bridgeRelayerMint(params, { burnTxHash: '0x...' })
10826
- * console.log('Relayer mint tx:', step.txHash)
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 bridgeRelayerMint(params, context) {
11315
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
10829
11316
  const step = {
10830
- name: 'mint',
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
- // Poll attestation API until relayer confirms the mint
10844
- const attestation = await fetchRelayerMint(params.source.chain.cctp.domain, context.burnTxHash, params.source.chain.isTestnet ?? false);
10845
- // Extract the relayer's mint transaction hash
10846
- const forwardTxHash = attestation.forwardTxHash;
10847
- if (!forwardTxHash) {
10848
- throw new KitError({
10849
- ...InputError.VALIDATION_FAILED,
10850
- recoverability: 'FATAL',
10851
- message: 'Relayer mint transaction hash not found in attestation response.'
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
- step.errorMessage = getErrorMessage(err);
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
- * Creates the ordered sequence of CCTP v2 bridge step executors.
11376
+ * Approves the TokenMessenger contract to spend USDC tokens for a bridge operation.
10867
11377
  *
10868
- * Defines the complete bridge flow: approve burn fetchAttestation → mint.
10869
- * Each step includes an executor function and optional context updater for
10870
- * passing data between steps.
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
- * When `useForwarder` is true, the mint step uses the relayer mint executor
10873
- * which polls for Circle's relayer to submit the mint transaction instead of
10874
- * requiring the user to sign and submit it.
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
- * @param useForwarder - When true, uses relayer mint instead of user mint
10877
- * @returns Array of step executor configurations
10878
- */ function createStepExecutors(useForwarder = false) {
10879
- return [
10880
- {
10881
- name: 'approve',
10882
- executor: async (params, provider)=>bridgeApproval({
10883
- params,
10884
- provider
10885
- })
10886
- },
10887
- {
10888
- name: 'burn',
10889
- executor: async (params, provider)=>bridgeBurn({
10890
- params,
10891
- provider
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
- * Helper function to create error step and set result state to error.
11408
+ * Executes a deposit-for-burn operation on the source chain to initiate a bridge.
10965
11409
  *
10966
- * @param stepName - The name of the step that failed
10967
- * @param error - The error that occurred
10968
- * @param result - The bridge result object to update
10969
- */ function handleStepError(stepName, error, result) {
10970
- result.state = 'error';
10971
- result.steps.push({
10972
- name: stepName,
10973
- state: 'error',
10974
- error,
10975
- errorMessage: getErrorMessage(error)
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
- * Dispatch a bridge step event through the provider's action dispatcher.
11436
+ * Fetches the attestation from Circle's API for a completed burn operation.
10981
11437
  *
10982
- * Constructs the appropriate action payload and dispatches it to any registered
10983
- * event listeners. Handles type-safe dispatching for different step types.
10984
- * When provided, traceId from the invocation context is included for end-to-end correlation.
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 name - The step name (approve, burn, fetchAttestation, or mint).
10987
- * @param step - The completed bridge step containing transaction details and explorerUrl.
10988
- * @param provider - The CCTP v2 provider with action dispatcher.
10989
- * @param invocation - Optional invocation context containing traceId for correlation.
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 step: BridgeStep = {
10994
- * name: 'burn',
10995
- * state: 'success',
10996
- * txHash: '0xabc...',
10997
- * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
10998
- * data: { ... }
10999
- * }
11000
- * dispatchStepEvent('burn', step, provider, invocationContext)
11001
- * ```
11002
- */ function dispatchStepEvent(name, step, provider, invocation) {
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
- switch(name){
11017
- case 'approve':
11018
- case 'burn':
11019
- case 'mint':
11020
- provider.actionDispatcher.dispatch(name, {
11021
- ...actionValues,
11022
- method: name
11023
- });
11024
- break;
11025
- case 'fetchAttestation':
11026
- case 'reAttest':
11027
- provider.actionDispatcher.dispatch(name, {
11028
- ...actionValues,
11029
- method: name,
11030
- values: step
11031
- });
11032
- break;
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
- * Default clock implementation.
11495
+ * Executes a mint operation on the destination chain to complete a bridge.
11038
11496
  *
11039
- * @packageDocumentation
11040
- */ /**
11041
- * Default clock implementation using `Date.now()`.
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
- * @remarks
11044
- * Use this in production code. For testing, inject a mock clock
11045
- * that returns controlled timestamps.
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
- * import { defaultClock } from '@core/runtime'
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
- */ const defaultClock = {
11056
- now: ()=>Date.now(),
11057
- since: (start)=>Date.now() - start
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
- * Create a W3C/OpenTelemetry-compatible trace ID.
11567
+ * Executes the mint step for forwarding mode where Circle's relayer handles the mint.
11068
11568
  *
11069
- * @returns 32-character lowercase hex string (128-bit).
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
- * @remarks
11072
- * **Standard function for generating `traceId` values.** Compatible with
11073
- * OpenTelemetry, Jaeger, Zipkin, and AWS X-Ray.
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 traceId = createTraceId() // "a1b2c3d4e5f6789012345678abcdef00"
11588
+ * const step = await bridgeRelayerMint(
11589
+ * { params, provider },
11590
+ * { burnTxHash: '0x...' }
11591
+ * )
11592
+ * console.log('Relayer mint tx:', step.txHash)
11078
11593
  * ```
11079
- */ function createTraceId() {
11080
- const bytes = new Uint8Array(16);
11081
- if (hasGetRandomValues()) {
11082
- crypto.getRandomValues(bytes);
11083
- } else {
11084
- for(let i = 0; i < 16; i++){
11085
- bytes[i] = Math.floor(Math.random() * 256) // NOSONAR:
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 Array.from(bytes, (b)=>b.toString(16).padStart(2, '0')).join('');
11628
+ return step;
11090
11629
  }
11091
11630
 
11092
11631
  /**
11093
- * Clean tags by removing keys with undefined values.
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
- * const empty = cleanTags(undefined)
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
- * @remarks
11123
- * Invalid patterns:
11124
- * - Empty segments (e.g., `a..b`, `.a`, `a.`)
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 pattern - The pattern to validate.
11128
- * @returns True if valid, false otherwise.
11129
- */ function isValidPattern(pattern) {
11130
- // Empty pattern is valid (matches empty topic)
11131
- if (pattern === '') return true;
11132
- const segments = pattern.split('.');
11133
- // Check for empty segments
11134
- if (segments.includes('')) return false;
11135
- // Check that ** only appears at the end
11136
- const doubleStarIndex = segments.indexOf('**');
11137
- if (doubleStarIndex !== -1 && doubleStarIndex !== segments.length - 1) {
11138
- return false;
11139
- }
11140
- return true;
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
- * Convert a pattern to a regular expression.
11733
+ * Helper function to create error step and set result state to error.
11144
11734
  *
11145
- * @param pattern - The pattern to convert.
11146
- * @returns A RegExp that matches topics according to the pattern.
11147
- */ function patternToRegex(pattern) {
11148
- const segments = pattern.split('.');
11149
- // Handle ** at end specially to match zero or more segments
11150
- const lastSegment = segments.at(-1);
11151
- if (lastSegment === '**') {
11152
- // Everything before ** must match, then optionally more segments
11153
- const prefix = segments.slice(0, -1).map((seg)=>{
11154
- if (seg === '*') return '[^.]+';
11155
- return seg.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
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
- * Execute an event handler with error isolation.
11749
+ * Dispatch a bridge step event through the provider's action dispatcher.
11174
11750
  *
11175
- * @param handler - The handler function to execute.
11176
- * @param event - The event to pass to the handler.
11177
- * @param logger - Optional logger for error reporting.
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
- * @remarks
11180
- * - Synchronous errors are caught and logged
11181
- * - Promise rejections are caught without awaiting
11182
- * - Handler errors never propagate to caller
11183
- */ function executeHandler(handler, event, logger) {
11184
- try {
11185
- const result = handler(event);
11186
- // Handle promise rejection without awaiting
11187
- if (result instanceof Promise) {
11188
- result.catch((err)=>{
11189
- logger?.error('Event handler promise rejected', {
11190
- event: event.name,
11191
- error: extractErrorInfo(err)
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
- } catch (err) {
11196
- // Isolate handler errors
11197
- logger?.error('Event handler threw', {
11198
- event: event.name,
11199
- error: extractErrorInfo(err)
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
- * Create an event bus.
11806
+ * Default clock implementation.
11205
11807
  *
11206
- * @param opts - Options for the event bus.
11207
- * @returns An EventBus instance.
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 { createEventBus } from '@core/runtime'
11818
+ * import { defaultClock } from '@core/runtime'
11212
11819
  *
11213
- * const bus = createEventBus({ logger })
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
- * // Subscribe to events
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
- * // Emit events
11221
- * bus.emit({ name: 'tx.sent', data: { txHash: '0x123' } })
11840
+ * @remarks
11841
+ * **Standard function for generating `traceId` values.** Compatible with
11842
+ * OpenTelemetry, Jaeger, Zipkin, and AWS X-Ray.
11222
11843
  *
11223
- * // Unsubscribe
11224
- * unsubscribe()
11844
+ * @example
11845
+ * ```typescript
11846
+ * const traceId = createTraceId() // "a1b2c3d4e5f6789012345678abcdef00"
11225
11847
  * ```
11226
- */ function createEventBus(opts) {
11227
- const logger = opts?.logger;
11228
- const subscriptions = new Set();
11229
- function createBus(baseTags) {
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
- * Schema for validating hexadecimal strings with '0x' prefix.
12502
- *
12503
- * This schema validates that a string:
12504
- * - Is a string type
12505
- * - Is not empty after trimming
12506
- * - Starts with '0x'
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
- * Schema for validating EVM addresses.
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
- * @example
12541
- * ```typescript
12542
- * import { evmAddressSchema } from '@core/adapter'
12943
+ * Creates or extends invocation context with the bridge caller.
12543
12944
  *
12544
- * const validAddress = '0x1234567890123456789012345678901234567890'
12945
+ * @param invocationMeta - Optional invocation metadata provided by caller.
12946
+ * @returns Resolved invocation context with bridge caller.
12545
12947
  *
12546
- * const result = evmAddressSchema.safeParse(validAddress)
12547
- * console.log(result.success) // true
12548
- * ```
12549
- */ hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
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
- * Schema for validating transaction hashes.
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
- * const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
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
- * const result = evmTransactionHashSchema.safeParse(validTxHash)
12566
- * console.log(result.success) // true
12567
- * ```
12568
- */ hexStringSchema.refine((value)=>value.length === 66, 'Transaction hash must be exactly 66 characters long (0x + 64 hex characters)');
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
- * Schema for validating EVM signatures.
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
- * const result = evmSignatureSchema.safeParse(validSignature)
12586
- * console.log(result.success) // true
12587
- * ```
12588
- */ hexStringSchema.refine((value)=>value.length === 132, 'EVM signature must be exactly 132 characters long (0x + 130 hex characters)').transform((value)=>value);
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
- * Schema for validating base58-encoded strings.
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
- * @example
12605
- * ```typescript
12606
- * import { base58StringSchema } from '@core/adapter'
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
- * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
12609
- * const validTxHash = '3Jf8k2L5mN9pQ7rS1tV4wX6yZ8aB2cD4eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5'
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
- * const addressResult = base58StringSchema.safeParse(validAddress)
12612
- * const txHashResult = base58StringSchema.safeParse(validTxHash)
12613
- * console.log(addressResult.success) // true
12614
- * console.log(txHashResult.success) // true
12615
- * ```
12616
- */ const base58StringSchema = zod.z.string().min(1, 'Base58 string is required').refine((value)=>value.trim().length > 0, 'Base58 string cannot be empty').refine((value)=>{
12617
- // Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
12618
- // Excludes: 0, O, I, l to avoid confusion
12619
- const base58Pattern = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
12620
- return base58Pattern.test(value);
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
- * Schema for validating Solana addresses.
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
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
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
- * @example
12632
- * ```typescript
12633
- * import { solanaAddressSchema } from '@core/adapter'
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
- * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
13051
+ * @param err - The error thrown by `wallet_sendCalls`.
13052
+ * @returns The derived error category, or `'unknown'` if no match.
12636
13053
  *
12637
- * const result = solanaAddressSchema.safeParse(validAddress)
12638
- * console.log(result.success) // true
12639
- * ```
12640
- */ base58StringSchema.refine((value)=>value.length >= 32 && value.length <= 44, 'Solana address must be between 32-44 characters long (base58-encoded 32-byte address)');
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
- * Schema for validating Solana transaction hashes.
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
- * const result = solanaTransactionHashSchema.safeParse(validTxHash)
12657
- * console.log(result.success) // true
12658
- * ```
12659
- */ base58StringSchema.refine((value)=>value.length >= 86 && value.length <= 88, 'Solana transaction hash must be between 86-88 characters long (base58-encoded 64-byte signature)');
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
- * This function checks if the adapter's current token balance is greater than or equal
12673
- * to the requested transaction amount. It throws a KitError with code 9001
12674
- * (BALANCE_INSUFFICIENT_TOKEN) if the balance is insufficient, providing detailed
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
- * @param params - The validation parameters containing adapter, amount, token, and token address.
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 { validateBalanceForTransaction } from '@core/adapter'
12684
- * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
12685
- * import { isKitError, ERROR_TYPES } from '@core/errors'
12686
- *
12687
- * const adapter = createViemAdapterFromPrivateKey({
12688
- * privateKey: '0x...',
12689
- * chain: 'Ethereum',
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
- * console.log('Balance validation passed')
12701
- * } catch (error) {
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
- */ const validateBalanceForTransaction = async (params)=>{
12708
- const { amount, adapter, token, tokenAddress, operationContext } = params;
12709
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
12710
- walletAddress: operationContext.address
12711
- }, operationContext);
12712
- const balance = await balancePrepared.execute();
12713
- if (BigInt(balance) < BigInt(amount)) {
12714
- // Extract chain name from operationContext
12715
- const chainName = extractChainInfo(operationContext.chain).name;
12716
- // Create KitError with rich context in trace
12717
- throw createInsufficientTokenBalanceError(chainName, token, {
12718
- balance: balance.toString(),
12719
- amount,
12720
- tokenAddress,
12721
- walletAddress: operationContext.address
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
- * Permit signature standards for gasless token approvals.
12728
- *
12729
- * Defines the permit types that can be used to approve token spending
12730
- * without requiring a separate approval transaction.
12731
- *
12732
- * @remarks
12733
- * - NONE: No permit, tokens must be pre-approved via separate transaction
12734
- * - EIP2612: Standard ERC-20 permit (USDC, DAI v2, and most modern tokens)
12735
- */ var PermitType;
12736
- (function(PermitType) {
12737
- /** No permit required - tokens must be pre-approved */ PermitType[PermitType["NONE"] = 0] = "NONE";
12738
- /** EIP-2612 standard permit */ PermitType[PermitType["EIP2612"] = 1] = "EIP2612";
12739
- })(PermitType || (PermitType = {}));
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
- // Merge configs: defaults <- global config <- per-call config
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
- // Merge configs: defaults <- global config <- per-call config
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