@circle-fin/provider-cctp-v2 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,106 @@
1
+ # @circle-fin/provider-cctp-v2
2
+
3
+ ## 1.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Improves the Solana CCTP v2 custom burn flow to ensure developer fee payouts never fail due to a missing associated token account (ATA). The instruction builder now checks for the existence of the developer fee recipient's ATA and, if absent, emits an idempotent creation instruction.
8
+
9
+ ## 1.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix CommonJS compatibility by correcting file extensions and package exports. This resolves a "ReferenceError: require is not defined" error that occurred when using packages in CommonJS projects with ts-node.
14
+
15
+ - Fixes a bug where custom recipient addresses were not respected in cross-chain USDC transfers. Previously, when using `recipientAddress` to specify a different destination address than the signer (common in custody solutions or when bridging to third-party addresses), the provider would incorrectly use the signer's address as the mint recipient, causing transfers to fail or mint to the wrong address.
16
+
17
+ With this fix, the provider now correctly uses `recipientAddress` when provided, while still using the signer's address for transaction authorization. This enables:
18
+ - **Third-party transfers**: Bridge USDC to any address without requiring them to sign
19
+ - **Custody integrations**: Separate signing authority from fund destination
20
+ - **Developer-controlled flows**: API-based signers can bridge to user-specified recipients
21
+
22
+ **No changes required** for existing code - the fix is backward compatible. If you don't provide `recipientAddress`, transfers work exactly as before. If you were previously experiencing failures when specifying a custom recipient, this update resolves those issues.
23
+
24
+ - Improves static gas estimate values for contract calls. Updates adapters' `prepare()` method so that transaction simulation is now performed only during `execute()`, not during `estimate()`. Adds an optional fallback parameter to `estimate()` for cases where gas estimation fails.
25
+
26
+ ## 1.0.1
27
+
28
+ ### Patch Changes
29
+
30
+ - Standardize `maxFee` parameter to accept human-readable values. The `maxFee` parameter in `BridgeConfig` now correctly accepts human-readable token amounts (e.g., `"1"` for 1 USDC, `"0.5"` for 0.5 USDC), matching the behavior of `customFee.value`. This resolves an undocumented inconsistency in the API. If you were previously passing values in smallest units, update to human-readable format: use `"1"` instead of `"1000000"` for 1 USDC.
31
+ - Add support for Arc Testnet chain definition. Arc is Circle's EVM-compatible Layer-1 blockchain designed for stablecoin finance and asset
32
+ tokenization, featuring USDC as the native gas token and sub-second finality via the Malachite BFT consensus engine.
33
+ - Fix bridge event dispatching in retry flow to include complete step metadata. The retry flow was incorrectly dispatching only step.data instead of the full step object, causing retry events to miss critical metadata like txHash, explorerUrl, name, and state. This change also introduces a shared dispatchStepEvent utility to eliminate code duplication between bridge and retry flows.
34
+ - These changes standardize and clarifiy error handling across bridge steps, make all bridge steps only need 1 blockchain confirmation when waiting for the step transaction, refactor burn transfer speed logic for simplicity, and improve polling configuration for slow attestation fetches.
35
+ - Update Sonic Testnet chain definition to canonical network. The `SonicTestnet` chain definition now points to the official Sonic Testnet (chainId: 14601) instead of the deprecated Sonic Blaze Testnet (chainId: 57054). The RPC endpoint has been updated to `https://rpc.testnet.soniclabs.com`, the display name simplified to "Sonic Testnet", and the USDC contract address updated to the new deployment.
36
+
37
+ **Breaking Changes:**
38
+ - **Chain ID:** 57054 → 14601
39
+ - **RPC Endpoint:** `https://rpc.blaze.soniclabs.com` → `https://rpc.testnet.soniclabs.com`
40
+ - **USDC Address:** `0xA4879Fed32Ecbef99399e5cbC247E533421C4eC6` → `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51`
41
+
42
+ **Migration:** If you were using `SonicTestnet`, your application will automatically connect to the new network upon upgrading. Any accounts, contracts, or transactions on the old Blaze testnet (chainId: 57054) will need to be recreated on the new testnet.
43
+
44
+ ## 1.0.0
45
+
46
+ ### Major Changes
47
+
48
+ - # CCTP v2 Provider 1.0.0 Release 🎉
49
+
50
+ Circle's Cross-Chain Transfer Protocol (CCTP) v2 integration - the primary transport layer for secure, native USDC transfers across supported blockchain networks.
51
+
52
+ ## 🚀 Core CCTP Features
53
+ - **Native USDC Bridging**: Direct integration with Circle's official CCTP v2 protocol
54
+ - **Multi-chain Support**: Seamless transfers between EVM chains and Solana
55
+ - **Attestation Management**: Automatic handling of Circle's attestation service
56
+
57
+ ## ⚡ Transfer Speed Options
58
+
59
+ **FAST Transfers**
60
+ - Optimized for speed with Circle's fast liquidity network
61
+ - Dynamic fee calculation based on transfer amount and network conditions
62
+ - Automatic fee estimation with 10% buffer for fluctuations
63
+ - Typical completion time: ~1 minute
64
+
65
+ **SLOW Transfers**
66
+ - Zero protocol fees for cost-optimized transfers
67
+ - Standard CCTP attestation flow
68
+ - Typical completion time: 10-20 minutes
69
+
70
+ ```typescript
71
+ // Fast transfer with automatic fee calculation
72
+ const result = await provider.bridge({
73
+ source: { adapter: sourceAdapter, chain: 'Ethereum' },
74
+ destination: { adapter: destAdapter, chain: 'Base' },
75
+ amount: '10.0',
76
+ config: { transferSpeed: 'FAST' },
77
+ })
78
+
79
+ // Slow transfer with zero fees
80
+ const result = await provider.bridge({
81
+ source: { adapter: sourceAdapter, chain: 'Ethereum' },
82
+ destination: { adapter: destAdapter, chain: 'Base' },
83
+ amount: '10.0',
84
+ config: { transferSpeed: 'SLOW' },
85
+ })
86
+ ```
87
+
88
+ ## 🔄 Advanced Retry Support
89
+ - **Step-by-step Recovery**: Resume from burn, attestation, or mint phases
90
+ - **Automatic State Analysis**: Intelligent detection of completion status
91
+ - **Network Resilience**: Handle temporary API and RPC failures
92
+ - **Transaction Resubmission**: Retry failed blockchain transactions
93
+
94
+ ## 💰 Fee Management
95
+ - **Dynamic Fee Calculation**: Real-time fee estimation from Circle's API
96
+ - **Custom Fee Limits**: Set maximum fees to control costs
97
+ - **Protocol Fee Transparency**: Clear breakdown of all fees involved
98
+ - **Multi-chain Fee Support**: Different fee structures per chain pair
99
+
100
+ ## 🛡️ Security & Reliability
101
+ - **No Custom Cryptography**: Uses only Circle's audited smart contracts
102
+ - **Deterministic Operations**: Predictable outcomes and gas estimation
103
+ - **Comprehensive Validation**: Runtime parameter validation and type safety
104
+ - **Production Ready**: Battle-tested protocol with institutional adoption
105
+
106
+ This provider implements Circle's official CCTP v2 specification, ensuring maximum security and compatibility with the broader USDC ecosystem.
@@ -20,7 +20,7 @@
20
20
 
