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