@circle-fin/provider-cctp-v2 1.6.2 → 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/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";
@@ -328,6 +332,57 @@ var BridgeChain;
328
332
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
329
333
  })(BridgeChain || (BridgeChain = {}));
330
334
  // -----------------------------------------------------------------------------
335
+ // Unified Balance Chain Enum (Gateway V1 Supported Chains)
336
+ // -----------------------------------------------------------------------------
337
+ /**
338
+ * Enumeration of blockchains that support Gateway V1 operations
339
+ * (deposit, spend, balance, delegate, removeFund).
340
+ *
341
+ * Derived from the full {@link Blockchain} enum but filtered to only
342
+ * include chains with active Gateway V1 contract support. When new chains
343
+ * gain Gateway V1 support, they are added to this enum.
344
+ *
345
+ * @enum
346
+ * @category Enums
347
+ *
348
+ * @remarks
349
+ * - This enum is the **canonical source** of Gateway-supported chains.
350
+ * - Use this enum (or its string literals) in unified-balance-kit calls
351
+ * for type safety.
352
+ *
353
+ * @see {@link Blockchain} for the complete list of all known blockchains.
354
+ * @see {@link UnifiedBalanceChainIdentifier} for the type that accepts these values.
355
+ */
356
+ var UnifiedBalanceChain;
357
+ (function (UnifiedBalanceChain) {
358
+ // Mainnet chains with Gateway V1 support
359
+ UnifiedBalanceChain["Arbitrum"] = "Arbitrum";
360
+ UnifiedBalanceChain["Avalanche"] = "Avalanche";
361
+ UnifiedBalanceChain["Base"] = "Base";
362
+ UnifiedBalanceChain["Ethereum"] = "Ethereum";
363
+ UnifiedBalanceChain["HyperEVM"] = "HyperEVM";
364
+ UnifiedBalanceChain["Optimism"] = "Optimism";
365
+ UnifiedBalanceChain["Polygon"] = "Polygon";
366
+ UnifiedBalanceChain["Sei"] = "Sei";
367
+ UnifiedBalanceChain["Solana"] = "Solana";
368
+ UnifiedBalanceChain["Sonic"] = "Sonic";
369
+ UnifiedBalanceChain["Unichain"] = "Unichain";
370
+ UnifiedBalanceChain["World_Chain"] = "World_Chain";
371
+ // Testnet chains with Gateway V1 support
372
+ UnifiedBalanceChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
373
+ UnifiedBalanceChain["Arc_Testnet"] = "Arc_Testnet";
374
+ UnifiedBalanceChain["Avalanche_Fuji"] = "Avalanche_Fuji";
375
+ UnifiedBalanceChain["Base_Sepolia"] = "Base_Sepolia";
376
+ UnifiedBalanceChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
377
+ UnifiedBalanceChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
378
+ UnifiedBalanceChain["Optimism_Sepolia"] = "Optimism_Sepolia";
379
+ UnifiedBalanceChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
380
+ UnifiedBalanceChain["Sei_Testnet"] = "Sei_Testnet";
381
+ UnifiedBalanceChain["Solana_Devnet"] = "Solana_Devnet";
382
+ UnifiedBalanceChain["Sonic_Testnet"] = "Sonic_Testnet";
383
+ UnifiedBalanceChain["Unichain_Sepolia"] = "Unichain_Sepolia";
384
+ UnifiedBalanceChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
385
+ })(UnifiedBalanceChain || (UnifiedBalanceChain = {}));
331
386
  // Earn Chain Enum
332
387
  // -----------------------------------------------------------------------------
333
388
  /**
@@ -612,6 +667,12 @@ const SWAP_TOKEN_REGISTRY = {
612
667
  category: 'wrapped',
613
668
  description: 'Wrapped Polygon',
614
669
  },
670
+ CIRBTC: {
671
+ symbol: 'CIRBTC',
672
+ decimals: 8,
673
+ category: 'wrapped',
674
+ description: 'Circle Bitcoin',
675
+ },
615
676
  };
616
677
  /**
617
678
  * Special NATIVE token constant for swap operations.
@@ -662,6 +723,62 @@ const ADAPTER_CONTRACT_EVM_MAINNET = '0x7FB8c7260b63934d8da38aF902f87ae6e284a845
662
723
  * integrations (e.g., Arc Testnet).
663
724
  */
664
725
  const ADAPTER_CONTRACT_EVM_TESTNET = '0xBBD70b01a1CAbc96d5b7b129Ae1AAabdf50dd40b';
726
+ /**
727
+ * The GatewayWallet contract address for EVM mainnet networks.
728
+ *
729
+ * This contract manages wallet operations for Gateway transactions
730
+ * on mainnet environments across EVM-compatible chains.
731
+ */
732
+ const GATEWAY_WALLET_EVM_MAINNET = '0x77777777Dcc4d5A8B6E418Fd04D8997ef11000eE';
733
+ /**
734
+ * The GatewayMinter contract address for EVM mainnet networks.
735
+ *
736
+ * This contract handles minting operations for Gateway transactions
737
+ * on mainnet environments across EVM-compatible chains.
738
+ */
739
+ const GATEWAY_MINTER_EVM_MAINNET = '0x2222222d7164433c4C09B0b0D809a9b52C04C205';
740
+ /**
741
+ * The GatewayWallet contract address for EVM testnet networks.
742
+ *
743
+ * This contract manages wallet operations for Gateway transactions
744
+ * on testnet environments across EVM-compatible chains.
745
+ */
746
+ const GATEWAY_WALLET_EVM_TESTNET = '0x0077777d7EBA4688BDeF3E311b846F25870A19B9';
747
+ /**
748
+ * The GatewayMinter contract address for EVM testnet networks.
749
+ *
750
+ * This contract handles minting operations for Gateway transactions
751
+ * on testnet environments across EVM-compatible chains.
752
+ */
753
+ const GATEWAY_MINTER_EVM_TESTNET = '0x0022222ABE238Cc2C7Bb1f21003F0a260052475B';
754
+ /**
755
+ * The GatewayWallet program address for Solana mainnet.
756
+ *
757
+ * This program manages wallet operations for Gateway transactions
758
+ * on Solana mainnet.
759
+ */
760
+ const GATEWAY_WALLET_SOLANA_MAINNET = 'GATEwy4YxeiEbRJLwB6dXgg7q61e6zBPrMzYj5h1pRXQ';
761
+ /**
762
+ * The GatewayMinter program address for Solana mainnet.
763
+ *
764
+ * This program handles minting operations for Gateway transactions
765
+ * on Solana mainnet.
766
+ */
767
+ const GATEWAY_MINTER_SOLANA_MAINNET = 'GATEm5SoBJiSw1v2Pz1iPBgUYkXzCUJ27XSXhDfSyzVZ';
768
+ /**
769
+ * The GatewayWallet program address for Solana devnet.
770
+ *
771
+ * This program manages wallet operations for Gateway transactions
772
+ * on Solana devnet.
773
+ */
774
+ const GATEWAY_WALLET_SOLANA_DEVNET = 'GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu';
775
+ /**
776
+ * The GatewayMinter program address for Solana devnet.
777
+ *
778
+ * This program handles minting operations for Gateway transactions
779
+ * on Solana devnet.
780
+ */
781
+ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
665
782
 
666
783
  /**
667
784
  * Arc Testnet chain definition
@@ -712,6 +829,19 @@ const ArcTestnet = defineChain({
712
829
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
713
830
  adapter: ADAPTER_CONTRACT_EVM_TESTNET,
714
831
  },
832
+ gateway: {
833
+ domain: 26,
834
+ contracts: {
835
+ v1: {
836
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
837
+ minter: GATEWAY_MINTER_EVM_TESTNET,
838
+ },
839
+ },
840
+ forwarderSupported: {
841
+ source: true,
842
+ destination: true,
843
+ },
844
+ },
715
845
  });
716
846
 
717
847
  /**
@@ -762,6 +892,19 @@ const Arbitrum = defineChain({
762
892
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
763
893
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
764
894
  },
895
+ gateway: {
896
+ domain: 3,
897
+ contracts: {
898
+ v1: {
899
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
900
+ minter: GATEWAY_MINTER_EVM_MAINNET,
901
+ },
902
+ },
903
+ forwarderSupported: {
904
+ source: true,
905
+ destination: true,
906
+ },
907
+ },
765
908
  });
766
909
 
767
910
  /**
@@ -811,6 +954,19 @@ const ArbitrumSepolia = defineChain({
811
954
  kitContracts: {
812
955
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
813
956
  },
957
+ gateway: {
958
+ domain: 3,
959
+ contracts: {
960
+ v1: {
961
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
962
+ minter: GATEWAY_MINTER_EVM_TESTNET,
963
+ },
964
+ },
965
+ forwarderSupported: {
966
+ source: true,
967
+ destination: true,
968
+ },
969
+ },
814
970
  });
815
971
 
816
972
  /**
@@ -861,6 +1017,19 @@ const Avalanche = defineChain({
861
1017
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
862
1018
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
863
1019
  },
1020
+ gateway: {
1021
+ domain: 1,
1022
+ contracts: {
1023
+ v1: {
1024
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
1025
+ minter: GATEWAY_MINTER_EVM_MAINNET,
1026
+ },
1027
+ },
1028
+ forwarderSupported: {
1029
+ source: true,
1030
+ destination: true,
1031
+ },
1032
+ },
864
1033
  });
865
1034
 
866
1035
  /**
@@ -910,6 +1079,19 @@ const AvalancheFuji = defineChain({
910
1079
  kitContracts: {
911
1080
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
912
1081
  },
1082
+ gateway: {
1083
+ domain: 1,
1084
+ contracts: {
1085
+ v1: {
1086
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
1087
+ minter: GATEWAY_MINTER_EVM_TESTNET,
1088
+ },
1089
+ },
1090
+ forwarderSupported: {
1091
+ source: true,
1092
+ destination: true,
1093
+ },
1094
+ },
913
1095
  });
914
1096
 
915
1097
  /**
@@ -960,6 +1142,19 @@ const Base = defineChain({
960
1142
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
961
1143
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
962
1144
  },
1145
+ gateway: {
1146
+ domain: 6,
1147
+ contracts: {
1148
+ v1: {
1149
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
1150
+ minter: GATEWAY_MINTER_EVM_MAINNET,
1151
+ },
1152
+ },
1153
+ forwarderSupported: {
1154
+ source: true,
1155
+ destination: true,
1156
+ },
1157
+ },
963
1158
  });
964
1159
 
965
1160
  /**
@@ -1009,6 +1204,19 @@ const BaseSepolia = defineChain({
1009
1204
  kitContracts: {
1010
1205
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1011
1206
  },
1207
+ gateway: {
1208
+ domain: 6,
1209
+ contracts: {
1210
+ v1: {
1211
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
1212
+ minter: GATEWAY_MINTER_EVM_TESTNET,
1213
+ },
1214
+ },
1215
+ forwarderSupported: {
1216
+ source: true,
1217
+ destination: true,
1218
+ },
1219
+ },
1012
1220
  });
1013
1221
 
1014
1222
  /**
@@ -1253,7 +1461,10 @@ const Ethereum = defineChain({
1253
1461
  chainId: 1,
1254
1462
  isTestnet: false,
1255
1463
  explorerUrl: 'https://etherscan.io/tx/{hash}',
1256
- rpcEndpoints: ['https://eth.merkle.io', 'https://ethereum.publicnode.com'],
1464
+ rpcEndpoints: [
1465
+ 'https://ethereum-rpc.publicnode.com',
1466
+ 'https://ethereum.publicnode.com',
1467
+ ],
1257
1468
  eurcAddress: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
1258
1469
  usdcAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
1259
1470
  usdtAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7',
@@ -1283,6 +1494,19 @@ const Ethereum = defineChain({
1283
1494
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
1284
1495
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
1285
1496
  },
1497
+ gateway: {
1498
+ domain: 0,
1499
+ contracts: {
1500
+ v1: {
1501
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
1502
+ minter: GATEWAY_MINTER_EVM_MAINNET,
1503
+ },
1504
+ },
1505
+ forwarderSupported: {
1506
+ source: true,
1507
+ destination: true,
1508
+ },
1509
+ },
1286
1510
  });
1287
1511
 
1288
1512
  /**
@@ -1303,7 +1527,7 @@ const EthereumSepolia = defineChain({
1303
1527
  chainId: 11155111,
1304
1528
  isTestnet: true,
1305
1529
  explorerUrl: 'https://sepolia.etherscan.io/tx/{hash}',
1306
- rpcEndpoints: ['https://sepolia.drpc.org'],
1530
+ rpcEndpoints: ['https://ethereum-sepolia-rpc.publicnode.com'],
1307
1531
  eurcAddress: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
1308
1532
  usdcAddress: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
1309
1533
  usdtAddress: null,
@@ -1332,6 +1556,19 @@ const EthereumSepolia = defineChain({
1332
1556
  kitContracts: {
1333
1557
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1334
1558
  },
1559
+ gateway: {
1560
+ domain: 0,
1561
+ contracts: {
1562
+ v1: {
1563
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
1564
+ minter: GATEWAY_MINTER_EVM_TESTNET,
1565
+ },
1566
+ },
1567
+ forwarderSupported: {
1568
+ source: true,
1569
+ destination: true,
1570
+ },
1571
+ },
1335
1572
  });
1336
1573
 
1337
1574
  /**
@@ -1426,6 +1663,19 @@ const HyperEVM = defineChain({
1426
1663
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
1427
1664
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
1428
1665
  },
1666
+ gateway: {
1667
+ domain: 19,
1668
+ contracts: {
1669
+ v1: {
1670
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
1671
+ minter: GATEWAY_MINTER_EVM_MAINNET,
1672
+ },
1673
+ },
1674
+ forwarderSupported: {
1675
+ source: true,
1676
+ destination: true,
1677
+ },
1678
+ },
1429
1679
  });
1430
1680
 
1431
1681
  /**
@@ -1470,6 +1720,19 @@ const HyperEVMTestnet = defineChain({
1470
1720
  kitContracts: {
1471
1721
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1472
1722
  },
1723
+ gateway: {
1724
+ domain: 19,
1725
+ contracts: {
1726
+ v1: {
1727
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
1728
+ minter: GATEWAY_MINTER_EVM_TESTNET,
1729
+ },
1730
+ },
1731
+ forwarderSupported: {
1732
+ source: true,
1733
+ destination: true,
1734
+ },
1735
+ },
1473
1736
  });
1474
1737
 
1475
1738
  /**
@@ -2004,6 +2267,19 @@ const Optimism = defineChain({
2004
2267
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2005
2268
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
2006
2269
  },
2270
+ gateway: {
2271
+ domain: 2,
2272
+ contracts: {
2273
+ v1: {
2274
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
2275
+ minter: GATEWAY_MINTER_EVM_MAINNET,
2276
+ },
2277
+ },
2278
+ forwarderSupported: {
2279
+ source: true,
2280
+ destination: true,
2281
+ },
2282
+ },
2007
2283
  });
2008
2284
 
2009
2285
  /**
@@ -2053,6 +2329,109 @@ const OptimismSepolia = defineChain({
2053
2329
  kitContracts: {
2054
2330
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2055
2331
  },
2332
+ gateway: {
2333
+ domain: 2,
2334
+ contracts: {
2335
+ v1: {
2336
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
2337
+ minter: GATEWAY_MINTER_EVM_TESTNET,
2338
+ },
2339
+ },
2340
+ forwarderSupported: {
2341
+ source: true,
2342
+ destination: true,
2343
+ },
2344
+ },
2345
+ });
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
+ },
2056
2435
  });
2057
2436
 
2058
2437
  /**
@@ -2241,6 +2620,19 @@ const Polygon = defineChain({
2241
2620
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2242
2621
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
2243
2622
  },
2623
+ gateway: {
2624
+ domain: 7,
2625
+ contracts: {
2626
+ v1: {
2627
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
2628
+ minter: GATEWAY_MINTER_EVM_MAINNET,
2629
+ },
2630
+ },
2631
+ forwarderSupported: {
2632
+ source: true,
2633
+ destination: true,
2634
+ },
2635
+ },
2244
2636
  });
2245
2637
 
2246
2638
  /**
@@ -2290,6 +2682,19 @@ const PolygonAmoy = defineChain({
2290
2682
  kitContracts: {
2291
2683
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2292
2684
  },
2685
+ gateway: {
2686
+ domain: 7,
2687
+ contracts: {
2688
+ v1: {
2689
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
2690
+ minter: GATEWAY_MINTER_EVM_TESTNET,
2691
+ },
2692
+ },
2693
+ forwarderSupported: {
2694
+ source: true,
2695
+ destination: true,
2696
+ },
2697
+ },
2293
2698
  });
2294
2699
 
2295
2700
  /**
@@ -2311,7 +2716,7 @@ const Sei = defineChain({
2311
2716
  },
2312
2717
  chainId: 1329,
2313
2718
  isTestnet: false,
2314
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=pacific-1',
2719
+ explorerUrl: 'https://seiscan.io/tx/{hash}',
2315
2720
  rpcEndpoints: ['https://evm-rpc.sei-apis.com'],
2316
2721
  eurcAddress: null,
2317
2722
  usdcAddress: '0xe15fC38F6D8c56aF07bbCBe3BAf5708A2Bf42392',
@@ -2336,13 +2741,26 @@ const Sei = defineChain({
2336
2741
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2337
2742
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
2338
2743
  },
2339
- });
2340
-
2341
- /**
2342
- * Sei Testnet chain definition
2343
- * @remarks
2344
- * This represents the official testnet for the Sei blockchain.
2345
- * Used for development and testing purposes before deploying to mainnet.
2744
+ gateway: {
2745
+ domain: 16,
2746
+ contracts: {
2747
+ v1: {
2748
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
2749
+ minter: GATEWAY_MINTER_EVM_MAINNET,
2750
+ },
2751
+ },
2752
+ forwarderSupported: {
2753
+ source: true,
2754
+ destination: true,
2755
+ },
2756
+ },
2757
+ });
2758
+
2759
+ /**
2760
+ * Sei Testnet chain definition
2761
+ * @remarks
2762
+ * This represents the official testnet for the Sei blockchain.
2763
+ * Used for development and testing purposes before deploying to mainnet.
2346
2764
  */