21
21
  // Buffer polyfill setup - executes before any other code
22
22
  // Ensures globalThis.Buffer is available for @solana/spl-token and other Solana libraries
23
- import { Buffer } from 'buffer';
23
+ const { Buffer } = require('buffer');
24
24
  if (typeof globalThis !== 'undefined' && typeof globalThis.Buffer === 'undefined') {
25
25
  globalThis.Buffer = Buffer;
26
26
  }
@@ -30,10 +30,10 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
30
30
 
31
31
 
32
32
  var zod = require('zod');
33
+ var units = require('@ethersproject/units');
33
34
  var bytes = require('@ethersproject/bytes');
34
35
  var address = require('@ethersproject/address');
35
36
  var bs58 = require('bs58');
36
- var units = require('@ethersproject/units');
37
37
 
38
38
  function _interopDefault (e) { return e && e.__esModule ? e.default : e; }
39
39
 
@@ -2997,6 +2997,84 @@ const pollApiGet = async (url, isValidType, config) => {
2997
2997
  return pollApiWithValidation(url, 'GET', isValidType, config);
2998
2998
  };
2999
2999
 
3000
+ /**
3001
+ * Convert a value from its smallest unit representation to a human-readable decimal string.
3002
+ *
3003
+ * This function normalizes token values from their blockchain representation (where
3004
+ * everything is stored as integers in the smallest denomination) to human-readable
3005
+ * decimal format. Uses the battle-tested implementation from @ethersproject/units.
3006
+ *
3007
+ * @param value - The value in smallest units (e.g., "1000000" for 1 USDC with 6 decimals)
3008
+ * @param decimals - The number of decimal places for the unit conversion
3009
+ * @returns A human-readable decimal string (e.g., "1.0")
3010
+ * @throws Error if the value is not a valid numeric string
3011
+ *
3012
+ * @example
3013
+ * ```typescript
3014
+ * import { formatUnits } from '@core/utils'
3015
+ *
3016
+ * // Format USDC (6 decimals)
3017
+ * const usdcFormatted = formatUnits('1000000', 6)
3018
+ * console.log(usdcFormatted) // "1.0"
3019
+ *
3020
+ * // Format ETH (18 decimals)
3021
+ * const ethFormatted = formatUnits('1000000000000000000', 18)
3022
+ * console.log(ethFormatted) // "1.0"
3023
+ *
3024
+ * // Format with fractional part
3025
+ * const fractionalFormatted = formatUnits('1500000', 6)
3026
+ * console.log(fractionalFormatted) // "1.5"
3027
+ * ```
3028
+ */
3029
+ const formatUnits = (value, decimals) => {
3030
+ return units.formatUnits(value, decimals);
3031
+ };
3032
+
3033
+ /**
3034
+ * Format a token amount into a human-readable decimal string.
3035
+ *
3036
+ * Accepts a smallest-unit string and either assumes USDC's 6 decimals or derives the
3037
+ * native decimals from the provided chain definition. Delegates to {@link formatUnits}
3038
+ * to preserve consistent rounding and formatting behaviour across the SDK.
3039
+ *
3040
+ * @remarks
3041
+ * When `token` is `'native'`, supply a chain identifier that {@link resolveChainIdentifier}
3042
+ * can resolve so the native currency decimals can be determined.
3043
+ *
3044
+ * @param params - The formatting input including the raw value and token selector.
3045
+ * @returns The decimal string representation of the amount.
3046
+ * @throws Error if the value cannot be parsed or if the chain identifier is unknown.
3047
+ *
3048
+ * @example
3049
+ * ```typescript
3050
+ * import { formatAmount } from '@core/utils'
3051
+ * import { Ethereum } from '@core/chains'
3052
+ *
3053
+ * const usdcAmount = formatAmount({ value: '1000000', token: 'USDC' })
3054
+ * console.log(usdcAmount) // "1"
3055
+ *
3056
+ * const ethAmount = formatAmount({
3057
+ * value: '3141592000000000000',
3058
+ * token: 'native',
3059
+ * chain: Ethereum,
3060
+ * })
3061
+ * console.log(ethAmount) // "3.141592"
3062
+ * ```
3063
+ */
3064
+ const formatAmount = (params) => {
3065
+ const { value, token } = params;
3066
+ switch (token) {
3067
+ case 'USDC':
3068
+ return formatUnits(value, 6);
3069
+ case 'native':
3070
+ return formatUnits(value, resolveChainIdentifier(params.chain).nativeCurrency.decimals);
3071
+ default:
3072
+ // This will cause a compile-time error if a new token type is added to
3073
+ // `FormatAmountParams` but not handled in this switch statement, ensuring exhaustiveness.
3074
+ throw new Error(`formatAmount: Unhandled token type: ${token}`);
3075
+ }
3076
+ };
3077
+
3000
3078
  /**
3001
3079
  * Custom error class for insufficient funds errors.
3002
3080
  * Provides structured error information while hiding implementation details.
@@ -3004,7 +3082,7 @@ const pollApiGet = async (url, isValidType, config) => {
3004
3082
  class InsufficientFundsError extends Error {
3005
3083
  balance;
3006
3084
  amount;
3007
- tokenName;
3085
+ token;
3008
3086
  tokenAddress;
3009
3087
  chain;
3010
3088
  /**
@@ -3012,16 +3090,27 @@ class InsufficientFundsError extends Error {
3012
3090
  *
3013
3091
  * @param balance - The current token balance in the wallet (smallest unit).
3014
3092
  * @param amount - The required transaction amount (smallest unit).
3015
- * @param tokenName - The human-readable token name (e.g., 'USDC').
3093
+ * @param token - The token ('USDC' or 'native').
3016
3094
  * @param tokenAddress - The contract address of the token.
3017
3095
  * @param chain - The blockchain network name where the transaction failed.
3018
3096
  */
