@circle-fin/provider-cctp-v2 1.6.3 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @circle-fin/provider-cctp-v2
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add support for bridging USDC to and from Pharos (mainnet and testnet) via CCTP v2.
8
+ - Bridge errors now expose a machine-readable `errorCategory` so apps can distinguish user rejections, wallet capability errors, and offchain vs onchain failures without string-matching.
9
+
3
10
  ## 1.6.3
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  **Circle's Cross-Chain Transfer Protocol v2 provider for Bridge Kit**
11
11
 
12
- _Native USDC bridging across 41 chains using Circle's battle-tested protocols._
12
+ _Native USDC bridging across 43 chains using Circle's battle-tested protocols._
13
13
 
14
14
  </div>
15
15
 
@@ -130,7 +130,7 @@ const result = await provider.bridge({
130
130
  - ✅ **Native USDC bridging** - Move real USDC between supported networks
131
131
  - ✅ **CCTP v2 integration** - Direct integration with Circle's CCTP v2 protocol
132
132
  - ✅ **Comprehensive validation** - Route validation and parameter checking
133
- - ✅ **Multi-chain support** - Works across all 41 CCTPv2-supported chains
133
+ - ✅ **Multi-chain support** - Works across all 43 CCTPv2-supported chains
134
134
  - ✅ **Type safety** - Full TypeScript support with detailed error handling
135
135
  - ✅ **Bridge speeds** - Support for both FAST and SLOW bridge configurations
136
136
  - ✅ **Forwarder integration** - Circle's Orbit relayer handles attestation and mint automatically
@@ -138,15 +138,15 @@ const result = await provider.bridge({
138
138
 
139
139
  ## Supported Chains & Routes
140
140
 
141
- The provider supports **800 total bridge routes** across these chains:
141
+ The provider supports **882 total bridge routes** across these chains:
142
142
 
143
143
  ### Mainnet Chains
144
144
 
145
- **Arbitrum**, **Avalanche**, **Base**, **Codex**, **Edge**, **Ethereum**, **HyperEVM**, **Ink**, **Linea**, **Monad**, **Morph**, **OP Mainnet**, **Plume**, **Polygon PoS**, **Sei**, **Solana**, **Sonic**, **Unichain**, **World Chain**, **XDC**
145
+ **Arbitrum**, **Avalanche**, **Base**, **Codex**, **Edge**, **Ethereum**, **HyperEVM**, **Ink**, **Linea**, **Monad**, **Morph**, **OP Mainnet**, **Pharos**, **Plume**, **Polygon PoS**, **Sei**, **Solana**, **Sonic**, **Unichain**, **World Chain**, **XDC**
146
146
 
147
147
  ### Testnet Chains
148
148
 
149
- **Arc Testnet**, **Arbitrum Sepolia**, **Avalanche Fuji**, **Base Sepolia**, **Codex Testnet**, **Edge Testnet**, **Ethereum Sepolia**, **HyperEVM Testnet**, **Ink Testnet**, **Linea Sepolia**, **Monad Testnet**, **Morph Testnet**, **OP Sepolia**, **Plume Testnet**, **Polygon PoS Amoy**, **Sei Testnet**, **Solana Devnet**, **Sonic Testnet**, **Unichain Sepolia**, **World Chain Sepolia**, **XDC Apothem**
149
+ **Arc Testnet**, **Arbitrum Sepolia**, **Avalanche Fuji**, **Base Sepolia**, **Codex Testnet**, **Edge Testnet**, **Ethereum Sepolia**, **HyperEVM Testnet**, **Ink Testnet**, **Linea Sepolia**, **Monad Testnet**, **Morph Testnet**, **OP Sepolia**, **Pharos Atlantic**, **Plume Testnet**, **Polygon PoS Amoy**, **Sei Testnet**, **Solana Devnet**, **Sonic Testnet**, **Unichain Sepolia**, **World Chain Sepolia**, **XDC Apothem**
150
150
 
151
151
  ## Error Handling
152
152
 
package/index.cjs CHANGED
@@ -95,6 +95,8 @@ var Blockchain;
95
95
  Blockchain["Noble_Testnet"] = "Noble_Testnet";
96
96
  Blockchain["Optimism"] = "Optimism";
97
97
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
98
+ Blockchain["Pharos"] = "Pharos";
99
+ Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
98
100
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
99
101
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
100
102
  Blockchain["Plume"] = "Plume";
@@ -303,6 +305,7 @@ var BridgeChain;
303
305
  BridgeChain["Monad"] = "Monad";
304
306
  BridgeChain["Morph"] = "Morph";
305
307
  BridgeChain["Optimism"] = "Optimism";
308
+ BridgeChain["Pharos"] = "Pharos";
306
309
  BridgeChain["Plume"] = "Plume";
307
310
  BridgeChain["Polygon"] = "Polygon";
308
311
  BridgeChain["Sei"] = "Sei";
@@ -325,6 +328,7 @@ var BridgeChain;
325
328
  BridgeChain["Monad_Testnet"] = "Monad_Testnet";
326
329
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
327
330
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
331
+ BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
328
332
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
329
333
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
330
334
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -670,6 +674,12 @@ const SWAP_TOKEN_REGISTRY = {
670
674
  category: 'wrapped',
671
675
  description: 'Wrapped Polygon',
672
676
  },
677
+ CIRBTC: {
678
+ symbol: 'CIRBTC',
679
+ decimals: 8,
680
+ category: 'wrapped',
681
+ description: 'Circle Bitcoin',
682
+ },
673
683
  };
674
684
  /**
675
685
  * Special NATIVE token constant for swap operations.
@@ -2341,6 +2351,96 @@ const OptimismSepolia = defineChain({
2341
2351
  },
2342
2352
  });
2343
2353
 
2354
+ /**
2355
+ * Pharos Mainnet chain definition
2356
+ * @remarks
2357
+ * This represents the official production network for the Pharos blockchain.
2358
+ * Pharos is a modular, full-stack parallel Layer 1 blockchain with
2359
+ * sub-second finality and EVM compatibility.
2360
+ */
2361
+ const Pharos = defineChain({
2362
+ type: 'evm',
2363
+ chain: Blockchain.Pharos,
2364
+ name: 'Pharos',
2365
+ title: 'Pharos Mainnet',
2366
+ nativeCurrency: {
2367
+ name: 'Pharos',
2368
+ symbol: 'PHAROS',
2369
+ decimals: 18,
2370
+ },
2371
+ chainId: 1672,
2372
+ isTestnet: false,
2373
+ explorerUrl: 'https://pharos.socialscan.io/tx/{hash}',
2374
+ rpcEndpoints: ['https://rpc.pharos.xyz'],
2375
+ eurcAddress: null,
2376
+ usdcAddress: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
2377
+ usdtAddress: null,
2378
+ cctp: {
2379
+ domain: 31,
2380
+ contracts: {
2381
+ v2: {
2382
+ type: 'split',
2383
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
2384
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
2385
+ confirmations: 1,
2386
+ fastConfirmations: 1,
2387
+ },
2388
+ },
2389
+ forwarderSupported: {
2390
+ source: false,
2391
+ destination: false,
2392
+ },
2393
+ },
2394
+ kitContracts: {
2395
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2396
+ },
2397
+ });
2398
+
2399
+ /**
2400
+ * Pharos Atlantic Testnet chain definition
2401
+ * @remarks
2402
+ * This represents the official test network for the Pharos blockchain.
2403
+ * Pharos is a modular, full-stack parallel Layer 1 blockchain with
2404
+ * sub-second finality and EVM compatibility.
2405
+ */
2406
+ const PharosTestnet = defineChain({
2407
+ type: 'evm',
2408
+ chain: Blockchain.Pharos_Testnet,
2409
+ name: 'Pharos Atlantic',
2410
+ title: 'Pharos Atlantic Testnet',
2411
+ nativeCurrency: {
2412
+ name: 'Pharos',
2413
+ symbol: 'PHAROS',
2414
+ decimals: 18,
2415
+ },
2416
+ chainId: 688689,
2417
+ isTestnet: true,
2418
+ explorerUrl: 'https://atlantic.pharosscan.xyz/tx/{hash}',
2419
+ rpcEndpoints: ['https://atlantic.dplabs-internal.com'],
2420
+ eurcAddress: null,
2421
+ usdcAddress: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
2422
+ usdtAddress: null,
2423
+ cctp: {
2424
+ domain: 31,
2425
+ contracts: {
2426
+ v2: {
2427
+ type: 'split',
2428
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
2429
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
2430
+ confirmations: 1,
2431
+ fastConfirmations: 1,
2432
+ },
2433
+ },
2434
+ forwarderSupported: {
2435
+ source: false,
2436
+ destination: false,
2437
+ },
2438
+ },
2439
+ kitContracts: {
2440
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2441
+ },
2442
+ });
2443
+
2344
2444
  /**
2345
2445
  * Plume Mainnet chain definition
2346
2446
  * @remarks
@@ -2623,7 +2723,7 @@ const Sei = defineChain({
2623
2723
  },
2624
2724
  chainId: 1329,
2625
2725
  isTestnet: false,
2626
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=pacific-1',
2726
+ explorerUrl: 'https://seiscan.io/tx/{hash}',
2627
2727
  rpcEndpoints: ['https://evm-rpc.sei-apis.com'],
2628
2728
  eurcAddress: null,
2629
2729
  usdcAddress: '0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392',
@@ -2681,7 +2781,7 @@ const SeiTestnet = defineChain({
2681
2781
  },
2682
2782
  chainId: 1328,
2683
2783
  isTestnet: true,
2684
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=atlantic-2',
2784
+ explorerUrl: 'https://testnet.seiscan.io/tx/{hash}',
2685
2785
  rpcEndpoints: ['https://evm-rpc-testnet.sei-apis.com'],
2686
2786
  eurcAddress: null,
2687
2787
  usdcAddress: '0x4fCF1784B31630811181f670Aea7A7bEF803eaED',
@@ -3498,6 +3598,8 @@ var Chains = {
3498
3598
  NobleTestnet: NobleTestnet,
3499
3599
  Optimism: Optimism,
3500
3600
  OptimismSepolia: OptimismSepolia,
3601
+ Pharos: Pharos,
3602
+ PharosTestnet: PharosTestnet,
3501
3603
  Plume: Plume,
3502
3604
  PlumeTestnet: PlumeTestnet,
3503
3605
  PolkadotAssetHub: PolkadotAssetHub,
@@ -5002,6 +5104,9 @@ const NetworkError = {
5002
5104
  name: 'NETWORK_CONNECTION_FAILED',
5003
5105
  type: 'NETWORK',
5004
5106
  },
5107
+ /** Network request timeout */
5108
+ TIMEOUT: {
5109
+ code: 3002},
5005
5110
  /** Circle relayer failed to process the forwarding/mint transaction */
5006
5111
  RELAYER_FORWARD_FAILED: {
5007
5112
  code: 3003,
@@ -7185,6 +7290,7 @@ const USDC = {
7185
7290
  [Blockchain.NEAR]: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
7186
7291
  [Blockchain.Noble]: 'uusdc',
7187
7292
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
7293
+ [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
7188
7294
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
7189
7295
  [Blockchain.Polkadot_Asset_Hub]: '1337',
7190
7296
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -7216,6 +7322,7 @@ const USDC = {
7216
7322
  [Blockchain.NEAR_Testnet]: '3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af',
7217
7323
  [Blockchain.Noble_Testnet]: 'uusdc',
7218
7324
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
7325
+ [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
7219
7326
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
7220
7327
  [Blockchain.Polkadot_Westmint]: '31337',
7221
7328
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -7496,6 +7603,32 @@ const MON = {
7496
7603
  },
7497
7604
  };
7498
7605
 
7606
+ /**
7607
+ * cirBTC (Circle Bitcoin) token definition with addresses and metadata.
7608
+ *
7609
+ * @remarks
7610
+ * Built-in cirBTC definition for the TokenRegistry. Currently deployed
7611
+ * on Arc Testnet.
7612
+ *
7613
+ * @example
7614
+ * ```typescript
7615
+ * import { CIRBTC } from '@core/tokens'
7616
+ * import { Blockchain } from '@core/chains'
7617
+ *
7618
+ * console.log(CIRBTC.symbol) // 'cirBTC'
7619
+ * console.log(CIRBTC.decimals) // 8
7620
+ * console.log(CIRBTC.locators[Blockchain.Arc_Testnet])
7621
+ * // '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF'
7622
+ * ```
7623
+ */
7624
+ const CIRBTC = {
7625
+ symbol: 'cirBTC',
7626
+ decimals: 8,
7627
+ locators: {
7628
+ [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF',
7629
+ },
7630
+ };
7631
+
7499
7632
  // Re-export for consumers
7500
7633
  /**
7501
7634
  * All default token definitions.
@@ -7504,7 +7637,7 @@ const MON = {
7504
7637
  * These tokens are automatically included in the TokenRegistry when created
7505
7638
  * without explicit defaults. Extensions can override these definitions.
7506
7639
  * Includes USDC, USDT, EURC, DAI, USDE, PYUSD, WETH, WBTC, WSOL, WAVAX,
7507
- * WPOL, ETH, POL, PLUME, and MON.
7640
+ * WPOL, ETH, POL, PLUME, MON, and cirBTC.
7508
7641
  *
7509
7642
  * @example
7510
7643
  * ```typescript
@@ -7535,6 +7668,7 @@ const DEFAULT_TOKENS = [
7535
7668
  POL,
7536
7669
  PLUME,
7537
7670
  MON,
7671
+ CIRBTC,
7538
7672
  ];
7539
7673
  /**
7540
7674
  * Uppercased token symbols approved for swap fee collection.
@@ -10311,11 +10445,18 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
10311
10445
  step.explorerUrl = buildExplorerUrl(chain, txHash);
10312
10446
  if (outcome.errorMessage) {
10313
10447
  step.errorMessage = outcome.errorMessage;
10448
+ // Transaction was mined but reverted on-chain.
10449
+ step.errorCategory = 'chain_revert';
10314
10450
  }
10315
10451
  }
10316
10452
  catch (err) {
10317
10453
  step.state = 'error';
10318
10454
  step.error = err;
10455
+ // Sequential path does not yet attempt fine-grained classification of
10456
+ // pre-submission errors (user_rejected, capability errors, etc.). Mark
10457
+ // as `unknown` so consumers can at least detect the category is
10458
+ // populated uniformly across batched and sequential flows.
10459
+ step.errorCategory = 'unknown';
10319
10460
  // Optionally parse for common blockchain error formats
10320
10461
  if (err instanceof Error) {
10321
10462
  step.errorMessage = err.message;
@@ -11864,16 +12005,70 @@ async function executeBatchedApproveAndBurn(params, provider) {
11864
12005
  const batchResult = await adapter.batchExecute([approveCallData, burnCallData], chain);
11865
12006
  const approveReceipt = batchResult.receipts[0];
11866
12007
  const burnReceipt = batchResult.receipts[1];
11867
- const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain);
11868
- const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain);
12008
+ const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
12009
+ const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
11869
12010
  if (burnStep.state !== 'error' && !burnStep.txHash) {
11870
12011
  burnStep.state = 'error';
11871
12012
  burnStep.errorMessage =
11872
12013
  'Batched burn step completed but no transaction hash was returned.';
12014
+ burnStep.errorCategory = 'unknown';
11873
12015
  }
11874
12016
  const context = { burnTxHash: burnStep.txHash ?? '' };
11875
12017
  return { approveStep, burnStep, context };
11876
12018
  }
12019
+ /**
12020
+ * Derive a {@link BridgeStepErrorCategory} for a missing receipt.
12021
+ *
12022
+ * Combines the EIP-5792 `statusCode` (when present) with the underlying
12023
+ * polling error (when set) to produce the most specific category available.
12024
+ * Falls back to `'unknown'` when neither signal is conclusive.
12025
+ *
12026
+ * @param statusCode - The terminal `statusCode` from `wallet_getCallsStatus`, if any.
12027
+ * @param batchError - The polling error from `batchExecute`, if any.
12028
+ * @returns The derived error category for a missing-receipt step.
12029
+ *
12030
+ * @internal
12031
+ */
12032
+ function categorizeMissingReceipt(statusCode, batchError) {
12033
+ if (statusCode === 400)
12034
+ return 'failed_offchain';
12035
+ if (statusCode === 500)
12036
+ return 'reverted_onchain';
12037
+ if (statusCode === 600)
12038
+ return 'partial_reverted';
12039
+ if (batchError instanceof KitError &&
12040
+ batchError.code === NetworkError.TIMEOUT.code) {
12041
+ return 'polling_timeout';
12042
+ }
12043
+ return 'unknown';
12044
+ }
12045
+ /**
12046
+ * Derive a {@link BridgeStepErrorCategory} for a receipt that was returned
12047
+ * but whose per-call `status` is not `'success'`.
12048
+ *
12049
+ * A numeric EIP-5792 `statusCode` of 500 or 600 tells us the whole batch
12050
+ * reverted on-chain (completely or partially); otherwise the receipt
12051
+ * itself signalled a revert without a distinguishing code, so classify
12052
+ * as a plain on-chain revert.
12053
+ *
12054
+ * @param statusCode - The terminal `statusCode` from `wallet_getCallsStatus`, if any.
12055
+ * @returns The derived error category for a failed-receipt step.
12056
+ *
12057
+ * @internal
12058
+ */
12059
+ function categorizeFailedReceipt(statusCode) {
12060
+ // Mirror categorizeMissingReceipt: if the wallet's terminal status is
12061
+ // 400 ("batch not included onchain"), that judgement is authoritative
12062
+ // even when a non-success receipt is attached. Without this, a wrapped
12063
+ // 400 receipt would fall through to `chain_revert` and mislead UX.
12064
+ if (statusCode === 400)
12065
+ return 'failed_offchain';
12066
+ if (statusCode === 600)
12067
+ return 'partial_reverted';
12068
+ if (statusCode === 500)
12069
+ return 'reverted_onchain';
12070
+ return 'chain_revert';
12071
+ }
11877
12072
  /**
11878
12073
  * Build a {@link BridgeStep} from a single receipt within a batch.
11879
12074
  *
@@ -11888,11 +12083,17 @@ async function executeBatchedApproveAndBurn(params, provider) {
11888
12083
  * @param batchId - Wallet-assigned batch identifier.
11889
12084
  * @param adapter - The batch-capable adapter (used for confirmation).
11890
12085
  * @param chain - The EVM chain the batch was executed on.
12086
+ * @param statusCode - Optional EIP-5792 `statusCode` for the batch.
12087
+ * Used to classify the step's error category when the receipt is
12088
+ * missing or failed.
12089
+ * @param batchError - Optional polling error from `batchExecute`.
12090
+ * Preserved on the step so callers can inspect underlying timeouts
12091
+ * or RPC failures.
11891
12092
  * @returns A fully-populated bridge step with state, tx hash and explorer URL.
11892
12093
  *
11893
12094
  * @internal
11894
12095
  */
11895
- async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
12096
+ async function buildBatchedStep(name, receipt, batchId, adapter, chain, statusCode, batchError) {
11896
12097
  const step = {
11897
12098
  name,
11898
12099
  state: 'pending',
@@ -11902,6 +12103,10 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11902
12103
  if (!receipt) {
11903
12104
  step.state = 'error';
11904
12105
  step.errorMessage = `No receipt returned for ${name} in batch ${batchId}.`;
12106
+ step.errorCategory = categorizeMissingReceipt(statusCode, batchError);
12107
+ if (batchError !== undefined) {
12108
+ step.error = batchError;
12109
+ }
11905
12110
  return step;
11906
12111
  }
11907
12112
  step.txHash = receipt.txHash;
@@ -11911,11 +12116,13 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11911
12116
  if (receipt.status !== 'success') {
11912
12117
  step.state = 'error';
11913
12118
  step.errorMessage = `${name} call failed within batch ${batchId}.`;
12119
+ step.errorCategory = categorizeFailedReceipt(statusCode);
11914
12120
  return step;
11915
12121
  }
11916
12122
  if (!receipt.txHash) {
11917
12123
  step.state = 'error';
11918
12124
  step.errorMessage = `${name} succeeded in batch but returned an empty transaction hash.`;
12125
+ step.errorCategory = 'unknown';
11919
12126
  return step;
11920
12127
  }
11921
12128
  try {
@@ -11930,6 +12137,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11930
12137
  step.data = transaction;
11931
12138
  if (outcome.errorMessage) {
11932
12139
  step.errorMessage = outcome.errorMessage;
12140
+ step.errorCategory = 'chain_revert';
11933
12141
  }
11934
12142
  }
11935
12143
  catch (err) {
@@ -11937,11 +12145,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11937
12145
  step.error = err;
11938
12146
  step.errorMessage =
11939
12147
  err instanceof Error ? err.message : 'Unknown error during confirmation.';
12148
+ step.errorCategory = 'unknown';
11940
12149
  }
11941
12150
  return step;
11942
12151
  }
11943
12152
 
11944
- var version = "1.6.3";
12153
+ var version = "1.7.0";
11945
12154
  var pkg = {
11946
12155
  version: version};
11947
12156
 
@@ -12017,6 +12226,7 @@ async function executeBatchedPath(params, provider, result, invocation) {
12017
12226
  errorMessage: error_ instanceof Error
12018
12227
  ? error_.message
12019
12228
  : 'Batched approve + burn failed.',
12229
+ errorCategory: classifyPreSubmissionError(error_),
12020
12230
  });
12021
12231
  return undefined;
12022
12232
  }
@@ -12031,6 +12241,89 @@ function ensureStepErrorMessage(name, step) {
12031
12241
  step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
12032
12242
  }
12033
12243
  }
12244
+ /**
12245
+ * Coerce a raw JSON-RPC `code` to a number.
12246
+ *
12247
+ * Some wallet SDKs serialize the JSON-RPC `code` as a string ("4001")
12248
+ * after round-tripping through JSON; accept both shapes so strict `===`
12249
+ * comparisons downstream still classify 5720/5730/5740 correctly — those
12250
+ * codes have no message-pattern fallback.
12251
+ *
12252
+ * @param rawCode - The raw `code` extracted from the error object.
12253
+ * @returns The numeric code, or `undefined` if the value cannot be parsed.
12254
+ *
12255
+ * @internal
12256
+ */
12257
+ function coerceRpcCode(rawCode) {
12258
+ if (typeof rawCode === 'number') {
12259
+ return rawCode;
12260
+ }
12261
+ if (typeof rawCode === 'string') {
12262
+ return Number.parseInt(rawCode, 10);
12263
+ }
12264
+ return undefined;
12265
+ }
12266
+ /**
12267
+ * Classify a pre-submission error thrown during `wallet_sendCalls`.
12268
+ *
12269
+ * Inspect the error's JSON-RPC `code` (falling back to message pattern
12270
+ * matching for wrapper errors like viem's `ChainMismatchError`) and map
12271
+ * it to a {@link BridgeStepErrorCategory}. This lets downstream consumers
12272
+ * distinguish user rejections, wallet capability gaps, and unknown
12273
+ * failures without parsing error messages.
12274
+ *
12275
+ * @remarks
12276
+ * Does NOT alter control flow — the SDK continues to surface a
12277
+ * `state: 'error'` step. Auto-fallback to sequential execution is
12278
+ * intentionally out of scope for this helper.
12279
+ *
12280
+ * @param err - The error thrown by `wallet_sendCalls`.
12281
+ * @returns The derived error category, or `'unknown'` if no match.
12282
+ *
12283
+ * @internal
12284
+ */
12285
+ function classifyPreSubmissionError(err) {
12286
+ // Cross-realm-safe duck typing: `instanceof Error` returns false for
12287
+ // errors thrown in a different JavaScript realm (e.g., a wallet
12288
+ // provider running inside an iframe, which is common with WalletConnect
12289
+ // and the Coinbase Wallet SDK).
12290
+ if (typeof err !== 'object' || err === null || !('message' in err)) {
12291
+ return 'unknown';
12292
+ }
12293
+ const code = coerceRpcCode(err.code);
12294
+ const message = String(err.message);
12295
+ // Numeric JSON-RPC codes are authoritative; check them before falling
12296
+ // back to message-pattern matching. Order matters: an error carrying
12297
+ // `code === 5750` with a message like "user rejected the upgrade"
12298
+ // is a capability problem, not a plain user rejection.
12299
+ if (code === 4001) {
12300
+ return 'user_rejected';
12301
+ }
12302
+ if (code === 5700 || code === 5710 || code === 5750) {
12303
+ return 'atomic_unsupported';
12304
+ }
12305
+ if (code === 5720) {
12306
+ return 'duplicate_batch_id';
12307
+ }
12308
+ if (code === 5730) {
12309
+ return 'unknown_bundle';
12310
+ }
12311
+ if (code === 5740) {
12312
+ return 'batch_too_large';
12313
+ }
12314
+ // Fall back to message patterns when no specific code is available —
12315
+ // viem (and other wrapper layers) sometimes strip the numeric code
12316
+ // while preserving the original wallet message in `Details:`.
12317
+ if (/EIP-7702 not supported/i.test(message) ||
12318
+ /does not support the requested chain/i.test(message) ||
12319
+ /rejected the upgrade/i.test(message)) {
12320
+ return 'atomic_unsupported';
12321
+ }
12322
+ if (/user rejected/i.test(message)) {
12323
+ return 'user_rejected';
12324
+ }
12325
+ return 'unknown';
12326
+ }
12034
12327
  /**
12035
12328
  * Execute a cross-chain USDC bridge using the CCTP v2 protocol.
12036
12329
  *
package/index.d.ts CHANGED
@@ -680,6 +680,8 @@ declare enum Blockchain {
680
680
  Noble_Testnet = "Noble_Testnet",
681
681
  Optimism = "Optimism",
682
682
  Optimism_Sepolia = "Optimism_Sepolia",
683
+ Pharos = "Pharos",
684
+ Pharos_Testnet = "Pharos_Testnet",
683
685
  Polkadot_Asset_Hub = "Polkadot_Asset_Hub",
684
686
  Polkadot_Westmint = "Polkadot_Westmint",
685
687
  Plume = "Plume",
@@ -3700,6 +3702,7 @@ declare module './types' {
3700
3702
  POL: true;
3701
3703
  PLUME: true;
3702
3704
  MON: true;
3705
+ cirBTC: true;
3703
3706
  }
3704
3707
  }
3705
3708
 
@@ -4894,6 +4897,63 @@ TChainDefinition extends ChainDefinition = ChainDefinition> {
4894
4897
  */
4895
4898
  invocationMeta?: InvocationMeta;
4896
4899
  }
4900
+ /**
4901
+ * Machine-readable classification of a {@link BridgeStep} error.
4902
+ *
4903
+ * Consumers may use this field for UX decisions (e.g. distinguish a user
4904
+ * rejection from a wallet capability error) without string-matching on
4905
+ * {@link BridgeStep.errorMessage}. The original human-readable error
4906
+ * message is always preserved for display/logging.
4907
+ *
4908
+ * @remarks
4909
+ * The categories map to the most common failure shapes observed across
4910
+ * the EIP-5792 batched bridge path and the sequential bridge path:
4911
+ *
4912
+ * - `user_rejected` — user declined a wallet request (JSON-RPC `4001`).
4913
+ * - `atomic_unsupported` — wallet reported it cannot perform EIP-5792
4914
+ * atomic batching on this chain. Covers JSON-RPC `5700` (unsupported
4915
+ * capabilities), `5710` (chain not supported for the requested
4916
+ * capability), and `5750` (atomicity requires a wallet upgrade which
4917
+ * the user declined), or equivalent viem-wrapped messages.
4918
+ * - `batch_too_large` — wallet rejected the batch for exceeding its
4919
+ * size limit (JSON-RPC `5740`).
4920
+ * - `duplicate_batch_id` — wallet reported a duplicate batchId
4921
+ * (JSON-RPC `5720`).
4922
+ * - `unknown_bundle` — wallet reported an unknown bundle id during
4923
+ * status polling (JSON-RPC `5730`).
4924
+ * - `polling_timeout` — SDK polled `wallet_getCallsStatus` until the
4925
+ * configured timeout without receiving a terminal status.
4926
+ * - `failed_offchain` — wallet reported EIP-5792 `statusCode: 400`
4927
+ * (batch not included onchain, wallet will not retry).
4928
+ * - `reverted_onchain` — wallet reported EIP-5792 `statusCode: 500`
4929
+ * (batch reverted completely onchain).
4930
+ * - `partial_reverted` — wallet reported EIP-5792 `statusCode: 600`
4931
+ * (batch reverted partially onchain).
4932
+ * - `chain_revert` — transaction was mined but reverted on-chain
4933
+ * (sequential path).
4934
+ * - `unknown` — error did not match any of the above categories.
4935
+ *
4936
+ * @since 2.0.0
4937
+ *
4938
+ * @example
4939
+ * ```typescript
4940
+ * import type { BridgeStep } from '@core/provider'
4941
+ *
4942
+ * const step: BridgeStep = {
4943
+ * name: 'approve',
4944
+ * state: 'error',
4945
+ * errorMessage: 'User rejected the request',
4946
+ * errorCategory: 'user_rejected',
4947
+ * }
4948
+ *
4949
+ * if (step.errorCategory === 'user_rejected') {
4950
+ * // silent abort: user intentionally cancelled
4951
+ * } else if (step.errorCategory === 'atomic_unsupported') {
4952
+ * // hint the user about switching to step-by-step signing
4953
+ * }
4954
+ * ```
4955
+ */
4956
+ type BridgeStepErrorCategory = 'user_rejected' | 'atomic_unsupported' | 'batch_too_large' | 'duplicate_batch_id' | 'unknown_bundle' | 'polling_timeout' | 'failed_offchain' | 'reverted_onchain' | 'partial_reverted' | 'chain_revert' | 'unknown';
4897
4957
  /**
4898
4958
  * A step in the bridge process.
4899
4959
  *
@@ -4950,6 +5010,21 @@ interface BridgeStep {
4950
5010
  errorMessage?: string;
4951
5011
  /** Optional raw error object (can be Viem/Ethers/Chain error) */
4952
5012
  error?: unknown;
5013
+ /**
5014
+ * Optional machine-readable classification of the error.
5015
+ *
5016
+ * Present when the step is in `state: 'error'` and the SDK was able to
5017
+ * categorize the failure. See {@link BridgeStepErrorCategory} for the
5018
+ * list of categories and how they map to underlying error shapes.
5019
+ *
5020
+ * @remarks
5021
+ * The human-readable {@link errorMessage} is always preserved for
5022
+ * logging and display; this field is additive and should be preferred
5023
+ * over string-matching `errorMessage` for machine decisions.
5024
+ *
5025
+ * @since 2.0.0
5026
+ */
5027
+ errorCategory?: BridgeStepErrorCategory;
4953
5028
  }
4954
5029
  /**
4955
5030
  * Result object returned after a successful cross-chain bridge operation.
package/index.mjs CHANGED
@@ -88,6 +88,8 @@ var Blockchain;
88
88
  Blockchain["Noble_Testnet"] = "Noble_Testnet";
89
89
  Blockchain["Optimism"] = "Optimism";
90
90
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
91
+ Blockchain["Pharos"] = "Pharos";
92
+ Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
91
93
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
92
94
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
93
95
  Blockchain["Plume"] = "Plume";
@@ -296,6 +298,7 @@ var BridgeChain;
296
298
  BridgeChain["Monad"] = "Monad";
297
299
  BridgeChain["Morph"] = "Morph";
298
300
  BridgeChain["Optimism"] = "Optimism";
301
+ BridgeChain["Pharos"] = "Pharos";
299
302
  BridgeChain["Plume"] = "Plume";
300
303
  BridgeChain["Polygon"] = "Polygon";
301
304
  BridgeChain["Sei"] = "Sei";
@@ -318,6 +321,7 @@ var BridgeChain;
318
321
  BridgeChain["Monad_Testnet"] = "Monad_Testnet";
319
322
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
320
323
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
324
+ BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
321
325
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
322
326
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
323
327
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -663,6 +667,12 @@ const SWAP_TOKEN_REGISTRY = {
663
667
  category: 'wrapped',
664
668
  description: 'Wrapped Polygon',
665
669
  },
670
+ CIRBTC: {
671
+ symbol: 'CIRBTC',
672
+ decimals: 8,
673
+ category: 'wrapped',
674
+ description: 'Circle Bitcoin',
675
+ },
666
676
  };
667
677
  /**
668
678
  * Special NATIVE token constant for swap operations.
@@ -2334,6 +2344,96 @@ const OptimismSepolia = defineChain({
2334
2344
  },
2335
2345
  });
2336
2346
 
2347
+ /**
2348
+ * Pharos Mainnet chain definition
2349
+ * @remarks
2350
+ * This represents the official production network for the Pharos blockchain.
2351
+ * Pharos is a modular, full-stack parallel Layer 1 blockchain with
2352
+ * sub-second finality and EVM compatibility.
2353
+ */
2354
+ const Pharos = defineChain({
2355
+ type: 'evm',
2356
+ chain: Blockchain.Pharos,
2357
+ name: 'Pharos',
2358
+ title: 'Pharos Mainnet',
2359
+ nativeCurrency: {
2360
+ name: 'Pharos',
2361
+ symbol: 'PHAROS',
2362
+ decimals: 18,
2363
+ },
2364
+ chainId: 1672,
2365
+ isTestnet: false,
2366
+ explorerUrl: 'https://pharos.socialscan.io/tx/{hash}',
2367
+ rpcEndpoints: ['https://rpc.pharos.xyz'],
2368
+ eurcAddress: null,
2369
+ usdcAddress: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
2370
+ usdtAddress: null,
2371
+ cctp: {
2372
+ domain: 31,
2373
+ contracts: {
2374
+ v2: {
2375
+ type: 'split',
2376
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
2377
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
2378
+ confirmations: 1,
2379
+ fastConfirmations: 1,
2380
+ },
2381
+ },
2382
+ forwarderSupported: {
2383
+ source: false,
2384
+ destination: false,
2385
+ },
2386
+ },
2387
+ kitContracts: {
2388
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2389
+ },
2390
+ });
2391
+
2392
+ /**
2393
+ * Pharos Atlantic Testnet chain definition
2394
+ * @remarks
2395
+ * This represents the official test network for the Pharos blockchain.
2396
+ * Pharos is a modular, full-stack parallel Layer 1 blockchain with
2397
+ * sub-second finality and EVM compatibility.
2398
+ */
2399
+ const PharosTestnet = defineChain({
2400
+ type: 'evm',
2401
+ chain: Blockchain.Pharos_Testnet,
2402
+ name: 'Pharos Atlantic',
2403
+ title: 'Pharos Atlantic Testnet',
2404
+ nativeCurrency: {
2405
+ name: 'Pharos',
2406
+ symbol: 'PHAROS',
2407
+ decimals: 18,
2408
+ },
2409
+ chainId: 688689,
2410
+ isTestnet: true,
2411
+ explorerUrl: 'https://atlantic.pharosscan.xyz/tx/{hash}',
2412
+ rpcEndpoints: ['https://atlantic.dplabs-internal.com'],
2413
+ eurcAddress: null,
2414
+ usdcAddress: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
2415
+ usdtAddress: null,
2416
+ cctp: {
2417
+ domain: 31,
2418
+ contracts: {
2419
+ v2: {
2420
+ type: 'split',
2421
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
2422
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
2423
+ confirmations: 1,
2424
+ fastConfirmations: 1,
2425
+ },
2426
+ },
2427
+ forwarderSupported: {
2428
+ source: false,
2429
+ destination: false,
2430
+ },
2431
+ },
2432
+ kitContracts: {
2433
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2434
+ },
2435
+ });
2436
+
2337
2437
  /**
2338
2438
  * Plume Mainnet chain definition
2339
2439
  * @remarks
@@ -2616,7 +2716,7 @@ const Sei = defineChain({
2616
2716
  },
2617
2717
  chainId: 1329,
2618
2718
  isTestnet: false,
2619
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=pacific-1',
2719
+ explorerUrl: 'https://seiscan.io/tx/{hash}',
2620
2720
  rpcEndpoints: ['https://evm-rpc.sei-apis.com'],
2621
2721
  eurcAddress: null,
2622
2722
  usdcAddress: '0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392',
@@ -2674,7 +2774,7 @@ const SeiTestnet = defineChain({
2674
2774
  },
2675
2775
  chainId: 1328,
2676
2776
  isTestnet: true,
2677
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=atlantic-2',
2777
+ explorerUrl: 'https://testnet.seiscan.io/tx/{hash}',
2678
2778
  rpcEndpoints: ['https://evm-rpc-testnet.sei-apis.com'],
2679
2779
  eurcAddress: null,
2680
2780
  usdcAddress: '0x4fCF1784B31630811181f670Aea7A7bEF803eaED',
@@ -3491,6 +3591,8 @@ var Chains = /*#__PURE__*/Object.freeze({
3491
3591
  NobleTestnet: NobleTestnet,
3492
3592
  Optimism: Optimism,
3493
3593
  OptimismSepolia: OptimismSepolia,
3594
+ Pharos: Pharos,
3595
+ PharosTestnet: PharosTestnet,
3494
3596
  Plume: Plume,
3495
3597
  PlumeTestnet: PlumeTestnet,
3496
3598
  PolkadotAssetHub: PolkadotAssetHub,
@@ -4995,6 +5097,9 @@ const NetworkError = {
4995
5097
  name: 'NETWORK_CONNECTION_FAILED',
4996
5098
  type: 'NETWORK',
4997
5099
  },
5100
+ /** Network request timeout */
5101
+ TIMEOUT: {
5102
+ code: 3002},
4998
5103
  /** Circle relayer failed to process the forwarding/mint transaction */
4999
5104
  RELAYER_FORWARD_FAILED: {
5000
5105
  code: 3003,
@@ -7178,6 +7283,7 @@ const USDC = {
7178
7283
  [Blockchain.NEAR]: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
7179
7284
  [Blockchain.Noble]: 'uusdc',
7180
7285
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
7286
+ [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
7181
7287
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
7182
7288
  [Blockchain.Polkadot_Asset_Hub]: '1337',
7183
7289
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -7209,6 +7315,7 @@ const USDC = {
7209
7315
  [Blockchain.NEAR_Testnet]: '3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af',
7210
7316
  [Blockchain.Noble_Testnet]: 'uusdc',
7211
7317
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
7318
+ [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
7212
7319
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
7213
7320
  [Blockchain.Polkadot_Westmint]: '31337',
7214
7321
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -7489,6 +7596,32 @@ const MON = {
7489
7596
  },
7490
7597
  };
7491
7598
 
7599
+ /**
7600
+ * cirBTC (Circle Bitcoin) token definition with addresses and metadata.
7601
+ *
7602
+ * @remarks
7603
+ * Built-in cirBTC definition for the TokenRegistry. Currently deployed
7604
+ * on Arc Testnet.
7605
+ *
7606
+ * @example
7607
+ * ```typescript
7608
+ * import { CIRBTC } from '@core/tokens'
7609
+ * import { Blockchain } from '@core/chains'
7610
+ *
7611
+ * console.log(CIRBTC.symbol) // 'cirBTC'
7612
+ * console.log(CIRBTC.decimals) // 8
7613
+ * console.log(CIRBTC.locators[Blockchain.Arc_Testnet])
7614
+ * // '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF'
7615
+ * ```
7616
+ */
7617
+ const CIRBTC = {
7618
+ symbol: 'cirBTC',
7619
+ decimals: 8,
7620
+ locators: {
7621
+ [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF',
7622
+ },
7623
+ };
7624
+
7492
7625
  // Re-export for consumers
7493
7626
  /**
7494
7627
  * All default token definitions.
@@ -7497,7 +7630,7 @@ const MON = {
7497
7630
  * These tokens are automatically included in the TokenRegistry when created
7498
7631
  * without explicit defaults. Extensions can override these definitions.
7499
7632
  * Includes USDC, USDT, EURC, DAI, USDE, PYUSD, WETH, WBTC, WSOL, WAVAX,
7500
- * WPOL, ETH, POL, PLUME, and MON.
7633
+ * WPOL, ETH, POL, PLUME, MON, and cirBTC.
7501
7634
  *
7502
7635
  * @example
7503
7636
  * ```typescript
@@ -7528,6 +7661,7 @@ const DEFAULT_TOKENS = [
7528
7661
  POL,
7529
7662
  PLUME,
7530
7663
  MON,
7664
+ CIRBTC,
7531
7665
  ];
7532
7666
  /**
7533
7667
  * Uppercased token symbols approved for swap fee collection.
@@ -10304,11 +10438,18 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
10304
10438
  step.explorerUrl = buildExplorerUrl(chain, txHash);
10305
10439
  if (outcome.errorMessage) {
10306
10440
  step.errorMessage = outcome.errorMessage;
10441
+ // Transaction was mined but reverted on-chain.
10442
+ step.errorCategory = 'chain_revert';
10307
10443
  }
10308
10444
  }
10309
10445
  catch (err) {
10310
10446
  step.state = 'error';
10311
10447
  step.error = err;
10448
+ // Sequential path does not yet attempt fine-grained classification of
10449
+ // pre-submission errors (user_rejected, capability errors, etc.). Mark
10450
+ // as `unknown` so consumers can at least detect the category is
10451
+ // populated uniformly across batched and sequential flows.
10452
+ step.errorCategory = 'unknown';
10312
10453
  // Optionally parse for common blockchain error formats
10313
10454
  if (err instanceof Error) {
10314
10455
  step.errorMessage = err.message;
@@ -11857,16 +11998,70 @@ async function executeBatchedApproveAndBurn(params, provider) {
11857
11998
  const batchResult = await adapter.batchExecute([approveCallData, burnCallData], chain);
11858
11999
  const approveReceipt = batchResult.receipts[0];
11859
12000
  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);
12001
+ const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
12002
+ const burnStep = await buildBatchedStep('burn', burnReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
11862
12003
  if (burnStep.state !== 'error' && !burnStep.txHash) {
11863
12004
  burnStep.state = 'error';
11864
12005
  burnStep.errorMessage =
11865
12006
  'Batched burn step completed but no transaction hash was returned.';
12007
+ burnStep.errorCategory = 'unknown';
11866
12008
  }
11867
12009
  const context = { burnTxHash: burnStep.txHash ?? '' };
11868
12010
  return { approveStep, burnStep, context };
11869
12011
  }
12012
+ /**
12013
+ * Derive a {@link BridgeStepErrorCategory} for a missing receipt.
12014
+ *
12015
+ * Combines the EIP-5792 `statusCode` (when present) with the underlying
12016
+ * polling error (when set) to produce the most specific category available.
12017
+ * Falls back to `'unknown'` when neither signal is conclusive.
12018
+ *
12019
+ * @param statusCode - The terminal `statusCode` from `wallet_getCallsStatus`, if any.
12020
+ * @param batchError - The polling error from `batchExecute`, if any.
12021
+ * @returns The derived error category for a missing-receipt step.
12022
+ *
12023
+ * @internal
12024
+ */
12025
+ function categorizeMissingReceipt(statusCode, batchError) {
12026
+ if (statusCode === 400)
12027
+ return 'failed_offchain';
12028
+ if (statusCode === 500)
12029
+ return 'reverted_onchain';
12030
+ if (statusCode === 600)
12031
+ return 'partial_reverted';
12032
+ if (batchError instanceof KitError &&
12033
+ batchError.code === NetworkError.TIMEOUT.code) {
12034
+ return 'polling_timeout';
12035
+ }
12036
+ return 'unknown';
12037
+ }
12038
+ /**
12039
+ * Derive a {@link BridgeStepErrorCategory} for a receipt that was returned
12040
+ * but whose per-call `status` is not `'success'`.
12041
+ *
12042
+ * A numeric EIP-5792 `statusCode` of 500 or 600 tells us the whole batch
12043
+ * reverted on-chain (completely or partially); otherwise the receipt
12044
+ * itself signalled a revert without a distinguishing code, so classify
12045
+ * as a plain on-chain revert.
12046
+ *
12047
+ * @param statusCode - The terminal `statusCode` from `wallet_getCallsStatus`, if any.
12048
+ * @returns The derived error category for a failed-receipt step.
12049
+ *
12050
+ * @internal
12051
+ */
12052
+ function categorizeFailedReceipt(statusCode) {
12053
+ // Mirror categorizeMissingReceipt: if the wallet's terminal status is
12054
+ // 400 ("batch not included onchain"), that judgement is authoritative
12055
+ // even when a non-success receipt is attached. Without this, a wrapped
12056
+ // 400 receipt would fall through to `chain_revert` and mislead UX.
12057
+ if (statusCode === 400)
12058
+ return 'failed_offchain';
12059
+ if (statusCode === 600)
12060
+ return 'partial_reverted';
12061
+ if (statusCode === 500)
12062
+ return 'reverted_onchain';
12063
+ return 'chain_revert';
12064
+ }
11870
12065
  /**
11871
12066
  * Build a {@link BridgeStep} from a single receipt within a batch.
11872
12067
  *
@@ -11881,11 +12076,17 @@ async function executeBatchedApproveAndBurn(params, provider) {
11881
12076
  * @param batchId - Wallet-assigned batch identifier.
11882
12077
  * @param adapter - The batch-capable adapter (used for confirmation).
11883
12078
  * @param chain - The EVM chain the batch was executed on.
12079
+ * @param statusCode - Optional EIP-5792 `statusCode` for the batch.
12080
+ * Used to classify the step's error category when the receipt is
12081
+ * missing or failed.
12082
+ * @param batchError - Optional polling error from `batchExecute`.
12083
+ * Preserved on the step so callers can inspect underlying timeouts
12084
+ * or RPC failures.
11884
12085
  * @returns A fully-populated bridge step with state, tx hash and explorer URL.
11885
12086
  *
11886
12087
  * @internal
11887
12088
  */
11888
- async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
12089
+ async function buildBatchedStep(name, receipt, batchId, adapter, chain, statusCode, batchError) {
11889
12090
  const step = {
11890
12091
  name,
11891
12092
  state: 'pending',
@@ -11895,6 +12096,10 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11895
12096
  if (!receipt) {
11896
12097
  step.state = 'error';
11897
12098
  step.errorMessage = `No receipt returned for ${name} in batch ${batchId}.`;
12099
+ step.errorCategory = categorizeMissingReceipt(statusCode, batchError);
12100
+ if (batchError !== undefined) {
12101
+ step.error = batchError;
12102
+ }
11898
12103
  return step;
11899
12104
  }
11900
12105
  step.txHash = receipt.txHash;
@@ -11904,11 +12109,13 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11904
12109
  if (receipt.status !== 'success') {
11905
12110
  step.state = 'error';
11906
12111
  step.errorMessage = `${name} call failed within batch ${batchId}.`;
12112
+ step.errorCategory = categorizeFailedReceipt(statusCode);
11907
12113
  return step;
11908
12114
  }
11909
12115
  if (!receipt.txHash) {
11910
12116
  step.state = 'error';
11911
12117
  step.errorMessage = `${name} succeeded in batch but returned an empty transaction hash.`;
12118
+ step.errorCategory = 'unknown';
11912
12119
  return step;
11913
12120
  }
11914
12121
  try {
@@ -11923,6 +12130,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11923
12130
  step.data = transaction;
11924
12131
  if (outcome.errorMessage) {
11925
12132
  step.errorMessage = outcome.errorMessage;
12133
+ step.errorCategory = 'chain_revert';
11926
12134
  }
11927
12135
  }
11928
12136
  catch (err) {
@@ -11930,11 +12138,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
11930
12138
  step.error = err;
11931
12139
  step.errorMessage =
11932
12140
  err instanceof Error ? err.message : 'Unknown error during confirmation.';
12141
+ step.errorCategory = 'unknown';
11933
12142
  }
11934
12143
  return step;
11935
12144
  }
11936
12145
 
11937
- var version = "1.6.3";
12146
+ var version = "1.7.0";
11938
12147
  var pkg = {
11939
12148
  version: version};
11940
12149
 
@@ -12010,6 +12219,7 @@ async function executeBatchedPath(params, provider, result, invocation) {
12010
12219
  errorMessage: error_ instanceof Error
12011
12220
  ? error_.message
12012
12221
  : 'Batched approve + burn failed.',
12222
+ errorCategory: classifyPreSubmissionError(error_),
12013
12223
  });
12014
12224
  return undefined;
12015
12225
  }
@@ -12024,6 +12234,89 @@ function ensureStepErrorMessage(name, step) {
12024
12234
  step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
12025
12235
  }
12026
12236
  }
12237
+ /**
12238
+ * Coerce a raw JSON-RPC `code` to a number.
12239
+ *
12240
+ * Some wallet SDKs serialize the JSON-RPC `code` as a string ("4001")
12241
+ * after round-tripping through JSON; accept both shapes so strict `===`
12242
+ * comparisons downstream still classify 5720/5730/5740 correctly — those
12243
+ * codes have no message-pattern fallback.
12244
+ *
12245
+ * @param rawCode - The raw `code` extracted from the error object.
12246
+ * @returns The numeric code, or `undefined` if the value cannot be parsed.
12247
+ *
12248
+ * @internal
12249
+ */
12250
+ function coerceRpcCode(rawCode) {
12251
+ if (typeof rawCode === 'number') {
12252
+ return rawCode;
12253
+ }
12254
+ if (typeof rawCode === 'string') {
12255
+ return Number.parseInt(rawCode, 10);
12256
+ }
12257
+ return undefined;
12258
+ }
12259
+ /**
12260
+ * Classify a pre-submission error thrown during `wallet_sendCalls`.
12261
+ *
12262
+ * Inspect the error's JSON-RPC `code` (falling back to message pattern
12263
+ * matching for wrapper errors like viem's `ChainMismatchError`) and map
12264
+ * it to a {@link BridgeStepErrorCategory}. This lets downstream consumers
12265
+ * distinguish user rejections, wallet capability gaps, and unknown
12266
+ * failures without parsing error messages.
12267
+ *
12268
+ * @remarks
12269
+ * Does NOT alter control flow — the SDK continues to surface a
12270
+ * `state: 'error'` step. Auto-fallback to sequential execution is
12271
+ * intentionally out of scope for this helper.
12272
+ *
12273
+ * @param err - The error thrown by `wallet_sendCalls`.
12274
+ * @returns The derived error category, or `'unknown'` if no match.
12275
+ *
12276
+ * @internal
12277
+ */
12278
+ function classifyPreSubmissionError(err) {
12279
+ // Cross-realm-safe duck typing: `instanceof Error` returns false for
12280
+ // errors thrown in a different JavaScript realm (e.g., a wallet
12281
+ // provider running inside an iframe, which is common with WalletConnect
12282
+ // and the Coinbase Wallet SDK).
12283
+ if (typeof err !== 'object' || err === null || !('message' in err)) {
12284
+ return 'unknown';
12285
+ }
12286
+ const code = coerceRpcCode(err.code);
12287
+ const message = String(err.message);
12288
+ // Numeric JSON-RPC codes are authoritative; check them before falling
12289
+ // back to message-pattern matching. Order matters: an error carrying
12290
+ // `code === 5750` with a message like "user rejected the upgrade"
12291
+ // is a capability problem, not a plain user rejection.
12292
+ if (code === 4001) {
12293
+ return 'user_rejected';
12294
+ }
12295
+ if (code === 5700 || code === 5710 || code === 5750) {
12296
+ return 'atomic_unsupported';
12297
+ }
12298
+ if (code === 5720) {
12299
+ return 'duplicate_batch_id';
12300
+ }
12301
+ if (code === 5730) {
12302
+ return 'unknown_bundle';
12303
+ }
12304
+ if (code === 5740) {
12305
+ return 'batch_too_large';
12306
+ }
12307
+ // Fall back to message patterns when no specific code is available —
12308
+ // viem (and other wrapper layers) sometimes strip the numeric code
12309
+ // while preserving the original wallet message in `Details:`.
12310
+ if (/EIP-7702 not supported/i.test(message) ||
12311
+ /does not support the requested chain/i.test(message) ||
12312
+ /rejected the upgrade/i.test(message)) {
12313
+ return 'atomic_unsupported';
12314
+ }
12315
+ if (/user rejected/i.test(message)) {
12316
+ return 'user_rejected';
12317
+ }
12318
+ return 'unknown';
12319
+ }
12027
12320
  /**
12028
12321
  * Execute a cross-chain USDC bridge using the CCTP v2 protocol.
12029
12322
  *
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.7.0",
4
4
  "description": "Circle's official Cross-Chain Transfer Protocol v2 provider for native USDC bridging",
5
5
  "keywords": [
6
6
  "circle",