2347
2765
  const SeiTestnet = defineChain({
2348
2766
  type: 'evm',
@@ -2356,7 +2774,7 @@ const SeiTestnet = defineChain({
2356
2774
  },
2357
2775
  chainId: 1328,
2358
2776
  isTestnet: true,
2359
- explorerUrl: 'https://seitrace.com/tx/{hash}?chain=atlantic-2',
2777
+ explorerUrl: 'https://testnet.seiscan.io/tx/{hash}',
2360
2778
  rpcEndpoints: ['https://evm-rpc-testnet.sei-apis.com'],
2361
2779
  eurcAddress: null,
2362
2780
  usdcAddress: '0x4fCF1784B31630811181f670Aea7A7bEF803eaED',
@@ -2380,6 +2798,19 @@ const SeiTestnet = defineChain({
2380
2798
  kitContracts: {
2381
2799
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2382
2800
  },
2801
+ gateway: {
2802
+ domain: 16,
2803
+ contracts: {
2804
+ v1: {
2805
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
2806
+ minter: GATEWAY_MINTER_EVM_TESTNET,
2807
+ },
2808
+ },
2809
+ forwarderSupported: {
2810
+ source: true,
2811
+ destination: true,
2812
+ },
2813
+ },
2383
2814
  });
2384
2815
 
2385
2816
  /**
@@ -2424,6 +2855,19 @@ const Sonic = defineChain({
2424
2855
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2425
2856
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
2426
2857
  },
2858
+ gateway: {
2859
+ domain: 13,
2860
+ contracts: {
2861
+ v1: {
2862
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
2863
+ minter: GATEWAY_MINTER_EVM_MAINNET,
2864
+ },
2865
+ },
2866
+ forwarderSupported: {
2867
+ source: true,
2868
+ destination: true,
2869
+ },
2870
+ },
2427
2871
  });
2428
2872
 
2429
2873
  /**
@@ -2467,6 +2911,19 @@ const SonicTestnet = defineChain({
2467
2911
  kitContracts: {
2468
2912
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2469
2913
  },
2914
+ gateway: {
2915
+ domain: 13,
2916
+ contracts: {
2917
+ v1: {
2918
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
2919
+ minter: GATEWAY_MINTER_EVM_TESTNET,
2920
+ },
2921
+ },
2922
+ forwarderSupported: {
2923
+ source: true,
2924
+ destination: true,
2925
+ },
2926
+ },
2470
2927
  });
2471
2928
 
2472
2929
  /**
@@ -2515,6 +2972,19 @@ const Solana = defineChain({
2515
2972
  kitContracts: {
2516
2973
  bridge: 'DFaauJEjmiHkPs1JG89A4p95hDWi9m9SAEERY1LQJiC3',
2517
2974
  },
2975
+ gateway: {
2976
+ domain: 5,
2977
+ contracts: {
2978
+ v1: {
2979
+ wallet: GATEWAY_WALLET_SOLANA_MAINNET,
2980
+ minter: GATEWAY_MINTER_SOLANA_MAINNET,
2981
+ },
2982
+ },
2983
+ forwarderSupported: {
2984
+ source: true,
2985
+ destination: false,
2986
+ },
2987
+ },
2518
2988
  });
2519
2989
 
2520
2990
  /**
@@ -2563,6 +3033,19 @@ const SolanaDevnet = defineChain({
2563
3033
  bridge: 'DFaauJEjmiHkPs1JG89A4p95hDWi9m9SAEERY1LQJiC3',
2564
3034
  },
2565
3035
  rpcEndpoints: ['https://api.devnet.solana.com'],
3036
+ gateway: {
3037
+ domain: 5,
3038
+ contracts: {
3039
+ v1: {
3040
+ wallet: GATEWAY_WALLET_SOLANA_DEVNET,
3041
+ minter: GATEWAY_MINTER_SOLANA_DEVNET,
3042
+ },
3043
+ },
3044
+ forwarderSupported: {
3045
+ source: true,
3046
+ destination: false,
3047
+ },
3048
+ },
2566
3049
  });
2567
3050
 
2568
3051
  /**
@@ -2737,6 +3220,19 @@ const Unichain = defineChain({
2737
3220
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2738
3221
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
2739
3222
  },
3223
+ gateway: {
3224
+ domain: 10,
3225
+ contracts: {
3226
+ v1: {
3227
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
3228
+ minter: GATEWAY_MINTER_EVM_MAINNET,
3229
+ },
3230
+ },
3231
+ forwarderSupported: {
3232
+ source: true,
3233
+ destination: true,
3234
+ },
3235
+ },
2740
3236
  });
2741
3237
 
2742
3238
  /**
@@ -2786,6 +3282,19 @@ const UnichainSepolia = defineChain({
2786
3282
  kitContracts: {
2787
3283
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2788
3284
  },
3285
+ gateway: {
3286
+ domain: 10,
3287
+ contracts: {
3288
+ v1: {
3289
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
3290
+ minter: GATEWAY_MINTER_EVM_TESTNET,
3291
+ },
3292
+ },
3293
+ forwarderSupported: {
3294
+ source: true,
3295
+ destination: true,
3296
+ },
3297
+ },
2789
3298
  });
2790
3299
 
2791
3300
  /**
@@ -2830,6 +3339,19 @@ const WorldChain = defineChain({
2830
3339
  bridge: BRIDGE_CONTRACT_EVM_MAINNET,
2831
3340
  adapter: ADAPTER_CONTRACT_EVM_MAINNET,
2832
3341
  },
3342
+ gateway: {
3343
+ domain: 14,
3344
+ contracts: {
3345
+ v1: {
3346
+ wallet: GATEWAY_WALLET_EVM_MAINNET,
3347
+ minter: GATEWAY_MINTER_EVM_MAINNET,
3348
+ },
3349
+ },
3350
+ forwarderSupported: {
3351
+ source: true,
3352
+ destination: true,
3353
+ },
3354
+ },
2833
3355
  });
2834
3356
 
2835
3357
  /**
@@ -2876,6 +3398,19 @@ const WorldChainSepolia = defineChain({
2876
3398
  kitContracts: {
2877
3399
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
2878
3400
  },
3401
+ gateway: {
3402
+ domain: 14,
3403
+ contracts: {
3404
+ v1: {
3405
+ wallet: GATEWAY_WALLET_EVM_TESTNET,
3406
+ minter: GATEWAY_MINTER_EVM_TESTNET,
3407
+ },
3408
+ },
3409
+ forwarderSupported: {
3410
+ source: true,
3411
+ destination: true,
3412
+ },
3413
+ },
2879
3414
  });
2880
3415
 
2881
3416
  /**
@@ -3056,6 +3591,8 @@ var Chains = /*#__PURE__*/Object.freeze({
3056
3591
  NobleTestnet: NobleTestnet,
3057
3592
  Optimism: Optimism,
3058
3593
  OptimismSepolia: OptimismSepolia,
3594
+ Pharos: Pharos,
3595
+ PharosTestnet: PharosTestnet,
3059
3596
  Plume: Plume,
3060
3597
  PlumeTestnet: PlumeTestnet,
3061
3598
  PolkadotAssetHub: PolkadotAssetHub,
@@ -3155,6 +3692,87 @@ function hasCustomContractSupport(chain, contractType) {
3155
3692
  return (typeof contractAddress === 'string' && contractAddress.trim().length > 0);
3156
3693
  }
3157
3694
 
3695
+ /**
3696
+ * Zod schema for validating Gateway v1 contract addresses.
3697
+ *
3698
+ * @example
3699
+ * ```typescript
3700
+ * gatewayV1ContractsSchema.parse({
3701
+ * wallet: '0x1234567890abcdef1234567890abcdef12345678',
3702
+ * minter: '0xabcdef1234567890abcdef1234567890abcdef12'
3703
+ * })
3704
+ * ```
3705
+ */
3706
+ const gatewayV1ContractsSchema = z
3707
+ .object({
3708
+ wallet: z
3709
+ .string({
3710
+ required_error: 'Gateway wallet address is required. Please provide a valid contract address.',
3711
+ invalid_type_error: 'Gateway wallet address must be a string.',
3712
+ })
3713
+ .min(1, 'Gateway wallet address cannot be empty.'),
3714
+ minter: z
3715
+ .string({
3716
+ required_error: 'Gateway minter address is required. Please provide a valid contract address.',
3717
+ invalid_type_error: 'Gateway minter address must be a string.',
3718
+ })
3719
+ .min(1, 'Gateway minter address cannot be empty.'),
3720
+ })
3721
+ .strict(); // Reject any additional properties not defined in the schema
3722
+ /**
3723
+ * Zod schema for validating the versioned Gateway contracts map.
3724
+ *
3725
+ * @description Mirrors the {@link GatewayContracts} type: a partial map of
3726
+ * protocol versions to their contract addresses, following the same pattern
3727
+ * as {@link CCTPContracts}.
3728
+ *
3729
+ * @example
3730
+ * ```typescript
3731
+ * gatewayContractsSchema.parse({
3732
+ * v1: {
3733
+ * wallet: '0x1234567890abcdef1234567890abcdef12345678',
3734
+ * minter: '0xabcdef1234567890abcdef1234567890abcdef12'
3735
+ * }
3736
+ * })
3737
+ * ```
3738
+ */
3739
+ const gatewayContractsSchema = z
3740
+ .object({
3741
+ v1: gatewayV1ContractsSchema.optional(),
3742
+ })
3743
+ .strict(); // Reject any additional properties not defined in the schema
3744
+ /**
3745
+ * Zod schema for validating the full Gateway configuration.
3746
+ *
3747
+ * @description Mirrors the {@link GatewayConfig} type: a domain number plus
3748
+ * a versioned contracts map, following the same pattern as {@link CCTPConfig}.
3749
+ *
3750
+ * @example
3751
+ * ```typescript
3752
+ * gatewayConfigSchema.parse({
3753
+ * domain: 6,
3754
+ * contracts: {
3755
+ * v1: {
3756
+ * wallet: '0x1234567890abcdef1234567890abcdef12345678',
3757
+ * minter: '0xabcdef1234567890abcdef1234567890abcdef12'
3758
+ * }
3759
+ * }
3760
+ * })
3761
+ * ```
3762
+ */
3763
+ const gatewayConfigSchema = z
3764
+ .object({
3765
+ domain: z.number({
3766
+ required_error: 'Gateway domain is required. Please provide a valid domain number.',
3767
+ invalid_type_error: 'Gateway domain must be a number.',
3768
+ }),
3769
+ contracts: gatewayContractsSchema,
3770
+ forwarderSupported: z.object({
3771
+ source: z.boolean(),
3772
+ destination: z.boolean(),
3773
+ }),
3774
+ })
3775
+ .strict(); // Reject any additional properties not defined in the schema
3158
3776
  /**
3159
3777
  * Base schema for common chain definition properties.
3160
3778
  * This contains all properties shared between EVM and non-EVM chains.
@@ -3193,6 +3811,7 @@ const baseChainDefinitionSchema = z.object({
3193
3811
  adapter: z.string().optional(),
3194
3812
  })
3195
3813
  .optional(),
3814
+ gateway: gatewayConfigSchema.optional(),
3196
3815
  });
3197
3816
  /**
3198
3817
  * Zod schema for validating EVM chain definitions specifically.
@@ -3401,6 +4020,42 @@ z.union([
3401
4020
  `Supported chains: ${Object.values(EarnChain).join(', ')}`,
3402
4021
  })),
3403
4022
  ]);
4023
+ /**
4024
+ * Zod schema for validating unified balance chain identifiers.
4025
+ *
4026
+ * This schema validates that the provided chain is supported for unified balance operations.
4027
+ * It accepts either a UnifiedBalanceChain enum value, a string matching a UnifiedBalanceChain value,
4028
+ * or a ChainDefinition for a supported chain.
4029
+ *
4030
+ * Use this schema when validating chain parameters for unified balance operations to ensure
4031
+ * only Gateway V1-supported chains are accepted at runtime.
4032
+ *
4033
+ * @example
4034
+ * ```typescript
4035
+ * import { unifiedBalanceChainIdentifierSchema } from '@core/chains/validation'
4036
+ * import { UnifiedBalanceChain, Chains } from '@core/chains'
4037
+ *
4038
+ * // Valid - UnifiedBalanceChain enum value
4039
+ * unifiedBalanceChainIdentifierSchema.parse(UnifiedBalanceChain.Ethereum)
4040
+ *
4041
+ * // Valid - string literal
4042
+ * unifiedBalanceChainIdentifierSchema.parse('Ethereum')
4043
+ *
4044
+ * // Invalid - Algorand is not in UnifiedBalanceChain (throws ZodError)
4045
+ * unifiedBalanceChainIdentifierSchema.parse('Algorand')
4046
+ * ```
4047
+ *
4048
+ * @see {@link UnifiedBalanceChain} for the enum of supported chains.
4049
+ */
4050
+ const supportedUnifiedBalanceChains = Object.keys(UnifiedBalanceChain).join(', ');
4051
+ z.union([
4052
+ z.string().refine((val) => val in UnifiedBalanceChain, (val) => ({
4053
+ message: `Chain "${val}" is not supported for unified balance operations. Supported chains: ${supportedUnifiedBalanceChains}.`,
4054
+ })),
4055
+ chainDefinitionSchema$2.refine((chainDef) => chainDef.chain in UnifiedBalanceChain, (chainDef) => ({
4056
+ message: `Chain "${chainDef.name}" (${chainDef.chain}) is not supported for unified balance operations. Supported chains: ${supportedUnifiedBalanceChains}.`,
4057
+ })),
4058
+ ]);
3404
4059
 
3405
4060
  /**
3406
4061
  * @packageDocumentation
@@ -3709,6 +4364,23 @@ class BridgingProvider {
3709
4364
  }
3710
4365
  }
3711
4366
 
4367
+ /**
4368
+ * Check whether the current runtime is Node.js.
4369
+ *
4370
+ * @returns `true` when running in Node.js, `false` otherwise.
4371
+ *
4372
+ * @example
4373
+ * ```typescript
4374
+ * import { isNodeEnvironment } from '@core/utils'
4375
+ *
4376
+ * if (isNodeEnvironment()) {
4377
+ * console.log('Running in Node.js')
4378
+ * }
4379
+ * ```
4380
+ */
4381
+ const isNodeEnvironment = () => typeof process !== 'undefined' &&
4382
+ typeof process.versions === 'object' &&
4383
+ typeof process.versions.node === 'string';
3712
4384
  /**
3713
4385
  * Detect the runtime environment and return a shortened identifier.
3714
4386
  *
@@ -3716,9 +4388,7 @@ class BridgingProvider {
3716
4388
  */
3717
4389
  const getRuntime = () => {
3718
4390
  // Node.js environment
3719
- if (typeof process !== 'undefined' &&
3720
- typeof process.versions === 'object' &&
3721
- typeof process.versions.node === 'string') {
4391
+ if (isNodeEnvironment()) {
3722
4392
  // Shorten to major version only
3723
4393
  const majorVersion = process.versions.node.split('.')[0] ?? 'unknown';
3724
4394
  return `node/${majorVersion}`;
@@ -4427,6 +5097,9 @@ const NetworkError = {
4427
5097
  name: 'NETWORK_CONNECTION_FAILED',
4428
5098
  type: 'NETWORK',
4429
5099
  },
5100
+ /** Network request timeout */
5101
+ TIMEOUT: {
5102
+ code: 3002},
4430
5103
  /** Circle relayer failed to process the forwarding/mint transaction */
4431
5104
  RELAYER_FORWARD_FAILED: {
4432
5105
  code: 3003,
@@ -4707,6 +5380,7 @@ function createInsufficientTokenBalanceError(chain, token, trace) {
4707
5380
  * as it requires user intervention to add gas funds.
4708
5381
  *
4709
5382
  * @param chain - The blockchain network where the gas check failed
5383
+ * @param nativeToken - Native token symbol (e.g. 'ETH', 'SOL', 'AVAX') for a specific message, defaults to 'native token'
4710
5384
  * @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
4711
5385
  * @returns KitError with insufficient gas details
4712
5386
  *
@@ -4715,25 +5389,25 @@ function createInsufficientTokenBalanceError(chain, token, trace) {
4715
5389
  * import { createInsufficientGasError } from '@core/errors'
4716
5390
  *
4717
5391
  * throw createInsufficientGasError('Ethereum')
4718
- * // Message: "Insufficient gas funds on Ethereum"
5392
+ * // Message: "Insufficient native token on Ethereum to cover gas fees"
5393
+ *
5394
+ * throw createInsufficientGasError('Ethereum', 'ETH')
5395
+ * // Message: "Insufficient ETH on Ethereum to cover gas fees"
4719
5396
  * ```
4720
5397
  *
4721
5398
  * @example
4722
5399
  * ```typescript
4723
- * // With trace context for debugging
4724
- * throw createInsufficientGasError('Ethereum', {
5400
+ * throw createInsufficientGasError('Ethereum', 'ETH', {
4725
5401
  * rawError: error,
4726
- * gasRequired: '21000',
4727
- * gasAvailable: '10000',
4728
5402
  * walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
4729
5403
  * })
4730
5404
  * ```
4731
5405
  */
4732
- function createInsufficientGasError(chain, trace) {
5406
+ function createInsufficientGasError(chain, nativeToken = 'native token', trace) {
4733
5407
  return new KitError({
4734
5408
  ...BalanceError.INSUFFICIENT_GAS,
4735
5409
  recoverability: 'FATAL',
4736
- message: `Insufficient gas funds on ${chain}`,
5410
+ message: `Insufficient ${nativeToken} on ${chain} to cover gas fees`,
4737
5411
  cause: {
4738
5412
  trace: {
4739
5413
  ...trace,
@@ -5099,7 +5773,7 @@ const SLIPPAGE_REVERT_PATTERN = /slippage|lower than the minimum required|less t
5099
5773
  *
5100
5774
  * @internal
5101
5775
  */
5102
- function handleRevertError(msg, error, context) {
5776
+ function parseRevertError(msg, error, context) {
5103
5777
  const reason = extractRevertReason(msg, error) ?? 'Transaction reverted';
5104
5778
  if (SLIPPAGE_REVERT_PATTERN.test(reason)) {
5105
5779
  return new KitError({
@@ -5188,17 +5862,34 @@ function handleRevertError(msg, error, context) {
5188
5862
  function parseBlockchainError(error, context) {
5189
5863
  const msg = extractMessage(error);
5190
5864
  const token = context.token ?? 'token';
5191
- // Pattern 1: Insufficient balance errors
5865
+ // Pattern 0: Insufficient native gas token errors
5866
+ // Must run BEFORE the generic balance pattern because RPC messages like
5867
+ // "insufficient funds for gas * price + value" and "insufficient funds
5868
+ // for intrinsic transaction cost" contain "insufficient funds" which
5869
+ // would otherwise match the token balance pattern below.
5870
+ if (/insufficient funds for (gas|intrinsic transaction cost)|sender doesn't have enough funds to send tx/i.test(msg)) {
5871
+ return createInsufficientGasError(context.chain, undefined, {
5872
+ rawError: error,
5873
+ });
5874
+ }
5875
+ // Pattern 1: Insufficient token balance errors
5192
5876
  // Matches balance-related errors from ERC20 contracts, native transfers, and Solana programs
5193
5877
  if (/transfer amount exceeds balance|insufficient (balance|funds)|burn amount exceeded/i.test(msg)) {
5194
5878
  return createInsufficientTokenBalanceError(context.chain, token, {
5195
5879
  rawError: error,
5196
5880
  });
5197
5881
  }
5882
+ // Pattern 1b: Gateway-specific contract reverts
5883
+ // Matched before the generic simulation/revert pattern so the error
5884
+ // messages are actionable rather than opaque hex selectors.
5885
+ const gatewayError = parseGatewayContractError(msg, context, error);
5886
+ if (gatewayError !== null) {
5887
+ return gatewayError;
5888
+ }
5198
5889
  // Pattern 2: Simulation and execution reverts
5199
5890
  // Matches contract revert errors and simulation failures
5200
5891
  if (/execution reverted|simulation failed|transaction reverted|transaction failed/i.test(msg)) {
5201
- return handleRevertError(msg, error, context);
5892
+ return parseRevertError(msg, error, context);
5202
5893
  }
5203
5894
  // Pattern 3: Gas-related errors
5204
5895
  // Matches gas estimation failures and gas exhaustion
@@ -5213,10 +5904,6 @@ function parseBlockchainError(error, context) {
5213
5904
  const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
5214
5905
  return createOutOfGasError(context.chain, { rawError: error }, txHash, explorerUrl);
5215
5906
  }
5216
- // Insufficient funds for gas
5217
- if (/insufficient funds for gas/i.test(msg)) {
5218
- return createInsufficientGasError(context.chain, { rawError: error });
5219
- }
5220
5907
  // Pattern 4: Network connectivity errors
5221
5908
  // Matches connection failures, DNS errors, and timeouts
5222
5909
  if (/connection (refused|failed)|network|timeout|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
@@ -5468,6 +6155,59 @@ function extractRevertReason(msg, error) {
5468
6155
  }
5469
6156
  return null;
5470
6157
  }
6158
+ /**
6159
+ * Parses Gateway smart-contract revert selectors into specific KitError types.
6160
+ *
6161
+ * Returns `null` when the message does not match any known Gateway revert,
6162
+ * allowing the caller to fall through to generic patterns.
6163
+ *
6164
+ * @param msg - The extracted error message string.
6165
+ * @param context - Parse error context (chain, token, operation).
6166
+ * @param error - The original raw error for the trace payload.
6167
+ * @returns A KitError if a Gateway pattern matched, otherwise `null`.
6168
+ *
6169
+ * @internal Called by {@link parseBlockchainError} — not re-exported from
6170
+ * the `@core/errors` barrel.
6171
+ *
6172
+ * @example
6173
+ * ```typescript
6174
+ * // Internal usage within parseBlockchainError:
6175
+ * import { parseGatewayContractError } from './parseBlockchainError'
6176
+ *
6177
+ * const gatewayError = parseGatewayContractError(
6178
+ * 'execution reverted: WithdrawalValueExceedsAvailableBalance',
6179
+ * { chain: 'Ethereum', token: 'USDC', operation: 'withdraw' },
6180
+ * new Error('execution reverted'),
6181
+ * )
6182
+ * if (gatewayError !== null) {
6183
+ * // KitError with INSUFFICIENT_TOKEN balance details
6184
+ * console.log(gatewayError.message)
6185
+ * }
6186
+ *
6187
+ * // Returns null for unrecognised reverts (caller falls through to generic parsing)
6188
+ * const unknown = parseGatewayContractError(
6189
+ * 'execution reverted: SomeUnknownError()',
6190
+ * { chain: 'Base', token: 'USDC', operation: 'deposit' },
6191
+ * new Error('execution reverted'),
6192
+ * )
6193
+ * console.log(unknown) // null
6194
+ * ```
6195
+ */
6196
+ function parseGatewayContractError(msg, context, error) {
6197
+ const token = context.token ?? 'token';
6198
+ if (/WithdrawalValueExceedsAvailableBalance|InsufficientDepositBalance/i.test(msg)) {
6199
+ return createInsufficientTokenBalanceError(context.chain, token, {
6200
+ rawError: error,
6201
+ });
6202
+ }
6203
+ if (/WithdrawalNotYetAvailable|WithdrawalDelayNotElapsed/i.test(msg)) {
6204
+ return createTransactionRevertedError(context.chain, 'Withdrawal is not yet available — the withdrawal delay has not elapsed', { rawError: error });
6205
+ }
6206
+ if (/NoWithdrawingBalance/i.test(msg)) {
6207
+ return createTransactionRevertedError(context.chain, 'No pending withdrawal balance — call initiateWithdrawal() first', { rawError: error });
6208
+ }
6209
+ return null;
6210
+ }
5471
6211
 
5472
6212
  /**
5473
6213
  * Type guard to check if an error is a KitError instance.
@@ -6082,12 +6822,12 @@ function validate(value, schema, context) {
6082
6822
  }
6083
6823
 
6084
6824
  /**
6085
- * Symbol used to track validation state on objects.
6086
- * This allows us to attach metadata to objects without interfering with their structure,
6087
- * enabling optimized validation by skipping already validated objects.
6825
+ * Module-level WeakMap for tracking which (schema, validator) combinations
6826
+ * have already processed a given object. This avoids mutating user-supplied
6827
+ * input objects while ensuring re-validation occurs when schemas change.
6088
6828
  * @internal
6089
6829
  */
6090
- const VALIDATION_STATE = Symbol('validationState');
6830
+ const validationStateMap = new WeakMap();
6091
6831
  /**
6092
6832
  * Validates data against a Zod schema with state tracking and enhanced error reporting.
6093
6833
  *
@@ -6104,11 +6844,12 @@ const VALIDATION_STATE = Symbol('validationState');
6104
6844
  *
6105
6845
  * @example
6106
6846
  * ```typescript
6107
- * const result = validateWithStateTracking(BridgeParamsSchema, params, 'bridge parameters')
6847
+ * const VALIDATOR = Symbol('bridgeValidator')
6848
+ * validateWithStateTracking(params, BridgeParamsSchema, 'bridge parameters', VALIDATOR)
6849
+ * // params is now narrowed to the schema's output type
6108
6850
  * ```
6109
6851
  */
6110
6852
  function validateWithStateTracking(value, schema, context, validatorName) {
6111
- // Skip validation for null or undefined values
6112
6853
  if (value === null) {
6113
6854
  throw new KitError({
6114
6855
  ...InputError.VALIDATION_FAILED,
@@ -6133,7 +6874,6 @@ function validateWithStateTracking(value, schema, context, validatorName) {
6133
6874
  },
6134
6875
  });
6135
6876
  }
6136
- // Ensure value is an object that can hold validation state
6137
6877
  if (typeof value !== 'object') {
6138
6878
  throw new KitError({
6139
6879
  ...InputError.VALIDATION_FAILED,
@@ -6146,18 +6886,28 @@ function validateWithStateTracking(value, schema, context, validatorName) {
6146
6886
  },
6147
6887
  });
6148
6888
  }
6149
- // Get or initialize validation state
6150
- const valueWithState = value;
6151
- const state = valueWithState[VALIDATION_STATE] ?? { validatedBy: [] };
6152
- // Skip validation if already validated by this validator
6153
- if (state.validatedBy.includes(validatorName)) {
6889
+ const state = validationStateMap.get(value) ?? {
6890
+ validatedSchemasByValidator: new Map(),
6891
+ };
6892
+ // Get the set of schemas that have already validated this object
6893
+ // for the given validator. Create a new WeakSet if this is first time.
6894
+ const validatedSchemas = state.validatedSchemasByValidator.get(validatorName) ?? new WeakSet();
6895
+ if (!(validatedSchemas instanceof WeakSet)) {
6896
+ throw new KitError({
6897
+ ...InputError.VALIDATION_FAILED,
6898
+ recoverability: 'FATAL',
6899
+ message: 'Invalid validation state: expected WeakSet',
6900
+ });
6901
+ }
6902
+ // Check if this exact schema instance has already validated this object
6903
+ if (validatedSchemas.has(schema)) {
6154
6904
  return;
6155
6905
  }
6156
- // Delegate to the validate function for actual validation (now throws KitError)
6157
6906
  validate(value, schema, context);
6158
- // Update validation state
6159
- state.validatedBy.push(validatorName);
6160
- valueWithState[VALIDATION_STATE] = state;
6907
+ // Record that this schema has validated the object for this validator
6908
+ validatedSchemas.add(schema);
6909
+ state.validatedSchemasByValidator.set(validatorName, validatedSchemas);
6910
+ validationStateMap.set(value, state);
6161
6911
  }
6162
6912
 
6163
6913
  /**
@@ -6533,6 +7283,7 @@ const USDC = {
6533
7283
  [Blockchain.NEAR]: '17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1',
6534
7284
  [Blockchain.Noble]: 'uusdc',
6535
7285
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
7286
+ [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
6536
7287
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
6537
7288
  [Blockchain.Polkadot_Asset_Hub]: '1337',
6538
7289
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -6564,6 +7315,7 @@ const USDC = {
6564
7315
  [Blockchain.NEAR_Testnet]: '3e2210e1184b45b64c8a434c0a7e7b23cc04ea7eb7a6c3c32520d03d4afcb8af',
6565
7316
  [Blockchain.Noble_Testnet]: 'uusdc',
6566
7317
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
7318
+ [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
6567
7319
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
6568
7320
  [Blockchain.Polkadot_Westmint]: '31337',
6569
7321
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -6844,6 +7596,32 @@ const MON = {
6844
7596
  },
6845
7597
  };
6846
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
+
6847
7625
  // Re-export for consumers
6848
7626
  /**
6849
7627
  * All default token definitions.
@@ -6852,7 +7630,7 @@ const MON = {
6852
7630
  * These tokens are automatically included in the TokenRegistry when created
6853
7631
  * without explicit defaults. Extensions can override these definitions.
6854
7632
  * Includes USDC, USDT, EURC, DAI, USDE, PYUSD, WETH, WBTC, WSOL, WAVAX,
6855
- * WPOL, ETH, POL, PLUME, and MON.
7633
+ * WPOL, ETH, POL, PLUME, MON, and cirBTC.
6856
7634
  *
6857
7635
  * @example
6858
7636
  * ```typescript
@@ -6883,6 +7661,7 @@ const DEFAULT_TOKENS = [
6883
7661
  POL,
6884
7662
  PLUME,
6885
7663
  MON,
7664
+ CIRBTC,
6886
7665
  ];
6887
7666
  /**
6888
7667
  * Uppercased token symbols approved for swap fee collection.
@@ -9006,34 +9785,633 @@ async function assertCCTPv2AttestationParams(attestation, params) {
9006
9785
  }
9007
9786
 
9008
9787
  /**
9009
- * Executes a prepared chain request and returns the result as a bridge step.
9010
- *
9011
- * This function takes a prepared chain request (containing transaction data) and executes
9012
- * it using the appropriate adapter. It handles the execution details and formats
9013
- * the result as a standardized bridge step with transaction details and explorer URLs.
9014
- *
9015
- * @param params - The execution parameters containing:
9016
- * - `name`: The name of the step
9017
- * - `request`: The prepared chain request containing transaction data
9018
- * - `adapter`: The adapter that will execute the transaction
9019
- * - `confirmations`: The number of confirmations to wait for (defaults to 1)
9020
- * - `timeout`: The timeout for the request in milliseconds
9021
- * @returns The bridge step with the transaction details and explorer URL
9022
- * @throws If the transaction execution fails
9788
+ * CCTP bridge step names that can occur in the bridging flow.
9023
9789
  *
9024
- * @example
9025
- * ```typescript
9026
- * const step = await executePreparedChainRequest({
9027
- * name: 'approve',
9028
- * request: preparedRequest,
9029
- * adapter: adapter,
9030
- * confirmations: 2,
9031
- * timeout: 30000
9032
- * })
9033
- * console.log('Transaction hash:', step.txHash)
9034
- * ```
9790
+ * This object provides type safety for step names and represents all possible
9791
+ * steps that can be executed during a CCTP bridge operation. Using const assertions
9792
+ * makes this tree-shakable and follows modern TypeScript best practices.
9035
9793
  */
9036
- async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, }) {
9794
+ const CCTPv2StepName = {
9795
+ approve: 'approve',
9796
+ burn: 'burn',
9797
+ fetchAttestation: 'fetchAttestation',
9798
+ mint: 'mint',
9799
+ reAttest: 'reAttest',
9800
+ };
9801
+ /**
9802
+ * Conditional step transition rules for CCTP bridge flow.
9803
+ *
9804
+ * Rules are evaluated in order - the first matching condition determines the next step.
9805
+ * This approach supports flexible flow logic and makes it easy to extend with new patterns.
9806
+ */
9807
+ const STEP_TRANSITION_RULES = {
9808
+ // Starting state - no steps executed yet
9809
+ '': [
9810
+ {
9811
+ condition: () => true,
9812
+ nextStep: CCTPv2StepName.approve,
9813
+ reason: 'Start with approval step',
9814
+ isActionable: true,
9815
+ },
9816
+ ],
9817
+ // After Approve step
9818
+ [CCTPv2StepName.approve]: [
9819
+ {
9820
+ condition: (ctx) => ctx.lastStep?.state === 'success',
9821
+ nextStep: CCTPv2StepName.burn,
9822
+ reason: 'Approval successful, proceed to burn',
9823
+ isActionable: true,
9824
+ },
9825
+ {
9826
+ condition: (ctx) => ctx.lastStep?.state === 'error',
9827
+ nextStep: CCTPv2StepName.approve,
9828
+ reason: 'Retry failed approval',
9829
+ isActionable: true,
9830
+ },
9831
+ {
9832
+ condition: (ctx) => ctx.lastStep?.state === 'noop',
9833
+ nextStep: CCTPv2StepName.burn,
9834
+ reason: 'No approval needed, proceed to burn',
9835
+ isActionable: true,
9836
+ },
9837
+ {
9838
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
9839
+ nextStep: CCTPv2StepName.approve,
9840
+ reason: 'Continue pending approval',
9841
+ isActionable: false, // Waiting for pending transaction
9842
+ },
9843
+ ],
9844
+ // After Burn step
9845
+ [CCTPv2StepName.burn]: [
9846
+ {
9847
+ condition: (ctx) => ctx.lastStep?.state === 'success',
9848
+ nextStep: CCTPv2StepName.fetchAttestation,
9849
+ reason: 'Burn successful, fetch attestation',
9850
+ isActionable: true,
9851
+ },
9852
+ {
9853
+ condition: (ctx) => ctx.lastStep?.state === 'error',
9854
+ nextStep: CCTPv2StepName.burn,
9855
+ reason: 'Retry failed burn',
9856
+ isActionable: true,
9857
+ },
9858
+ {
9859
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
9860
+ nextStep: CCTPv2StepName.burn,
9861
+ reason: 'Continue pending burn',
9862
+ isActionable: false, // Waiting for pending transaction
9863
+ },
9864
+ ],
9865
+ // After FetchAttestation step
9866
+ [CCTPv2StepName.fetchAttestation]: [
9867
+ {
9868
+ condition: (ctx) => ctx.lastStep?.state === 'success',
9869
+ nextStep: CCTPv2StepName.mint,
9870
+ reason: 'Attestation fetched, proceed to mint',
9871
+ isActionable: true,
9872
+ },
9873
+ {
9874
+ condition: (ctx) => ctx.lastStep?.state === 'error',
9875
+ nextStep: CCTPv2StepName.fetchAttestation,
9876
+ reason: 'Retry fetching attestation',
9877
+ isActionable: true,
9878
+ },
9879
+ {
9880
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
9881
+ nextStep: CCTPv2StepName.fetchAttestation,
9882
+ reason: 'Continue pending attestation fetch',
9883
+ isActionable: false, // Waiting for attestation to be ready
9884
+ },
9885
+ ],
9886
+ // After Mint step
9887
+ [CCTPv2StepName.mint]: [
9888
+ {
9889
+ condition: (ctx) => ctx.lastStep?.state === 'success',
9890
+ nextStep: null,
9891
+ reason: 'Bridge completed successfully',
9892
+ isActionable: false, // Nothing more to do
9893
+ },
9894
+ {
9895
+ condition: (ctx) => ctx.lastStep?.state === 'error',
9896
+ nextStep: CCTPv2StepName.mint,
9897
+ reason: 'Retry failed mint',
9898
+ isActionable: true,
9899
+ },
9900
+ {
9901
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
9902
+ nextStep: CCTPv2StepName.mint,
9903
+ reason: 'Continue pending mint',
9904
+ isActionable: false, // Waiting for pending transaction
9905
+ },
9906
+ ],
9907
+ // After ReAttest step
9908
+ [CCTPv2StepName.reAttest]: [
9909
+ {
9910
+ condition: (ctx) => ctx.lastStep?.state === 'success',
9911
+ nextStep: CCTPv2StepName.mint,
9912
+ reason: 'Re-attestation successful, proceed to mint',
9913
+ isActionable: true,
9914
+ },
9915
+ {
9916
+ condition: (ctx) => ctx.lastStep?.state === 'error',
9917
+ nextStep: CCTPv2StepName.mint,
9918
+ reason: 'Re-attestation failed, retry mint to re-initiate recovery',
9919
+ isActionable: true,
9920
+ },
9921
+ {
9922
+ condition: (ctx) => ctx.lastStep?.state === 'pending',
9923
+ nextStep: CCTPv2StepName.mint,
9924
+ reason: 'Re-attestation pending, retry mint to re-initiate recovery',
9925
+ isActionable: true,
9926
+ },
9927
+ ],
9928
+ };
9929
+ /**
9930
+ * Analyze bridge steps to determine retry feasibility and continuation point.
9931
+ *
9932
+ * This function examines the current state of bridge steps to determine the optimal
9933
+ * continuation strategy. It uses a rule-based approach that makes it easy to extend
9934
+ * with new flow patterns and step types in the future.
9935
+ *
9936
+ * The current analysis supports the standard CCTP flow:
9937
+ * **Traditional flow**: Approve → Burn → FetchAttestation → Mint
9938
+ *
9939
+ * Key features:
9940
+ * - Rule-based transitions: Easy to extend with new step types and logic
9941
+ * - Context-aware decisions: Considers execution history and step states
9942
+ * - Actionable logic: Distinguishes between steps requiring user action vs waiting
9943
+ * - Terminal states: Properly handles completion and non-actionable states
9944
+ *
9945
+ * @param bridgeResult - The bridge result containing step execution history.
9946
+ * @returns Analysis result with continuation step and actionability information.
9947
+ * @throws Error when bridgeResult is invalid or contains no steps array.
9948
+ *
9949
+ * @example
9950
+ * ```typescript
9951
+ * import { analyzeSteps } from './analyzeSteps'
9952
+ *
9953
+ * // Failed approval step (requires user action)
9954
+ * const bridgeResult = {
9955
+ * steps: [
9956
+ * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
9957
+ * ]
9958
+ * }
9959
+ *
9960
+ * const analysis = analyzeSteps(bridgeResult)
9961
+ * // Result: { continuationStep: 'Approve', isRetryable: true,
9962
+ * // reason: 'Retry failed approval' }
9963
+ * ```
9964
+ *
9965
+ * @example
9966
+ * ```typescript
9967
+ * // Pending transaction (requires waiting, not actionable)
9968
+ * const bridgeResult = {
9969
+ * steps: [
9970
+ * { name: 'Approve', state: 'pending' }
9971
+ * ]
9972
+ * }
9973
+ *
9974
+ * const analysis = analyzeSteps(bridgeResult)
9975
+ * // Result: { continuationStep: 'Approve', isRetryable: false,
9976
+ * // reason: 'Continue pending approval' }
9977
+ * ```
9978
+ *
9979
+ * @example
9980
+ * ```typescript
9981
+ * // Completed bridge (nothing to do)
9982
+ * const bridgeResult = {
9983
+ * steps: [
9984
+ * { name: 'Approve', state: 'success' },
9985
+ * { name: 'Burn', state: 'success' },
9986
+ * { name: 'FetchAttestation', state: 'success' },
9987
+ * { name: 'Mint', state: 'success' }
9988
+ * ]
9989
+ * }
9990
+ *
9991
+ * const analysis = analyzeSteps(bridgeResult)
9992
+ * // Result: { continuationStep: null, isRetryable: false,
9993
+ * // reason: 'Bridge completed successfully' }
9994
+ * ```
9995
+ */
9996
+ const analyzeSteps = (bridgeResult) => {
9997
+ // Input validation
9998
+ if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
9999
+ throw new Error('Invalid bridgeResult: must contain a steps array');
10000
+ }
10001
+ const { steps } = bridgeResult;
10002
+ // Build execution context from step history
10003
+ const context = buildFlowContext(steps);
10004
+ // Determine continuation logic using rule engine
10005
+ const continuation = determineContinuationFromRules(context);
10006
+ return {
10007
+ continuationStep: continuation.nextStep,
10008
+ isActionable: continuation.isActionable,
10009
+ completedSteps: Array.from(context.completedSteps),
10010
+ failedSteps: Array.from(context.failedSteps),
10011
+ reason: continuation.reason,
10012
+ };
10013
+ };
10014
+ /**
10015
+ * Build flow context from the execution history.
10016
+ *
10017
+ * @param steps - Array of executed bridge steps.
10018
+ * @returns Flow context with execution state and history.
10019
+ */
10020
+ function buildFlowContext(steps) {
10021
+ const completedSteps = new Set();
10022
+ const failedSteps = new Set();
10023
+ let lastStep;
10024
+ // Process step history to build context
10025
+ for (const step of steps) {
10026
+ if (step.state === 'success' || step.state === 'noop') {
10027
+ completedSteps.add(step.name);
10028
+ }
10029
+ else if (step.state === 'error') {
10030
+ failedSteps.add(step.name);
10031
+ }
10032
+ // Track the last step for continuation logic
10033
+ lastStep = {
10034
+ name: step.name,
10035
+ state: step.state,
10036
+ };
10037
+ }
10038
+ return {
10039
+ completedSteps,
10040
+ failedSteps,
10041
+ ...(lastStep && { lastStep }),
10042
+ };
10043
+ }
10044
+ /**
10045
+ * Determine continuation step using the rule engine.
10046
+ *
10047
+ * @param context - The flow context with execution history.
10048
+ * @returns Continuation decision with next step and actionability information.
10049
+ */
10050
+ function determineContinuationFromRules(context) {
10051
+ const lastStepName = context.lastStep?.name;
10052
+ // Handle initial state when no steps have been executed
10053
+ if (lastStepName === undefined) {
10054
+ const rules = STEP_TRANSITION_RULES[''];
10055
+ const matchingRule = rules?.find((rule) => rule.condition(context));
10056
+ if (!matchingRule) {
10057
+ return {
10058
+ nextStep: null,
10059
+ isActionable: false,
10060
+ reason: 'No initial state rule found',
10061
+ };
10062
+ }
10063
+ return {
10064
+ nextStep: matchingRule.nextStep,
10065
+ isActionable: matchingRule.isActionable,
10066
+ reason: matchingRule.reason,
10067
+ };
10068
+ }
10069
+ // A step with an empty name is ambiguous and should be treated as an unrecoverable state.
10070
+ if (lastStepName === '') {
10071
+ return {
10072
+ nextStep: null,
10073
+ isActionable: false,
10074
+ reason: 'No transition rules defined for step with empty name',
10075
+ };
10076
+ }
10077
+ const rules = STEP_TRANSITION_RULES[lastStepName];
10078
+ if (!rules) {
10079
+ return {
10080
+ nextStep: null,
10081
+ isActionable: false,
10082
+ reason: `No transition rules defined for step: ${lastStepName}`,
10083
+ };
10084
+ }
10085
+ // Find the first matching rule
10086
+ const matchingRule = rules.find((rule) => rule.condition(context));
10087
+ if (!matchingRule) {
10088
+ return {
10089
+ nextStep: null,
10090
+ isActionable: false,
10091
+ reason: `No matching transition rule for current context`,
10092
+ };
10093
+ }
10094
+ return {
10095
+ nextStep: matchingRule.nextStep,
10096
+ isActionable: matchingRule.isActionable,
10097
+ reason: matchingRule.reason,
10098
+ };
10099
+ }
10100
+
10101
+ /**
10102
+ * Find a step by name in the bridge result.
10103
+ *
10104
+ * @param result - The bridge result to search.
10105
+ * @param stepName - The name of the step to find.
10106
+ * @returns The step if found, undefined otherwise.
10107
+ *
10108
+ * @example
10109
+ * ```typescript
10110
+ * import { findStepByName } from './findStep'
10111
+ *
10112
+ * const burnStep = findStepByName(result, 'burn')
10113
+ * if (burnStep) {
10114
+ * console.log('Burn tx:', burnStep.txHash)
10115
+ * }
10116
+ * ```
10117
+ */
10118
+ function findStepByName(result, stepName) {
10119
+ return result.steps.find((step) => step.name === stepName);
10120
+ }
10121
+ /**
10122
+ * Find a pending step by name and return it with its index.
10123
+ *
10124
+ * Searches for a step that matches both the step name and has a pending state.
10125
+ *
10126
+ * @param result - The bridge result containing steps to search through.
10127
+ * @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
10128
+ * @returns An object containing the step and its index in the steps array.
10129
+ * @throws KitError if the specified pending step is not found.
10130
+ *
10131
+ * @example
10132
+ * ```typescript
10133
+ * import { findPendingStep } from './findStep'
10134
+ *
10135
+ * const { step, index } = findPendingStep(result, 'burn')
10136
+ * console.log('Pending step:', step.name, 'at index:', index)
10137
+ * ```
10138
+ */
10139
+ function findPendingStep(result, stepName) {
10140
+ const index = result.steps.findIndex((step) => step.name === stepName && step.state === 'pending');
10141
+ if (index === -1) {
10142
+ throw new KitError({
10143
+ ...InputError.VALIDATION_FAILED,
10144
+ recoverability: 'FATAL',
10145
+ message: `Pending step "${stepName}" not found in result`,
10146
+ });
10147
+ }
10148
+ const step = result.steps[index];
10149
+ if (!step) {
10150
+ throw new KitError({
10151
+ ...InputError.VALIDATION_FAILED,
10152
+ recoverability: 'FATAL',
10153
+ message: 'Pending step is undefined',
10154
+ });
10155
+ }
10156
+ return { step, index };
10157
+ }
10158
+ /**
10159
+ * Get the burn transaction hash from bridge result.
10160
+ *
10161
+ * @param result - The bridge result.
10162
+ * @returns The burn transaction hash, or undefined if not found.
10163
+ *
10164
+ * @example
10165
+ * ```typescript
10166
+ * import { getBurnTxHash } from './findStep'
10167
+ *
10168
+ * const burnTxHash = getBurnTxHash(result)
10169
+ * if (burnTxHash) {
10170
+ * console.log('Burn tx hash:', burnTxHash)
10171
+ * }
10172
+ * ```
10173
+ */
10174
+ function getBurnTxHash(result) {
10175
+ return findStepByName(result, CCTPv2StepName.burn)?.txHash;
10176
+ }
10177
+ /**
10178
+ * Get the attestation data from bridge result.
10179
+ *
10180
+ * @param result - The bridge result.
10181
+ * @returns The attestation data, or undefined if not found.
10182
+ *
10183
+ * @example
10184
+ * ```typescript
10185
+ * import { getAttestationData } from './findStep'
10186
+ *
10187
+ * const attestation = getAttestationData(result)
10188
+ * if (attestation) {
10189
+ * console.log('Attestation:', attestation.message)
10190
+ * }
10191
+ * ```
10192
+ */
10193
+ function getAttestationData(result) {
10194
+ // Prefer reAttest data (most recent attestation after expiry)
10195
+ const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
10196
+ if (reAttestStep?.state === 'success' && reAttestStep.data) {
10197
+ return reAttestStep.data;
10198
+ }
10199
+ // Fall back to fetchAttestation step
10200
+ const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
10201
+ return fetchStep?.data;
10202
+ }
10203
+
10204
+ /**
10205
+ * Check if the analysis indicates a non-actionable pending state.
10206
+ *
10207
+ * A pending state is non-actionable when there's a continuation step but
10208
+ * the analysis marks it as not actionable, typically because we need to
10209
+ * wait for an ongoing operation to complete.
10210
+ *
10211
+ * @param analysis - The step analysis result from analyzeSteps.
10212
+ * @param result - The bridge result to check for pending steps.
10213
+ * @returns True if there is a pending step that we should wait for.
10214
+ *
10215
+ * @example
10216
+ * ```typescript
10217
+ * import { hasPendingState } from './stepUtils'
10218
+ * import { analyzeSteps } from '../analyzeSteps'
10219
+ *
10220
+ * const analysis = analyzeSteps(bridgeResult)
10221
+ * if (hasPendingState(analysis, bridgeResult)) {
10222
+ * // Wait for the pending operation to complete
10223
+ * }
10224
+ * ```
10225
+ */
10226
+ /**
10227
+ * Evaluate a transaction receipt and return the corresponding step state
10228
+ * and error message. Centralises the success/revert/unconfirmed logic so
10229
+ * every call-site behaves identically.
10230
+ *
10231
+ * @param receipt - The transaction receipt containing status and block info.
10232
+ * @param txHash - The transaction hash used in error messages.
10233
+ * @returns An object with `state` and an optional `errorMessage`.
10234
+ *
10235
+ * @example
10236
+ * ```typescript
10237
+ * const outcome = evaluateTransactionOutcome(receipt, '0xabc...')
10238
+ * step.state = outcome.state
10239
+ * if (outcome.errorMessage) step.errorMessage = outcome.errorMessage
10240
+ * ```
10241
+ */
10242
+ function evaluateTransactionOutcome(receipt, txHash) {
10243
+ if (receipt.status === 'success' && receipt.blockNumber) {
10244
+ return { state: 'success' };
10245
+ }
10246
+ return {
10247
+ state: 'error',
10248
+ errorMessage: receipt.status === 'reverted'
10249
+ ? `Transaction ${txHash} was reverted`
10250
+ : 'Transaction was not confirmed on-chain',
10251
+ };
10252
+ }
10253
+ function hasPendingState(analysis, result) {
10254
+ // Check if there's a continuation step that's marked as non-actionable
10255
+ if (analysis.continuationStep === null || analysis.isActionable) {
10256
+ return false;
10257
+ }
10258
+ // Verify that the continuation step actually exists and is in pending state
10259
+ const pendingStep = result.steps.find((step) => step.name === analysis.continuationStep && step.state === 'pending');
10260
+ return pendingStep !== undefined;
10261
+ }
10262
+ /**
10263
+ * Check if the step is the last one in the execution flow.
10264
+ *
10265
+ * @param step - The step object to check.
10266
+ * @param stepNames - The ordered list of step names in the execution flow.
10267
+ * @returns True if this is the last step in the flow.
10268
+ *
10269
+ * @example
10270
+ * ```typescript
10271
+ * import { isLastStep } from './stepUtils'
10272
+ *
10273
+ * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
10274
+ * isLastStep({ name: 'mint' }, stepNames) // true
10275
+ * isLastStep({ name: 'burn' }, stepNames) // false
10276
+ * ```
10277
+ */
10278
+ function isLastStep(step, stepNames) {
10279
+ const stepIndex = stepNames.indexOf(step.name);
10280
+ return stepIndex === -1 || stepIndex >= stepNames.length - 1;
10281
+ }
10282
+ /**
10283
+ * Wait for a pending transaction to complete.
10284
+ *
10285
+ * Poll the adapter until the transaction is confirmed on-chain and return
10286
+ * the updated step with success or error state based on the receipt.
10287
+ *
10288
+ * @param pendingStep - The full step object containing the transaction hash.
10289
+ * @param adapter - The adapter to use for waiting.
10290
+ * @param chain - The chain where the transaction was submitted.
10291
+ * @returns The updated step object with success or error state.
10292
+ *
10293
+ * @throws KitError when the pending step has no transaction hash.
10294
+ *
10295
+ * @example
10296
+ * ```typescript
10297
+ * import { waitForPendingTransaction } from './bridgeStepUtils'
10298
+ *
10299
+ * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
10300
+ * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
10301
+ * // updatedStep.state is now 'success' or 'error'
10302
+ * ```
10303
+ */
10304
+ async function waitForPendingTransaction(pendingStep, adapter, chain) {
10305
+ if (!pendingStep.txHash) {
10306
+ throw new KitError({
10307
+ ...InputError.VALIDATION_FAILED,
10308
+ recoverability: 'FATAL',
10309
+ message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`,
10310
+ });
10311
+ }
10312
+ const txHash = pendingStep.txHash;
10313
+ const txReceipt = await retryAsync(async () => adapter.waitForTransaction(txHash, undefined, chain), {
10314
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
10315
+ });
10316
+ const outcome = evaluateTransactionOutcome(txReceipt, txHash);
10317
+ return {
10318
+ ...pendingStep,
10319
+ state: outcome.state,
10320
+ data: txReceipt,
10321
+ explorerUrl: buildExplorerUrl(chain, txHash),
10322
+ ...(outcome.errorMessage ? { errorMessage: outcome.errorMessage } : {}),
10323
+ };
10324
+ }
10325
+ /**
10326
+ * Wait for a pending step to complete.
10327
+ *
10328
+ * For transaction steps: waits for the transaction to be confirmed.
10329
+ * For attestation: re-executes the attestation fetch.
10330
+ *
10331
+ * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
10332
+ * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
10333
+ * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
10334
+ * @param adapter - The adapter to use.
10335
+ * @param chain - The chain where the step is executing.
10336
+ * @param context - The retry context.
10337
+ * @param result - The bridge result.
10338
+ * @param provider - The CCTP v2 bridging provider.
10339
+ * @returns The resolved step object with updated state.
10340
+ *
10341
+ * @throws KitError when fetching attestation but burn transaction hash is not found.
10342
+ *
10343
+ * @example
10344
+ * ```typescript
10345
+ * import { waitForStepToComplete } from './bridgeStepUtils'
10346
+ *
10347
+ * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
10348
+ * const updatedStep = await waitForStepToComplete(
10349
+ * pendingStep,
10350
+ * adapter,
10351
+ * chain,
10352
+ * context,
10353
+ * result,
10354
+ * provider,
10355
+ * )
10356
+ * // updatedStep.state is now 'success' or 'error'
10357
+ * ```
10358
+ */
10359
+ async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
10360
+ if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
10361
+ // For attestation, re-run the fetch (it has built-in polling)
10362
+ const burnTxHash = getBurnTxHash(result);
10363
+ if (!burnTxHash) {
10364
+ throw new KitError({
10365
+ ...InputError.VALIDATION_FAILED,
10366
+ recoverability: 'FATAL',
10367
+ message: 'Cannot fetch attestation: burn transaction hash not found',
10368
+ });
10369
+ }
10370
+ const sourceAddress = result.source.address;
10371
+ const attestation = await provider.fetchAttestation({
10372
+ chain: result.source.chain,
10373
+ adapter: context.from,
10374
+ address: sourceAddress,
10375
+ }, burnTxHash);
10376
+ return {
10377
+ ...pendingStep,
10378
+ state: 'success',
10379
+ data: attestation,
10380
+ };
10381
+ }
10382
+ // For transaction steps, wait for the transaction to complete
10383
+ return waitForPendingTransaction(pendingStep, adapter, chain);
10384
+ }
10385
+
10386
+ /**
10387
+ * Executes a prepared chain request and returns the result as a bridge step.
10388
+ *
10389
+ * This function takes a prepared chain request (containing transaction data) and executes
10390
+ * it using the appropriate adapter. It handles the execution details and formats
10391
+ * the result as a standardized bridge step with transaction details and explorer URLs.
10392
+ *
10393
+ * @param params - The execution parameters containing:
10394
+ * - `name`: The name of the step
10395
+ * - `request`: The prepared chain request containing transaction data
10396
+ * - `adapter`: The adapter that will execute the transaction
10397
+ * - `confirmations`: The number of confirmations to wait for (defaults to 1)
10398
+ * - `timeout`: The timeout for the request in milliseconds
10399
+ * @returns The bridge step with the transaction details and explorer URL
10400
+ * @throws If the transaction execution fails
10401
+ *
10402
+ * @example
10403
+ * ```typescript
10404
+ * const step = await executePreparedChainRequest({
10405
+ * name: 'approve',
10406
+ * request: preparedRequest,
10407
+ * adapter: adapter,
10408
+ * confirmations: 2,
10409
+ * timeout: 30000
10410
+ * })
10411
+ * console.log('Transaction hash:', step.txHash)
10412
+ * ```
10413
+ */
10414
+ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, }) {
9037
10415
  const step = { name, state: 'pending' };
9038
10416
  try {
9039
10417
  /**
@@ -9053,17 +10431,25 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
9053
10431
  retryOptions.deadlineMs = Date.now() + timeout;
9054
10432
  }
9055
10433
  const transaction = await retryAsync(async () => adapter.waitForTransaction(txHash, { confirmations, timeout }, chain), retryOptions);
9056
- step.state = transaction.blockNumber ? 'success' : 'error';
10434
+ const outcome = evaluateTransactionOutcome(transaction, txHash);
10435
+ step.state = outcome.state;
9057
10436
  step.data = transaction;
9058
10437
  // Generate explorer URL for the step
9059
10438
  step.explorerUrl = buildExplorerUrl(chain, txHash);
9060
- if (!transaction.blockNumber) {
9061
- step.errorMessage = 'Transaction was not confirmed on-chain.';
10439
+ if (outcome.errorMessage) {
10440
+ step.errorMessage = outcome.errorMessage;
10441
+ // Transaction was mined but reverted on-chain.
10442
+ step.errorCategory = 'chain_revert';
9062
10443
  }
9063
10444
  }
9064
10445
  catch (err) {
9065
10446
  step.state = 'error';
9066
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';
9067
10453
  // Optionally parse for common blockchain error formats
9068
10454
  if (err instanceof Error) {
9069
10455
  step.errorMessage = err.message;
@@ -9072,7 +10458,7 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
9072
10458
  step.errorMessage = String(err.message);
9073
10459
  }
9074
10460
  else {
9075
- step.errorMessage = 'Unknown error occurred during approval step.';
10461
+ step.errorMessage = `Unknown error occurred during ${name} step.`;
9076
10462
  }
9077
10463
  }
9078
10464
  return step;
@@ -10612,16 +11998,70 @@ async function executeBatchedApproveAndBurn(params, provider) {
10612
11998
  const batchResult = await adapter.batchExecute([approveCallData, burnCallData], chain);
10613
11999
  const approveReceipt = batchResult.receipts[0];
10614
12000
  const burnReceipt = batchResult.receipts[1];
10615
- const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain);
10616
- 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);
10617
12003
  if (burnStep.state !== 'error' && !burnStep.txHash) {
10618
12004
  burnStep.state = 'error';
10619
12005
  burnStep.errorMessage =
10620
12006
  'Batched burn step completed but no transaction hash was returned.';
12007
+ burnStep.errorCategory = 'unknown';
10621
12008
  }
10622
12009
  const context = { burnTxHash: burnStep.txHash ?? '' };
10623
12010
  return { approveStep, burnStep, context };
10624
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
+ }
10625
12065
  /**
10626
12066
  * Build a {@link BridgeStep} from a single receipt within a batch.
10627
12067
  *
@@ -10636,11 +12076,17 @@ async function executeBatchedApproveAndBurn(params, provider) {
10636
12076
  * @param batchId - Wallet-assigned batch identifier.
10637
12077
  * @param adapter - The batch-capable adapter (used for confirmation).
10638
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.
10639
12085
  * @returns A fully-populated bridge step with state, tx hash and explorer URL.
10640
12086
  *
10641
12087
  * @internal
10642
12088
  */
10643
- async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
12089
+ async function buildBatchedStep(name, receipt, batchId, adapter, chain, statusCode, batchError) {
10644
12090
  const step = {
10645
12091
  name,
10646
12092
  state: 'pending',
@@ -10650,6 +12096,10 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
10650
12096
  if (!receipt) {
10651
12097
  step.state = 'error';
10652
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
+ }
10653
12103
  return step;
10654
12104
  }
10655
12105
  step.txHash = receipt.txHash;
@@ -10659,11 +12109,13 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
10659
12109
  if (receipt.status !== 'success') {
10660
12110
  step.state = 'error';
10661
12111
  step.errorMessage = `${name} call failed within batch ${batchId}.`;
12112
+ step.errorCategory = categorizeFailedReceipt(statusCode);
10662
12113
  return step;
10663
12114
  }
10664
12115
  if (!receipt.txHash) {
10665
12116
  step.state = 'error';
10666
12117
  step.errorMessage = `${name} succeeded in batch but returned an empty transaction hash.`;
12118
+ step.errorCategory = 'unknown';
10667
12119
  return step;
10668
12120
  }
10669
12121
  try {
@@ -10673,10 +12125,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
10673
12125
  txHash: receipt.txHash,
10674
12126
  })),
10675
12127
  });
10676
- step.state = transaction.blockNumber === undefined ? 'error' : 'success';
12128
+ const outcome = evaluateTransactionOutcome(transaction, receipt.txHash);
12129
+ step.state = outcome.state;
10677
12130
  step.data = transaction;
10678
- if (transaction.blockNumber === undefined) {
10679
- step.errorMessage = 'Transaction was not confirmed on-chain.';
12131
+ if (outcome.errorMessage) {
12132
+ step.errorMessage = outcome.errorMessage;
12133
+ step.errorCategory = 'chain_revert';
10680
12134
  }
10681
12135
  }
10682
12136
  catch (err) {
@@ -10684,11 +12138,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
10684
12138
  step.error = err;
10685
12139
  step.errorMessage =
10686
12140
  err instanceof Error ? err.message : 'Unknown error during confirmation.';
12141
+ step.errorCategory = 'unknown';
10687
12142
  }
10688
12143
  return step;
10689
12144
  }
10690
12145
 
10691
- var version = "1.6.2";
12146
+ var version = "1.7.0";
10692
12147
  var pkg = {
10693
12148
  version: version};
10694
12149
 
@@ -10764,6 +12219,7 @@ async function executeBatchedPath(params, provider, result, invocation) {
10764
12219
  errorMessage: error_ instanceof Error
10765
12220
  ? error_.message
10766
12221
  : 'Batched approve + burn failed.',
12222
+ errorCategory: classifyPreSubmissionError(error_),
10767
12223
  });
10768
12224
  return undefined;
10769
12225
  }
@@ -10778,6 +12234,89 @@ function ensureStepErrorMessage(name, step) {
10778
12234
  step.errorMessage = `${name} step failed: ${getErrorMessage(step.error)}`;
10779
12235
  }
10780
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
+ }
10781
12320
  /**
10782
12321
  * Execute a cross-chain USDC bridge using the CCTP v2 protocol.
10783
12322
  *
@@ -10996,681 +12535,266 @@ const hexStringSchema = z
10996
12535
  .refine((value) => value.trim().length > 0, 'Hex string cannot be empty')
10997
12536
  .refine((value) => value.startsWith('0x'), 'Hex string must start with 0x prefix')
10998
12537
  .refine((value) => {
10999
- const hexPattern = /^0x[0-9a-fA-F]+$/;
11000
- return hexPattern.test(value);
11001
- }, 'Hex string contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x');
11002
- /**
11003
- * Schema for validating EVM addresses.
11004
- *
11005
- * This schema validates that a string is a properly formatted EVM address:
11006
- * - Must be a valid hex string with '0x' prefix
11007
- * - Must be exactly 42 characters long (0x + 40 hex characters)
11008
- *
11009
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11010
- *
11011
- * @example
11012
- * ```typescript
11013
- * import { evmAddressSchema } from '@core/adapter'
11014
- *
11015
- * const validAddress = '0x1234567890123456789012345678901234567890'
11016
- *
11017
- * const result = evmAddressSchema.safeParse(validAddress)
11018
- * console.log(result.success) // true
11019
- * ```
11020
- */
11021
- hexStringSchema.refine((value) => value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)');
11022
- /**
11023
- * Schema for validating transaction hashes.
11024
- *
11025
- * This schema validates that a string is a properly formatted transaction hash:
11026
- * - Must be a valid hex string with '0x' prefix
11027
- * - Must be exactly 66 characters long (0x + 64 hex characters)
11028
- *
11029
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11030
- *
11031
- * @example
11032
- * ```typescript
11033
- * import { evmTransactionHashSchema } from '@core/adapter'
11034
- *
11035
- * const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
11036
- *
11037
- * const result = evmTransactionHashSchema.safeParse(validTxHash)
11038
- * console.log(result.success) // true
11039
- * ```
11040
- */
11041
- hexStringSchema.refine((value) => value.length === 66, 'Transaction hash must be exactly 66 characters long (0x + 64 hex characters)');
11042
- /**
11043
- * Schema for validating base58-encoded strings.
11044
- *
11045
- * This schema validates that a string:
11046
- * - Is a string type
11047
- * - Is not empty after trimming
11048
- * - Contains only valid base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z)
11049
- * - Does not contain commonly confused characters (0, O, I, l)
11050
- *
11051
- * @remarks
11052
- * This schema does not validate length, making it suitable for various base58-encoded data
11053
- * like Solana addresses, transaction signatures, and other base58-encoded data.
11054
- *
11055
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11056
- *
11057
- * @example
11058
- * ```typescript
11059
- * import { base58StringSchema } from '@core/adapter'
11060
- *
11061
- * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
11062
- * const validTxHash = '3Jf8k2L5mN9pQ7rS1tV4wX6yZ8aB2cD4eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5'
11063
- *
11064
- * const addressResult = base58StringSchema.safeParse(validAddress)
11065
- * const txHashResult = base58StringSchema.safeParse(validTxHash)
11066
- * console.log(addressResult.success) // true
11067
- * console.log(txHashResult.success) // true
11068
- * ```
11069
- */
11070
- const base58StringSchema = z
11071
- .string()
11072
- .min(1, 'Base58 string is required')
11073
- .refine((value) => value.trim().length > 0, 'Base58 string cannot be empty')
11074
- .refine((value) => {
11075
- // Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
11076
- // Excludes: 0, O, I, l to avoid confusion
11077
- const base58Pattern = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
11078
- return base58Pattern.test(value);
11079
- }, 'Base58 string contains invalid characters. Only base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z) are allowed');
11080
- /**
11081
- * Schema for validating Solana addresses.
11082
- *
11083
- * This schema validates that a string is a properly formatted Solana address:
11084
- * - Must be a valid base58-encoded string
11085
- * - Must be between 32-44 characters long (typical length for base58-encoded 32-byte addresses)
11086
- *
11087
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11088
- *
11089
- * @example
11090
- * ```typescript
11091
- * import { solanaAddressSchema } from '@core/adapter'
11092
- *
11093
- * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
11094
- *
11095
- * const result = solanaAddressSchema.safeParse(validAddress)
11096
- * console.log(result.success) // true
11097
- * ```
11098
- */
11099
- base58StringSchema.refine((value) => value.length >= 32 && value.length <= 44, 'Solana address must be between 32-44 characters long (base58-encoded 32-byte address)');
11100
- /**
11101
- * Schema for validating Solana transaction hashes.
11102
- *
11103
- * This schema validates that a string is a properly formatted Solana transaction hash:
11104
- * - Must be a valid base58-encoded string
11105
- * - Must be between 86-88 characters long (typical length for base58-encoded 64-byte signatures)
11106
- *
11107
- * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11108
- *
11109
- * @example
11110
- * ```typescript
11111
- * import { solanaTransactionHashSchema } from '@core/adapter'
11112
- *
11113
- * const validTxHash = '5VfYmGBjvQKe3xgLtTQPSMEUdpEVHrJwLK7pKBJWKzYpNBE2g3kJrq7RSe9M8DqzQJ5J2aZPTjHLvd4WgxPpJKS'
11114
- *
11115
- * const result = solanaTransactionHashSchema.safeParse(validTxHash)
11116
- * console.log(result.success) // true
11117
- * ```
11118
- */
11119
- base58StringSchema.refine((value) => value.length >= 86 && value.length <= 88, 'Solana transaction hash must be between 86-88 characters long (base58-encoded 64-byte signature)');
11120
- /**
11121
- * Schema for validating Adapter objects.
11122
- * Checks for the required methods that define an Adapter.
11123
- */
11124
- z.object({
11125
- prepare: z.function(),
11126
- waitForTransaction: z.function(),
11127
- getAddress: z.function(),
11128
- });
11129
-
11130
- /**
11131
- * Validate that the adapter has sufficient token balance for a transaction.
11132
- *
11133
- * This function checks if the adapter's current token balance is greater than or equal
11134
- * to the requested transaction amount. It throws a KitError with code 9001
11135
- * (BALANCE_INSUFFICIENT_TOKEN) if the balance is insufficient, providing detailed
11136
- * information about the shortfall.
11137
- *
11138
- * @param params - The validation parameters containing adapter, amount, token, and token address.
11139
- * @returns A promise that resolves to void if validation passes.
11140
- * @throws {KitError} When the adapter's balance is less than the required amount (code: 9001).
11141
- *
11142
- * @example
11143
- * ```typescript
11144
- * import { validateBalanceForTransaction } from '@core/adapter'
11145
- * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
11146
- * import { isKitError, ERROR_TYPES } from '@core/errors'
11147
- *
11148
- * const adapter = createViemAdapterFromPrivateKey({
11149
- * privateKey: '0x...',
11150
- * chain: 'Ethereum',
11151
- * })
11152
- *
11153
- * try {
11154
- * await validateBalanceForTransaction({
11155
- * adapter,
11156
- * amount: '1000000', // 1 USDC (6 decimals)
11157
- * token: 'USDC',
11158
- * tokenAddress: '0xA0b86a33E6441c8C1c7C16e4c5e3e5b5e4c5e3e5b5e4c5e',
11159
- * operationContext: { chain: 'Ethereum' },
11160
- * })
11161
- * console.log('Balance validation passed')
11162
- * } catch (error) {
11163
- * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
11164
- * console.error('Insufficient funds:', error.message)
11165
- * }
11166
- * }
11167
- * ```
11168
- */
11169
- const validateBalanceForTransaction = async (params) => {
11170
- const { amount, adapter, token, tokenAddress, operationContext } = params;
11171
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
11172
- walletAddress: operationContext.address,
11173
- }, operationContext);
11174
- const balance = await balancePrepared.execute();
11175
- if (BigInt(balance) < BigInt(amount)) {
11176
- // Extract chain name from operationContext
11177
- const chainName = extractChainInfo(operationContext.chain).name;
11178
- // Create KitError with rich context in trace
11179
- throw createInsufficientTokenBalanceError(chainName, token, {
11180
- balance: balance.toString(),
11181
- amount,
11182
- tokenAddress,
11183
- walletAddress: operationContext.address,
11184
- });
11185
- }
11186
- };
11187
-
12538
+ const hexPattern = /^0x[0-9a-fA-F]+$/;
12539
+ return hexPattern.test(value);
12540
+ }, 'Hex string contains invalid characters. Only hexadecimal characters (0-9, a-f, A-F) are allowed after 0x');
11188
12541
  /**
11189
- * Validate that the adapter has sufficient native token balance for transaction fees.
12542
+ * Schema for validating EVM addresses.
11190
12543
  *
11191
- * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
11192
- * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
11193
- * if the balance is zero, indicating the wallet cannot pay for transaction fees.
12544
+ * This schema validates that a string is a properly formatted EVM address:
12545
+ * - Must be a valid hex string with '0x' prefix
12546
+ * - Must be exactly 42 characters long (0x + 40 hex characters)
11194
12547
  *
11195
- * @param params - The validation parameters containing adapter and operation context.
11196
- * @returns A promise that resolves to void if validation passes.
11197
- * @throws {KitError} When the adapter's native balance is zero (code: 9002).
12548
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11198
12549
  *
11199
12550
  * @example
11200
12551
  * ```typescript
11201
- * import { validateNativeBalanceForTransaction } from '@core/adapter'
11202
- * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
11203
- * import { isKitError, ERROR_TYPES } from '@core/errors'
12552
+ * import { evmAddressSchema } from '@core/adapter'
11204
12553
  *
11205
- * const adapter = createViemAdapterFromPrivateKey({
11206
- * privateKey: '0x...',
11207
- * chain: 'Ethereum',
11208
- * })
12554
+ * const validAddress = '0x1234567890123456789012345678901234567890'
11209
12555
  *
11210
- * try {
11211
- * await validateNativeBalanceForTransaction({
11212
- * adapter,
11213
- * operationContext: { chain: 'Ethereum' },
11214
- * })
11215
- * console.log('Native balance validation passed')
11216
- * } catch (error) {
11217
- * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
11218
- * console.error('Insufficient gas funds:', error.message)
11219
- * }
11220
- * }
12556
+ * const result = evmAddressSchema.safeParse(validAddress)
12557
+ * console.log(result.success) // true
11221
12558
  * ```
11222
12559
  */
11223
- const validateNativeBalanceForTransaction = async (params) => {
11224
- const { adapter, operationContext } = params;
11225
- const balancePrepared = await adapter.prepareAction('native.balanceOf', {
11226
- walletAddress: operationContext.address,
11227
- }, operationContext);
11228
- const balance = await balancePrepared.execute();
11229
- if (BigInt(balance) === 0n) {
11230
- // Extract chain name from operationContext
11231
- const chainName = extractChainInfo(operationContext.chain).name;
11232
- // Create KitError with rich context in trace
11233
- throw createInsufficientGasError(chainName, {
11234
- balance: '0',
11235
- walletAddress: operationContext.address,
11236
- });
11237
- }
11238
- };
11239
-
12560
+ hexStringSchema.refine((value) => value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)');
11240
12561
  /**
11241
- * Permit signature standards for gasless token approvals.
12562
+ * Schema for validating transaction hashes.
11242
12563
  *
11243
- * Defines the permit types that can be used to approve token spending
11244
- * without requiring a separate approval transaction.
12564
+ * This schema validates that a string is a properly formatted transaction hash:
12565
+ * - Must be a valid hex string with '0x' prefix
12566
+ * - Must be exactly 66 characters long (0x + 64 hex characters)
11245
12567
  *
11246
- * @remarks
11247
- * - NONE: No permit, tokens must be pre-approved via separate transaction
11248
- * - EIP2612: Standard ERC-20 permit (USDC, DAI v2, and most modern tokens)
11249
- */
11250
- var PermitType;
11251
- (function (PermitType) {
11252
- /** No permit required - tokens must be pre-approved */
11253
- PermitType[PermitType["NONE"] = 0] = "NONE";
11254
- /** EIP-2612 standard permit */
11255
- PermitType[PermitType["EIP2612"] = 1] = "EIP2612";
11256
- })(PermitType || (PermitType = {}));
11257
-
11258
- /**
11259
- * CCTP bridge step names that can occur in the bridging flow.
12568
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11260
12569
  *
11261
- * This object provides type safety for step names and represents all possible
11262
- * steps that can be executed during a CCTP bridge operation. Using const assertions
11263
- * makes this tree-shakable and follows modern TypeScript best practices.
11264
- */
11265
- const CCTPv2StepName = {
11266
- approve: 'approve',
11267
- burn: 'burn',
11268
- fetchAttestation: 'fetchAttestation',
11269
- mint: 'mint',
11270
- reAttest: 'reAttest',
11271
- };
11272
- /**
11273
- * Conditional step transition rules for CCTP bridge flow.
12570
+ * @example
12571
+ * ```typescript
12572
+ * import { evmTransactionHashSchema } from '@core/adapter'
11274
12573
  *
11275
- * Rules are evaluated in order - the first matching condition determines the next step.
11276
- * This approach supports flexible flow logic and makes it easy to extend with new patterns.
12574
+ * const validTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
12575
+ *
12576
+ * const result = evmTransactionHashSchema.safeParse(validTxHash)
12577
+ * console.log(result.success) // true
12578
+ * ```
11277
12579
  */
11278
- const STEP_TRANSITION_RULES = {
11279
- // Starting state - no steps executed yet
11280
- '': [
11281
- {
11282
- condition: () => true,
11283
- nextStep: CCTPv2StepName.approve,
11284
- reason: 'Start with approval step',
11285
- isActionable: true,
11286
- },
11287
- ],
11288
- // After Approve step
11289
- [CCTPv2StepName.approve]: [
11290
- {
11291
- condition: (ctx) => ctx.lastStep?.state === 'success',
11292
- nextStep: CCTPv2StepName.burn,
11293
- reason: 'Approval successful, proceed to burn',
11294
- isActionable: true,
11295
- },
11296
- {
11297
- condition: (ctx) => ctx.lastStep?.state === 'error',
11298
- nextStep: CCTPv2StepName.approve,
11299
- reason: 'Retry failed approval',
11300
- isActionable: true,
11301
- },
11302
- {
11303
- condition: (ctx) => ctx.lastStep?.state === 'noop',
11304
- nextStep: CCTPv2StepName.burn,
11305
- reason: 'No approval needed, proceed to burn',
11306
- isActionable: true,
11307
- },
11308
- {
11309
- condition: (ctx) => ctx.lastStep?.state === 'pending',
11310
- nextStep: CCTPv2StepName.approve,
11311
- reason: 'Continue pending approval',
11312
- isActionable: false, // Waiting for pending transaction
11313
- },
11314
- ],
11315
- // After Burn step
11316
- [CCTPv2StepName.burn]: [
11317
- {
11318
- condition: (ctx) => ctx.lastStep?.state === 'success',
11319
- nextStep: CCTPv2StepName.fetchAttestation,
11320
- reason: 'Burn successful, fetch attestation',
11321
- isActionable: true,
11322
- },
11323
- {
11324
- condition: (ctx) => ctx.lastStep?.state === 'error',
11325
- nextStep: CCTPv2StepName.burn,
11326
- reason: 'Retry failed burn',
11327
- isActionable: true,
11328
- },
11329
- {
11330
- condition: (ctx) => ctx.lastStep?.state === 'pending',
11331
- nextStep: CCTPv2StepName.burn,
11332
- reason: 'Continue pending burn',
11333
- isActionable: false, // Waiting for pending transaction
11334
- },
11335
- ],
11336
- // After FetchAttestation step
11337
- [CCTPv2StepName.fetchAttestation]: [
11338
- {
11339
- condition: (ctx) => ctx.lastStep?.state === 'success',
11340
- nextStep: CCTPv2StepName.mint,
11341
- reason: 'Attestation fetched, proceed to mint',
11342
- isActionable: true,
11343
- },
11344
- {
11345
- condition: (ctx) => ctx.lastStep?.state === 'error',
11346
- nextStep: CCTPv2StepName.fetchAttestation,
11347
- reason: 'Retry fetching attestation',
11348
- isActionable: true,
11349
- },
11350
- {
11351
- condition: (ctx) => ctx.lastStep?.state === 'pending',
11352
- nextStep: CCTPv2StepName.fetchAttestation,
11353
- reason: 'Continue pending attestation fetch',
11354
- isActionable: false, // Waiting for attestation to be ready
11355
- },
11356
- ],
11357
- // After Mint step
11358
- [CCTPv2StepName.mint]: [
11359
- {
11360
- condition: (ctx) => ctx.lastStep?.state === 'success',
11361
- nextStep: null,
11362
- reason: 'Bridge completed successfully',
11363
- isActionable: false, // Nothing more to do
11364
- },
11365
- {
11366
- condition: (ctx) => ctx.lastStep?.state === 'error',
11367
- nextStep: CCTPv2StepName.mint,
11368
- reason: 'Retry failed mint',
11369
- isActionable: true,
11370
- },
11371
- {
11372
- condition: (ctx) => ctx.lastStep?.state === 'pending',
11373
- nextStep: CCTPv2StepName.mint,
11374
- reason: 'Continue pending mint',
11375
- isActionable: false, // Waiting for pending transaction
11376
- },
11377
- ],
11378
- // After ReAttest step
11379
- [CCTPv2StepName.reAttest]: [
11380
- {
11381
- condition: (ctx) => ctx.lastStep?.state === 'success',
11382
- nextStep: CCTPv2StepName.mint,
11383
- reason: 'Re-attestation successful, proceed to mint',
11384
- isActionable: true,
11385
- },
11386
- {
11387
- condition: (ctx) => ctx.lastStep?.state === 'error',
11388
- nextStep: CCTPv2StepName.mint,
11389
- reason: 'Re-attestation failed, retry mint to re-initiate recovery',
11390
- isActionable: true,
11391
- },
11392
- {
11393
- condition: (ctx) => ctx.lastStep?.state === 'pending',
11394
- nextStep: CCTPv2StepName.mint,
11395
- reason: 'Re-attestation pending, retry mint to re-initiate recovery',
11396
- isActionable: true,
11397
- },
11398
- ],
11399
- };
12580
+ hexStringSchema.refine((value) => value.length === 66, 'Transaction hash must be exactly 66 characters long (0x + 64 hex characters)');
11400
12581
  /**
11401
- * Analyze bridge steps to determine retry feasibility and continuation point.
11402
- *
11403
- * This function examines the current state of bridge steps to determine the optimal
11404
- * continuation strategy. It uses a rule-based approach that makes it easy to extend
11405
- * with new flow patterns and step types in the future.
11406
- *
11407
- * The current analysis supports the standard CCTP flow:
11408
- * **Traditional flow**: Approve → Burn → FetchAttestation → Mint
11409
- *
11410
- * Key features:
11411
- * - Rule-based transitions: Easy to extend with new step types and logic
11412
- * - Context-aware decisions: Considers execution history and step states
11413
- * - Actionable logic: Distinguishes between steps requiring user action vs waiting
11414
- * - Terminal states: Properly handles completion and non-actionable states
11415
- *
11416
- * @param bridgeResult - The bridge result containing step execution history.
11417
- * @returns Analysis result with continuation step and actionability information.
11418
- * @throws Error when bridgeResult is invalid or contains no steps array.
12582
+ * Schema for validating base58-encoded strings.
11419
12583
  *
11420
- * @example
11421
- * ```typescript
11422
- * import { analyzeSteps } from './analyzeSteps'
12584
+ * This schema validates that a string:
12585
+ * - Is a string type
12586
+ * - Is not empty after trimming
12587
+ * - Contains only valid base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z)
12588
+ * - Does not contain commonly confused characters (0, O, I, l)
11423
12589
  *
11424
- * // Failed approval step (requires user action)
11425
- * const bridgeResult = {
11426
- * steps: [
11427
- * { name: 'Approve', state: 'error', errorMessage: 'User rejected' }
11428
- * ]
11429
- * }
12590
+ * @remarks
12591
+ * This schema does not validate length, making it suitable for various base58-encoded data
12592
+ * like Solana addresses, transaction signatures, and other base58-encoded data.
11430
12593
  *
11431
- * const analysis = analyzeSteps(bridgeResult)
11432
- * // Result: { continuationStep: 'Approve', isRetryable: true,
11433
- * // reason: 'Retry failed approval' }
11434
- * ```
12594
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11435
12595
  *
11436
12596
  * @example
11437
12597
  * ```typescript
11438
- * // Pending transaction (requires waiting, not actionable)
11439
- * const bridgeResult = {
11440
- * steps: [
11441
- * { name: 'Approve', state: 'pending' }
11442
- * ]
11443
- * }
12598
+ * import { base58StringSchema } from '@core/adapter'
11444
12599
  *
11445
- * const analysis = analyzeSteps(bridgeResult)
11446
- * // Result: { continuationStep: 'Approve', isRetryable: false,
11447
- * // reason: 'Continue pending approval' }
12600
+ * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
12601
+ * const validTxHash = '3Jf8k2L5mN9pQ7rS1tV4wX6yZ8aB2cD4eF5gH7iJ9kL1mN3oP5qR7sT9uV1wX3yZ5'
12602
+ *
12603
+ * const addressResult = base58StringSchema.safeParse(validAddress)
12604
+ * const txHashResult = base58StringSchema.safeParse(validTxHash)
12605
+ * console.log(addressResult.success) // true
12606
+ * console.log(txHashResult.success) // true
11448
12607
  * ```
12608
+ */
12609
+ const base58StringSchema = z
12610
+ .string()
12611
+ .min(1, 'Base58 string is required')
12612
+ .refine((value) => value.trim().length > 0, 'Base58 string cannot be empty')
12613
+ .refine((value) => {
12614
+ // Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
12615
+ // Excludes: 0, O, I, l to avoid confusion
12616
+ const base58Pattern = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
12617
+ return base58Pattern.test(value);
12618
+ }, 'Base58 string contains invalid characters. Only base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z) are allowed');
12619
+ /**
12620
+ * Schema for validating Solana addresses.
12621
+ *
12622
+ * This schema validates that a string is a properly formatted Solana address:
12623
+ * - Must be a valid base58-encoded string
12624
+ * - Must be between 32-44 characters long (typical length for base58-encoded 32-byte addresses)
12625
+ *
12626
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11449
12627
  *
11450
12628
  * @example
11451
12629
  * ```typescript
11452
- * // Completed bridge (nothing to do)
11453
- * const bridgeResult = {
11454
- * steps: [
11455
- * { name: 'Approve', state: 'success' },
11456
- * { name: 'Burn', state: 'success' },
11457
- * { name: 'FetchAttestation', state: 'success' },
11458
- * { name: 'Mint', state: 'success' }
11459
- * ]
11460
- * }
12630
+ * import { solanaAddressSchema } from '@core/adapter'
11461
12631
  *
11462
- * const analysis = analyzeSteps(bridgeResult)
11463
- * // Result: { continuationStep: null, isRetryable: false,
11464
- * // reason: 'Bridge completed successfully' }
11465
- * ```
11466
- */
11467
- const analyzeSteps = (bridgeResult) => {
11468
- // Input validation
11469
- if (!bridgeResult || !Array.isArray(bridgeResult.steps)) {
11470
- throw new Error('Invalid bridgeResult: must contain a steps array');
11471
- }
11472
- const { steps } = bridgeResult;
11473
- // Build execution context from step history
11474
- const context = buildFlowContext(steps);
11475
- // Determine continuation logic using rule engine
11476
- const continuation = determineContinuationFromRules(context);
11477
- return {
11478
- continuationStep: continuation.nextStep,
11479
- isActionable: continuation.isActionable,
11480
- completedSteps: Array.from(context.completedSteps),
11481
- failedSteps: Array.from(context.failedSteps),
11482
- reason: continuation.reason,
11483
- };
11484
- };
11485
- /**
11486
- * Build flow context from the execution history.
12632
+ * const validAddress = 'DhzPkKCLJGHBZbs1AzmK2tRNLZkV8J3yWF3LuWMuKJpN'
11487
12633
  *
11488
- * @param steps - Array of executed bridge steps.
11489
- * @returns Flow context with execution state and history.
12634
+ * const result = solanaAddressSchema.safeParse(validAddress)
12635
+ * console.log(result.success) // true
12636
+ * ```
11490
12637
  */
11491
- function buildFlowContext(steps) {
11492
- const completedSteps = new Set();
11493
- const failedSteps = new Set();
11494
- let lastStep;
11495
- // Process step history to build context
11496
- for (const step of steps) {
11497
- if (step.state === 'success' || step.state === 'noop') {
11498
- completedSteps.add(step.name);
11499
- }
11500
- else if (step.state === 'error') {
11501
- failedSteps.add(step.name);
11502
- }
11503
- // Track the last step for continuation logic
11504
- lastStep = {
11505
- name: step.name,
11506
- state: step.state,
11507
- };
11508
- }
11509
- return {
11510
- completedSteps,
11511
- failedSteps,
11512
- ...(lastStep && { lastStep }),
11513
- };
11514
- }
12638
+ base58StringSchema.refine((value) => value.length >= 32 && value.length <= 44, 'Solana address must be between 32-44 characters long (base58-encoded 32-byte address)');
11515
12639
  /**
11516
- * Determine continuation step using the rule engine.
12640
+ * Schema for validating Solana transaction hashes.
11517
12641
  *
11518
- * @param context - The flow context with execution history.
11519
- * @returns Continuation decision with next step and actionability information.
11520
- */
11521
- function determineContinuationFromRules(context) {
11522
- const lastStepName = context.lastStep?.name;
11523
- // Handle initial state when no steps have been executed
11524
- if (lastStepName === undefined) {
11525
- const rules = STEP_TRANSITION_RULES[''];
11526
- const matchingRule = rules?.find((rule) => rule.condition(context));
11527
- if (!matchingRule) {
11528
- return {
11529
- nextStep: null,
11530
- isActionable: false,
11531
- reason: 'No initial state rule found',
11532
- };
11533
- }
11534
- return {
11535
- nextStep: matchingRule.nextStep,
11536
- isActionable: matchingRule.isActionable,
11537
- reason: matchingRule.reason,
11538
- };
11539
- }
11540
- // A step with an empty name is ambiguous and should be treated as an unrecoverable state.
11541
- if (lastStepName === '') {
11542
- return {
11543
- nextStep: null,
11544
- isActionable: false,
11545
- reason: 'No transition rules defined for step with empty name',
11546
- };
11547
- }
11548
- const rules = STEP_TRANSITION_RULES[lastStepName];
11549
- if (!rules) {
11550
- return {
11551
- nextStep: null,
11552
- isActionable: false,
11553
- reason: `No transition rules defined for step: ${lastStepName}`,
11554
- };
11555
- }
11556
- // Find the first matching rule
11557
- const matchingRule = rules.find((rule) => rule.condition(context));
11558
- if (!matchingRule) {
11559
- return {
11560
- nextStep: null,
11561
- isActionable: false,
11562
- reason: `No matching transition rule for current context`,
11563
- };
11564
- }
11565
- return {
11566
- nextStep: matchingRule.nextStep,
11567
- isActionable: matchingRule.isActionable,
11568
- reason: matchingRule.reason,
11569
- };
11570
- }
11571
-
11572
- /**
11573
- * Find a step by name in the bridge result.
12642
+ * This schema validates that a string is a properly formatted Solana transaction hash:
12643
+ * - Must be a valid base58-encoded string
12644
+ * - Must be between 86-88 characters long (typical length for base58-encoded 64-byte signatures)
11574
12645
  *
11575
- * @param result - The bridge result to search.
11576
- * @param stepName - The name of the step to find.
11577
- * @returns The step if found, undefined otherwise.
12646
+ * @throws {KitError} If validation fails with INPUT_VALIDATION_FAILED code (1098), with details about which properties failed
11578
12647
  *
11579
12648
  * @example
11580
12649
  * ```typescript
11581
- * import { findStepByName } from './findStep'
12650
+ * import { solanaTransactionHashSchema } from '@core/adapter'
11582
12651
  *
11583
- * const burnStep = findStepByName(result, 'burn')
11584
- * if (burnStep) {
11585
- * console.log('Burn tx:', burnStep.txHash)
11586
- * }
12652
+ * const validTxHash = '5VfYmGBjvQKe3xgLtTQPSMEUdpEVHrJwLK7pKBJWKzYpNBE2g3kJrq7RSe9M8DqzQJ5J2aZPTjHLvd4WgxPpJKS'
12653
+ *
12654
+ * const result = solanaTransactionHashSchema.safeParse(validTxHash)
12655
+ * console.log(result.success) // true
11587
12656
  * ```
11588
12657
  */
11589
- function findStepByName(result, stepName) {
11590
- return result.steps.find((step) => step.name === stepName);
11591
- }
12658
+ base58StringSchema.refine((value) => value.length >= 86 && value.length <= 88, 'Solana transaction hash must be between 86-88 characters long (base58-encoded 64-byte signature)');
11592
12659
  /**
11593
- * Find a pending step by name and return it with its index.
12660
+ * Schema for validating Adapter objects.
12661
+ * Checks for the required methods that define an Adapter.
12662
+ */
12663
+ z.object({
12664
+ prepare: z.function(),
12665
+ waitForTransaction: z.function(),
12666
+ getAddress: z.function(),
12667
+ });
12668
+
12669
+ /**
12670
+ * Validate that the adapter has sufficient token balance for a transaction.
11594
12671
  *
11595
- * Searches for a step that matches both the step name and has a pending state.
12672
+ * This function checks if the adapter's current token balance is greater than or equal
12673
+ * to the requested transaction amount. It throws a KitError with code 9001
12674
+ * (BALANCE_INSUFFICIENT_TOKEN) if the balance is insufficient, providing detailed
12675
+ * information about the shortfall.
11596
12676
  *
11597
- * @param result - The bridge result containing steps to search through.
11598
- * @param stepName - The step name to find (e.g., 'burn', 'mint', 'fetchAttestation').
11599
- * @returns An object containing the step and its index in the steps array.
11600
- * @throws KitError if the specified pending step is not found.
12677
+ * @param params - The validation parameters containing adapter, amount, token, and token address.
12678
+ * @returns A promise that resolves to void if validation passes.
12679
+ * @throws {KitError} When the adapter's balance is less than the required amount (code: 9001).
11601
12680
  *
11602
12681
  * @example
11603
12682
  * ```typescript
11604
- * import { findPendingStep } from './findStep'
12683
+ * import { validateBalanceForTransaction } from '@core/adapter'
12684
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
12685
+ * import { isKitError, ERROR_TYPES } from '@core/errors'
11605
12686
  *
11606
- * const { step, index } = findPendingStep(result, 'burn')
11607
- * console.log('Pending step:', step.name, 'at index:', index)
12687
+ * const adapter = createViemAdapterFromPrivateKey({
12688
+ * privateKey: '0x...',
12689
+ * chain: 'Ethereum',
12690
+ * })
12691
+ *
12692
+ * try {
12693
+ * await validateBalanceForTransaction({
12694
+ * adapter,
12695
+ * amount: '1000000', // 1 USDC (6 decimals)
12696
+ * token: 'USDC',
12697
+ * tokenAddress: '0xA0b86a33E6441c8C1c7C16e4c5e3e5b5e4c5e3e5b5e4c5e',
12698
+ * operationContext: { chain: 'Ethereum' },
12699
+ * })
12700
+ * console.log('Balance validation passed')
12701
+ * } catch (error) {
12702
+ * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
12703
+ * console.error('Insufficient funds:', error.message)
12704
+ * }
12705
+ * }
11608
12706
  * ```
11609
12707
  */
11610
- function findPendingStep(result, stepName) {
11611
- const index = result.steps.findIndex((step) => step.name === stepName && step.state === 'pending');
11612
- if (index === -1) {
11613
- throw new KitError({
11614
- ...InputError.VALIDATION_FAILED,
11615
- recoverability: 'FATAL',
11616
- message: `Pending step "${stepName}" not found in result`,
11617
- });
11618
- }
11619
- const step = result.steps[index];
11620
- if (!step) {
11621
- throw new KitError({
11622
- ...InputError.VALIDATION_FAILED,
11623
- recoverability: 'FATAL',
11624
- message: 'Pending step is undefined',
12708
+ const validateBalanceForTransaction = async (params) => {
12709
+ const { amount, adapter, token, tokenAddress, operationContext } = params;
12710
+ const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
12711
+ walletAddress: operationContext.address,
12712
+ }, operationContext);
12713
+ const balance = await balancePrepared.execute();
12714
+ if (BigInt(balance) < BigInt(amount)) {
12715
+ // Extract chain name from operationContext
12716
+ const chainName = extractChainInfo(operationContext.chain).name;
12717
+ // Create KitError with rich context in trace
12718
+ throw createInsufficientTokenBalanceError(chainName, token, {
12719
+ balance: balance.toString(),
12720
+ amount,
12721
+ tokenAddress,
12722
+ walletAddress: operationContext.address,
11625
12723
  });
11626
12724
  }
11627
- return { step, index };
11628
- }
12725
+ };
12726
+
11629
12727
  /**
11630
- * Get the burn transaction hash from bridge result.
12728
+ * Validate that the adapter has sufficient native token balance for transaction fees.
11631
12729
  *
11632
- * @param result - The bridge result.
11633
- * @returns The burn transaction hash, or undefined if not found.
12730
+ * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
12731
+ * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
12732
+ * if the balance is zero, indicating the wallet cannot pay for transaction fees.
12733
+ *
12734
+ * @param params - The validation parameters containing adapter and operation context.
12735
+ * @returns A promise that resolves to void if validation passes.
12736
+ * @throws {KitError} When the adapter's native balance is zero (code: 9002).
11634
12737
  *
11635
12738
  * @example
11636
12739
  * ```typescript
11637
- * import { getBurnTxHash } from './findStep'
12740
+ * import { validateNativeBalanceForTransaction } from '@core/adapter'
12741
+ * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
12742
+ * import { isKitError, ERROR_TYPES } from '@core/errors'
11638
12743
  *
11639
- * const burnTxHash = getBurnTxHash(result)
11640
- * if (burnTxHash) {
11641
- * console.log('Burn tx hash:', burnTxHash)
12744
+ * const adapter = createViemAdapterFromPrivateKey({
12745
+ * privateKey: '0x...',
12746
+ * chain: 'Ethereum',
12747
+ * })
12748
+ *
12749
+ * try {
12750
+ * await validateNativeBalanceForTransaction({
12751
+ * adapter,
12752
+ * operationContext: { chain: 'Ethereum' },
12753
+ * })
12754
+ * console.log('Native balance validation passed')
12755
+ * } catch (error) {
12756
+ * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
12757
+ * console.error('Insufficient gas funds:', error.message)
12758
+ * }
11642
12759
  * }
11643
12760
  * ```
11644
12761
  */
11645
- function getBurnTxHash(result) {
11646
- return findStepByName(result, CCTPv2StepName.burn)?.txHash;
11647
- }
12762
+ const validateNativeBalanceForTransaction = async (params) => {
12763
+ const { adapter, operationContext } = params;
12764
+ const balancePrepared = await adapter.prepareAction('native.balanceOf', {
12765
+ walletAddress: operationContext.address,
12766
+ }, operationContext);
12767
+ const balance = await balancePrepared.execute();
12768
+ if (BigInt(balance) === 0n) {
12769
+ const { chain } = operationContext;
12770
+ const chainName = extractChainInfo(chain).name;
12771
+ const nativeSymbol = typeof chain === 'object' && chain !== null && 'nativeCurrency' in chain
12772
+ ? chain.nativeCurrency.symbol
12773
+ : undefined;
12774
+ throw createInsufficientGasError(chainName, nativeSymbol, {
12775
+ balance: '0',
12776
+ walletAddress: operationContext.address,
12777
+ });
12778
+ }
12779
+ };
12780
+
11648
12781
  /**
11649
- * Get the attestation data from bridge result.
11650
- *
11651
- * @param result - The bridge result.
11652
- * @returns The attestation data, or undefined if not found.
12782
+ * Permit signature standards for gasless token approvals.
11653
12783
  *
11654
- * @example
11655
- * ```typescript
11656
- * import { getAttestationData } from './findStep'
12784
+ * Defines the permit types that can be used to approve token spending
12785
+ * without requiring a separate approval transaction.
11657
12786
  *
11658
- * const attestation = getAttestationData(result)
11659
- * if (attestation) {
11660
- * console.log('Attestation:', attestation.message)
11661
- * }
11662
- * ```
12787
+ * @remarks
12788
+ * - NONE: No permit, tokens must be pre-approved via separate transaction
12789
+ * - EIP2612: Standard ERC-20 permit (USDC, DAI v2, and most modern tokens)
11663
12790
  */
11664
- function getAttestationData(result) {
11665
- // Prefer reAttest data (most recent attestation after expiry)
11666
- const reAttestStep = findStepByName(result, CCTPv2StepName.reAttest);
11667
- if (reAttestStep?.state === 'success' && reAttestStep.data) {
11668
- return reAttestStep.data;
11669
- }
11670
- // Fall back to fetchAttestation step
11671
- const fetchStep = findStepByName(result, CCTPv2StepName.fetchAttestation);
11672
- return fetchStep?.data;
11673
- }
12791
+ var PermitType;
12792
+ (function (PermitType) {
12793
+ /** No permit required - tokens must be pre-approved */
12794
+ PermitType[PermitType["NONE"] = 0] = "NONE";
12795
+ /** EIP-2612 standard permit */
12796
+ PermitType[PermitType["EIP2612"] = 1] = "EIP2612";
12797
+ })(PermitType || (PermitType = {}));
11674
12798
 
11675
12799
  /**
11676
12800
  * Determine if a step executes on the source chain.
@@ -11723,169 +12847,6 @@ function getStepAdapterAndChain(step, context, result) {
11723
12847
  };
11724
12848
  }
11725
12849
 
11726
- /**
11727
- * Check if the analysis indicates a non-actionable pending state.
11728
- *
11729
- * A pending state is non-actionable when there's a continuation step but
11730
- * the analysis marks it as not actionable, typically because we need to
11731
- * wait for an ongoing operation to complete.
11732
- *
11733
- * @param analysis - The step analysis result from analyzeSteps.
11734
- * @param result - The bridge result to check for pending steps.
11735
- * @returns True if there is a pending step that we should wait for.
11736
- *
11737
- * @example
11738
- * ```typescript
11739
- * import { hasPendingState } from './stepUtils'
11740
- * import { analyzeSteps } from '../analyzeSteps'
11741
- *
11742
- * const analysis = analyzeSteps(bridgeResult)
11743
- * if (hasPendingState(analysis, bridgeResult)) {
11744
- * // Wait for the pending operation to complete
11745
- * }
11746
- * ```
11747
- */
11748
- function hasPendingState(analysis, result) {
11749
- // Check if there's a continuation step that's marked as non-actionable
11750
- if (analysis.continuationStep === null || analysis.isActionable) {
11751
- return false;
11752
- }
11753
- // Verify that the continuation step actually exists and is in pending state
11754
- const pendingStep = result.steps.find((step) => step.name === analysis.continuationStep && step.state === 'pending');
11755
- return pendingStep !== undefined;
11756
- }
11757
- /**
11758
- * Check if the step is the last one in the execution flow.
11759
- *
11760
- * @param step - The step object to check.
11761
- * @param stepNames - The ordered list of step names in the execution flow.
11762
- * @returns True if this is the last step in the flow.
11763
- *
11764
- * @example
11765
- * ```typescript
11766
- * import { isLastStep } from './stepUtils'
11767
- *
11768
- * const stepNames = ['approve', 'burn', 'fetchAttestation', 'mint']
11769
- * isLastStep({ name: 'mint' }, stepNames) // true
11770
- * isLastStep({ name: 'burn' }, stepNames) // false
11771
- * ```
11772
- */
11773
- function isLastStep(step, stepNames) {
11774
- const stepIndex = stepNames.indexOf(step.name);
11775
- return stepIndex === -1 || stepIndex >= stepNames.length - 1;
11776
- }
11777
- /**
11778
- * Wait for a pending transaction to complete.
11779
- *
11780
- * Poll the adapter until the transaction is confirmed on-chain and return
11781
- * the updated step with success or error state based on the receipt.
11782
- *
11783
- * @param pendingStep - The full step object containing the transaction hash.
11784
- * @param adapter - The adapter to use for waiting.
11785
- * @param chain - The chain where the transaction was submitted.
11786
- * @returns The updated step object with success or error state.
11787
- *
11788
- * @throws KitError when the pending step has no transaction hash.
11789
- *
11790
- * @example
11791
- * ```typescript
11792
- * import { waitForPendingTransaction } from './bridgeStepUtils'
11793
- *
11794
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
11795
- * const updatedStep = await waitForPendingTransaction(pendingStep, adapter, chain)
11796
- * // updatedStep.state is now 'success' or 'error'
11797
- * ```
11798
- */
11799
- async function waitForPendingTransaction(pendingStep, adapter, chain) {
11800
- if (!pendingStep.txHash) {
11801
- throw new KitError({
11802
- ...InputError.VALIDATION_FAILED,
11803
- recoverability: 'FATAL',
11804
- message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`,
11805
- });
11806
- }
11807
- const txHash = pendingStep.txHash;
11808
- const txReceipt = await retryAsync(async () => adapter.waitForTransaction(txHash, undefined, chain), {
11809
- isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
11810
- });
11811
- // Check if transaction was confirmed on-chain
11812
- if (!txReceipt.blockNumber) {
11813
- return {
11814
- ...pendingStep,
11815
- state: 'error',
11816
- errorMessage: txReceipt.status === 'reverted'
11817
- ? `Transaction ${pendingStep.txHash} was reverted`
11818
- : 'Transaction was not confirmed on-chain',
11819
- data: txReceipt,
11820
- };
11821
- }
11822
- return {
11823
- ...pendingStep,
11824
- state: 'success',
11825
- data: txReceipt,
11826
- };
11827
- }
11828
- /**
11829
- * Wait for a pending step to complete.
11830
- *
11831
- * For transaction steps: waits for the transaction to be confirmed.
11832
- * For attestation: re-executes the attestation fetch.
11833
- *
11834
- * @typeParam TFromAdapterCapabilities - The capabilities of the source adapter.
11835
- * @typeParam TToAdapterCapabilities - The capabilities of the destination adapter.
11836
- * @param pendingStep - The full step object (with name, state, txHash, data, etc.) to resolve.
11837
- * @param adapter - The adapter to use.
11838
- * @param chain - The chain where the step is executing.
11839
- * @param context - The retry context.
11840
- * @param result - The bridge result.
11841
- * @param provider - The CCTP v2 bridging provider.
11842
- * @returns The resolved step object with updated state.
11843
- *
11844
- * @throws KitError when fetching attestation but burn transaction hash is not found.
11845
- *
11846
- * @example
11847
- * ```typescript
11848
- * import { waitForStepToComplete } from './bridgeStepUtils'
11849
- *
11850
- * const pendingStep = { name: 'burn', state: 'pending', txHash: '0x123...' }
11851
- * const updatedStep = await waitForStepToComplete(
11852
- * pendingStep,
11853
- * adapter,
11854
- * chain,
11855
- * context,
11856
- * result,
11857
- * provider,
11858
- * )
11859
- * // updatedStep.state is now 'success' or 'error'
11860
- * ```
11861
- */
11862
- async function waitForStepToComplete(pendingStep, adapter, chain, context, result, provider) {
11863
- if (pendingStep.name === CCTPv2StepName.fetchAttestation) {
11864
- // For attestation, re-run the fetch (it has built-in polling)
11865
- const burnTxHash = getBurnTxHash(result);
11866
- if (!burnTxHash) {
11867
- throw new KitError({
11868
- ...InputError.VALIDATION_FAILED,
11869
- recoverability: 'FATAL',
11870
- message: 'Cannot fetch attestation: burn transaction hash not found',
11871
- });
11872
- }
11873
- const sourceAddress = result.source.address;
11874
- const attestation = await provider.fetchAttestation({
11875
- chain: result.source.chain,
11876
- adapter: context.from,
11877
- address: sourceAddress,
11878
- }, burnTxHash);
11879
- return {
11880
- ...pendingStep,
11881
- state: 'success',
11882
- data: attestation,
11883
- };
11884
- }
11885
- // For transaction steps, wait for the transaction to complete
11886
- return waitForPendingTransaction(pendingStep, adapter, chain);
11887
- }
11888
-
11889
12850
  /**
11890
12851
  * Executes a re-attestation operation to obtain a fresh attestation for an expired message.
11891
12852
  *