3019
- constructor(balance, amount, tokenName, tokenAddress, chain) {
3020
- const message = `Insufficient funds for ${tokenName} (${tokenAddress}) transaction on ${chain}. Balance: ${balance} ${tokenName}, is less than transaction amount: ${amount} ${tokenName}.`;
3097
+ constructor(balance, amount, token, tokenAddress, chain) {
3098
+ const formattedBalance = formatAmount({
3099
+ value: balance,
3100
+ token,
3101
+ chain,
3102
+ });
3103
+ const formattedAmount = formatAmount({
3104
+ value: amount,
3105
+ token,
3106
+ chain,
3107
+ });
3108
+ const chainName = typeof chain === 'object' && 'name' in chain ? chain.name : chain;
3109
+ const message = `Insufficient funds for ${token} (${tokenAddress}) transaction on ${chainName}. Balance: ${formattedBalance} ${token}, is less than transaction amount: ${formattedAmount} ${token}.`;
3021
3110
  super(message);
3022
3111
  this.balance = balance;
3023
3112
  this.amount = amount;
3024
- this.tokenName = tokenName;
3113
+ this.token = token;
3025
3114
  this.tokenAddress = tokenAddress;
3026
3115
  this.chain = chain;
3027
3116
  this.name = 'InsufficientFundsError';
@@ -3400,39 +3489,6 @@ const convertAddress = (address, targetFormat) => {
3400
3489
  throw new Error(`Unsupported address format: ${address}`);
3401
3490
  };
3402
3491
 
3403
- /**
3404
- * Convert a value from its smallest unit representation to a human-readable decimal string.
3405
- *
3406
- * This function normalizes token values from their blockchain representation (where
3407
- * everything is stored as integers in the smallest denomination) to human-readable
3408
- * decimal format. Uses the battle-tested implementation from @ethersproject/units.
3409
- *
3410
- * @param value - The value in smallest units (e.g., "1000000" for 1 USDC with 6 decimals)
3411
- * @param decimals - The number of decimal places for the unit conversion
3412
- * @returns A human-readable decimal string (e.g., "1.0")
3413
- * @throws Error if the value is not a valid numeric string
3414
- *
3415
- * @example
3416
- * ```typescript
3417
- * import { formatUnits } from '@core/utils'
3418
- *
3419
- * // Format USDC (6 decimals)
3420
- * const usdcFormatted = formatUnits('1000000', 6)
3421
- * console.log(usdcFormatted) // "1.0"
3422
- *
3423
- * // Format ETH (18 decimals)
3424
- * const ethFormatted = formatUnits('1000000000000000000', 18)
3425
- * console.log(ethFormatted) // "1.0"
3426
- *
3427
- * // Format with fractional part
3428
- * const fractionalFormatted = formatUnits('1500000', 6)
3429
- * console.log(fractionalFormatted) // "1.5"
3430
- * ```
3431
- */
3432
- const formatUnits = (value, decimals) => {
3433
- return units.formatUnits(value, decimals);
3434
- };
3435
-
3436
3492
  /**
3437
3493
  * Build a complete explorer URL from a chain definition and transaction hash.
3438
3494
  *
@@ -3858,9 +3914,11 @@ async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet)
3858
3914
  * amount and invalid atttestation messages.
3859
3915
  *
3860
3916
  * These values were obtained by averaging gas for the function call from the last 3 months.
3917
+ * see: https://dune.com/queries/6022210/9697710/
3861
3918
  */
3862
- const DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM = 124527n;
3863
- const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 177778n;
3919
+ const DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM = 169914n; // (99p: 111_521n + max: 226_506n) / 2 = 169_914n
3920
+ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201525n; // p99 and max are same here: 201_525n
3921
+ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237401n; // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
3864
3922
  /**
3865
3923
  * The minimum finality threshold for CCTPv2 transfers.
3866
3924
  *
@@ -4675,7 +4733,9 @@ async function assertCCTPv2AttestationParams(attestation, params) {
4675
4733
  const errors = [];
4676
4734
  const message = attestation.decodedMessage;
4677
4735
  const messageBody = message.decodedMessageBody;
4678
- const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, params.destination.address, params.destination.chain.usdcAddress);
4736
+ // Use recipientAddress if provided, otherwise use destination.address
4737
+ const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
4738
+ const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
4679
4739
  let sender;
4680
4740
  if (hasCustomContractSupport(params.source.chain, 'bridge')) {
4681
4741
  if (params.source.chain.type === 'solana') {
@@ -4933,6 +4993,35 @@ async function bridgeMint({ params, provider, }, attestation) {
4933
4993
  request: await provider.mint(params.source, params.destination, attestation),
4934
4994
  });
4935
4995
  }
4996
+ const mockAttestationMessage = {
4997
+ attestation: '0x8bd9b9e63eb05128eb2896b63e3e1df39bfd6bbfb893b69dc53c39252aeb85df1ded70263b07da17abf88e6e1b77f16ebcdb4eb1c6b4ea4c625215e5cb9dddb81bffe0b9d75e2094f05e48f18f70d0b254999bde90bab26f5c0da29b2d7e00feca1cfed119cba0d2a0e887648a40e803e0ca34aa8fd94ce068515eeaef72b520aa1b',
4998
+ message: '0x000000010000000000000006f446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f30000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000003e8000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
4999
+ eventNonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
5000
+ cctpVersion: 2,
5001
+ status: 'complete',
5002
+ decodedMessage: {
5003
+ sourceDomain: '0',
5004
+ destinationDomain: '6',
5005
+ nonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
5006
+ sender: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5007
+ recipient: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5008
+ destinationCaller: '0x0000000000000000000000000000000000000000000000000000000000000000',
5009
+ minFinalityThreshold: '1000',
5010
+ finalityThresholdExecuted: '1000',
5011
+ messageBody: '0x000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
5012
+ decodedMessageBody: {
5013
+ burnToken: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238',
5014
+ mintRecipient: '0x23f9a5bea7b92a0638520607407bc7f0310aeed4',
5015
+ amount: '100000',
5016
+ messageSender: '0xc5567a5e3370d4dbfb0540025078e283e36a363d',
5017
+ maxFee: '11',
5018
+ feeExecuted: '10',
5019
+ expirationBlock: '32944472',
5020
+ hookData: null,
5021
+ },
5022
+ },
5023
+ delayReason: null,
5024
+ };
4936
5025
 
4937
5026
  /**
4938
5027
  * Ordered sequence of CCTP v2 bridge step executors.
@@ -5430,10 +5519,12 @@ zod.z.object({
5430
5519
  */
5431
5520
  const validateBalanceForTransaction = async (params) => {
5432
5521
  const { amount, adapter, token, tokenAddress, operationContext } = params;
5433
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {}, operationContext);
5522
+ const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
5523
+ walletAddress: operationContext.address,
5524
+ }, operationContext);
5434
5525
  const balance = await balancePrepared.execute();
5435
5526
  if (BigInt(balance) < BigInt(amount)) {
5436
- throw new InsufficientFundsError(balance.toString(), amount, token, tokenAddress, resolveChainIdentifier(operationContext.chain).chain);
5527
+ throw new InsufficientFundsError(balance.toString(), amount, token, tokenAddress, operationContext.chain);
5437
5528
  }
5438
5529
  };
5439
5530
 
