@circle-fin/provider-cctp-v2 1.6.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -74,6 +74,8 @@ var Blockchain;
74
74
  Blockchain["Hedera_Testnet"] = "Hedera_Testnet";
75
75
  Blockchain["HyperEVM"] = "HyperEVM";
76
76
  Blockchain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
77
+ Blockchain["Injective"] = "Injective";
78
+ Blockchain["Injective_Testnet"] = "Injective_Testnet";
77
79
  Blockchain["Ink"] = "Ink";
78
80
  Blockchain["Ink_Testnet"] = "Ink_Testnet";
79
81
  Blockchain["Linea"] = "Linea";
@@ -88,6 +90,8 @@ var Blockchain;
88
90
  Blockchain["Noble_Testnet"] = "Noble_Testnet";
89
91
  Blockchain["Optimism"] = "Optimism";
90
92
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
93
+ Blockchain["Pharos"] = "Pharos";
94
+ Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
91
95
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
92
96
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
93
97
  Blockchain["Plume"] = "Plume";
@@ -291,11 +295,13 @@ var BridgeChain;
291
295
  BridgeChain["Edge"] = "Edge";
292
296
  BridgeChain["Ethereum"] = "Ethereum";
293
297
  BridgeChain["HyperEVM"] = "HyperEVM";
298
+ BridgeChain["Injective"] = "Injective";
294
299
  BridgeChain["Ink"] = "Ink";
295
300
  BridgeChain["Linea"] = "Linea";
296
301
  BridgeChain["Monad"] = "Monad";
297
302
  BridgeChain["Morph"] = "Morph";
298
303
  BridgeChain["Optimism"] = "Optimism";
304
+ BridgeChain["Pharos"] = "Pharos";
299
305
  BridgeChain["Plume"] = "Plume";
300
306
  BridgeChain["Polygon"] = "Polygon";
301
307
  BridgeChain["Sei"] = "Sei";
@@ -313,11 +319,13 @@ var BridgeChain;
313
319
  BridgeChain["Edge_Testnet"] = "Edge_Testnet";
314
320
  BridgeChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
315
321
  BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
322
+ BridgeChain["Injective_Testnet"] = "Injective_Testnet";
316
323
  BridgeChain["Ink_Testnet"] = "Ink_Testnet";
317
324
  BridgeChain["Linea_Sepolia"] = "Linea_Sepolia";
318
325
  BridgeChain["Monad_Testnet"] = "Monad_Testnet";
319
326
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
320
327
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
328
+ BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
321
329
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
322
330
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
323
331
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -663,6 +671,12 @@ const SWAP_TOKEN_REGISTRY = {
663
671
  category: 'wrapped',
664
672
  description: 'Wrapped Polygon',
665
673
  },
674
+ CIRBTC: {
675
+ symbol: 'CIRBTC',
676
+ decimals: 8,
677
+ category: 'wrapped',
678
+ description: 'Circle Bitcoin',
679
+ },
666
680
  };
