@circle-fin/provider-cctp-v2 1.5.0 → 1.6.1

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @circle-fin/provider-cctp-v2
2
2
 
3
+ ## 1.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Add support for Solana as a forwarder destination
8
+
9
+ ## 1.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Add support for EIP-5792 batched transactions in the bridge flow
14
+
15
+ ### Patch Changes
16
+
17
+ - Enable forwarder destination support for Codex, Plume, and XDC chains on both mainnet and testnet
18
+ - Update HyperEVM explorer URLs to use the official Hyperliquid explorer
19
+
3
20
  ## 1.5.0
4
21
 
5
22
  ### Minor Changes
package/index.cjs CHANGED
@@ -1066,7 +1066,7 @@ const Codex = defineChain({
1066
1066
  },
1067
1067
  forwarderSupported: {
1068
1068
  source: true,
1069
- destination: false,
1069
+ destination: true,
1070
1070
  },
1071
1071
  },
1072
1072
  kitContracts: {
@@ -1109,7 +1109,7 @@ const CodexTestnet = defineChain({
1109
1109
  },
1110
1110
  forwarderSupported: {
1111
1111
  source: true,
1112
- destination: false,
1112
+ destination: true,
1113
1113
  },
1114
1114
  },
1115
1115
  kitContracts: {
@@ -1371,7 +1371,7 @@ const HyperEVM = defineChain({
1371
1371
  },
1372
1372
  chainId: 999,
1373
1373
  isTestnet: false,
1374
- explorerUrl: 'https://hyperevmscan.io/tx/{hash}',
1374
+ explorerUrl: 'https://app.hyperliquid.xyz/explorer/tx/{hash}',
1375
1375
  rpcEndpoints: ['https://rpc.hyperliquid.xyz/evm'],
1376
1376
  eurcAddress: null,
1377
1377
  usdcAddress: '0xb88339CB7199b77E23DB6E890353E22632Ba630f',
@@ -1416,7 +1416,7 @@ const HyperEVMTestnet = defineChain({
1416
1416
  },
1417
1417
  chainId: 998,
1418
1418
  isTestnet: true,
1419
- explorerUrl: 'https://testnet.hyperliquid.xyz/explorer/tx/{hash}',
1419
+ explorerUrl: 'https://app.hyperliquid-testnet.xyz/explorer/tx/{hash}',
1420
1420
  rpcEndpoints: ['https://rpc.hyperliquid-testnet.xyz/evm'],
1421
1421
  eurcAddress: null,
1422
1422
  usdcAddress: '0x2B3370eE501B4a559b57D449569354196457D8Ab',
@@ -2062,7 +2062,7 @@ const Plume = defineChain({
2062
2062
  },
2063
2063
  forwarderSupported: {
2064
2064
  source: true,
2065
- destination: false,
2065
+ destination: true,
2066
2066
  },
2067
2067
  },
2068
2068
  kitContracts: {
@@ -2107,7 +2107,7 @@ const PlumeTestnet = defineChain({
2107
2107
  },
2108
2108
  forwarderSupported: {
2109
2109
  source: true,
2110
- destination: false,
2110
+ destination: true,
2111
2111
  },
2112
2112
  },
2113
2113
  kitContracts: {
@@ -2479,7 +2479,7 @@ const Solana = defineChain({
2479
2479
  },
2480
2480
  forwarderSupported: {
2481
2481
  source: true,
2482
- destination: false,
2482
+ destination: true,
2483
2483
  },
2484
2484
  },
2485
2485
  kitContracts: {
@@ -2526,7 +2526,7 @@ const SolanaDevnet = defineChain({
2526
2526
  },
2527
2527
  forwarderSupported: {
2528
2528
  source: true,
2529
- destination: false,
2529
+ destination: true,
2530
2530
  },
2531
2531
  },
2532
2532
  kitContracts: {
@@ -2885,7 +2885,7 @@ const XDC = defineChain({
2885
2885
  },
2886
2886
  forwarderSupported: {
2887
2887
  source: true,
2888
- destination: false,
2888
+ destination: true,
2889
2889
  },
2890
2890
  },
2891
2891
  kitContracts: {
@@ -2929,7 +2929,7 @@ const XDCApothem = defineChain({
2929
2929
  },
2930
2930
  forwarderSupported: {
2931
2931
  source: true,
2932
- destination: false,
2932
+ destination: true,
2933
2933
  },
2934
2934
  },
2935
2935
  kitContracts: {
@@ -9509,7 +9509,184 @@ zod.z
9509
9509
  })
9510
9510
  .passthrough();
9511
9511
 
9512
- var version = "1.5.0";
9512
+ /**
9513
+ * Check whether the source adapter supports EIP-5792 atomic batching and
9514
+ * the consumer has not explicitly opted out via `config.batchTransactions`.
9515
+ *
9516
+ * @param params - Bridge parameters (used for adapter and config access).
9517
+ * @returns `true` when batched execution should be attempted.
9518
+ *
9519
+ * @example
9520
+ * ```typescript
9521
+ * const useBatched = await shouldUseBatchedExecution(params)
9522
+ * if (useBatched) {
9523
+ * // take the batched approve + burn path
9524
+ * }
9525
+ * ```
9526
+ */
9527
+ async function shouldUseBatchedExecution(params) {
9528
+ if (params.config?.batchTransactions === false) {
9529
+ return false;
9530
+ }
9531
+ const { chain } = params.source;
9532
+ if (chain.type !== 'evm') {
9533
+ return false;
9534
+ }
9535
+ const adapter = params.source
9536
+ .adapter;
9537
+ if (typeof adapter.supportsAtomicBatch !== 'function' ||
9538
+ typeof adapter.batchExecute !== 'function') {
9539
+ return false;
9540
+ }
9541
+ try {
9542
+ return await adapter.supportsAtomicBatch(chain);
9543
+ }
9544
+ catch {
9545
+ return false;
9546
+ }
9547
+ }
9548
+ /**
9549
+ * Execute the approve and burn steps as a single EIP-5792 batched call.
9550
+ *
9551
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw
9552
+ * call data via `getCallData()`, submit both via `adapter.batchExecute()`,
9553
+ * then map the individual receipts back to standard {@link BridgeStep}
9554
+ * objects so downstream consumers (event callbacks, result tracking) are
9555
+ * unaffected.
9556
+ *
9557
+ * @param params - The CCTP v2 bridge parameters.
9558
+ * @param provider - The CCTP v2 bridging provider.
9559
+ * @returns Approve and burn steps with a shared context containing the burn tx hash.
9560
+ * @throws {@link KitError} when the source chain is not EVM.
9561
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not supported
9562
+ * by the prepared requests.
9563
+ * @remarks
9564
+ * Errors that occur after the batch has been submitted to the wallet
9565
+ * (e.g. polling timeout, insufficient receipts) are **not thrown** — they
9566
+ * are captured as `state: 'error'` on the returned steps to prevent
9567
+ * accidental double-spend on retry.
9568
+ *
9569
+ * @example
9570
+ * ```typescript
9571
+ * const { approveStep, burnStep, context } = await executeBatchedApproveAndBurn(
9572
+ * params,
9573
+ * provider,
9574
+ * )
9575
+ * result.steps.push(approveStep, burnStep)
9576
+ * ```
9577
+ */
9578
+ async function executeBatchedApproveAndBurn(params, provider) {
9579
+ // Double-unknown cast: Adapter<TFrom> has no structural overlap with
9580
+ // BatchCapableAdapter (a duck-typed interface for EIP-5792 methods).
9581
+ // A direct cast fails because TS cannot prove the intersection; the
9582
+ // runtime capability check below guards against unsupported adapters.
9583
+ const adapter = params.source.adapter;
9584
+ const sourceChain = params.source.chain;
9585
+ if (sourceChain.type !== 'evm') {
9586
+ throw new KitError({
9587
+ ...InputError.INVALID_CHAIN,
9588
+ recoverability: 'FATAL',
9589
+ message: 'Batched execution is only supported on EVM chains.',
9590
+ });
9591
+ }
9592
+ const chain = sourceChain;
9593
+ // customFee.value is in base units (integer string) at this point.
9594
+ const customFee = BigInt(params.config?.customFee?.value ?? '0');
9595
+ const amountBigInt = BigInt(params.amount);
9596
+ const approvalAmount = (amountBigInt + customFee).toString();
9597
+ const [approveRequest, burnRequest] = await Promise.all([
9598
+ provider.approve(params.source, approvalAmount),
9599
+ provider.burn(params),
9600
+ ]);
9601
+ if (approveRequest.type !== 'evm' ||
9602
+ burnRequest.type !== 'evm' ||
9603
+ !approveRequest.getCallData ||
9604
+ !burnRequest.getCallData) {
9605
+ throw new KitError({
9606
+ ...InputError.UNSUPPORTED_ACTION,
9607
+ recoverability: 'FATAL',
9608
+ message: 'Batched execution requires EVM prepared requests with getCallData() support.',
9609
+ });
9610
+ }
9611
+ const approveCallData = approveRequest.getCallData();
9612
+ const burnCallData = burnRequest.getCallData();
9613
+ // batchExecute may throw before submission (wallet declined) but never
9614
+ // after — post-submission errors are returned as empty receipts.
9615
+ const batchResult = await adapter.batchExecute([approveCallData, burnCallData], chain);
9616
+ const approveReceipt = batchResult.receipts[0];
9617
+ const burnReceipt = batchResult.receipts[1];
9618
+ const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain);
9619
+ const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain);
9620
+ if (burnStep.state !== 'error' && !burnStep.txHash) {
9621
+ burnStep.state = 'error';
9622
+ burnStep.errorMessage =
9623
+ 'Batched burn step completed but no transaction hash was returned.';
9624
+ }
9625
+ const context = { burnTxHash: burnStep.txHash ?? '' };
9626
+ return { approveStep, burnStep, context };
9627
+ }
9628
+ /**
9629
+ * Build a {@link BridgeStep} from a single receipt within a batch.
9630
+ *
9631
+ * Map the raw receipt from `batchExecute` into a standard `BridgeStep`,
9632
+ * waiting for on-chain confirmation via `adapter.waitForTransaction`.
9633
+ * All errors are captured on the step (never thrown) so the caller
9634
+ * can inspect each step independently.
9635
+ *
9636
+ * @param name - Human-readable step name (e.g. `'approve'`, `'burn'`).
9637
+ * @param receipt - Per-call receipt from `batchExecute`, or `undefined`
9638
+ * when the wallet returned fewer receipts than submitted calls.
9639
+ * @param batchId - Wallet-assigned batch identifier.
9640
+ * @param adapter - The batch-capable adapter (used for confirmation).
9641
+ * @param chain - The EVM chain the batch was executed on.
9642
+ * @returns A fully-populated bridge step with state, tx hash and explorer URL.
9643
+ *
9644
+ * @internal
9645
+ */
9646
+ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
9647
+ const step = {
9648
+ name,
9649
+ state: 'pending',
9650
+ batched: true,
9651
+ batchId,
9652
+ };
9653
+ if (!receipt) {
9654
+ step.state = 'error';
9655
+ step.errorMessage = `No receipt returned for ${name} in batch ${batchId}.`;
9656
+ return step;
9657
+ }
9658
+ step.txHash = receipt.txHash;
9659
+ if (receipt.txHash) {
9660
+ step.explorerUrl = buildExplorerUrl(chain, receipt.txHash);
9661
+ }
9662
+ if (receipt.status !== 'success') {
9663
+ step.state = 'error';
9664
+ step.errorMessage = `${name} call failed within batch ${batchId}.`;
9665
+ return step;
9666
+ }
9667
+ if (!receipt.txHash) {
9668
+ step.state = 'error';
9669
+ step.errorMessage = `${name} succeeded in batch but returned an empty transaction hash.`;
9670
+ return step;
9671
+ }
9672
+ try {
9673
+ const transaction = await adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain);
9674
+ step.state = transaction.blockNumber === undefined ? 'error' : 'success';
9675
+ step.data = transaction;
9676
+ if (transaction.blockNumber === undefined) {
9677
+ step.errorMessage = 'Transaction was not confirmed on-chain.';
9678
+ }
9679
+ }
9680
+ catch (err) {
9681
+ step.state = 'error';
9682
+ step.error = err;
9683
+ step.errorMessage =
9684
+ err instanceof Error ? err.message : 'Unknown error during confirmation.';
9685
+ }
9686
+ return step;
9687
+ }
9688
+
9689
+ var version = "1.6.1";
9513
9690
  var pkg = {
9514
9691
  version: version};
9515
9692
 
@@ -9538,6 +9715,67 @@ function resolveBridgeInvocation(invocationMeta) {
9538
9715
  };
9539
9716
  return extendInvocationContext(resolveInvocationContext(invocationMeta, defaults), BRIDGE_CALLER);
9540
9717
  }
9718
+ /**
9719
+ * Execute the batched approve + burn path via EIP-5792.
9720
+ *
9721
+ * Mutate `result` with step data and error state as needed. Return the
9722
+ * batch context on success, or `undefined` when the batch failed (in
9723
+ * which case `result.state` is set to `'error'`).
9724
+ *
9725
+ * @internal
9726
+ * @param params - Bridge parameters (read-only).
9727
+ * @param provider - The CCTP v2 bridging provider (read-only).
9728
+ * @param result - Bridge result object — **mutated in place** with step
9729
+ * data and, on failure, `state: 'error'` plus an `error` payload.
9730
+ * @param invocation - Invocation context for telemetry (read-only).
9731
+ * @returns The step context on success, or `undefined` when the batch failed.
9732
+ */
9733
+ async function executeBatchedPath(params, provider, result, invocation) {
9734
+ // IMPORTANT: once executeBatchedApproveAndBurn is called, we NEVER
9735
+ // fall back to sequential. The wallet may have already signed &
9736
+ // submitted the batch; retrying as individual txs would double-spend.
9737
+ try {
9738
+ const { approveStep, burnStep, context: batchContext, } = await executeBatchedApproveAndBurn(params, provider);
9739
+ for (const step of [approveStep, burnStep]) {
9740
+ const stepName = step.name;
9741
+ if (step.state === 'error') {
9742
+ ensureStepErrorMessage(step.name, step);
9743
+ result.steps.push(step);
9744
+ result.state = 'error';
9745
+ dispatchStepEvent(stepName, step, provider, invocation);
9746
+ return undefined;
9747
+ }
9748
+ dispatchStepEvent(stepName, step, provider, invocation);
9749
+ result.steps.push(step);
9750
+ }
9751
+ return batchContext;
9752
+ }
9753
+ catch (error_) {
9754
+ // Only handles pre-submission failures (prepare rejected, wallet
9755
+ // declined, etc.). batchExecute never throws after sendCalls succeeds.
9756
+ result.state = 'error';
9757
+ result.steps.push({
9758
+ name: 'batch',
9759
+ state: 'error',
9760
+ batched: true,
9761
+ error: error_,
9762
+ errorMessage: error_ instanceof Error
9763
+ ? error_.message
9764
+ : 'Batched approve + burn failed.',
9765
+ });
9766
+ return undefined;
9767
+ }
9768
+ }
9769
+ /**
9770
+ * Ensure `step.errorMessage` is populated when an error object exists.
9771
+ *
9772
+ * @internal
9773
+ */
9774
+ function ensureStepErrorMessage(name, step) {
9775
+ if (!step.errorMessage && step.error) {
9776
+ step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
9777
+ }
9778
+ }
9541
9779
  /**
9542
9780
  * Execute a cross-chain USDC bridge using the CCTP v2 protocol.
9543
9781
  *
@@ -9571,9 +9809,7 @@ function resolveBridgeInvocation(invocationMeta) {
9571
9809
  * ```
9572
9810
  */
9573
9811
  async function bridge(params, provider) {
9574
- // Check if forwarder is enabled (on destination)
9575
9812
  const useForwarder = params.destination.useForwarder === true;
9576
- // Resolve invocation metadata to full context for event dispatching
9577
9813
  const invocation = resolveBridgeInvocation(params.invocationMeta);
9578
9814
  const result = {
9579
9815
  state: 'pending',
@@ -9586,11 +9822,9 @@ async function bridge(params, provider) {
9586
9822
  destination: {
9587
9823
  address: params.destination.address,
9588
9824
  chain: params.destination.chain,
9589
- // Preserve recipientAddress
9590
9825
  ...(params.destination.recipientAddress && {
9591
9826
  recipientAddress: params.destination.recipientAddress,
9592
9827
  }),
9593
- // Preserve useForwarder
9594
9828
  ...(useForwarder && {
9595
9829
  useForwarder: true,
9596
9830
  }),
@@ -9599,29 +9833,38 @@ async function bridge(params, provider) {
9599
9833
  config: params.config,
9600
9834
  provider: provider.name,
9601
9835
  };
9602
- // Context shared between steps
9603
9836
  let context = undefined;
9604
- // Create step executors based on useForwarder config
9837
+ let useBatched = false;
9838
+ try {
9839
+ useBatched = await shouldUseBatchedExecution(params);
9840
+ }
9841
+ catch {
9842
+ // Silently fall back to sequential
9843
+ }
9605
9844
  const executors = createStepExecutors(useForwarder);
9606
- // Execute each step in sequence
9607
- for (const { name, executor, updateContext } of executors) {
9845
+ if (useBatched) {
9846
+ const batchContext = await executeBatchedPath(params, provider, result, invocation);
9847
+ if (result.state === 'error') {
9848
+ return result;
9849
+ }
9850
+ context = batchContext;
9851
+ }
9852
+ const stepsToRun = useBatched
9853
+ ? executors.filter(({ name }) => name !== 'approve' && name !== 'burn')
9854
+ : executors;
9855
+ for (const { name, executor, updateContext } of stepsToRun) {
9608
9856
  try {
9609
9857
  const step = await executor(params, provider, context);
9610
9858
  if (step.state === 'error') {
9611
- // Ensure errorMessage is set with proper formatting if not already present
9612
- if (!step.errorMessage && step.error) {
9613
- step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
9614
- }
9859
+ ensureStepErrorMessage(name, step);
9615
9860
  result.steps.push(step);
9616
9861
  result.state = 'error';
9617
- // Dispatch event even for error steps
9618
9862
  dispatchStepEvent(name, step, provider, invocation);
9619
9863
  return result;
9620
9864
  }
9621
- // Merge new context with existing context to preserve data from previous steps
9622
9865
  const newContext = updateContext?.(step);
9623
9866
  if (newContext) {
9624
- context = { ...(context ?? {}), ...newContext };
9867
+ context = { ...context, ...newContext };
9625
9868
  }
9626
9869
  dispatchStepEvent(name, step, provider, invocation);
9627
9870
  result.steps.push(step);
package/index.d.ts CHANGED
@@ -665,6 +665,32 @@ interface EvmExecuteOverrides extends EvmEstimateOverrides {
665
665
  */
666
666
  nonce?: number;
667
667
  }
668
+ /**
669
+ * Raw EVM call data tuple for a single contract interaction.
670
+ *
671
+ * Represents the minimal data needed to submit an EVM transaction:
672
+ * the target contract address, the ABI-encoded calldata, and an
673
+ * optional native token value. Used by EIP-5792 batched execution
674
+ * to compose multiple calls into a single `wallet_sendCalls` request.
675
+ *
676
+ * @interface EvmCallData
677
+ *
678
+ * @example
679
+ * ```typescript
680
+ * const callData: EvmCallData = {
681
+ * to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
682
+ * data: '0x095ea7b3000000000000000000000000...',
683
+ * }
684
+ * ```
685
+ */
686
+ interface EvmCallData {
687
+ /** The target contract address. */
688
+ to: `0x${string}`;
689
+ /** The ABI-encoded function calldata. */
690
+ data: `0x${string}`;
691
+ /** Optional native token value to send with the call. */
692
+ value?: bigint | undefined;
693
+ }
668
694
  /**
669
695
  * Prepared contract execution for EVM chains.
670
696
  *
@@ -693,6 +719,28 @@ interface EvmPreparedChainRequest {
693
719
  * @throws If the execution fails
694
720
  */
695
721
  execute(overrides?: EvmExecuteOverrides): Promise<string>;
722
+ /**
723
+ * Return the raw call tuple without executing or estimating.
724
+ *
725
+ * Expose the `{ to, data, value }` triple that would be sent on-chain so
726
+ * callers can feed it into EIP-5792 `wallet_sendCalls` or other batching
727
+ * mechanisms. This method is optional -- adapters that do not support
728
+ * calldata extraction (e.g. Ethers v6) may omit it.
729
+ *
730
+ * @returns The raw EVM call data for this prepared request.
731
+ * @throws Never — synchronous accessor with no failure path.
732
+ * @since 2.0.0
733
+ *
734
+ * @example
735
+ * ```typescript
736
+ * const prepared = await adapter.prepare(params, ctx)
737
+ * if (prepared.getCallData) {
738
+ * const { to, data, value } = prepared.getCallData()
739
+ * console.log('Target:', to, 'Data:', data)
740
+ * }
741
+ * ```
742
+ */
743
+ getCallData?(): EvmCallData;
696
744
  }
697
745
  /**
698
746
  * Union type for all supported prepared contract executions.
@@ -4417,6 +4465,21 @@ interface BridgeStep {
4417
4465
  * - `undefined`: Not applicable (non-mint steps)
4418
4466
  */
4419
4467
  forwarded?: boolean;
4468
+ /**
4469
+ * Whether this step was executed as part of an EIP-5792 batched
4470
+ * `wallet_sendCalls` request.
4471
+ *
4472
+ * - `true`: The step was included in a batched call bundle
4473
+ * - `undefined`: The step was executed individually (sequential flow)
4474
+ */
4475
+ batched?: boolean | undefined;
4476
+ /**
4477
+ * The wallet-assigned batch identifier from `wallet_sendCalls`.
4478
+ *
4479
+ * Present only when {@link batched} is `true`. Can be used with
4480
+ * `wallet_getCallsStatus` to query the status of the entire bundle.
4481
+ */
4482
+ batchId?: string | undefined;
4420
4483
  /** Optional human-readable error message */
4421
4484
  errorMessage?: string;
4422
4485
  /** Optional raw error object (can be Viem/Ethers/Chain error) */
@@ -4561,6 +4624,24 @@ interface BridgeConfig {
4561
4624
  * @defaultValue TransferSpeed.FAST
4562
4625
  */
4563
4626
  transferSpeed?: TransferSpeed | `${TransferSpeed}` | undefined;
4627
+ /**
4628
+ * Enable or disable EIP-5792 batched transaction execution.
4629
+ *
4630
+ * When `true` (or `undefined` / omitted), the bridge will attempt to batch
4631
+ * the approve and burn calls into a single `wallet_sendCalls` request if
4632
+ * the connected wallet supports it. Set to `false` to explicitly opt out
4633
+ * and always use the sequential approve -> burn flow.
4634
+ *
4635
+ * @defaultValue `undefined` (batching attempted when the wallet supports it)
4636
+ *
4637
+ * @example
4638
+ * ```typescript
4639
+ * const config: BridgeConfig = {
4640
+ * batchTransactions: false, // force sequential flow
4641
+ * }
4642
+ * ```
4643
+ */
4644
+ batchTransactions?: boolean | undefined;
4564
4645
  /**
4565
4646
  * The maximum fee to pay for the burn operation.
4566
4647
  *
package/index.mjs CHANGED
@@ -1059,7 +1059,7 @@ const Codex = defineChain({
1059
1059
  },
1060
1060
  forwarderSupported: {
1061
1061
  source: true,
1062
- destination: false,
1062
+ destination: true,
1063
1063
  },
1064
1064
  },
1065
1065
  kitContracts: {
@@ -1102,7 +1102,7 @@ const CodexTestnet = defineChain({
1102
1102
  },
1103
1103
  forwarderSupported: {
1104
1104
  source: true,
1105
- destination: false,
1105
+ destination: true,
1106
1106
  },
1107
1107
  },
1108
1108
  kitContracts: {
@@ -1364,7 +1364,7 @@ const HyperEVM = defineChain({
1364
1364
  },
1365
1365
  chainId: 999,
1366
1366
  isTestnet: false,
1367
- explorerUrl: 'https://hyperevmscan.io/tx/{hash}',
1367
+ explorerUrl: 'https://app.hyperliquid.xyz/explorer/tx/{hash}',
1368
1368
  rpcEndpoints: ['https://rpc.hyperliquid.xyz/evm'],
1369
1369
  eurcAddress: null,
1370
1370
  usdcAddress: '0xb88339CB7199b77E23DB6E890353E22632Ba630f',
@@ -1409,7 +1409,7 @@ const HyperEVMTestnet = defineChain({
1409
1409
  },
1410
1410
  chainId: 998,
1411
1411
  isTestnet: true,
1412
- explorerUrl: 'https://testnet.hyperliquid.xyz/explorer/tx/{hash}',
1412
+ explorerUrl: 'https://app.hyperliquid-testnet.xyz/explorer/tx/{hash}',
1413
1413
  rpcEndpoints: ['https://rpc.hyperliquid-testnet.xyz/evm'],
1414
1414
  eurcAddress: null,
1415
1415
  usdcAddress: '0x2B3370eE501B4a559b57D449569354196457D8Ab',
@@ -2055,7 +2055,7 @@ const Plume = defineChain({
2055
2055
  },
2056
2056
  forwarderSupported: {
2057
2057
  source: true,
2058
- destination: false,
2058
+ destination: true,
2059
2059
  },
2060
2060
  },
2061
2061
  kitContracts: {
@@ -2100,7 +2100,7 @@ const PlumeTestnet = defineChain({
2100
2100
  },
2101
2101
  forwarderSupported: {
2102
2102
  source: true,
2103
- destination: false,
2103
+ destination: true,
2104
2104
  },
2105
2105
  },
2106
2106
  kitContracts: {
@@ -2472,7 +2472,7 @@ const Solana = defineChain({
2472
2472
  },
2473
2473
  forwarderSupported: {
2474
2474
  source: true,
2475
- destination: false,
2475
+ destination: true,
2476
2476
  },
2477
2477
  },
2478
2478
  kitContracts: {
@@ -2519,7 +2519,7 @@ const SolanaDevnet = defineChain({
2519
2519
  },
2520
2520
  forwarderSupported: {
2521
2521
  source: true,
2522
- destination: false,
2522
+ destination: true,
2523
2523
  },
2524
2524
  },
2525
2525
  kitContracts: {
@@ -2878,7 +2878,7 @@ const XDC = defineChain({
2878
2878
  },
2879
2879
  forwarderSupported: {
2880
2880
  source: true,
2881
- destination: false,
2881
+ destination: true,
2882
2882
  },
2883
2883
  },
2884
2884
  kitContracts: {
@@ -2922,7 +2922,7 @@ const XDCApothem = defineChain({
2922
2922
  },
2923
2923
  forwarderSupported: {
2924
2924
  source: true,
2925
- destination: false,
2925
+ destination: true,
2926
2926
  },
2927
2927
  },
2928
2928
  kitContracts: {
@@ -9502,7 +9502,184 @@ z
9502
9502
  })
9503
9503
  .passthrough();
9504
9504
 
9505
- var version = "1.5.0";
9505
+ /**
9506
+ * Check whether the source adapter supports EIP-5792 atomic batching and
9507
+ * the consumer has not explicitly opted out via `config.batchTransactions`.
9508
+ *
9509
+ * @param params - Bridge parameters (used for adapter and config access).
9510
+ * @returns `true` when batched execution should be attempted.
9511
+ *
9512
+ * @example
9513
+ * ```typescript
9514
+ * const useBatched = await shouldUseBatchedExecution(params)
9515
+ * if (useBatched) {
9516
+ * // take the batched approve + burn path
9517
+ * }
9518
+ * ```
9519
+ */
9520
+ async function shouldUseBatchedExecution(params) {
9521
+ if (params.config?.batchTransactions === false) {
9522
+ return false;
9523
+ }
9524
+ const { chain } = params.source;
9525
+ if (chain.type !== 'evm') {
9526
+ return false;
9527
+ }
9528
+ const adapter = params.source
9529
+ .adapter;
9530
+ if (typeof adapter.supportsAtomicBatch !== 'function' ||
9531
+ typeof adapter.batchExecute !== 'function') {
9532
+ return false;
9533
+ }
9534
+ try {
9535
+ return await adapter.supportsAtomicBatch(chain);
9536
+ }
9537
+ catch {
9538
+ return false;
9539
+ }
9540
+ }
9541
+ /**
9542
+ * Execute the approve and burn steps as a single EIP-5792 batched call.
9543
+ *
9544
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw
9545
+ * call data via `getCallData()`, submit both via `adapter.batchExecute()`,
9546
+ * then map the individual receipts back to standard {@link BridgeStep}
9547
+ * objects so downstream consumers (event callbacks, result tracking) are
9548
+ * unaffected.
9549
+ *
9550
+ * @param params - The CCTP v2 bridge parameters.
9551
+ * @param provider - The CCTP v2 bridging provider.
9552
+ * @returns Approve and burn steps with a shared context containing the burn tx hash.
9553
+ * @throws {@link KitError} when the source chain is not EVM.
9554
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not supported
9555
+ * by the prepared requests.
9556
+ * @remarks
9557
+ * Errors that occur after the batch has been submitted to the wallet
9558
+ * (e.g. polling timeout, insufficient receipts) are **not thrown** — they
9559
+ * are captured as `state: 'error'` on the returned steps to prevent
9560
+ * accidental double-spend on retry.
9561
+ *
9562
+ * @example
9563
+ * ```typescript
9564
+ * const { approveStep, burnStep, context } = await executeBatchedApproveAndBurn(
9565
+ * params,
9566
+ * provider,
9567
+ * )
9568
+ * result.steps.push(approveStep, burnStep)
9569
+ * ```
9570
+ */
9571
+ async function executeBatchedApproveAndBurn(params, provider) {
9572
+ // Double-unknown cast: Adapter<TFrom> has no structural overlap with
9573
+ // BatchCapableAdapter (a duck-typed interface for EIP-5792 methods).
9574
+ // A direct cast fails because TS cannot prove the intersection; the
9575
+ // runtime capability check below guards against unsupported adapters.
9576
+ const adapter = params.source.adapter;
9577
+ const sourceChain = params.source.chain;
9578
+ if (sourceChain.type !== 'evm') {
9579
+ throw new KitError({
9580
+ ...InputError.INVALID_CHAIN,
9581
+ recoverability: 'FATAL',
9582
+ message: 'Batched execution is only supported on EVM chains.',
9583
+ });
9584
+ }
9585
+ const chain = sourceChain;
9586
+ // customFee.value is in base units (integer string) at this point.
9587
+ const customFee = BigInt(params.config?.customFee?.value ?? '0');
9588
+ const amountBigInt = BigInt(params.amount);
9589
+ const approvalAmount = (amountBigInt + customFee).toString();
9590
+ const [approveRequest, burnRequest] = await Promise.all([
9591
+ provider.approve(params.source, approvalAmount),
9592
+ provider.burn(params),
9593
+ ]);
9594
+ if (approveRequest.type !== 'evm' ||
9595
+ burnRequest.type !== 'evm' ||
9596
+ !approveRequest.getCallData ||
9597
+ !burnRequest.getCallData) {
9598
+ throw new KitError({
9599
+ ...InputError.UNSUPPORTED_ACTION,
9600
+ recoverability: 'FATAL',
9601
+ message: 'Batched execution requires EVM prepared requests with getCallData() support.',
9602
+ });
9603
+ }
9604
+ const approveCallData = approveRequest.getCallData();
9605
+ const burnCallData = burnRequest.getCallData();
9606
+ // batchExecute may throw before submission (wallet declined) but never
9607
+ // after — post-submission errors are returned as empty receipts.
9608
+ const batchResult = await adapter.batchExecute([approveCallData, burnCallData], chain);
9609
+ const approveReceipt = batchResult.receipts[0];
9610
+ const burnReceipt = batchResult.receipts[1];
9611
+ const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain);
9612
+ const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain);
9613
+ if (burnStep.state !== 'error' && !burnStep.txHash) {
9614
+ burnStep.state = 'error';
9615
+ burnStep.errorMessage =
9616
+ 'Batched burn step completed but no transaction hash was returned.';
9617
+ }
9618
+ const context = { burnTxHash: burnStep.txHash ?? '' };
9619
+ return { approveStep, burnStep, context };
9620
+ }
9621
+ /**
9622
+ * Build a {@link BridgeStep} from a single receipt within a batch.
9623
+ *
9624
+ * Map the raw receipt from `batchExecute` into a standard `BridgeStep`,
9625
+ * waiting for on-chain confirmation via `adapter.waitForTransaction`.
9626
+ * All errors are captured on the step (never thrown) so the caller
9627
+ * can inspect each step independently.
9628
+ *
9629
+ * @param name - Human-readable step name (e.g. `'approve'`, `'burn'`).
9630
+ * @param receipt - Per-call receipt from `batchExecute`, or `undefined`
9631
+ * when the wallet returned fewer receipts than submitted calls.
9632
+ * @param batchId - Wallet-assigned batch identifier.
9633
+ * @param adapter - The batch-capable adapter (used for confirmation).
9634
+ * @param chain - The EVM chain the batch was executed on.
9635
+ * @returns A fully-populated bridge step with state, tx hash and explorer URL.
9636
+ *
9637
+ * @internal
9638
+ */
9639
+ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
9640
+ const step = {
9641
+ name,
9642
+ state: 'pending',
9643
+ batched: true,
9644
+ batchId,
9645
+ };
9646
+ if (!receipt) {
9647
+ step.state = 'error';
9648
+ step.errorMessage = `No receipt returned for ${name} in batch ${batchId}.`;
9649
+ return step;
9650
+ }
9651
+ step.txHash = receipt.txHash;
9652
+ if (receipt.txHash) {
9653
+ step.explorerUrl = buildExplorerUrl(chain, receipt.txHash);
9654
+ }
9655
+ if (receipt.status !== 'success') {
9656
+ step.state = 'error';
9657
+ step.errorMessage = `${name} call failed within batch ${batchId}.`;
9658
+ return step;
9659
+ }
9660
+ if (!receipt.txHash) {
9661
+ step.state = 'error';
9662
+ step.errorMessage = `${name} succeeded in batch but returned an empty transaction hash.`;
9663
+ return step;
9664
+ }
9665
+ try {
9666
+ const transaction = await adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain);
9667
+ step.state = transaction.blockNumber === undefined ? 'error' : 'success';
9668
+ step.data = transaction;
9669
+ if (transaction.blockNumber === undefined) {
9670
+ step.errorMessage = 'Transaction was not confirmed on-chain.';
9671
+ }
9672
+ }
9673
+ catch (err) {
9674
+ step.state = 'error';
9675
+ step.error = err;
9676
+ step.errorMessage =
9677
+ err instanceof Error ? err.message : 'Unknown error during confirmation.';
9678
+ }
9679
+ return step;
9680
+ }
9681
+
9682
+ var version = "1.6.1";
9506
9683
  var pkg = {
9507
9684
  version: version};
9508
9685
 
@@ -9531,6 +9708,67 @@ function resolveBridgeInvocation(invocationMeta) {
9531
9708
  };
9532
9709
  return extendInvocationContext(resolveInvocationContext(invocationMeta, defaults), BRIDGE_CALLER);
9533
9710
  }
9711
+ /**
9712
+ * Execute the batched approve + burn path via EIP-5792.
9713
+ *
9714
+ * Mutate `result` with step data and error state as needed. Return the
9715
+ * batch context on success, or `undefined` when the batch failed (in
9716
+ * which case `result.state` is set to `'error'`).
9717
+ *
9718
+ * @internal
9719
+ * @param params - Bridge parameters (read-only).
9720
+ * @param provider - The CCTP v2 bridging provider (read-only).
9721
+ * @param result - Bridge result object — **mutated in place** with step
9722
+ * data and, on failure, `state: 'error'` plus an `error` payload.
9723
+ * @param invocation - Invocation context for telemetry (read-only).
9724
+ * @returns The step context on success, or `undefined` when the batch failed.
9725
+ */
9726
+ async function executeBatchedPath(params, provider, result, invocation) {
9727
+ // IMPORTANT: once executeBatchedApproveAndBurn is called, we NEVER
9728
+ // fall back to sequential. The wallet may have already signed &
9729
+ // submitted the batch; retrying as individual txs would double-spend.
9730
+ try {
9731
+ const { approveStep, burnStep, context: batchContext, } = await executeBatchedApproveAndBurn(params, provider);
9732
+ for (const step of [approveStep, burnStep]) {
9733
+ const stepName = step.name;
9734
+ if (step.state === 'error') {
9735
+ ensureStepErrorMessage(step.name, step);
9736
+ result.steps.push(step);
9737
+ result.state = 'error';
9738
+ dispatchStepEvent(stepName, step, provider, invocation);
9739
+ return undefined;
9740
+ }
9741
+ dispatchStepEvent(stepName, step, provider, invocation);
9742
+ result.steps.push(step);
9743
+ }
9744
+ return batchContext;
9745
+ }
9746
+ catch (error_) {
9747
+ // Only handles pre-submission failures (prepare rejected, wallet
9748
+ // declined, etc.). batchExecute never throws after sendCalls succeeds.
9749
+ result.state = 'error';
9750
+ result.steps.push({
9751
+ name: 'batch',
9752
+ state: 'error',
9753
+ batched: true,
9754
+ error: error_,
9755
+ errorMessage: error_ instanceof Error
9756
+ ? error_.message
9757
+ : 'Batched approve + burn failed.',
9758
+ });
9759
+ return undefined;
9760
+ }
9761
+ }
9762
+ /**
9763
+ * Ensure `step.errorMessage` is populated when an error object exists.
9764
+ *
9765
+ * @internal
9766
+ */
9767
+ function ensureStepErrorMessage(name, step) {
9768
+ if (!step.errorMessage && step.error) {
9769
+ step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
9770
+ }
9771
+ }
9534
9772
  /**
9535
9773
  * Execute a cross-chain USDC bridge using the CCTP v2 protocol.
9536
9774
  *
@@ -9564,9 +9802,7 @@ function resolveBridgeInvocation(invocationMeta) {
9564
9802
  * ```
9565
9803
  */
9566
9804
  async function bridge(params, provider) {
9567
- // Check if forwarder is enabled (on destination)
9568
9805
  const useForwarder = params.destination.useForwarder === true;
9569
- // Resolve invocation metadata to full context for event dispatching
9570
9806
  const invocation = resolveBridgeInvocation(params.invocationMeta);
9571
9807
  const result = {
9572
9808
  state: 'pending',
@@ -9579,11 +9815,9 @@ async function bridge(params, provider) {
9579
9815
  destination: {
9580
9816
  address: params.destination.address,
9581
9817
  chain: params.destination.chain,
9582
- // Preserve recipientAddress
9583
9818
  ...(params.destination.recipientAddress && {
9584
9819
  recipientAddress: params.destination.recipientAddress,
9585
9820
  }),
9586
- // Preserve useForwarder
9587
9821
  ...(useForwarder && {
9588
9822
  useForwarder: true,
9589
9823
  }),
@@ -9592,29 +9826,38 @@ async function bridge(params, provider) {
9592
9826
  config: params.config,
9593
9827
  provider: provider.name,
9594
9828
  };
9595
- // Context shared between steps
9596
9829
  let context = undefined;
9597
- // Create step executors based on useForwarder config
9830
+ let useBatched = false;
9831
+ try {
9832
+ useBatched = await shouldUseBatchedExecution(params);
9833
+ }
9834
+ catch {
9835
+ // Silently fall back to sequential
9836
+ }
9598
9837
  const executors = createStepExecutors(useForwarder);
9599
- // Execute each step in sequence
9600
- for (const { name, executor, updateContext } of executors) {
9838
+ if (useBatched) {
9839
+ const batchContext = await executeBatchedPath(params, provider, result, invocation);
9840
+ if (result.state === 'error') {
9841
+ return result;
9842
+ }
9843
+ context = batchContext;
9844
+ }
9845
+ const stepsToRun = useBatched
9846
+ ? executors.filter(({ name }) => name !== 'approve' && name !== 'burn')
9847
+ : executors;
9848
+ for (const { name, executor, updateContext } of stepsToRun) {
9601
9849
  try {
9602
9850
  const step = await executor(params, provider, context);
9603
9851
  if (step.state === 'error') {
9604
- // Ensure errorMessage is set with proper formatting if not already present
9605
- if (!step.errorMessage && step.error) {
9606
- step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
9607
- }
9852
+ ensureStepErrorMessage(name, step);
9608
9853
  result.steps.push(step);
9609
9854
  result.state = 'error';
9610
- // Dispatch event even for error steps
9611
9855
  dispatchStepEvent(name, step, provider, invocation);
9612
9856
  return result;
9613
9857
  }
9614
- // Merge new context with existing context to preserve data from previous steps
9615
9858
  const newContext = updateContext?.(step);
9616
9859
  if (newContext) {
9617
- context = { ...(context ?? {}), ...newContext };
9860
+ context = { ...context, ...newContext };
9618
9861
  }
9619
9862
  dispatchStepEvent(name, step, provider, invocation);
9620
9863
  result.steps.push(step);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circle-fin/provider-cctp-v2",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
5
5
  "keywords": [
6
6
  "circle",