@@ -5997,22 +6088,22 @@ class CCTPV2BridgingProvider extends BridgingProvider {
5997
6088
  async estimate(params) {
5998
6089
  // CCTP-specific transfer params validation (includes base validation)
5999
6090
  assertCCTPv2BridgeParams(params);
6000
- const { source, destination, amount, token } = params;
6001
- // Extract operation context from source wallet context for balance validation
6002
- const operationContext = this.extractOperationContext(source);
6003
- // Validate balance for transaction
6004
- await validateBalanceForTransaction({
6005
- amount,
6006
- token,
6007
- tokenAddress: source.chain.usdcAddress,
6008
- adapter: source.adapter,
6009
- operationContext,
6010
- });
6091
+ const { source, destination, amount } = params;
6092
+ const estimateBurn = async () => {
6093
+ const burn = await this.burn(params);
6094
+ return await burn.estimate(undefined, await source.adapter.calculateTransactionFee(hasCustomContractSupport(source.chain, 'bridge')
6095
+ ? CUSTOM_BURN_GAS_ESTIMATE_EVM
6096
+ : DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM, undefined, source.chain));
6097
+ };
6098
+ const estimateMint = async () => {
6099
+ const mint = await this.mint(source, destination, mockAttestationMessage);
6100
+ return await mint.estimate(undefined, await destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, undefined, destination.chain));
6101
+ };
6011
6102
  // Parallelize all independent async operations
6012
6103
  const [approveEstimate, depositForBurnFee, receiveMessageFee, maxFee] = await Promise.allSettled([
6013
6104
  this.approve(source, amount).then(async (approve) => approve.estimate()),
6014
- source.adapter.calculateTransactionFee(DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM, 500n, source.chain),
6015
- destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, 500n, destination.chain),
6105
+ estimateBurn(),
6106
+ estimateMint(),
6016
6107
  params.config?.transferSpeed === undefined ||
6017
6108
  params.config?.transferSpeed === TransferSpeed.FAST
6018
6109
  ? this.getMaxFee(params)
@@ -6179,14 +6270,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6179
6270
  delegate: spenderAddress,
6180
6271
  chain: resolvedContext?.chain ?? chain,
6181
6272
  };
6182
- // Single call with or without context
6183
- if (resolvedContext) {
6184
- return await adapter.prepareAction('usdc.increaseAllowance', actionParams, resolvedContext);
6185
- }
6186
- else {
6187
- // Legacy fallback for adapters without capabilities
6188
- return await adapter.prepareAction('usdc.increaseAllowance', actionParams);
6189
- }
6273
+ return await adapter.prepareAction('usdc.increaseAllowance', actionParams, resolvedContext);
6190
6274
  }
6191
6275
  /**
6192
6276
  * Prepares a CCTP v2 token minting transaction on the destination chain.
@@ -6230,14 +6314,8 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6230
6314
  assertCCTPv2WalletContext(destination);
6231
6315
  // Extract operation context from destination wallet context
6232
6316
  const operationContext = this.extractOperationContext(destination);
6233
- // Resolve operation context to get concrete chain and address values
6234
- let resolvedContext;
6235
- try {
6236
- resolvedContext = await resolveOperationContext(destination.adapter, operationContext);
6237
- }
6238
- catch (error) {
6239
- throw new Error(`Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
6240
- }
6317
+ // Use recipientAddress if provided, otherwise use destination.address
6318
+ const destinationAddressForMint = destination.recipientAddress ?? destination.address;
6241
6319
  // Prepare action parameters
6242
6320
  const actionParams = {
6243
6321
  fromChain: source.chain,
@@ -6245,16 +6323,9 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6245
6323
  message: attestation.message,
6246
6324
  attestation: attestation.attestation,
6247
6325
  eventNonce: attestation.eventNonce,
6248
- destinationAddress: destination.address,
6326
+ destinationAddress: destinationAddressForMint,
6249
6327
  };
6250
- // Single call with or without context
6251
- if (resolvedContext) {
6252
- return await destination.adapter.prepareAction('cctp.v2.receiveMessage', actionParams, resolvedContext);
6253
- }
6254
- else {
6255
- // Legacy fallback for adapters without capabilities
6256
- return await destination.adapter.prepareAction('cctp.v2.receiveMessage', actionParams);
6257
- }
6328
+ return await destination.adapter.prepareAction('cctp.v2.receiveMessage', actionParams, operationContext);
6258
6329
  }
6259
6330
  /**
6260
6331
  * Fetches attestation data for a burn transaction from the IRIS API.
@@ -6453,10 +6524,11 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6453
6524
  // Get the min finality threshold based on the transfer speed
6454
6525
  const minFinalityThreshold = CCTPv2MinFinalityThreshold[transferSpeed];
6455
6526
  // Calculate the max fee
6527
+ // Use recipientAddress if provided, otherwise use destination.address
6528
+ const destinationAddressForMint = destination.recipientAddress ?? destination.address;
6456
6529
  const [maxFee, mintRecipient] = await Promise.all([
6457
6530
  this.getMaxFee(params),
6458
- getMintRecipientAccount(destination.chain.type, destination.address ??
6459
- (await destination.adapter.getAddress(destination.chain)), destination.chain.usdcAddress),
6531
+ getMintRecipientAccount(destination.chain.type, destinationAddressForMint, destination.chain.usdcAddress),
6460
6532
  ]);
6461
6533
  const actionParams = {
6462
6534
  fromChain: source.chain,
@@ -6486,14 +6558,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6486
6558
  const finalParams = useCustomBurn
6487
6559
  ? { ...actionParams, feeRecipient: recipientAddress, protocolFee }
6488
6560
  : actionParams;
6489
- // Single call with or without context
6490
- if (resolvedContext) {
6491
- return await source.adapter.prepareAction(actionType, finalParams, resolvedContext);
6492
- }
6493
- else {
6494
- // Legacy fallback for adapters without capabilities
6495
- return await source.adapter.prepareAction(actionType, finalParams);
6496
- }
6561
+ return await source.adapter.prepareAction(actionType, finalParams, resolvedContext);
6497
6562
  }
6498
6563
  /**
6499
6564
  * Waits for a transaction to be mined and confirmed on the blockchain.
@@ -6523,4 +6588,4 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6523
6588
 
6524
6589
  exports.CCTPV2BridgingProvider = CCTPV2BridgingProvider;
6525
6590
  exports.getMintRecipientAccount = getMintRecipientAccount;
6526
- //# sourceMappingURL=index.cjs.js.map
6591
+ //# sourceMappingURL=index.cjs.map
package/index.cjs.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/index.d.ts CHANGED
@@ -594,10 +594,11 @@ interface EvmPreparedChainRequest {
594
594
  * Estimate the gas cost for the contract execution.
595
595
  *
596
596
  * @param overrides - Optional parameters to override the default estimation behavior
597
+ * @param fallback - Optional fallback gas information to use if the estimation fails
597
598
  * @returns A promise that resolves to the estimated gas information
598
599
  * @throws If the estimation fails
599
600
  */
600
- estimate(overrides?: EvmEstimateOverrides): Promise<EstimatedGas>;
601
+ estimate(overrides?: EvmEstimateOverrides, fallback?: EstimatedGas): Promise<EstimatedGas>;
601
602
  /**
602
603
  * Execute the prepared contract call.
603
604
  *
@@ -675,7 +676,7 @@ interface SolanaPreparedChainRequest {
675
676
  /** The type of the chain request. */
676
677
  type: 'solana';
677
678
  /** Estimate the compute units and fee for the transaction. */
678
- estimate(overrides?: SolanaEstimateOverrides): Promise<EstimatedGas>;
679
+ estimate(overrides?: SolanaEstimateOverrides, fallback?: EstimatedGas): Promise<EstimatedGas>;
679
680
  /** Execute the prepared transaction. */