667
681
  /**
668
682
  * Special NATIVE token constant for swap operations.
@@ -1725,6 +1739,98 @@ const HyperEVMTestnet = defineChain({
1725
1739
  },
1726
1740
  });
1727
1741
 
1742
+ /**
1743
+ * Injective Mainnet chain definition
1744
+ * @remarks
1745
+ * This represents the official production network for the Injective blockchain.
1746
+ * Injective is a high-performance, interoperable Layer-1 blockchain built for
1747
+ * finance, with an EVM execution layer on top of a Cosmos SDK base and
1748
+ * sub-second block finality.
1749
+ */
1750
+ const Injective = defineChain({
1751
+ type: 'evm',
1752
+ chain: Blockchain.Injective,
1753
+ name: 'Injective',
1754
+ title: 'Injective Mainnet',
1755
+ nativeCurrency: {
1756
+ name: 'Injective',
1757
+ symbol: 'INJ',
1758
+ decimals: 18,
1759
+ },
1760
+ chainId: 1776,
1761
+ isTestnet: false,
1762
+ explorerUrl: 'https://injscan.com/transaction/{hash}',
1763
+ rpcEndpoints: ['https://sentry.evm-rpc.injective.network'],
1764
+ eurcAddress: null,
1765
+ usdcAddress: '0xa00C59fF5a080D2b954d0c75e46E22a0c371235a',
1766
+ usdtAddress: null,
1767
+ cctp: {
1768
+ domain: 29,
1769
+ contracts: {
1770
+ v2: {
1771
+ type: 'split',
1772
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
1773
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
1774
+ confirmations: 1,
1775
+ fastConfirmations: 1,
1776
+ },
1777
+ },
1778
+ forwarderSupported: {
1779
+ source: false,
1780
+ destination: false,
1781
+ },
1782
+ },
1783
+ kitContracts: {
1784
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET,
1785
+ },
1786
+ });
1787
+
1788
+ /**
1789
+ * Injective Testnet chain definition
1790
+ * @remarks
1791
+ * This represents the official test network for the Injective blockchain.
1792
+ * Injective is a high-performance, interoperable Layer-1 blockchain built for
1793
+ * finance, with an EVM execution layer on top of a Cosmos SDK base and
1794
+ * sub-second block finality.
1795
+ */
1796
+ const InjectiveTestnet = defineChain({
1797
+ type: 'evm',
1798
+ chain: Blockchain.Injective_Testnet,
1799
+ name: 'Injective Testnet',
1800
+ title: 'Injective Testnet',
1801
+ nativeCurrency: {
1802
+ name: 'Injective',
1803
+ symbol: 'INJ',
1804
+ decimals: 18,
1805
+ },
1806
+ chainId: 1439,
1807
+ isTestnet: true,
1808
+ explorerUrl: 'https://testnet.explorer.injective.network/transaction/{hash}',
1809
+ rpcEndpoints: ['https://k8s.testnet.json-rpc.injective.network'],
1810
+ eurcAddress: null,
1811
+ usdcAddress: '0x0C382e685bbeeFE5d3d9C29e29E341fEE8E84C5d',
1812
+ usdtAddress: null,
1813
+ cctp: {
1814
+ domain: 29,
1815
+ contracts: {
1816
+ v2: {
1817
+ type: 'split',
1818
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
1819
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
1820
+ confirmations: 1,
1821
+ fastConfirmations: 1,
1822
+ },
1823
+ },
1824
+ forwarderSupported: {
1825
+ source: false,
1826
+ destination: false,
1827
+ },
1828
+ },
1829
+ kitContracts: {
1830
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1831
+ },
1832
+ });
1833
+
1728
1834
  /**
1729
1835
  * Ink Mainnet chain definition
1730
1836
  * @remarks
@@ -2334,6 +2440,96 @@ const OptimismSepolia = defineChain({
2334
2440
  },
2335
2441
  });
2336
2442
 
2443
+ /**
2444
+ * Pharos Mainnet chain definition
2445
+ * @remarks
2446
+ * This represents the official production network for the Pharos blockchain.
2447
+ * Pharos is a modular, full-stack parallel Layer 1 blockchain with
2448
+ * sub-second finality and EVM compatibility.
2449
+ */
2450
+ const Pharos = defineChain({
2451
+ type: 'evm',
2452
+ chain: Blockchain.Pharos,
2453
+ name: 'Pharos',
2454
+ title: 'Pharos Mainnet',
2455
+ nativeCurrency: {
2456
+ name: 'Pharos',
2457
+ symbol: 'PHAROS',
2458
+ decimals: 18,
2459
+ },
2460
+ chainId: 1672,
2461
+ isTestnet: false,
2462
+ explorerUrl: 'https://pharos.socialscan.io/tx/{hash}',
2463
+ rpcEndpoints: ['https://rpc.pharos.xyz'],
2464
+ eurcAddress: null,
2465
+ usdcAddress: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
2466
+ usdtAddress: null,
2467
+ cctp: {
2468
+ domain: 31,
2469
+ contracts: {
2470
+ v2: {
2471
+ type: 'split',
2472
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
2473
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
2474
+ confirmations: 1,
2475
+ fastConfirmations: 1,
2476
+ },
2477
+ },
2478
+ forwarderSupported: {
2479
+ source: false,
2480
+ destination: false,
2481
+ },
2482
+ },
2483
+ kitContracts: {
2484
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2485
+ },
2486
+ });
2487
+
2488
+ /**
2489
+ * Pharos Atlantic Testnet chain definition
2490
+ * @remarks
2491
+ * This represents the official test network for the Pharos blockchain.
2492
+ * Pharos is a modular, full-stack parallel Layer 1 blockchain with
2493
+ * sub-second finality and EVM compatibility.
2494
+ */
2495
+ const PharosTestnet = defineChain({
2496
+ type: 'evm',
2497
+ chain: Blockchain.Pharos_Testnet,
2498
+ name: 'Pharos Atlantic',
2499
+ title: 'Pharos Atlantic Testnet',
2500
+ nativeCurrency: {
2501
+ name: 'Pharos',
2502
+ symbol: 'PHAROS',
2503
+ decimals: 18,
2504
+ },
2505
+ chainId: 688689,
2506
+ isTestnet: true,
2507
+ explorerUrl: 'https://atlantic.pharosscan.xyz/tx/{hash}',
2508
+ rpcEndpoints: ['https://atlantic.dplabs-internal.com'],
2509
+ eurcAddress: null,
2510
+ usdcAddress: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
2511
+ usdtAddress: null,
2512
+ cctp: {
2513
+ domain: 31,
2514
+ contracts: {
2515
+ v2: {
2516
+ type: 'split',
2517
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
2518
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
2519
+ confirmations: 1,
2520
+ fastConfirmations: 1,
2521
+ },
2522
+ },
2523
+ forwarderSupported: {
2524
+ source: false,
2525
+ destination: false,
2526
+ },
2527
+ },
2528
+ kitContracts: {
2529
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2530
+ },
2531
+ });
2532
+
2337
2533
  /**
2338
2534
  * Plume Mainnet chain definition
2339
2535
  * @remarks
@@ -2616,7 +2812,7 @@ const Sei = defineChain({
2616
2812
  },
2617
2813
  chainId: 1329,
2618
2814
  isTestnet: false,
2619
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=pacific-1',
2815
+ explorerUrl: 'https://seiscan.io/tx/{hash}',
2620
2816
  rpcEndpoints: ['https://evm-rpc.sei-apis.com'],
2621
2817
  eurcAddress: null,
2622
2818
  usdcAddress: '0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392',
@@ -2674,7 +2870,7 @@ const SeiTestnet = defineChain({
2674
2870
  },
2675
2871
  chainId: 1328,
2676
2872
  isTestnet: true,
2677
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=atlantic-2',
2873
+ explorerUrl: 'https://testnet.seiscan.io/tx/{hash}',
2678
2874
  rpcEndpoints: ['https://evm-rpc-testnet.sei-apis.com'],
2679
2875
  eurcAddress: null,
2680
2876
  usdcAddress: '0x4fCF1784B31630811181f670Aea7A7bEF803eaED',
@@ -3477,6 +3673,8 @@ var Chains = /*#__PURE__*/Object.freeze({
3477
3673
  HederaTestnet: HederaTestnet,
3478
3674
  HyperEVM: HyperEVM,
3479
3675
  HyperEVMTestnet: HyperEVMTestnet,
3676
+ Injective: Injective,
3677
+ InjectiveTestnet: InjectiveTestnet,
3480
3678
  Ink: Ink,
3481
3679
  InkTestnet: InkTestnet,
3482
3680
  Linea: Linea,
@@ -3491,6 +3689,8 @@ var Chains = /*#__PURE__*/Object.freeze({
3491
3689
  NobleTestnet: NobleTestnet,
3492
3690
  Optimism: Optimism,
3493
3691
  OptimismSepolia: OptimismSepolia,
3692
+ Pharos: Pharos,
3693
+ PharosTestnet: PharosTestnet,
3494
3694
  Plume: Plume,
3495
3695
  PlumeTestnet: PlumeTestnet,
3496
3696
  PolkadotAssetHub: PolkadotAssetHub,
@@ -4995,6 +5195,9 @@ const NetworkError = {
4995
5195
  name: 'NETWORK_CONNECTION_FAILED',
4996
5196
  type: 'NETWORK',
4997
5197
  },
5198
+ /** Network request timeout */
5199
+ TIMEOUT: {
5200
+ code: 3002},
4998
5201
  /** Circle relayer failed to process the forwarding/mint transaction */
4999
5202
  RELAYER_FORWARD_FAILED: {
5000
5203
  code: 3003,
@@ -7171,6 +7374,7 @@ const USDC = {
7171
7374
  [Blockchain.Ethereum]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
7172
7375
  [Blockchain.Hedera]: '0.0.456858',
7173
7376
  [Blockchain.HyperEVM]: '0xb88339CB7199b77E23DB6E890353E22632Ba630f',
7377
+ [Blockchain.Injective]: '0xa00C59fF5a080D2b954d0c75e46E22a0c371235a',
7174
7378
  [Blockchain.Ink]: '0x2D270e6886d130D724215A266106e6832161EAEd',
7175
7379
  [Blockchain.Linea]: '0x176211869ca2b568f2a7d4ee941e073a821ee1ff',
7176
7380
  [Blockchain.Monad]: '0x754704Bc059F8C67012fEd69BC8A327a5aafb603',
@@ -7178,6 +7382,7 @@ const USDC = {
7178
7382
  [Blockchain.NEAR]: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
7179
7383
  [Blockchain.Noble]: 'uusdc',
7180
7384
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
7385
+ [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
7181
7386
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
7182
7387
  [Blockchain.Polkadot_Asset_Hub]: '1337',
7183
7388
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -7202,6 +7407,7 @@ const USDC = {
7202
7407
  [Blockchain.Ethereum_Sepolia]: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
7203
7408
  [Blockchain.Hedera_Testnet]: '0.0.429274',
7204
7409
  [Blockchain.HyperEVM_Testnet]: '0x2B3370eE501B4a559b57D449569354196457D8Ab',
7410
+ [Blockchain.Injective_Testnet]: '0x0C382e685bbeeFE5d3d9C29e29E341fEE8E84C5d',
7205
7411
  [Blockchain.Ink_Testnet]: '0xFabab97dCE620294D2B0b0e46C68964e326300Ac',
7206
7412
  [Blockchain.Linea_Sepolia]: '0xfece4462d57bd51a6a552365a011b95f0e16d9b7',
7207
7413
  [Blockchain.Monad_Testnet]: '0x534b2f3A21130d7a60830c2Df862319e593943A3',
@@ -7209,6 +7415,7 @@ const USDC = {
7209
7415
  [Blockchain.NEAR_Testnet]: '3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af',
7210
7416
  [Blockchain.Noble_Testnet]: 'uusdc',
7211
7417
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
7418
+ [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
7212
7419
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
7213
7420
  [Blockchain.Polkadot_Westmint]: '31337',
7214
7421
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -7489,6 +7696,32 @@ const MON = {
7489
7696
  },
7490
7697
  };
7491
7698
 
7699
+ /**
7700
+ * cirBTC (Circle Bitcoin) token definition with addresses and metadata.
7701
+ *
7702
+ * @remarks
7703
+ * Built-in cirBTC definition for the TokenRegistry. Currently deployed
7704
+ * on Arc Testnet.
7705
+ *
7706
+ * @example
7707
+ * ```typescript
7708
+ * import { CIRBTC } from '@core/tokens'
7709
+ * import { Blockchain } from '@core/chains'
7710
+ *
7711
+ * console.log(CIRBTC.symbol) // 'cirBTC'
7712
+ * console.log(CIRBTC.decimals) // 8
7713
+ * console.log(CIRBTC.locators[Blockchain.Arc_Testnet])
7714
+ * // '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF'
7715
+ * ```
7716
+ */
7717
+ const CIRBTC = {
7718
+ symbol: 'cirBTC',
7719
+ decimals: 8,
7720
+ locators: {
7721
+ [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF',
7722
+ },
7723
+ };
7724
+
7492
7725
  // Re-export for consumers
7493
7726
  /**
7494
7727
  * All default token definitions.
@@ -7497,7 +7730,7 @@ const MON = {
7497
7730
  * These tokens are automatically included in the TokenRegistry when created
7498
7731
  * without explicit defaults. Extensions can override these definitions.
7499
7732
  * Includes USDC, USDT, EURC, DAI, USDE, PYUSD, WETH, WBTC, WSOL, WAVAX,
7500
- * WPOL, ETH, POL, PLUME, and MON.
7733
+ * WPOL, ETH, POL, PLUME, MON, and cirBTC.
7501
7734
  *
7502
7735
  * @example
7503
7736
  * ```typescript
@@ -7528,6 +7761,7 @@ const DEFAULT_TOKENS = [
7528
7761
  POL,
7529
7762
  PLUME,
7530
7763
  MON,
7764
+ CIRBTC,
7531
7765
  ];
7532
7766
  /**
7533
7767
  * Uppercased token symbols approved for swap fee collection.
@@ -9138,18 +9372,16 @@ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet) => {
9138
9372
  };
9139
9373
  /**
9140
9374
  * Fetches attestation data from the IRIS API with retry and timeout handling.
9141
- * Since fetching starts after a confirmed burn transaction, we attempt up to 10 retries.
9142
- * Implements a conservative delay between requests to respect the API rate
9143
- * limit of 35 requests per second. To avoid rate limiting when multiple processes are
9144
- * running concurrently, we enforce a 200ms delay between retries.
9145
9375
  *
9146
- * Each HTTP request is given a maximum of 2 seconds before it is aborted.
9147
- * We retry up to 10 times, waiting 200ms between attempts.
9376
+ * Polls the IRIS API until a complete attestation is available. The default
9377
+ * window is sized for slow source chains where finality may take many
9378
+ * confirmations.
9148
9379
  *
9149
- * The total maximum time this function might take (worst case) is:
9150
- * - Perattempt timeout: 2 000 ms
9151
- * - Retry delays: 9 × 200 ms = 1 800 ms
9152
- * - Total max time: 2 000 ms + 1 800 ms = 3 800 ms
9380
+ * Defaults (see `DEFAULT_CONFIG`):
9381
+ * - Per-attempt timeout: 2 000 ms (each HTTP request aborts after 2 s)
9382
+ * - Retry delay: 2 000 ms between attempts
9383
+ * - Max retries: 600 (30 × 20)
9384
+ * - Total worst-case polling window: 600 × (2 000 ms + 2 000 ms) ≈ 40 minutes
9153
9385
  *
9154
9386
  * @param sourceDomainId - The CCTP domain ID.
9155
9387
  * @param transactionHash - The transaction hash to fetch attestation for.
@@ -10304,11 +10536,18 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
10304
10536
  step.explorerUrl = buildExplorerUrl(chain, txHash);
10305
10537
  if (outcome.errorMessage) {
10306
10538
  step.errorMessage = outcome.errorMessage;
10539
+ // Transaction was mined but reverted on-chain.
10540
+ step.errorCategory = 'chain_revert';
10307
10541
  }
10308
10542
  }
10309
10543
  catch (err) {
10310
10544
  step.state = 'error';
10311
10545
  step.error = err;
10546
+ // Sequential path does not yet attempt fine-grained classification of
10547
+ // pre-submission errors (user_rejected, capability errors, etc.). Mark
10548
+ // as `unknown` so consumers can at least detect the category is
10549
+ // populated uniformly across batched and sequential flows.
10550
+ step.errorCategory = 'unknown';
10312
10551
  // Optionally parse for common blockchain error formats
10313
10552
  if (err instanceof Error) {
10314
10553
  step.errorMessage = err.message;
@@ -11857,16 +12096,70 @@ async function executeBatchedApproveAndBurn(params, provider) {
11857
12096
  const batchResult = await adapter.batchExecute([approveCallData, burnCallData], chain);
11858
12097
  const approveReceipt = batchResult.receipts[0];
11859
12098
  const burnReceipt = batchResult.receipts[1];
11860
- const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain);
11861
- const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain);
12099
+ const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
12100
+ const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
11862
12101
  if (burnStep.state !== 'error' && !burnStep.txHash) {
11863
12102
  burnStep.state = 'error';
11864
12103
  burnStep.errorMessage =
11865
12104
  'Batched burn step completed but no transaction hash was returned.';
12105
+ burnStep.errorCategory = 'unknown';
11866
12106
  }
11867
12107
  const context = { burnTxHash: burnStep.txHash ?? '' };
11868
12108
  return { approveStep, burnStep, context };
11869
12109
  }
12110
+ /**
12111
+ * Derive a {@link BridgeStepErrorCategory} for a missing receipt.
12112
+ *
12113
+ * Combines the EIP-5792 `statusCode` (when present) with the underlying
12114
+ * polling error (when set) to produce the most specific category available.
12115
+ * Falls back to `'unknown'` when neither signal is conclusive.
12116
+ *
12117
+ * @param statusCode - The terminal `statusCode` from `wallet_getCallsStatus`, if any.
12118
+ * @param batchError - The polling error from `batchExecute`, if any.
12119
+ * @returns The derived error category for a missing-receipt step.
12120
+ *
12121
+ * @internal
12122
+ */
12123
+ function categorizeMissingReceipt(statusCode, batchError) {
12124
+ if (statusCode === 400)
12125
+ return 'failed_offchain';
12126
+ if (statusCode === 500)
12127
+ return 'reverted_onchain';
12128
+ if (statusCode === 600)
12129
+ return 'partial_reverted';
12130
+ if (batchError instanceof KitError &&
12131
+ batchError.code === NetworkError.TIMEOUT.code) {
12132
+ return 'polling_timeout';
12133
+ }
12134
+ return 'unknown';
12135
+ }
12136
+ /**
12137
+ * Derive a {@link BridgeStepErrorCategory} for a receipt that was returned
12138
+ * but whose per-call `status` is not `'success'`.
12139
+ *
12140
+ * A numeric EIP-5792 `statusCode` of 500 or 600 tells us the whole batch
12141
+ * reverted on-chain (completely or partially); otherwise the receipt
12142
+ * itself signalled a revert without a distinguishing code, so classify
12143
+ * as a plain on-chain revert.
12144
+ *
12145
+ * @param statusCode - The terminal `statusCode` from `wallet_getCallsStatus`, if any.
12146
+ * @returns The derived error category for a failed-receipt step.
12147
+ *
12148
+ * @internal
12149
+ */
12150
+ function categorizeFailedReceipt(statusCode) {
12151
+ // Mirror categorizeMissingReceipt: if the wallet's terminal status is
12152
+ // 400 ("batch not included onchain"), that judgement is authoritative
12153
+ // even when a non-success receipt is attached. Without this, a wrapped
12154
+ // 400 receipt would fall through to `chain_revert` and mislead UX.
12155
+ if (statusCode === 400)
12156
+ return 'failed_offchain';
12157
+ if (statusCode === 600)
12158
+ return 'partial_reverted';
12159
+ if (statusCode === 500)
12160
+ return 'reverted_onchain';
12161
+ return 'chain_revert';
12162
+ }
11870
12163
  /**
11871
12164
  * Build a {@link BridgeStep} from a single receipt within a batch.
11872
12165
  *
@@ -11881,11 +12174,17 @@ async function executeBatchedApproveAndBurn(params, provider) {
11881
12174
  * @param batchId - Wallet-assigned batch identifier.
11882
12175
  * @param adapter - The batch-capable adapter (used for confirmation).
11883
12176
  * @param chain - The EVM chain the batch was executed on.
12177
+ * @param statusCode - Optional EIP-5792 `statusCode` for the batch.
12178
+ * Used to classify the step's error category when the receipt is
12179
+ * missing or failed.
12180
+ * @param batchError - Optional polling error from `batchExecute`.
12181
+ * Preserved on the step so callers can inspect underlying timeouts
12182
+ * or RPC failures.
11884
12183
  * @returns A fully-populated bridge step with state, tx hash and explorer URL.
11885
12184
  *
11886
12185
  * @internal
11887
12186
  */
11888
- async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
12187
+ async function buildBatchedStep(name, receipt, batchId, adapter, chain, statusCode, batchError) {
11889
12188
  const step = {
11890
12189
  name,
11891
12190
  state: 'pending',
@@ -11895,6 +12194,10 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11895
12194
  if (!receipt) {
11896
12195
  step.state = 'error';
11897
12196
  step.errorMessage = `No receipt returned for ${name} in batch ${batchId}.`;
12197
+ step.errorCategory = categorizeMissingReceipt(statusCode, batchError);
12198
+ if (batchError !== undefined) {
12199
+ step.error = batchError;
12200
+ }
11898
12201
  return step;
11899
12202
  }
11900
12203
  step.txHash = receipt.txHash;
@@ -11904,11 +12207,13 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11904
12207
  if (receipt.status !== 'success') {
11905
12208
  step.state = 'error';
11906
12209
  step.errorMessage = `${name} call failed within batch ${batchId}.`;
12210
+ step.errorCategory = categorizeFailedReceipt(statusCode);
11907
12211
  return step;
11908
12212
  }
11909
12213
  if (!receipt.txHash) {
11910
12214
  step.state = 'error';
11911
12215
  step.errorMessage = `${name} succeeded in batch but returned an empty transaction hash.`;
12216
+ step.errorCategory = 'unknown';
11912
12217
  return step;
11913
12218
  }
11914
12219
  try {
@@ -11923,6 +12228,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11923
12228
  step.data = transaction;
11924
12229
  if (outcome.errorMessage) {
11925
12230
  step.errorMessage = outcome.errorMessage;
12231
+ step.errorCategory = 'chain_revert';
11926
12232
  }
11927
12233
  }
11928
12234
  catch (err) {
@@ -11930,11 +12236,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11930
12236
  step.error = err;
11931
12237
  step.errorMessage =
11932
12238
  err instanceof Error ? err.message : 'Unknown error during confirmation.';
12239
+ step.errorCategory = 'unknown';
11933
12240
  }
11934
12241
  return step;
11935
12242
  }
11936
12243
 
11937
- var version = "1.6.3";
12244
+ var version = "1.8.0";
11938
12245
  var pkg = {
11939
12246
  version: version};
11940
12247
 
@@ -12010,6 +12317,7 @@ async function executeBatchedPath(params, provider, result, invocation) {
12010
12317
  errorMessage: error_ instanceof Error
12011
12318
  ? error_.message
12012
12319
  : 'Batched approve + burn failed.',
12320
+ errorCategory: classifyPreSubmissionError(error_),
12013
12321
  });
12014
12322
  return undefined;
12015
12323
  }
@@ -12024,6 +12332,89 @@ function ensureStepErrorMessage(name, step) {
12024
12332
  step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
12025
12333
  }
12026
12334
  }
12335
+ /**
12336
+ * Coerce a raw JSON-RPC `code` to a number.
12337
+ *
12338
+ * Some wallet SDKs serialize the JSON-RPC `code` as a string ("4001")
12339
+ * after round-tripping through JSON; accept both shapes so strict `===`
12340
+ * comparisons downstream still classify 5720/5730/5740 correctly — those
12341
+ * codes have no message-pattern fallback.
12342
+ *
12343
+ * @param rawCode - The raw `code` extracted from the error object.
12344
+ * @returns The numeric code, or `undefined` if the value cannot be parsed.
12345
+ *
12346
+ * @internal
12347
+ */
12348
+ function coerceRpcCode(rawCode) {
12349
+ if (typeof rawCode === 'number') {
12350
+ return rawCode;
12351
+ }
12352
+ if (typeof rawCode === 'string') {
12353
+ return Number.parseInt(rawCode, 10);
12354
+ }
12355
+ return undefined;
12356
+ }
12357
+ /**
12358
+ * Classify a pre-submission error thrown during `wallet_sendCalls`.
12359
+ *
12360
+ * Inspect the error's JSON-RPC `code` (falling back to message pattern
12361
+ * matching for wrapper errors like viem's `ChainMismatchError`) and map
12362
+ * it to a {@link BridgeStepErrorCategory}. This lets downstream consumers
12363
+ * distinguish user rejections, wallet capability gaps, and unknown
12364
+ * failures without parsing error messages.
12365
+ *
12366
+ * @remarks
12367
+ * Does NOT alter control flow — the SDK continues to surface a
12368
+ * `state: 'error'` step. Auto-fallback to sequential execution is
12369
+ * intentionally out of scope for this helper.
12370
+ *
12371
+ * @param err - The error thrown by `wallet_sendCalls`.
12372
+ * @returns The derived error category, or `'unknown'` if no match.
12373
+ *
12374
+ * @internal
12375
+ */
12376
+ function classifyPreSubmissionError(err) {
12377
+ // Cross-realm-safe duck typing: `instanceof Error` returns false for
12378
+ // errors thrown in a different JavaScript realm (e.g., a wallet
12379
+ // provider running inside an iframe, which is common with WalletConnect
12380
+ // and the Coinbase Wallet SDK).
12381
+ if (typeof err !== 'object' || err === null || !('message' in err)) {
12382
+ return 'unknown';
12383
+ }
12384
+ const code = coerceRpcCode(err.code);
12385
+ const message = String(err.message);
12386
+ // Numeric JSON-RPC codes are authoritative; check them before falling
12387
+ // back to message-pattern matching. Order matters: an error carrying
12388
+ // `code === 5750` with a message like "user rejected the upgrade"
12389
+ // is a capability problem, not a plain user rejection.
12390
+ if (code === 4001) {
12391
+ return 'user_rejected';
12392
+ }
12393
+ if (code === 5700 || code === 5710 || code === 5750) {
12394
+ return 'atomic_unsupported';
12395
+ }
12396
+ if (code === 5720) {
12397
+ return 'duplicate_batch_id';
12398
+ }
12399
+ if (code === 5730) {
12400
+ return 'unknown_bundle';
12401
+ }
12402
+ if (code === 5740) {
12403
+ return 'batch_too_large';
12404
+ }
12405
+ // Fall back to message patterns when no specific code is available —
12406
+ // viem (and other wrapper layers) sometimes strip the numeric code
12407
+ // while preserving the original wallet message in `Details:`.
12408
+ if (/EIP-7702 not supported/i.test(message) ||
12409
+ /does not support the requested chain/i.test(message) ||
12410
+ /rejected the upgrade/i.test(message)) {
12411
+ return 'atomic_unsupported';
12412
+ }
12413
+ if (/user rejected/i.test(message)) {
12414
+ return 'user_rejected';
12415
+ }
12416
+ return 'unknown';
12417
+ }
12027
12418
  /**
12028
12419
  * Execute a cross-chain USDC bridge using the CCTP v2 protocol.
12029
12420
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circle-fin/provider-cctp-v2",
3
- "version": "1.6.3",
3
+ "version": "1.8.0",
4
4
  "description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
5
5
  "keywords": [
6
6
  "circle",