680
681
  execute(overrides?: SolanaExecuteOverrides): Promise<string>;
681
682
  }
@@ -707,7 +708,7 @@ interface NoopPreparedChainRequest {
707
708
  * Placeholder for the estimate method.
708
709
  * @returns The estimated gas cost.
709
710
  */
710
- estimate: () => Promise<EstimatedGas>;
711
+ estimate: (overrides?: EvmEstimateOverrides | SolanaEstimateOverrides, fallback?: EstimatedGas) => Promise<EstimatedGas>;
711
712
  /**
712
713
  * Placeholder for the execute method.
713
714
  * @returns The transaction hash.
@@ -1965,10 +1966,23 @@ interface AdapterCapabilities {
1965
1966
  */
1966
1967
  declare abstract class Adapter<TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities> {
1967
1968
  /**
1968
- * The type of the chain.
1969
- * @example 'evm', 'solana'
1969
+ * The type of the chain for this adapter.
1970
+ *
1971
+ * - For concrete adapters, this should be a real chain type (e.g., `'evm'`, `'solana'`, etc.) from the ChainType union.
1972
+ * - For hybrid adapters (adapters that route to concrete adapters supporting multiple ecosystems),
1973
+ * set this property to the string literal `'hybrid'`.
1974
+ *
1975
+ * Note: `'hybrid'` is not a legal ChainType and should only be used as a marker for multi-ecosystem adapters.
1976
+ * Hybrid adapters do not interact directly with any chain, but instead route requests to a concrete underlying adapter.
1977
+ *
1978
+ * @example
1979
+ * // For an EVM-only adapter:
1980
+ * chainType = 'evm'
1981
+ *
1982
+ * // For a hybrid adapter:
1983
+ * chainType = 'hybrid'
1970
1984
  */
1971
- abstract chainType: ChainType;
1985
+ abstract chainType: ChainType | 'hybrid';
1972
1986
  /**
1973
1987
  * Capabilities of this adapter, defining address control model and supported chains.
1974
1988
  *
@@ -2050,7 +2064,7 @@ declare abstract class Adapter<TAdapterCapabilities extends AdapterCapabilities
2050
2064
  * })
2051
2065
  * ```
2052
2066
  */
2053
- prepareAction<TActionKey extends ActionKeys>(action: TActionKey, params: ActionPayload<TActionKey>, ctx?: OperationContext<TAdapterCapabilities>): Promise<PreparedChainRequest>;
2067
+ prepareAction<TActionKey extends ActionKeys>(action: TActionKey, params: ActionPayload<TActionKey>, ctx: OperationContext<TAdapterCapabilities>): Promise<PreparedChainRequest>;
2054
2068
  /**
2055
2069
  * Prepares a transaction for future gas estimation and execution.
2056
2070
  *
@@ -2435,6 +2449,49 @@ TChainDefinition extends ChainDefinition = ChainDefinition> {
2435
2449
  */
2436
2450
  chain: TChainDefinition;
2437
2451
  }
2452
+ /**
2453
+ * Wallet context for bridge destinations with optional custom recipient.
2454
+ *
2455
+ * Extends WalletContext to support scenarios where the recipient address
2456
+ * differs from the signer address (e.g., bridging to a third-party wallet).
2457
+ * The signer address is used for transaction authorization, while the
2458
+ * recipient address specifies where the minted funds should be sent.
2459
+ *
2460
+ * @typeParam TAdapterCapabilities - The adapter capabilities type to use for the wallet context.
2461
+ * @typeParam TChainDefinition - The chain definition type to use for the wallet context.
2462
+ *
2463
+ * @example
2464
+ * ```typescript
2465
+ * import type { DestinationWalletContext } from '@core/provider'
2466
+ * import { adapter, blockchain } from './setup'
2467
+ *
2468
+ * // Bridge to a custom recipient address
2469
+ * const destination: DestinationWalletContext = {
2470
+ * adapter,
2471
+ * address: '0x1234...abcd', // Signer address
2472
+ * chain: blockchain,
2473
+ * recipientAddress: '0x9876...fedc' // Custom recipient
2474
+ * }
2475
+ * ```
2476
+ */
2477
+ interface DestinationWalletContext<TAdapterCapabilities extends AdapterCapabilities = AdapterCapabilities,
2478
+ /**
2479
+ * The chain definition type to use for the wallet context.
2480
+ *
2481
+ * @defaultValue ChainDefinition
2482
+ */
2483
+ TChainDefinition extends ChainDefinition = ChainDefinition> extends WalletContext<TAdapterCapabilities, TChainDefinition> {
2484
+ /**
2485
+ * Optional custom recipient address for minted funds.
2486
+ *
2487
+ * When provided, minted tokens will be sent to this address instead of
2488
+ * the address specified in the wallet context. The wallet context address
2489
+ * is still used for transaction signing and authorization.
2490
+ *
2491
+ * Must be a valid address format for the specified blockchain.
2492
+ */
2493
+ recipientAddress?: string;
2494
+ }
2438
2495
  /**
2439
2496
  * Parameters for executing a cross-chain bridge operation.
2440
2497
  */
@@ -2448,7 +2505,7 @@ TChainDefinition extends ChainDefinition = ChainDefinition> {
2448
2505
  /** The source adapter containing wallet and chain information */
2449
2506
  source: WalletContext<TFromCapabilities, TChainDefinition>;
2450
2507
  /** The destination adapter containing wallet and chain information */
2451
- destination: WalletContext<TToCapabilities, TChainDefinition>;
2508
+ destination: DestinationWalletContext<TToCapabilities, TChainDefinition>;
2452
2509
  /** The amount to transfer (as a string to avoid precision issues) */
2453
2510
  amount: string;
2454
2511
  /** The token to transfer (currently only USDC is supported) */
@@ -3150,7 +3207,7 @@ type CCTPV2BridgeParams<TFromAdapterCapabilities extends AdapterCapabilities = A
3150
3207
  /**
3151
3208
  * The destination wallet context containing the chain definition and wallet address.
3152
3209
  */
3153
- destination: WalletContext<TToAdapterCapabilities, ChainDefinitionWithCCTPv2>;
3210
+ destination: DestinationWalletContext<TToAdapterCapabilities, ChainDefinitionWithCCTPv2>;
3154
3211
  /**
3155
3212
  * The bridge configuration for the CCTPv2 transfer.
3156
3213
  */
@@ -3523,7 +3580,7 @@ declare class CCTPV2BridgingProvider extends BridgingProvider<CCTPV2Actions> imp
3523
3580
  * console.log('Mint transaction:', txHash)
3524
3581
  * ```
3525
3582
  */
3526
- mint<TFromAdapterCapabilities extends AdapterCapabilities, TToAdapterCapabilities extends AdapterCapabilities>(source: WalletContext<TFromAdapterCapabilities, ChainDefinitionWithCCTPv2>, destination: WalletContext<TToAdapterCapabilities, ChainDefinitionWithCCTPv2>, attestation: AttestationMessage): Promise<PreparedChainRequest>;
3583
+ mint<TFromAdapterCapabilities extends AdapterCapabilities, TToAdapterCapabilities extends AdapterCapabilities>(source: WalletContext<TFromAdapterCapabilities, ChainDefinitionWithCCTPv2>, destination: BridgeParams<TFromAdapterCapabilities, TToAdapterCapabilities>['destination'], attestation: AttestationMessage): Promise<PreparedChainRequest>;
3527
3584
  /**
3528
3585
  * Fetches attestation data for a burn transaction from the IRIS API.
3529
3586
  *
package/index.mjs CHANGED
@@ -28,10 +28,10 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
28
28
 
29
29
 
30
30
  import { z } from 'zod';
31
+ import { formatUnits as formatUnits$1 } from '@ethersproject/units';
31
32
  import { hexlify, hexZeroPad } from '@ethersproject/bytes';
32
33
  import { getAddress } from '@ethersproject/address';
33
34
  import bs58 from 'bs58';
34
- import { formatUnits as formatUnits$1 } from '@ethersproject/units';
35
35
 
36
36
  // -----------------------------------------------------------------------------
37
37
  // Blockchain Enum
@@ -2991,6 +2991,84 @@ const pollApiGet = async (url, isValidType, config) => {
2991
2991
  return pollApiWithValidation(url, 'GET', isValidType, config);
2992
2992
  };
2993
2993
 
2994
+ /**
2995
+ * Convert a value from its smallest unit representation to a human-readable decimal string.
2996
+ *
2997
+ * This function normalizes token values from their blockchain representation (where
2998
+ * everything is stored as integers in the smallest denomination) to human-readable
2999
+ * decimal format. Uses the battle-tested implementation from @ethersproject/units.
3000
+ *
3001
+ * @param value - The value in smallest units (e.g., "1000000" for 1 USDC with 6 decimals)
3002
+ * @param decimals - The number of decimal places for the unit conversion
3003
+ * @returns A human-readable decimal string (e.g., "1.0")
3004
+ * @throws Error if the value is not a valid numeric string
3005
+ *
3006
+ * @example
3007
+ * ```typescript
3008
+ * import { formatUnits } from '@core/utils'
3009
+ *
3010
+ * // Format USDC (6 decimals)
3011
+ * const usdcFormatted = formatUnits('1000000', 6)
3012
+ * console.log(usdcFormatted) // "1.0"
3013
+ *
3014
+ * // Format ETH (18 decimals)
3015
+ * const ethFormatted = formatUnits('1000000000000000000', 18)
3016
+ * console.log(ethFormatted) // "1.0"
3017
+ *
3018
+ * // Format with fractional part
3019
+ * const fractionalFormatted = formatUnits('1500000', 6)
3020
+ * console.log(fractionalFormatted) // "1.5"
3021
+ * ```
3022
+ */
3023
+ const formatUnits = (value, decimals) => {
3024
+ return formatUnits$1(value, decimals);
3025
+ };
3026
+
3027
+ /**
3028
+ * Format a token amount into a human-readable decimal string.
3029
+ *
3030
+ * Accepts a smallest-unit string and either assumes USDC's 6 decimals or derives the
3031
+ * native decimals from the provided chain definition. Delegates to {@link formatUnits}
3032
+ * to preserve consistent rounding and formatting behaviour across the SDK.
3033
+ *
3034
+ * @remarks
3035
+ * When `token` is `'native'`, supply a chain identifier that {@link resolveChainIdentifier}
3036
+ * can resolve so the native currency decimals can be determined.
3037
+ *
3038
+ * @param params - The formatting input including the raw value and token selector.
3039
+ * @returns The decimal string representation of the amount.
3040
+ * @throws Error if the value cannot be parsed or if the chain identifier is unknown.
3041
+ *
3042
+ * @example
3043
+ * ```typescript
3044
+ * import { formatAmount } from '@core/utils'
3045
+ * import { Ethereum } from '@core/chains'
3046
+ *
3047
+ * const usdcAmount = formatAmount({ value: '1000000', token: 'USDC' })
3048
+ * console.log(usdcAmount) // "1"
3049
+ *
3050
+ * const ethAmount = formatAmount({
3051
+ * value: '3141592000000000000',
3052
+ * token: 'native',
3053
+ * chain: Ethereum,
3054
+ * })
3055
+ * console.log(ethAmount) // "3.141592"
3056
+ * ```
3057
+ */
3058
+ const formatAmount = (params) => {
3059
+ const { value, token } = params;
3060
+ switch (token) {
3061
+ case 'USDC':
3062
+ return formatUnits(value, 6);
3063
+ case 'native':
3064
+ return formatUnits(value, resolveChainIdentifier(params.chain).nativeCurrency.decimals);
3065
+ default:
3066
+ // This will cause a compile-time error if a new token type is added to
3067
+ // `FormatAmountParams` but not handled in this switch statement, ensuring exhaustiveness.
3068
+ throw new Error(`formatAmount: Unhandled token type: ${token}`);
3069
+ }
3070
+ };
3071
+
2994
3072
  /**
2995
3073
  * Custom error class for insufficient funds errors.
2996
3074
  * Provides structured error information while hiding implementation details.
@@ -2998,7 +3076,7 @@ const pollApiGet = async (url, isValidType, config) => {
2998
3076
  class InsufficientFundsError extends Error {
2999
3077
  balance;
3000
3078
  amount;
3001
- tokenName;
3079
+ token;
3002
3080
  tokenAddress;
3003
3081
  chain;
3004
3082
  /**
@@ -3006,16 +3084,27 @@ class InsufficientFundsError extends Error {
3006
3084
  *
3007
3085
  * @param balance - The current token balance in the wallet (smallest unit).
3008
3086
  * @param amount - The required transaction amount (smallest unit).
3009
- * @param tokenName - The human-readable token name (e.g., 'USDC').
3087
+ * @param token - The token ('USDC' or 'native').
3010
3088
  * @param tokenAddress - The contract address of the token.
3011
3089
  * @param chain - The blockchain network name where the transaction failed.
3012
3090
  */
3013
- constructor(balance, amount, tokenName, tokenAddress, chain) {
3014
- const message = `Insufficient funds for ${tokenName} (${tokenAddress}) transaction on ${chain}. Balance: ${balance} ${tokenName}, is less than transaction amount: ${amount} ${tokenName}.`;
3091
+ constructor(balance, amount, token, tokenAddress, chain) {
3092
+ const formattedBalance = formatAmount({
3093
+ value: balance,
3094
+ token,
3095
+ chain,
3096
+ });
3097
+ const formattedAmount = formatAmount({
3098
+ value: amount,
3099
+ token,
3100
+ chain,
3101
+ });
3102
+ const chainName = typeof chain === 'object' && 'name' in chain ? chain.name : chain;
3103
+ const message = `Insufficient funds for ${token} (${tokenAddress}) transaction on ${chainName}. Balance: ${formattedBalance} ${token}, is less than transaction amount: ${formattedAmount} ${token}.`;
3015
3104
  super(message);
3016
3105
  this.balance = balance;
3017
3106
  this.amount = amount;
3018
- this.tokenName = tokenName;
3107
+ this.token = token;
3019
3108
  this.tokenAddress = tokenAddress;
3020
3109
  this.chain = chain;
3021
3110
  this.name = 'InsufficientFundsError';
@@ -3394,39 +3483,6 @@ const convertAddress = (address, targetFormat) => {
3394
3483
  throw new Error(`Unsupported address format: ${address}`);
3395
3484
  };
3396
3485
 
3397
- /**
3398
- * Convert a value from its smallest unit representation to a human-readable decimal string.
3399
- *
3400
- * This function normalizes token values from their blockchain representation (where
3401
- * everything is stored as integers in the smallest denomination) to human-readable
3402
- * decimal format. Uses the battle-tested implementation from @ethersproject/units.
3403
- *
3404
- * @param value - The value in smallest units (e.g., "1000000" for 1 USDC with 6 decimals)
3405
- * @param decimals - The number of decimal places for the unit conversion
3406
- * @returns A human-readable decimal string (e.g., "1.0")
3407
- * @throws Error if the value is not a valid numeric string
3408
- *
3409
- * @example
3410
- * ```typescript
3411
- * import { formatUnits } from '@core/utils'
3412
- *
3413
- * // Format USDC (6 decimals)
3414
- * const usdcFormatted = formatUnits('1000000', 6)
3415
- * console.log(usdcFormatted) // "1.0"
3416
- *
3417
- * // Format ETH (18 decimals)
3418
- * const ethFormatted = formatUnits('1000000000000000000', 18)
3419
- * console.log(ethFormatted) // "1.0"
3420
- *
3421
- * // Format with fractional part
3422
- * const fractionalFormatted = formatUnits('1500000', 6)
3423
- * console.log(fractionalFormatted) // "1.5"
3424
- * ```
3425
- */
3426
- const formatUnits = (value, decimals) => {
3427
- return formatUnits$1(value, decimals);
3428
- };
3429
-
3430
3486
  /**
3431
3487
  * Build a complete explorer URL from a chain definition and transaction hash.
3432
3488
  *
@@ -3852,9 +3908,11 @@ async function fetchUsdcFastBurnFee(sourceDomain, destinationDomain, isTestnet)
3852
3908
  * amount and invalid atttestation messages.
3853
3909
  *
3854
3910
  * These values were obtained by averaging gas for the function call from the last 3 months.
3911
+ * see: https://dune.com/queries/6022210/9697710/
3855
3912
  */
3856
- const DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM = 124527n;
3857
- const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 177778n;
3913
+ const DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM = 169914n; // (99p: 111_521n + max: 226_506n) / 2 = 169_914n
3914
+ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201525n; // p99 and max are same here: 201_525n
3915
+ const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237401n; // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
3858
3916
  /**
3859
3917
  * The minimum finality threshold for CCTPv2 transfers.
3860
3918
  *
@@ -4669,7 +4727,9 @@ async function assertCCTPv2AttestationParams(attestation, params) {
4669
4727
  const errors = [];
4670
4728
  const message = attestation.decodedMessage;
4671
4729
  const messageBody = message.decodedMessageBody;
4672
- const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, params.destination.address, params.destination.chain.usdcAddress);
4730
+ // Use recipientAddress if provided, otherwise use destination.address
4731
+ const destinationAddressForMint = params.destination.recipientAddress ?? params.destination.address;
4732
+ const mintRecipient = await getMintRecipientAccount(params.destination.chain.type, destinationAddressForMint, params.destination.chain.usdcAddress);
4673
4733
  let sender;
4674
4734
  if (hasCustomContractSupport(params.source.chain, 'bridge')) {
4675
4735
  if (params.source.chain.type === 'solana') {
@@ -4927,6 +4987,35 @@ async function bridgeMint({ params, provider, }, attestation) {
4927
4987
  request: await provider.mint(params.source, params.destination, attestation),
4928
4988
  });
4929
4989
  }
4990
+ const mockAttestationMessage = {
4991
+ attestation: '0x8bd9b9e63eb05128eb2896b63e3e1df39bfd6bbfb893b69dc53c39252aeb85df1ded70263b07da17abf88e6e1b77f16ebcdb4eb1c6b4ea4c625215e5cb9dddb81bffe0b9d75e2094f05e48f18f70d0b254999bde90bab26f5c0da29b2d7e00feca1cfed119cba0d2a0e887648a40e803e0ca34aa8fd94ce068515eeaef72b520aa1b',
4992
+ message: '0x000000010000000000000006f446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f30000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000008fe6b999dc680ccfdd5bf7eb0974218be2542daa0000000000000000000000000000000000000000000000000000000000000000000003e8000003e8000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
4993
+ eventNonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
4994
+ cctpVersion: 2,
4995
+ status: 'complete',
4996
+ decodedMessage: {
4997
+ sourceDomain: '0',
4998
+ destinationDomain: '6',
4999
+ nonce: '0xf446cb82eb486fef485c14c301eaab73aa35f87e3feda3547acf0f33cd4b40f3',
5000
+ sender: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5001
+ recipient: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5002
+ destinationCaller: '0x0000000000000000000000000000000000000000000000000000000000000000',
5003
+ minFinalityThreshold: '1000',
5004
+ finalityThresholdExecuted: '1000',
5005
+ messageBody: '0x000000010000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000023f9a5bea7b92a0638520607407bc7f0310aeed400000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000c5567a5e3370d4dbfb0540025078e283e36a363d000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000001f6b158',
5006
+ decodedMessageBody: {
5007
+ burnToken: '0x1c7d4b196cb0c7b01d743fbc6116a902379c7238',
5008
+ mintRecipient: '0x23f9a5bea7b92a0638520607407bc7f0310aeed4',
5009
+ amount: '100000',
5010
+ messageSender: '0xc5567a5e3370d4dbfb0540025078e283e36a363d',
5011
+ maxFee: '11',
5012
+ feeExecuted: '10',
5013
+ expirationBlock: '32944472',
5014
+ hookData: null,
5015
+ },
5016
+ },
5017
+ delayReason: null,
5018
+ };
4930
5019
 
4931
5020
  /**
4932
5021
  * Ordered sequence of CCTP v2 bridge step executors.
@@ -5424,10 +5513,12 @@ z.object({
5424
5513
  */
5425
5514
  const validateBalanceForTransaction = async (params) => {
5426
5515
  const { amount, adapter, token, tokenAddress, operationContext } = params;
5427
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {}, operationContext);
5516
+ const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
5517
+ walletAddress: operationContext.address,
5518
+ }, operationContext);
5428
5519
  const balance = await balancePrepared.execute();
5429
5520
  if (BigInt(balance) < BigInt(amount)) {
5430
- throw new InsufficientFundsError(balance.toString(), amount, token, tokenAddress, resolveChainIdentifier(operationContext.chain).chain);
5521
+ throw new InsufficientFundsError(balance.toString(), amount, token, tokenAddress, operationContext.chain);
5431
5522
  }
5432
5523
  };
5433
5524
 
@@ -5991,22 +6082,22 @@ class CCTPV2BridgingProvider extends BridgingProvider {
5991
6082
  async estimate(params) {
5992
6083
  // CCTP-specific transfer params validation (includes base validation)
5993
6084
  assertCCTPv2BridgeParams(params);
5994
- const { source, destination, amount, token } = params;
5995
- // Extract operation context from source wallet context for balance validation
5996
- const operationContext = this.extractOperationContext(source);
5997
- // Validate balance for transaction
5998
- await validateBalanceForTransaction({
5999
- amount,
6000
- token,
6001
- tokenAddress: source.chain.usdcAddress,
6002
- adapter: source.adapter,
6003
- operationContext,
6004
- });
6085
+ const { source, destination, amount } = params;
6086
+ const estimateBurn = async () => {
6087
+ const burn = await this.burn(params);
6088
+ return await burn.estimate(undefined, await source.adapter.calculateTransactionFee(hasCustomContractSupport(source.chain, 'bridge')
6089
+ ? CUSTOM_BURN_GAS_ESTIMATE_EVM
6090
+ : DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM, undefined, source.chain));
6091
+ };
6092
+ const estimateMint = async () => {
6093
+ const mint = await this.mint(source, destination, mockAttestationMessage);
6094
+ return await mint.estimate(undefined, await destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, undefined, destination.chain));
6095
+ };
6005
6096
  // Parallelize all independent async operations
6006
6097
  const [approveEstimate, depositForBurnFee, receiveMessageFee, maxFee] = await Promise.allSettled([
6007
6098
  this.approve(source, amount).then(async (approve) => approve.estimate()),
6008
- source.adapter.calculateTransactionFee(DEPOSIT_FOR_BURN_GAS_ESTIMATE_EVM, 500n, source.chain),
6009
- destination.adapter.calculateTransactionFee(RECEIVE_MESSAGE_GAS_ESTIMATE_EVM, 500n, destination.chain),
6099
+ estimateBurn(),
6100
+ estimateMint(),
6010
6101
  params.config?.transferSpeed === undefined ||
6011
6102
  params.config?.transferSpeed === TransferSpeed.FAST
6012
6103
  ? this.getMaxFee(params)
@@ -6173,14 +6264,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6173
6264
  delegate: spenderAddress,
6174
6265
  chain: resolvedContext?.chain ?? chain,
6175
6266
  };
6176
- // Single call with or without context
6177
- if (resolvedContext) {
6178
- return await adapter.prepareAction('usdc.increaseAllowance', actionParams, resolvedContext);
6179
- }
6180
- else {
6181
- // Legacy fallback for adapters without capabilities
6182
- return await adapter.prepareAction('usdc.increaseAllowance', actionParams);
6183
- }
6267
+ return await adapter.prepareAction('usdc.increaseAllowance', actionParams, resolvedContext);
6184
6268
  }
6185
6269
  /**
6186
6270
  * Prepares a CCTP v2 token minting transaction on the destination chain.
@@ -6224,14 +6308,8 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6224
6308
  assertCCTPv2WalletContext(destination);
6225
6309
  // Extract operation context from destination wallet context
6226
6310
  const operationContext = this.extractOperationContext(destination);
6227
- // Resolve operation context to get concrete chain and address values
6228
- let resolvedContext;
6229
- try {
6230
- resolvedContext = await resolveOperationContext(destination.adapter, operationContext);
6231
- }
6232
- catch (error) {
6233
- throw new Error(`Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
6234
- }
6311
+ // Use recipientAddress if provided, otherwise use destination.address
6312
+ const destinationAddressForMint = destination.recipientAddress ?? destination.address;
6235
6313
  // Prepare action parameters
6236
6314
  const actionParams = {
6237
6315
  fromChain: source.chain,
@@ -6239,16 +6317,9 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6239
6317
  message: attestation.message,
6240
6318
  attestation: attestation.attestation,
6241
6319
  eventNonce: attestation.eventNonce,
6242
- destinationAddress: destination.address,
6320
+ destinationAddress: destinationAddressForMint,
6243
6321
  };
6244
- // Single call with or without context
6245
- if (resolvedContext) {
6246
- return await destination.adapter.prepareAction('cctp.v2.receiveMessage', actionParams, resolvedContext);
6247
- }
6248
- else {
6249
- // Legacy fallback for adapters without capabilities
6250
- return await destination.adapter.prepareAction('cctp.v2.receiveMessage', actionParams);
6251
- }
6322
+ return await destination.adapter.prepareAction('cctp.v2.receiveMessage', actionParams, operationContext);
6252
6323
  }
6253
6324
  /**
6254
6325
  * Fetches attestation data for a burn transaction from the IRIS API.
@@ -6447,10 +6518,11 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6447
6518
  // Get the min finality threshold based on the transfer speed
6448
6519
  const minFinalityThreshold = CCTPv2MinFinalityThreshold[transferSpeed];
6449
6520
  // Calculate the max fee
6521
+ // Use recipientAddress if provided, otherwise use destination.address
6522
+ const destinationAddressForMint = destination.recipientAddress ?? destination.address;
6450
6523
  const [maxFee, mintRecipient] = await Promise.all([
6451
6524
  this.getMaxFee(params),
6452
- getMintRecipientAccount(destination.chain.type, destination.address ??
6453
- (await destination.adapter.getAddress(destination.chain)), destination.chain.usdcAddress),
6525
+ getMintRecipientAccount(destination.chain.type, destinationAddressForMint, destination.chain.usdcAddress),
6454
6526
  ]);
6455
6527
  const actionParams = {
6456
6528
  fromChain: source.chain,
@@ -6480,14 +6552,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
6480
6552
  const finalParams = useCustomBurn
6481
6553
  ? { ...actionParams, feeRecipient: recipientAddress, protocolFee }
6482
6554
  : actionParams;
6483
- // Single call with or without context
6484
- if (resolvedContext) {
6485
- return await source.adapter.prepareAction(actionType, finalParams, resolvedContext);
6486
- }
6487
- else {
6488
- // Legacy fallback for adapters without capabilities
6489
- return await source.adapter.prepareAction(actionType, finalParams);
6490
- }
6555
+ return await source.adapter.prepareAction(actionType, finalParams, resolvedContext);
6491
6556
  }
6492
6557
  /**
6493
6558
  * Waits for a transaction to be mined and confirmed on the blockchain.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@circle-fin/provider-cctp-v2",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
5
- "main": "./index.cjs.js",
5
+ "main": "./index.cjs",
6
6
  "module": "./index.mjs",
7
7
  "types": "./index.d.ts",
8
8
  "dependencies": {
@@ -34,12 +34,14 @@
34
34
  ".": {
35
35
  "types": "./index.d.ts",
36
36
  "import": "./index.mjs",
37
- "default": "./index.cjs.js"
37
+ "require": "./index.cjs",
38
+ "default": "./index.cjs"
38
39
  }
39
40
  },
40
41
  "files": [
41
42
  "index.*",
42
43
  "README.md",
44
+ "CHANGELOG.md",
43
45
  "LICENSE",
44
46
  "package.json"
45
47
  ],
package/index.cjs.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}