@circle-fin/app-kit 1.12.1 → 1.13.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.cjs CHANGED
@@ -4071,6 +4071,8 @@ function getOptionalString(value) {
4071
4071
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
4072
4072
  Blockchain["Pharos"] = "Pharos";
4073
4073
  Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
4074
+ Blockchain["Plasma"] = "Plasma";
4075
+ Blockchain["Plasma_Testnet"] = "Plasma_Testnet";
4074
4076
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
4075
4077
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
4076
4078
  Blockchain["Plume"] = "Plume";
@@ -4140,6 +4142,7 @@ exports.BridgeChain = void 0;
4140
4142
  BridgeChain["Morph"] = "Morph";
4141
4143
  BridgeChain["Optimism"] = "Optimism";
4142
4144
  BridgeChain["Pharos"] = "Pharos";
4145
+ BridgeChain["Plasma"] = "Plasma";
4143
4146
  BridgeChain["Plume"] = "Plume";
4144
4147
  BridgeChain["Polygon"] = "Polygon";
4145
4148
  BridgeChain["Sei"] = "Sei";
@@ -4166,6 +4169,7 @@ exports.BridgeChain = void 0;
4166
4169
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
4167
4170
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
4168
4171
  BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
4172
+ BridgeChain["Plasma_Testnet"] = "Plasma_Testnet";
4169
4173
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
4170
4174
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
4171
4175
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -4623,6 +4627,8 @@ exports.EarnChain = void 0;
4623
4627
  * This program handles minting operations for Gateway transactions
4624
4628
  * on Solana devnet.
4625
4629
  */ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
4630
+ /** TokenMessengerWithFees address shared by enabled EVM mainnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
4631
+ /** TokenMessengerWithFees address shared by enabled EVM testnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
4626
4632
  /**
4627
4633
  * Circle's fee recipient address for EVM chains.
4628
4634
  *
@@ -4670,6 +4676,7 @@ exports.EarnChain = void 0;
4670
4676
  v2: {
4671
4677
  type: 'split',
4672
4678
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4679
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4673
4680
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4674
4681
  confirmations: 1,
4675
4682
  fastConfirmations: 1
@@ -4737,6 +4744,7 @@ exports.EarnChain = void 0;
4737
4744
  v2: {
4738
4745
  type: 'split',
4739
4746
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4747
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4740
4748
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4741
4749
  confirmations: 65,
4742
4750
  fastConfirmations: 1
@@ -4801,6 +4809,7 @@ exports.EarnChain = void 0;
4801
4809
  v2: {
4802
4810
  type: 'split',
4803
4811
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4812
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4804
4813
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4805
4814
  confirmations: 65,
4806
4815
  fastConfirmations: 1
@@ -4865,6 +4874,7 @@ exports.EarnChain = void 0;
4865
4874
  v2: {
4866
4875
  type: 'split',
4867
4876
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4877
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4868
4878
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4869
4879
  confirmations: 1,
4870
4880
  fastConfirmations: 1
@@ -4926,6 +4936,7 @@ exports.EarnChain = void 0;
4926
4936
  v2: {
4927
4937
  type: 'split',
4928
4938
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4939
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4929
4940
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
4930
4941
  confirmations: 1,
4931
4942
  fastConfirmations: 1
@@ -4993,6 +5004,7 @@ exports.EarnChain = void 0;
4993
5004
  v2: {
4994
5005
  type: 'split',
4995
5006
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5007
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4996
5008
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4997
5009
  confirmations: 65,
4998
5010
  fastConfirmations: 1
@@ -5057,6 +5069,7 @@ exports.EarnChain = void 0;
5057
5069
  v2: {
5058
5070
  type: 'split',
5059
5071
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5072
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5060
5073
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5061
5074
  confirmations: 65,
5062
5075
  fastConfirmations: 1
@@ -5167,6 +5180,7 @@ exports.EarnChain = void 0;
5167
5180
  v2: {
5168
5181
  type: 'split',
5169
5182
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5183
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5170
5184
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5171
5185
  confirmations: 65,
5172
5186
  fastConfirmations: 1
@@ -5211,6 +5225,7 @@ exports.EarnChain = void 0;
5211
5225
  v2: {
5212
5226
  type: 'split',
5213
5227
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5228
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5214
5229
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5215
5230
  confirmations: 65,
5216
5231
  fastConfirmations: 1
@@ -5442,6 +5457,7 @@ exports.EarnChain = void 0;
5442
5457
  v2: {
5443
5458
  type: 'split',
5444
5459
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5460
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5445
5461
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5446
5462
  confirmations: 65,
5447
5463
  fastConfirmations: 2
@@ -5506,6 +5522,7 @@ exports.EarnChain = void 0;
5506
5522
  v2: {
5507
5523
  type: 'split',
5508
5524
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5525
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5509
5526
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5510
5527
  confirmations: 65,
5511
5528
  fastConfirmations: 2
@@ -5616,6 +5633,7 @@ exports.EarnChain = void 0;
5616
5633
  v2: {
5617
5634
  type: 'split',
5618
5635
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5636
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5619
5637
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5620
5638
  confirmations: 1,
5621
5639
  fastConfirmations: 1
@@ -5675,6 +5693,7 @@ exports.EarnChain = void 0;
5675
5693
  v2: {
5676
5694
  type: 'split',
5677
5695
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5696
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5678
5697
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5679
5698
  confirmations: 1,
5680
5699
  fastConfirmations: 1
@@ -5829,6 +5848,7 @@ exports.EarnChain = void 0;
5829
5848
  v2: {
5830
5849
  type: 'split',
5831
5850
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5851
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5832
5852
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5833
5853
  confirmations: 65,
5834
5854
  fastConfirmations: 1
@@ -5876,6 +5896,7 @@ exports.EarnChain = void 0;
5876
5896
  v2: {
5877
5897
  type: 'split',
5878
5898
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5899
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5879
5900
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5880
5901
  confirmations: 65,
5881
5902
  fastConfirmations: 1
@@ -5920,6 +5941,7 @@ exports.EarnChain = void 0;
5920
5941
  v2: {
5921
5942
  type: 'split',
5922
5943
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5944
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5923
5945
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5924
5946
  confirmations: 1,
5925
5947
  fastConfirmations: 1
@@ -5965,6 +5987,7 @@ exports.EarnChain = void 0;
5965
5987
  v2: {
5966
5988
  type: 'split',
5967
5989
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5990
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5968
5991
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5969
5992
  confirmations: 1,
5970
5993
  fastConfirmations: 1
@@ -6011,6 +6034,7 @@ exports.EarnChain = void 0;
6011
6034
  v2: {
6012
6035
  type: 'split',
6013
6036
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6037
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6014
6038
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6015
6039
  confirmations: 1,
6016
6040
  fastConfirmations: 1
@@ -6324,6 +6348,7 @@ exports.EarnChain = void 0;
6324
6348
  v2: {
6325
6349
  type: 'split',
6326
6350
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6351
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6327
6352
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6328
6353
  confirmations: 65,
6329
6354
  fastConfirmations: 1
@@ -6388,6 +6413,7 @@ exports.EarnChain = void 0;
6388
6413
  v2: {
6389
6414
  type: 'split',
6390
6415
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
6416
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6391
6417
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
6392
6418
  confirmations: 65,
6393
6419
  fastConfirmations: 1
@@ -6508,6 +6534,98 @@ exports.EarnChain = void 0;
6508
6534
  }
6509
6535
  });
6510
6536
 
6537
+ /**
6538
+ * Plasma Mainnet chain definition
6539
+ * @remarks
6540
+ * This represents the official production network for the Plasma blockchain.
6541
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
6542
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
6543
+ */ const Plasma = defineChain({
6544
+ type: 'evm',
6545
+ chain: exports.Blockchain.Plasma,
6546
+ name: 'Plasma',
6547
+ title: 'Plasma Mainnet',
6548
+ nativeCurrency: {
6549
+ name: 'Plasma',
6550
+ symbol: 'XPL',
6551
+ decimals: 18
6552
+ },
6553
+ chainId: 9745,
6554
+ isTestnet: false,
6555
+ explorerUrl: 'https://plasmascan.to/tx/{hash}',
6556
+ rpcEndpoints: [
6557
+ 'https://rpc.plasma.to'
6558
+ ],
6559
+ eurcAddress: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
6560
+ usdcAddress: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
6561
+ usdtAddress: null,
6562
+ cctp: {
6563
+ domain: 33,
6564
+ contracts: {
6565
+ v2: {
6566
+ type: 'split',
6567
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6568
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6569
+ confirmations: 3,
6570
+ fastConfirmations: 1
6571
+ }
6572
+ },
6573
+ forwarderSupported: {
6574
+ source: false,
6575
+ destination: false
6576
+ }
6577
+ },
6578
+ kitContracts: {
6579
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
6580
+ }
6581
+ });
6582
+
6583
+ /**
6584
+ * Plasma Testnet chain definition
6585
+ * @remarks
6586
+ * This represents the official test network for the Plasma blockchain.
6587
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
6588
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
6589
+ */ const PlasmaTestnet = defineChain({
6590
+ type: 'evm',
6591
+ chain: exports.Blockchain.Plasma_Testnet,
6592
+ name: 'Plasma Testnet',
6593
+ title: 'Plasma Testnet',
6594
+ nativeCurrency: {
6595
+ name: 'Plasma',
6596
+ symbol: 'XPL',
6597
+ decimals: 18
6598
+ },
6599
+ chainId: 9746,
6600
+ isTestnet: true,
6601
+ explorerUrl: 'https://testnet.plasmascan.to/tx/{hash}',
6602
+ rpcEndpoints: [
6603
+ 'https://testnet-rpc.plasma.to'
6604
+ ],
6605
+ eurcAddress: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330',
6606
+ usdcAddress: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
6607
+ usdtAddress: null,
6608
+ cctp: {
6609
+ domain: 33,
6610
+ contracts: {
6611
+ v2: {
6612
+ type: 'split',
6613
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6614
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6615
+ confirmations: 3,
6616
+ fastConfirmations: 1
6617
+ }
6618
+ },
6619
+ forwarderSupported: {
6620
+ source: false,
6621
+ destination: false
6622
+ }
6623
+ },
6624
+ kitContracts: {
6625
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
6626
+ }
6627
+ });
6628
+
6511
6629
  /**
6512
6630
  * Plume Mainnet chain definition
6513
6631
  * @remarks
@@ -6539,6 +6657,7 @@ exports.EarnChain = void 0;
6539
6657
  v2: {
6540
6658
  type: 'split',
6541
6659
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6660
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6542
6661
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6543
6662
  confirmations: 65,
6544
6663
  fastConfirmations: 1
@@ -6585,6 +6704,7 @@ exports.EarnChain = void 0;
6585
6704
  v2: {
6586
6705
  type: 'split',
6587
6706
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6707
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6588
6708
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6589
6709
  confirmations: 65,
6590
6710
  fastConfirmations: 1
@@ -6686,6 +6806,7 @@ exports.EarnChain = void 0;
6686
6806
  v2: {
6687
6807
  type: 'split',
6688
6808
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6809
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6689
6810
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6690
6811
  confirmations: 33,
6691
6812
  fastConfirmations: 13
@@ -6751,6 +6872,7 @@ exports.EarnChain = void 0;
6751
6872
  v2: {
6752
6873
  type: 'split',
6753
6874
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6875
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6754
6876
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6755
6877
  confirmations: 33,
6756
6878
  fastConfirmations: 13
@@ -6810,6 +6932,7 @@ exports.EarnChain = void 0;
6810
6932
  v2: {
6811
6933
  type: 'split',
6812
6934
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6935
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6813
6936
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6814
6937
  confirmations: 1,
6815
6938
  fastConfirmations: 1
@@ -6869,6 +6992,7 @@ exports.EarnChain = void 0;
6869
6992
  v2: {
6870
6993
  type: 'split',
6871
6994
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6995
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6872
6996
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6873
6997
  confirmations: 1,
6874
6998
  fastConfirmations: 1
@@ -6926,6 +7050,7 @@ exports.EarnChain = void 0;
6926
7050
  v2: {
6927
7051
  type: 'split',
6928
7052
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7053
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6929
7054
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6930
7055
  confirmations: 1,
6931
7056
  fastConfirmations: 1
@@ -6984,6 +7109,7 @@ exports.EarnChain = void 0;
6984
7109
  v2: {
6985
7110
  type: 'split',
6986
7111
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7112
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6987
7113
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6988
7114
  confirmations: 1,
6989
7115
  fastConfirmations: 1
@@ -7299,6 +7425,7 @@ exports.EarnChain = void 0;
7299
7425
  v2: {
7300
7426
  type: 'split',
7301
7427
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7428
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
7302
7429
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7303
7430
  confirmations: 65,
7304
7431
  fastConfirmations: 1
@@ -7363,6 +7490,7 @@ exports.EarnChain = void 0;
7363
7490
  v2: {
7364
7491
  type: 'split',
7365
7492
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7493
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
7366
7494
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
7367
7495
  confirmations: 65,
7368
7496
  fastConfirmations: 1
@@ -7420,6 +7548,7 @@ exports.EarnChain = void 0;
7420
7548
  v2: {
7421
7549
  type: 'split',
7422
7550
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cF5d',
7551
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
7423
7552
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7424
7553
  confirmations: 65,
7425
7554
  fastConfirmations: 1
@@ -7479,6 +7608,7 @@ exports.EarnChain = void 0;
7479
7608
  v2: {
7480
7609
  type: 'split',
7481
7610
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
7611
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
7482
7612
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
7483
7613
  confirmations: 65,
7484
7614
  fastConfirmations: 1
@@ -7539,6 +7669,7 @@ exports.EarnChain = void 0;
7539
7669
  v2: {
7540
7670
  type: 'split',
7541
7671
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7672
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
7542
7673
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7543
7674
  confirmations: 3,
7544
7675
  fastConfirmations: 3
@@ -7584,6 +7715,7 @@ exports.EarnChain = void 0;
7584
7715
  v2: {
7585
7716
  type: 'split',
7586
7717
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7718
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
7587
7719
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
7588
7720
  confirmations: 3,
7589
7721
  fastConfirmations: 1
@@ -7794,6 +7926,8 @@ var Chains = {
7794
7926
  OptimismSepolia: OptimismSepolia,
7795
7927
  Pharos: Pharos,
7796
7928
  PharosTestnet: PharosTestnet,
7929
+ Plasma: Plasma,
7930
+ PlasmaTestnet: PlasmaTestnet,
7797
7931
  Plume: Plume,
7798
7932
  PlumeTestnet: PlumeTestnet,
7799
7933
  PolkadotAssetHub: PolkadotAssetHub,
@@ -7843,6 +7977,34 @@ var Chains = {
7843
7977
  return chain.cctp?.contracts.v2 !== undefined;
7844
7978
  }
7845
7979
 
7980
+ /**
7981
+ * Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
7982
+ *
7983
+ * A chain supports source-paid fees when its CCTP v2 configuration carries a
7984
+ * deployed `TokenMessengerWithFees` wrapper address. Bridge Kit routes
7985
+ * `feePayment: 'source'` transfers through this wrapper via
7986
+ * `depositForBurnWithHookAndFees`, so a chain without the wrapper cannot be a
7987
+ * source for receive-exact bridging.
7988
+ *
7989
+ * @param chain - The chain definition to check.
7990
+ * @returns `true` when the chain has a `tokenMessengerWithFees` wrapper
7991
+ * configured, `false` otherwise.
7992
+ *
7993
+ * @example
7994
+ * ```typescript
7995
+ * import { Chains, hasSourceFeeSupport } from '@core/chains'
7996
+ *
7997
+ * hasSourceFeeSupport(Chains.Optimism) // true
7998
+ * hasSourceFeeSupport(Chains.Solana) // false
7999
+ * ```
8000
+ */ function hasSourceFeeSupport(chain) {
8001
+ if (!isCCTPV2Supported(chain)) {
8002
+ return false;
8003
+ }
8004
+ const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
8005
+ return typeof wrapper === 'string' && wrapper.length > 0;
8006
+ }
8007
+
7846
8008
  /**
7847
8009
  * Check if a chain supports a specific type of custom smart contract logic.
7848
8010
  *
@@ -10170,6 +10332,7 @@ function parseOrThrow(value, schema, context) {
10170
10332
  [exports.Blockchain.Noble]: 'uusdc',
10171
10333
  [exports.Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
10172
10334
  [exports.Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
10335
+ [exports.Blockchain.Plasma]: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
10173
10336
  [exports.Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
10174
10337
  [exports.Blockchain.Polkadot_Asset_Hub]: '1337',
10175
10338
  [exports.Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -10206,6 +10369,7 @@ function parseOrThrow(value, schema, context) {
10206
10369
  [exports.Blockchain.Noble_Testnet]: 'uusdc',
10207
10370
  [exports.Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
10208
10371
  [exports.Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
10372
+ [exports.Blockchain.Plasma_Testnet]: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
10209
10373
  [exports.Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
10210
10374
  [exports.Blockchain.Polkadot_Westmint]: '31337',
10211
10375
  [exports.Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -10268,6 +10432,7 @@ function parseOrThrow(value, schema, context) {
10268
10432
  [exports.Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
10269
10433
  [exports.Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
10270
10434
  [exports.Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
10435
+ [exports.Blockchain.Plasma]: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
10271
10436
  [exports.Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
10272
10437
  [exports.Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
10273
10438
  // =========================================================================
@@ -10276,7 +10441,8 @@ function parseOrThrow(value, schema, context) {
10276
10441
  [exports.Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
10277
10442
  [exports.Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
10278
10443
  [exports.Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
10279
- [exports.Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
10444
+ [exports.Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
10445
+ [exports.Blockchain.Plasma_Testnet]: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330'
10280
10446
  }
10281
10447
  };
10282
10448
 
@@ -11229,6 +11395,16 @@ function parseOrThrow(value, schema, context) {
11229
11395
  *
11230
11396
  * Set to 0 when no additional Circle-reserved data is needed.
11231
11397
  */ const CCTP_FORWARD_PAYLOAD_LENGTH = 0;
11398
+ /**
11399
+ * Length in bytes of a Solana owner (ed25519 / PDA) public key.
11400
+ */ const SOLANA_PUBKEY_LENGTH = 32;
11401
+ /**
11402
+ * Byte length of the Solana ATA-creation forwarding payload appended after the
11403
+ * `cctp-forward` frame: `createAta` (1 byte) + `ataOwner` (32 bytes).
11404
+ *
11405
+ * Circle's Orbit relayer decodes exactly this many bytes; see
11406
+ * {@link buildSolanaAtaForwardingHookData}.
11407
+ */ const SOLANA_ATA_FORWARD_PAYLOAD_LENGTH = 1 + SOLANA_PUBKEY_LENGTH;
11232
11408
  /**
11233
11409
  * Build the hookData bytes for CCTP forwarding.
11234
11410
  *
@@ -11285,6 +11461,108 @@ function buildForwardingHookData() {
11285
11461
  cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
11286
11462
  return cachedHookDataHex;
11287
11463
  }
11464
+ /**
11465
+ * Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
11466
+ * create the recipient's Associated Token Account (ATA) before minting on Solana.
11467
+ *
11468
+ * When an EVM→Solana bridge is forwarded, the destination mint targets the
11469
+ * recipient's USDC ATA — which does not exist for a fresh wallet. This frame
11470
+ * tells the relayer to prepend an idempotent `createAssociatedTokenAccount`
11471
+ * instruction (the relayer pays the rent) so the mint always succeeds.
11472
+ *
11473
+ * Unlike {@link buildForwardingHookData} (an empty version-0 frame), this emits
11474
+ * a version-0 frame whose 32-bit `dataLength` is set to
11475
+ * {@link SOLANA_ATA_FORWARD_PAYLOAD_LENGTH} (33), followed by the payload the
11476
+ * relayer decodes:
11477
+ * - Byte 0: `createAta` flag, always `1`
11478
+ * - Bytes 1-32: the recipient's 32-byte Solana owner public key (`ataOwner`)
11479
+ *
11480
+ * @remarks
11481
+ * `ataOwner` is the recipient's *wallet* public key, not the derived ATA. The
11482
+ * relayer re-derives the ATA from `ataOwner` and the USDC mint and requires it
11483
+ * to equal the burn's `mintRecipient`, so callers must pass the same owner used
11484
+ * to derive `mintRecipient`. The all-zero key is reserved as "absent owner" and
11485
+ * is rejected.
11486
+ *
11487
+ * @param ataOwner - The recipient's 32-byte Solana owner public key.
11488
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by the 33-byte
11489
+ * Solana ATA payload.
11490
+ * @throws {KitError} If `ataOwner` is not exactly 32 bytes, or is the all-zero
11491
+ * key (INPUT_VALIDATION_FAILED).
11492
+ *
11493
+ * @example
11494
+ * ```typescript
11495
+ * import { PublicKey } from '@solana/web3.js'
11496
+ * import { buildSolanaAtaForwardingHookData } from '@core/utils'
11497
+ *
11498
+ * const owner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM')
11499
+ * const hookData = buildSolanaAtaForwardingHookData(owner.toBytes())
11500
+ *
11501
+ * // Use with the forwarded depositForBurnWithHook action so the relayer
11502
+ * // creates the recipient ATA before minting.
11503
+ * await adapter.prepareAction('cctp.v2.depositForBurnWithHook', {
11504
+ * amount: BigInt('1000000'),
11505
+ * mintRecipient: '0x...',
11506
+ * maxFee: BigInt('50000'),
11507
+ * minFinalityThreshold: 1000,
11508
+ * fromChain: ethereum,
11509
+ * toChain: solana,
11510
+ * hookData,
11511
+ * })
11512
+ * ```
11513
+ */ function buildSolanaAtaForwardingHookData(ataOwner) {
11514
+ if (!(ataOwner instanceof Uint8Array) || ataOwner.length !== SOLANA_PUBKEY_LENGTH) {
11515
+ throw createValidationFailedError$1('ataOwner', ataOwner, `Expected a ${String(SOLANA_PUBKEY_LENGTH)}-byte Solana owner public key`);
11516
+ }
11517
+ if (ataOwner.every((byte)=>byte === 0)) {
11518
+ throw createValidationFailedError$1('ataOwner', ataOwner, 'Expected a non-zero Solana owner public key; the all-zero key is reserved as "absent owner"');
11519
+ }
11520
+ // Inner payload: createAta(1) + ataOwner(32).
11521
+ const payload = new Uint8Array(SOLANA_ATA_FORWARD_PAYLOAD_LENGTH);
11522
+ payload[0] = 1 // createAta = true
11523
+ ;
11524
+ payload.set(ataOwner, 1);
11525
+ // 32-byte header: 24-byte magic + uint32 version(0) + uint32 dataLength(33).
11526
+ // The relayer reads dataLength from the v0 frame to slice the inner payload,
11527
+ // so it MUST reflect the appended byte count (unlike the GenericExecutor path).
11528
+ const frame = new Uint8Array(32);
11529
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
11530
+ const view = new DataView(frame.buffer);
11531
+ view.setUint32(24, CCTP_FORWARD_VERSION, false) // big-endian, 0
11532
+ ;
11533
+ view.setUint32(28, SOLANA_ATA_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 33
11534
+ ;
11535
+ return bytes.hexlify(bytes.concat([
11536
+ frame,
11537
+ payload
11538
+ ]));
11539
+ }
11540
+
11541
+ /**
11542
+ * Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
11543
+ *
11544
+ * Mirrors viem's `pad(address, size 32)` and CCTP's `mintRecipient`
11545
+ * convention. Solana addresses are already 32 bytes and need no padding.
11546
+ *
11547
+ * @param address - A 0x-prefixed 20-byte EVM address.
11548
+ * @returns The address left-zero-padded to a 0x-prefixed 32-byte hex string.
11549
+ * @throws {KitError} If `address` is not a valid EVM address (INPUT_VALIDATION_FAILED).
11550
+ *
11551
+ * @example
11552
+ * ```typescript
11553
+ * import { padAddressToBytes32 } from '@core/utils'
11554
+ *
11555
+ * padAddressToBytes32('0x75275Aff2D01699D922f045b69ed291311209738')
11556
+ * // '0x00000000000000000000000075275aff2d01699d922f045b69ed291311209738'
11557
+ * ```
11558
+ */ function padAddressToBytes32(address$1) {
11559
+ if (!address.isAddress(address$1)) {
11560
+ throw createValidationFailedError$1('address', address$1, 'Expected a valid 20-byte EVM address');
11561
+ }
11562
+ // bytes32 is raw bytes, not a checksummed address — emit lowercase so it
11563
+ // matches ABI-decoded output.
11564
+ return bytes.hexZeroPad(address.getAddress(address$1), 32).toLowerCase();
11565
+ }
11288
11566
 
11289
11567
  /**
11290
11568
  * Configuration for {@link retryAsync}.
@@ -11873,7 +12151,7 @@ function resolveOptions(options) {
11873
12151
  }
11874
12152
 
11875
12153
  var name$4 = "@circle-fin/bridge-kit";
11876
- var version$5 = "1.13.0";
12154
+ var version$5 = "1.14.0";
11877
12155
  var pkg$5 = {
11878
12156
  name: name$4,
11879
12157
  version: version$5};
@@ -12323,7 +12601,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
12323
12601
  * const result = evmAddressSchema.safeParse(validAddress)
12324
12602
  * console.log(result.success) // true
12325
12603
  * ```
12326
- */ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
12604
+ */ const evmAddressSchema$1 = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
12327
12605
  /**
12328
12606
  * Schema for validating transaction hashes.
12329
12607
  *
@@ -13012,6 +13290,10 @@ exports.TransferSpeed = void 0;
13012
13290
  token: zod.z.literal('USDC').optional(),
13013
13291
  config: zod.z.object({
13014
13292
  transferSpeed: zod.z.nativeEnum(exports.TransferSpeed).optional(),
13293
+ feePayment: zod.z.enum([
13294
+ 'source',
13295
+ 'destination'
13296
+ ]).optional(),
13015
13297
  maxFee: zod.z.string().min(1, 'Required').pipe(createDecimalStringValidator({
13016
13298
  allowZero: true,
13017
13299
  regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
@@ -13019,7 +13301,8 @@ exports.TransferSpeed = void 0;
13019
13301
  maxDecimals: 6
13020
13302
  })(zod.z.string())).optional(),
13021
13303
  customFee: customFeeSchema.optional()
13022
- }).optional()
13304
+ }).optional(),
13305
+ quote: zod.z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex').optional()
13023
13306
  });
13024
13307
 
13025
13308
  /**
@@ -14233,16 +14516,73 @@ exports.TransferSpeed = void 0;
14233
14516
  };
14234
14517
  }
14235
14518
 
14519
+ /**
14520
+ * Dispatch a bridge step event through the provider's action dispatcher.
14521
+ *
14522
+ * Constructs the appropriate action payload and dispatches it to any registered
14523
+ * event listeners. Handles type-safe dispatching for different step types.
14524
+ * When provided, traceId from the invocation context is included for end-to-end correlation.
14525
+ *
14526
+ * @param name - The step name (approve, burn, fetchAttestation, or mint).
14527
+ * @param step - The completed bridge step containing transaction details and explorerUrl.
14528
+ * @param provider - The CCTP v2 provider with action dispatcher.
14529
+ * @param invocation - Optional invocation context containing traceId for correlation.
14530
+ *
14531
+ * @example
14532
+ * ```typescript
14533
+ * const step: BridgeStep = {
14534
+ * name: 'burn',
14535
+ * state: 'success',
14536
+ * txHash: '0xabc...',
14537
+ * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
14538
+ * data: { ... }
14539
+ * }
14540
+ * dispatchStepEvent('burn', step, provider, invocationContext)
14541
+ * ```
14542
+ */ function dispatchStepEvent(name, step, provider, invocation) {
14543
+ if (!provider.actionDispatcher) {
14544
+ return;
14545
+ }
14546
+ // Extract traceId from invocation context if provided
14547
+ const traceId = invocation?.traceId;
14548
+ const actionValues = {
14549
+ protocol: 'cctp',
14550
+ version: 'v2',
14551
+ ...traceId !== undefined && {
14552
+ traceId
14553
+ },
14554
+ values: step
14555
+ };
14556
+ switch(name){
14557
+ case 'approve':
14558
+ case 'burn':
14559
+ case 'mint':
14560
+ provider.actionDispatcher.dispatch(name, {
14561
+ ...actionValues,
14562
+ method: name
14563
+ });
14564
+ break;
14565
+ case 'fetchAttestation':
14566
+ case 'reAttest':
14567
+ provider.actionDispatcher.dispatch(name, {
14568
+ ...actionValues,
14569
+ method: name,
14570
+ values: step
14571
+ });
14572
+ break;
14573
+ }
14574
+ }
14575
+
14236
14576
  /**
14237
14577
  * Base URL for Circle's IRIS API (mainnet/production).
14238
14578
  *
14239
14579
  * The IRIS API provides attestation services for CCTP cross-chain transfers.
14240
- */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
14580
+ */ const IRIS_API_BASE_URL$1 = 'https://iris-api.circle.com';
14241
14581
  /**
14242
14582
  * Base URL for Circle's IRIS API (testnet/sandbox).
14243
14583
  *
14244
14584
  * Used for development and testing on testnet chains.
14245
- */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
14585
+ */ const IRIS_API_SANDBOX_BASE_URL$1 = 'https://iris-api-sandbox.circle.com';
14246
14586
 
14247
14587
  /**
14248
14588
  * Type guard to validate the API response structure.
@@ -14285,7 +14625,7 @@ const isFastBurnFeeResponse = (data)=>{
14285
14625
  * @param isTestnet - Whether the request is for a testnet chain
14286
14626
  * @returns The complete API URL
14287
14627
  */ function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
14288
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
14628
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
14289
14629
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
14290
14630
  }
14291
14631
  const FAST_TIER_FINALITY_THRESHOLD = 1000;
@@ -14496,6 +14836,77 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14496
14836
  }
14497
14837
  };
14498
14838
 
14839
+ /**
14840
+ * Build the forwarding `hookData` for a forwarded (Orbit-relayed) CCTP v2 burn,
14841
+ * tailored to the destination chain.
14842
+ *
14843
+ * For EVM destinations the recipient already holds ERC-20 USDC directly, so the
14844
+ * empty version-0 `cctp-forward` frame is sufficient. For Solana destinations
14845
+ * USDC is held in an Associated Token Account (ATA) that may not exist for a
14846
+ * fresh wallet, so this emits a frame carrying `createAta` + `ataOwner` that
14847
+ * instructs the relayer to create the recipient ATA (idempotently, at the
14848
+ * relayer's expense) before minting.
14849
+ *
14850
+ * `@solana/web3.js` is imported lazily so EVM-only consumers never load Solana
14851
+ * code, mirroring {@link getMintRecipientAccount}.
14852
+ *
14853
+ * @param chainType - The destination blockchain type ('evm' or 'solana').
14854
+ * @param ownerAddress - The recipient's wallet address on the destination chain
14855
+ * (base58 for Solana). Must be the same owner used to derive `mintRecipient`.
14856
+ * @returns A 0x-prefixed hookData hex string for the forwarded burn.
14857
+ * @throws {KitError} If `chainType` is neither 'evm' nor 'solana', if
14858
+ * `@solana/web3.js` cannot be loaded, or if `ownerAddress` is not a valid
14859
+ * Solana public key (all FATAL).
14860
+ *
14861
+ * @example
14862
+ * ```typescript
14863
+ * import { getForwarderHookData } from './getForwarderHookData'
14864
+ *
14865
+ * // EVM: empty forwarding frame
14866
+ * const evmHook = await getForwarderHookData('evm', '0x742d35Cc...')
14867
+ *
14868
+ * // Solana: frame instructing the relayer to create the recipient ATA
14869
+ * const solanaHook = await getForwarderHookData(
14870
+ * 'solana',
14871
+ * '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
14872
+ * )
14873
+ * ```
14874
+ */ const getForwarderHookData = async (/** The destination blockchain type - determines the hookData shape */ chainType, /** The recipient's wallet address (hex for EVM, base58 for Solana) */ ownerAddress)=>{
14875
+ if (chainType === 'evm') {
14876
+ // EVM: the recipient holds USDC directly; no ATA setup is needed.
14877
+ return buildForwardingHookData();
14878
+ }
14879
+ // Fail closed: only EVM and Solana forwarding destinations are supported.
14880
+ // Without this guard any future non-EVM chain type would silently fall
14881
+ // through to the Solana path and mis-encode hookData on a money-movement path.
14882
+ if (chainType !== 'solana') {
14883
+ throw new KitError({
14884
+ ...InputError.VALIDATION_FAILED,
14885
+ recoverability: 'FATAL',
14886
+ message: `Forwarded burns are not supported for destination chain type "${chainType}"`
14887
+ });
14888
+ }
14889
+ // Solana: encode the owner so the relayer creates the recipient ATA.
14890
+ // Resolve @solana/web3.js lazily so EVM-only consumers never load Solana code.
14891
+ const { PublicKey } = await import('@solana/web3.js').catch(()=>{
14892
+ throw new KitError({
14893
+ ...InputError.VALIDATION_FAILED,
14894
+ recoverability: 'FATAL',
14895
+ message: 'Failed to load @solana/web3.js. Please ensure it is installed: npm install @solana/web3.js'
14896
+ });
14897
+ });
14898
+ try {
14899
+ const owner = new PublicKey(ownerAddress);
14900
+ return buildSolanaAtaForwardingHookData(owner.toBytes());
14901
+ } catch (error) {
14902
+ throw new KitError({
14903
+ ...InputError.INVALID_ADDRESS,
14904
+ recoverability: 'FATAL',
14905
+ message: `Failed to build Solana forwarder hookData for recipient "${ownerAddress}": ${error instanceof Error ? error.message : String(error)}`
14906
+ });
14907
+ }
14908
+ };
14909
+
14499
14910
  /**
14500
14911
  * Validates and converts a fee value to bigint.
14501
14912
  *
@@ -14598,7 +15009,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14598
15009
 
14599
15010
  /**
14600
15011
  * The zero address, denoting a native-currency fee in a signed quote.
14601
- */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
15012
+ */ const ZERO_ADDRESS$2 = '0x0000000000000000000000000000000000000000';
14602
15013
  /**
14603
15014
  * Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
14604
15015
  *
@@ -14639,7 +15050,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14639
15050
  if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
14640
15051
  throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
14641
15052
  }
14642
- const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
15053
+ const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$2;
14643
15054
  const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
14644
15055
  if (isNativeFee) {
14645
15056
  return {
@@ -14777,7 +15188,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14777
15188
  * @param isTestnet - Whether the request is for a testnet chain
14778
15189
  * @returns The complete API URL with forward=true query parameter
14779
15190
  */ function buildForwardingFeeUrl(sourceDomain, destinationDomain, isTestnet) {
14780
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15191
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
14781
15192
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}?forward=true`;
14782
15193
  }
14783
15194
  /**
@@ -15838,7 +16249,7 @@ function hasPendingState(analysis, result) {
15838
16249
  * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
15839
16250
  * ```
15840
16251
  */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
15841
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
16252
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
15842
16253
  const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
15843
16254
  url.searchParams.set('transactionHash', transactionHash);
15844
16255
  return url.toString();
@@ -16006,7 +16417,7 @@ function hasPendingState(analysis, result) {
16006
16417
  * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
16007
16418
  * ```
16008
16419
  */ const buildReAttestUrl = (nonce, isTestnet)=>{
16009
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
16420
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
16010
16421
  const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
16011
16422
  return url.toString();
16012
16423
  };
@@ -16461,7 +16872,8 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16461
16872
  * - `destinationChain` — present and supports CCTP v2
16462
16873
  * - source and destination chains must both be testnet or both mainnet
16463
16874
  * - source and destination chains must differ
16464
- * - `executor` non-empty string
16875
+ * - destination — either `executor`, or both `mintRecipient` and
16876
+ * `destinationCaller`; not both
16465
16877
  * - `amount` — bigint or non-empty string coercible to bigint
16466
16878
  * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
16467
16879
  * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
@@ -16503,10 +16915,17 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16503
16915
  if (source.chain.name === dest.name) {
16504
16916
  throw createUnsupportedRouteError(source.chain.name, dest.name);
16505
16917
  }
16506
- // executor
16918
+ // Destination: GenericExecutor shorthand or explicit recipient + caller.
16507
16919
  const executor = p['executor'];
16508
- if (typeof executor !== 'string' || executor === '') {
16509
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
16920
+ const mintRecipient = p['mintRecipient'];
16921
+ const destinationCaller = p['destinationCaller'];
16922
+ const hasExecutor = typeof executor === 'string' && executor !== '';
16923
+ const hasDirectDestination = typeof mintRecipient === 'string' && mintRecipient !== '' && typeof destinationCaller === 'string' && destinationCaller !== '';
16924
+ if (!hasExecutor && !hasDirectDestination) {
16925
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor, or both mintRecipient and destinationCaller');
16926
+ }
16927
+ if (hasExecutor && hasDirectDestination) {
16928
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor or direct destination fields, not both');
16510
16929
  }
16511
16930
  // amount
16512
16931
  const rawAmount = p['amount'];
@@ -16529,7 +16948,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16529
16948
  throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
16530
16949
  }
16531
16950
  // feeToken
16532
- if (!evmAddressSchema.safeParse(p['feeToken']).success) {
16951
+ if (!evmAddressSchema$1.safeParse(p['feeToken']).success) {
16533
16952
  throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
16534
16953
  }
16535
16954
  // claim
@@ -16541,7 +16960,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16541
16960
  if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
16542
16961
  throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
16543
16962
  }
16544
- if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
16963
+ if (!evmAddressSchema$1.safeParse(claim['refundAddress']).success) {
16545
16964
  throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
16546
16965
  }
16547
16966
  // hookData (optional)
@@ -16923,63 +17342,6 @@ const mockAttestationMessage = {
16923
17342
  });
16924
17343
  }
16925
17344
 
16926
- /**
16927
- * Dispatch a bridge step event through the provider's action dispatcher.
16928
- *
16929
- * Constructs the appropriate action payload and dispatches it to any registered
16930
- * event listeners. Handles type-safe dispatching for different step types.
16931
- * When provided, traceId from the invocation context is included for end-to-end correlation.
16932
- *
16933
- * @param name - The step name (approve, burn, fetchAttestation, or mint).
16934
- * @param step - The completed bridge step containing transaction details and explorerUrl.
16935
- * @param provider - The CCTP v2 provider with action dispatcher.
16936
- * @param invocation - Optional invocation context containing traceId for correlation.
16937
- *
16938
- * @example
16939
- * ```typescript
16940
- * const step: BridgeStep = {
16941
- * name: 'burn',
16942
- * state: 'success',
16943
- * txHash: '0xabc...',
16944
- * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
16945
- * data: { ... }
16946
- * }
16947
- * dispatchStepEvent('burn', step, provider, invocationContext)
16948
- * ```
16949
- */ function dispatchStepEvent(name, step, provider, invocation) {
16950
- if (!provider.actionDispatcher) {
16951
- return;
16952
- }
16953
- // Extract traceId from invocation context if provided
16954
- const traceId = invocation?.traceId;
16955
- const actionValues = {
16956
- protocol: 'cctp',
16957
- version: 'v2',
16958
- ...traceId !== undefined && {
16959
- traceId
16960
- },
16961
- values: step
16962
- };
16963
- switch(name){
16964
- case 'approve':
16965
- case 'burn':
16966
- case 'mint':
16967
- provider.actionDispatcher.dispatch(name, {
16968
- ...actionValues,
16969
- method: name
16970
- });
16971
- break;
16972
- case 'fetchAttestation':
16973
- case 'reAttest':
16974
- provider.actionDispatcher.dispatch(name, {
16975
- ...actionValues,
16976
- method: name,
16977
- values: step
16978
- });
16979
- break;
16980
- }
16981
- }
16982
-
16983
17345
  /**
16984
17346
  * Check whether the source adapter supports EIP-5792 atomic batching and
16985
17347
  * the consumer has not explicitly opted out via `config.batchTransactions`.
@@ -17226,7 +17588,7 @@ const mockAttestationMessage = {
17226
17588
  return step;
17227
17589
  }
17228
17590
 
17229
- var version$4 = "1.11.0";
17591
+ var version$4 = "1.12.0";
17230
17592
  var pkg$4 = {
17231
17593
  version: version$4};
17232
17594
 
@@ -17932,18 +18294,18 @@ var pkg$4 = {
17932
18294
  }
17933
18295
  }
17934
18296
 
17935
- function isPlainObject(value) {
18297
+ function isPlainObject$1(value) {
17936
18298
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
17937
18299
  return false;
17938
18300
  }
17939
18301
  const prototype = Object.getPrototypeOf(value);
17940
18302
  return prototype === Object.prototype || prototype === null;
17941
18303
  }
17942
- function assertHeadersConfig(headers, field) {
18304
+ function assertHeadersConfig$1(headers, field) {
17943
18305
  if (headers === undefined) {
17944
18306
  return;
17945
18307
  }
17946
- if (!isPlainObject(headers)) {
18308
+ if (!isPlainObject$1(headers)) {
17947
18309
  throw createValidationFailedError$1(field, headers, `${field} must be a plain object with string header values when provided`);
17948
18310
  }
17949
18311
  for (const [name, value] of Object.entries(headers)){
@@ -17953,16 +18315,16 @@ function assertHeadersConfig(headers, field) {
17953
18315
  }
17954
18316
  }
17955
18317
  function assertCCTPV2Config(config) {
17956
- if (!isPlainObject(config)) {
18318
+ if (!isPlainObject$1(config)) {
17957
18319
  throw createValidationFailedError$1('config', config, 'config must be a plain object when provided');
17958
18320
  }
17959
- assertHeadersConfig(config['headers'], 'config.headers');
18321
+ assertHeadersConfig$1(config['headers'], 'config.headers');
17960
18322
  const attestation = config['attestation'];
17961
18323
  if (attestation !== undefined) {
17962
- if (!isPlainObject(attestation)) {
18324
+ if (!isPlainObject$1(attestation)) {
17963
18325
  throw createValidationFailedError$1('config.attestation', attestation, 'config.attestation must be a plain object when provided');
17964
18326
  }
17965
- assertHeadersConfig(attestation['headers'], 'config.attestation.headers');
18327
+ assertHeadersConfig$1(attestation['headers'], 'config.attestation.headers');
17966
18328
  }
17967
18329
  }
17968
18330
  /**
@@ -18037,6 +18399,34 @@ function assertCCTPV2Config(config) {
18037
18399
  this.config = config;
18038
18400
  }
18039
18401
  /**
18402
+ * Emit a bridge step event through the provider's registered action
18403
+ * dispatcher.
18404
+ *
18405
+ * Kit-level orchestration that drives the burn primitives directly instead
18406
+ * of {@link CCTPV2BridgingProvider.bridge} (for example the receive-exact
18407
+ * source-fee flow) uses this to surface the same `approve`/`burn`/`mint`
18408
+ * events as the standard bridge path. It is a no-op when no dispatcher is
18409
+ * registered.
18410
+ *
18411
+ * @param name - The step name (`approve`, `burn`, `mint`, ...).
18412
+ * @param step - The completed bridge step to broadcast.
18413
+ * @param invocation - Optional invocation context carrying a `traceId` for
18414
+ * end-to-end correlation.
18415
+ * @returns Nothing.
18416
+ *
18417
+ * @example
18418
+ * ```typescript
18419
+ * const provider = new CCTPV2BridgingProvider()
18420
+ * provider.emitBridgeStep('burn', {
18421
+ * name: 'burn',
18422
+ * state: 'success',
18423
+ * txHash: '0xabc...',
18424
+ * })
18425
+ * ```
18426
+ */ emitBridgeStep(name, step, invocation) {
18427
+ dispatchStepEvent(name, step, this, invocation);
18428
+ }
18429
+ /**
18040
18430
  * Resolves the effective polling configuration for an attestation request.
18041
18431
  *
18042
18432
  * Precedence (lowest to highest): provider `config.attestation`, then the
@@ -18862,8 +19252,11 @@ function assertCCTPV2Config(config) {
18862
19252
  // 2. Forwarder: Does the user want Circle's relayer to handle attestation/mint?
18863
19253
  const useCustomBurn = hasCustomContractSupport(source.chain, 'bridge');
18864
19254
  const useForwarder = destination.useForwarder === true;
18865
- // Build hookData once if forwarder is enabled (memoized internally)
18866
- const hookData = useForwarder ? buildForwardingHookData() : undefined;
19255
+ // Build hookData once if forwarder is enabled. EVM destinations get the
19256
+ // empty forwarding frame; Solana destinations get a frame instructing the
19257
+ // relayer to create the recipient's ATA (using the same owner that derived
19258
+ // `mintRecipient`) so the mint succeeds even for a fresh wallet.
19259
+ const hookData = useForwarder ? await getForwarderHookData(destination.chain.type, destinationAddressForMint) : undefined;
18867
19260
  if (useCustomBurn) {
18868
19261
  // Custom burn path: use bridge contract (with or without hook)
18869
19262
  const customBurnParams = {
@@ -18891,13 +19284,14 @@ function assertCCTPV2Config(config) {
18891
19284
  /**
18892
19285
  * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
18893
19286
  *
18894
- * Builds the source-chain `depositForBurnWithHookAndFees` call for the
18895
- * GenericExecutor FORWARD path: fees are collected up front on the source chain
18896
- * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
18897
- * the GenericExecutor, and the GE `hookData` is passed through unchanged.
19287
+ * Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
19288
+ * collected up front on the source chain against a signed quote. The
19289
+ * destination may use the GenericExecutor shorthand, or an explicit mint
19290
+ * recipient and destination caller for direct forwarding.
18898
19291
  *
18899
- * This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
18900
- * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
19292
+ * This is the low-level on-chain primitive behind the Unified Balance Kit
19293
+ * `fastCrossChainDeposit` and the Bridge Kit source-fee
19294
+ * (`feePayment: 'source'`) flow. The `hookData` and signed-quote
18901
19295
  * `claim` are produced elsewhere and passed in here:
18902
19296
  * - `hookData`: `buildForwardingHookDataWithPayload(version,
18903
19297
  * buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
@@ -18914,32 +19308,48 @@ function assertCCTPV2Config(config) {
18914
19308
  * approval covers both; the redundant second approval is skipped.
18915
19309
  *
18916
19310
  * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18917
- * @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
19311
+ * @param params - The burn amount, destination, hook data, signed quote, and fee.
18918
19312
  * @returns The prepared approvals, the prepared burn, and the resolved fee plan.
18919
19313
  * @throws {KitError} If the wallet context is invalid, `destinationChain` does not
18920
- * support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
18921
- * a bigint or a numeric string coercible to bigint, the hookData lacks a
18922
- * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
18923
- * context cannot be resolved.
19314
+ * support CCTP v2, the destination fields are missing, `amount` or
19315
+ * `feeTotalAmount` is not a bigint or a numeric string coercible to bigint,
19316
+ * the hook data lacks a `cctp-forward` frame (guaranteed
19317
+ * `ForwardFeeWithoutHook`), or the operation context cannot be resolved.
18924
19318
  *
18925
19319
  * @example
18926
19320
  * ```typescript
19321
+ * import {
19322
+ * CCTPV2BridgingProvider,
19323
+ * type BurnWithFeesParams,
19324
+ * } from '@circle-fin/provider-cctp-v2'
19325
+ *
19326
+ * declare const source: BurnWithFeesParams['source']
19327
+ * declare const destinationChain: BurnWithFeesParams['destinationChain']
19328
+ * declare const recipient: string
19329
+ * declare const hookData: string
19330
+ * declare const claim: BurnWithFeesParams['claim']
19331
+ *
19332
+ * const provider = new CCTPV2BridgingProvider()
18927
19333
  * const { approvals, burn } = await provider.burnWithFees({
18928
19334
  * source,
18929
- * destinationChain: Arc,
19335
+ * destinationChain,
18930
19336
  * amount: 1_000_000n,
18931
- * executor: genericExecutorAddress,
18932
- * hookData: geForwardHookData,
18933
- * claim: { signedQuote: '0x01...', refundAddress: userAddress },
18934
- * feeToken: '0x0000000000000000000000000000000000000000', // native
18935
- * feeTotalAmount: 3_500_000n,
19337
+ * mintRecipient: recipient,
19338
+ * destinationCaller: '0x0000000000000000000000000000000000000000',
19339
+ * hookData,
19340
+ * claim,
19341
+ * feeToken: source.chain.usdcAddress,
19342
+ * feeTotalAmount: 10_000n,
18936
19343
  * })
18937
19344
  * for (const approval of approvals) await approval.execute()
18938
19345
  * const txHash = await burn.execute()
18939
19346
  * ```
18940
19347
  */ async burnWithFees(params) {
18941
19348
  assertBurnWithFeesParams(params);
18942
- const { source, destinationChain, executor, hookData, claim, feeToken } = params;
19349
+ const { source, destinationChain, hookData, claim, feeToken } = params;
19350
+ const hasExecutor = 'executor' in params && params.executor !== undefined;
19351
+ const mintRecipient = hasExecutor ? params.executor : params.mintRecipient;
19352
+ const destinationCaller = hasExecutor ? params.executor : params.destinationCaller;
18943
19353
  const amount = BigInt(params.amount);
18944
19354
  const feeTotalAmount = BigInt(params.feeTotalAmount);
18945
19355
  // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
@@ -18970,13 +19380,14 @@ function assertCCTPV2Config(config) {
18970
19380
  delegate: wrapperAddress,
18971
19381
  amount: approval.amount
18972
19382
  }, context)));
18973
- // Build the burn: mintRecipient AND destinationCaller are both the executor.
19383
+ // Build the burn with either the GenericExecutor shorthand or the explicit
19384
+ // direct-forwarding recipient and caller.
18974
19385
  const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18975
19386
  fromChain: source.chain,
18976
19387
  toChain: destinationChain,
18977
19388
  amount,
18978
- mintRecipient: executor,
18979
- destinationCaller: executor,
19389
+ mintRecipient,
19390
+ destinationCaller,
18980
19391
  hookData,
18981
19392
  claim,
18982
19393
  feeToken,
@@ -19133,6 +19544,948 @@ function assertCCTPV2Config(config) {
19133
19544
  ]
19134
19545
  ];
19135
19546
 
19547
+ /**
19548
+ * Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
19549
+ */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
19550
+ /**
19551
+ * Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
19552
+ */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
19553
+ /**
19554
+ * Native fee-token sentinel (the zero address).
19555
+ *
19556
+ * When `feeToken` is the zero address the quote prices fees in the source
19557
+ * chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
19558
+ * address instead to denominate fees in USDC.
19559
+ */ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
19560
+ /**
19561
+ * API path prefix for the CCTP v2 USDC burn quote endpoint.
19562
+ *
19563
+ * The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
19564
+ * `usdc` is a fixed literal, not a token parameter.
19565
+ */ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
19566
+ /**
19567
+ * API path prefix for the CCTP v2 USDC quote validate endpoint.
19568
+ *
19569
+ * The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
19570
+ * `POST { abiSignature, args }` body and returns whether the signed quote is
19571
+ * currently claimable together with its authoritative expiry status.
19572
+ */ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
19573
+ /**
19574
+ * Default polling configuration for Quote API calls.
19575
+ *
19576
+ * A signed quote is short-lived (typically ~2 minutes, varying per chain) and
19577
+ * a feature-flag-disabled source chain returns a
19578
+ * permanent `503 SERVICE_NOT_ENABLED`, so retrying buys little and risks
19579
+ * outliving the quote. The client therefore makes a single attempt
19580
+ * (`maxRetries: 1`) with a 15s timeout, mirroring the reference
19581
+ * implementation; callers refresh by requesting a new quote rather than
19582
+ * relying on transport retries.
19583
+ *
19584
+ * No `headers` are set here: `pollApiWithValidation` always injects
19585
+ * `Content-Type: application/json` and adds `User-Agent` in Node. Browser
19586
+ * requests omit a user-agent header to avoid a CORS preflight, so duplicating
19587
+ * either header here would be dead configuration.
19588
+ */ const FEE_QUOTE_DEFAULT_CONFIG = {
19589
+ timeout: 15_000,
19590
+ maxRetries: 1,
19591
+ retryDelay: 200
19592
+ };
19593
+
19594
+ /** Decimal string in token minor units, constrained to be strictly positive. */ const positiveAmountSchema = zod.z.string().regex(/^\d+$/, 'must be a non-negative integer string')// Re-check the digit shape here: zod still runs this refinement when the
19595
+ // regex check above fails ("dirty"), so guard BigInt() against throwing on a
19596
+ // non-numeric value before comparing.
19597
+ .refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
19598
+ /**
19599
+ * A 20-byte EVM address in `0x` hex.
19600
+ *
19601
+ * The MVP prepaid-`FORWARD` `burn/usdc` path targets EVM contracts
19602
+ * (`TokenMessengerWithFees` / `GenericExecutor`), so `feeToken` and
19603
+ * `destinationCaller` are constrained to EVM addresses by design. This is an
19604
+ * intentional scope limit, not a permanent one: it can be widened to other
19605
+ * address formats as the fee service expands to more chains.
19606
+ */ const evmAddressSchema = zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'must be a 20-byte 0x address');
19607
+ /** Even-length `0x` hex (the empty `0x` is allowed). */ const hexSchema = zod.z.string().regex(/^0x([a-fA-F0-9]{2})*$/, 'must be even-length 0x hex');
19608
+ /** Non-empty, even-length `0x` hex. */ const nonEmptyHexSchema = zod.z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex');
19609
+ /** A 32-byte `0x` hex hash. */ const bytes32Schema = zod.z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'must be a 32-byte 0x hash');
19610
+ /** Decimal string in minor units, allowing zero. */ const numericStringSchema$1 = zod.z.string().regex(/^\d+$/, 'must be a non-negative integer string');
19611
+ /** An `https:` URL, used for the optional base-URL override. */ const httpsUrlSchema = zod.z.string().refine((value)=>{
19612
+ try {
19613
+ return new URL(value).protocol === 'https:';
19614
+ } catch {
19615
+ return false;
19616
+ }
19617
+ }, 'must be an https URL');
19618
+ const forwardParamsSchema = zod.z.object({
19619
+ hookData: hexSchema.optional(),
19620
+ destinationCaller: evmAddressSchema.optional()
19621
+ }).strict();
19622
+ const forwardRequestSchema = zod.z.object({
19623
+ type: zod.z.literal('FORWARD'),
19624
+ params: forwardParamsSchema.optional()
19625
+ }).strict();
19626
+ const preFinalityRequestSchema = zod.z.object({
19627
+ type: zod.z.literal('PRE_FINALITY')
19628
+ }).strict();
19629
+ /** A single quote request item (`FORWARD` or `PRE_FINALITY`). */ const feeQuoteRequestSchema = zod.z.discriminatedUnion('type', [
19630
+ forwardRequestSchema,
19631
+ preFinalityRequestSchema
19632
+ ]);
19633
+ /** A non-empty list of quote request items with unique types. */ const feeQuoteRequestsSchema = zod.z.array(feeQuoteRequestSchema).min(1, 'at least one request item is required').refine((items)=>new Set(items.map((item)=>item.type)).size === items.length, 'request item types must be unique');
19634
+ /**
19635
+ * A structured `Partial<ApiPollingConfig>` polling override.
19636
+ *
19637
+ * Validates the field types callers actually set, so a plain-JS caller passing
19638
+ * `{ timeout: 'soon' }` is rejected at the boundary rather than failing opaquely
19639
+ * inside the transport. Unknown keys pass through so a future `ApiPollingConfig`
19640
+ * field is forwarded rather than silently dropped.
19641
+ */ const apiPollingConfigSchema = zod.z.object({
19642
+ timeout: zod.z.number().int().positive().optional(),
19643
+ maxRetries: zod.z.number().int().nonnegative().optional(),
19644
+ retryDelay: zod.z.number().int().nonnegative().optional(),
19645
+ backoff: zod.z.enum([
19646
+ 'fixed',
19647
+ 'exponential'
19648
+ ]).optional(),
19649
+ maxRetryDelayMs: zod.z.number().int().positive().optional(),
19650
+ headers: zod.z.record(zod.z.string()).optional()
19651
+ }).passthrough();
19652
+ /**
19653
+ * The validatable input for {@link fetchFeeQuote}.
19654
+ *
19655
+ * This is the single source of truth for input validation, including the CCTP
19656
+ * domains and the `isTestnet` environment flag. Validating `isTestnet` at
19657
+ * runtime matters because a plain-JS caller who omits it would otherwise leave
19658
+ * it `undefined`, which is falsy and silently selects the production base URL.
19659
+ * (`buildFeeQuoteUrl` independently re-validates the domains for standalone
19660
+ * callers.)
19661
+ */ const fetchFeeQuoteInputSchema = zod.z.object({
19662
+ sourceDomain: zod.z.number().int().nonnegative(),
19663
+ destinationDomain: zod.z.number().int().nonnegative(),
19664
+ amount: positiveAmountSchema,
19665
+ feeToken: evmAddressSchema.optional(),
19666
+ requests: feeQuoteRequestsSchema,
19667
+ isTestnet: zod.z.boolean(),
19668
+ baseUrl: httpsUrlSchema.optional(),
19669
+ config: apiPollingConfigSchema.optional()
19670
+ }).strict();
19671
+ const feeQuoteItemSchema = zod.z.object({
19672
+ type: zod.z.string().min(1),
19673
+ amount: numericStringSchema$1,
19674
+ args: zod.z.array(zod.z.string()),
19675
+ argsHash: bytes32Schema
19676
+ }).passthrough();
19677
+ const exchangeRatesSchema = zod.z.object({
19678
+ feeTokenUsd: zod.z.string(),
19679
+ destinationTokenUsd: zod.z.string()
19680
+ }).passthrough();
19681
+ const metadataSchema = zod.z.object({
19682
+ destinationGasPrice: zod.z.string().optional(),
19683
+ exchangeRates: exchangeRatesSchema.optional()
19684
+ }).passthrough();
19685
+ /** The `expiry` object the Quote API nests the quote TTL under. */ const expirySchema = zod.z.discriminatedUnion('mode', [
19686
+ zod.z.object({
19687
+ mode: zod.z.literal('TIMESTAMP'),
19688
+ expiresAt: zod.z.number().int().nonnegative()
19689
+ }).passthrough(),
19690
+ zod.z.object({
19691
+ mode: zod.z.literal('BLOCK_NUMBER'),
19692
+ expiresAtBlock: zod.z.number().int().nonnegative(),
19693
+ blockEstimatedAt: zod.z.number().int().nonnegative().optional()
19694
+ }).passthrough()
19695
+ ]);
19696
+ /** Schema for a signed fee quote returned by the Quote API. */ const signedFeeQuoteSchema = zod.z.object({
19697
+ // The runtime YAML spec maps signedQuote to a looser `hex` (which allows
19698
+ // an empty `0x`); we keep the stricter non-empty form. Do not relax
19699
+ // without a reason.
19700
+ signedQuote: nonEmptyHexSchema,
19701
+ issuedAt: zod.z.number().int().nonnegative(),
19702
+ // The API returns a mode-specific timestamp or source-block deadline.
19703
+ expiry: expirySchema,
19704
+ feeTotalAmount: numericStringSchema$1,
19705
+ feeToken: evmAddressSchema,
19706
+ nonce: numericStringSchema$1,
19707
+ items: zod.z.array(feeQuoteItemSchema),
19708
+ metadata: metadataSchema.optional()
19709
+ }).passthrough();
19710
+ /**
19711
+ * Validate that an unknown value is a signed fee quote.
19712
+ *
19713
+ * @param value - The unknown value to validate.
19714
+ * @returns `true` when the value matches the signed-quote response shape.
19715
+ *
19716
+ * @example
19717
+ * ```typescript
19718
+ * import { isSignedFeeQuote } from '@circle-fin/provider-fee-v1'
19719
+ *
19720
+ * declare const payload: unknown
19721
+ * if (isSignedFeeQuote(payload)) {
19722
+ * console.log(payload.feeTotalAmount)
19723
+ * }
19724
+ * ```
19725
+ */ function isSignedFeeQuote(value) {
19726
+ return signedFeeQuoteSchema.safeParse(value).success;
19727
+ }
19728
+ /** Validate input to {@link validateQuote}. */ const validateQuoteInputSchema = zod.z.object({
19729
+ sourceDomain: zod.z.number().int().nonnegative(),
19730
+ abiSignature: zod.z.string().min(1),
19731
+ args: zod.z.array(zod.z.union([
19732
+ zod.z.string(),
19733
+ zod.z.array(zod.z.string())
19734
+ ])),
19735
+ isTestnet: zod.z.boolean(),
19736
+ baseUrl: httpsUrlSchema.optional(),
19737
+ config: apiPollingConfigSchema.optional()
19738
+ }).strict();
19739
+ const quoteExpiryStatusSchema = zod.z.discriminatedUnion('mode', [
19740
+ zod.z.object({
19741
+ mode: zod.z.literal('TIMESTAMP'),
19742
+ expired: zod.z.boolean(),
19743
+ secondsRemaining: zod.z.number().int().nonnegative(),
19744
+ expiresAt: zod.z.number().int().nonnegative()
19745
+ }).passthrough(),
19746
+ zod.z.object({
19747
+ mode: zod.z.literal('BLOCK_NUMBER'),
19748
+ expired: zod.z.boolean(),
19749
+ secondsRemaining: zod.z.number().int().nonnegative(),
19750
+ expiresAtBlock: zod.z.number().int().nonnegative(),
19751
+ blockEstimatedAt: zod.z.number().int().nonnegative().optional()
19752
+ }).passthrough()
19753
+ ]);
19754
+ const validateQuoteItemSchema = zod.z.object({
19755
+ type: zod.z.string().min(1),
19756
+ argsMatch: zod.z.boolean(),
19757
+ amount: numericStringSchema$1.optional(),
19758
+ args: zod.z.array(zod.z.string()).optional(),
19759
+ argsHash: bytes32Schema.optional(),
19760
+ computedArgsHash: bytes32Schema.optional()
19761
+ }).passthrough();
19762
+ /**
19763
+ * Schema for a response from the Quote API validation endpoint.
19764
+ *
19765
+ * The endpoint takes the source domain as a URL path parameter and does not
19766
+ * return it in the response body, so `sourceDomain` is intentionally not part
19767
+ * of this schema.
19768
+ */ const validateQuoteResultSchema = zod.z.object({
19769
+ signedQuote: nonEmptyHexSchema,
19770
+ expiry: quoteExpiryStatusSchema,
19771
+ feeTotalAmount: numericStringSchema$1,
19772
+ feeToken: evmAddressSchema,
19773
+ nonce: numericStringSchema$1,
19774
+ claimable: zod.z.boolean(),
19775
+ // Preserve newly introduced server-side reasons as opaque strings rather
19776
+ // than rejecting the entire safety response before the SDK is updated.
19777
+ failedChecks: zod.z.array(zod.z.string().min(1)),
19778
+ items: zod.z.array(validateQuoteItemSchema)
19779
+ }).passthrough();
19780
+ /**
19781
+ * Validate a Quote API validation response.
19782
+ *
19783
+ * @param value - The unknown response value.
19784
+ * @returns `true` when the value has the expected validation response shape.
19785
+ *
19786
+ * @example
19787
+ * ```typescript
19788
+ * import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
19789
+ *
19790
+ * declare const response: unknown
19791
+ * if (isValidateQuoteResult(response)) {
19792
+ * console.log(response.claimable)
19793
+ * }
19794
+ * ```
19795
+ */ function isValidateQuoteResult(value) {
19796
+ return validateQuoteResultSchema.safeParse(value).success;
19797
+ }
19798
+
19799
+ /**
19800
+ * Validate that a CCTP domain id is a non-negative integer.
19801
+ *
19802
+ * @param value - The domain id to validate.
19803
+ * @param label - The parameter name, used in the error message.
19804
+ * @returns Nothing.
19805
+ * @throws {@link KitError} When the value is not a non-negative integer.
19806
+ * @internal
19807
+ */ function assertDomain(value, label) {
19808
+ if (!Number.isInteger(value) || value < 0) {
19809
+ throw new KitError({
19810
+ ...InputError.VALIDATION_FAILED,
19811
+ recoverability: 'FATAL',
19812
+ message: `Quote API getFeeQuote failed: ${label} must be a ` + `non-negative integer, received ${String(value)}`,
19813
+ cause: {
19814
+ trace: {
19815
+ [label]: value
19816
+ }
19817
+ }
19818
+ });
19819
+ }
19820
+ }
19821
+ /**
19822
+ * Build the Quote API URL for a CCTP v2 USDC burn quote.
19823
+ *
19824
+ * Resolves the environment base URL (or an explicit `baseUrl` override) and
19825
+ * appends the burn/usdc path with the source and destination CCTP domains.
19826
+ * `usdc` is a fixed path literal, not a token parameter.
19827
+ *
19828
+ * @param params - The domains and environment selector.
19829
+ * @returns The fully-qualified Quote API URL.
19830
+ * @throws {@link KitError} When either domain is not a non-negative integer.
19831
+ *
19832
+ * @example
19833
+ * ```typescript
19834
+ * import { buildFeeQuoteUrl } from '@circle-fin/provider-fee-v1'
19835
+ *
19836
+ * const url = buildFeeQuoteUrl({
19837
+ * sourceDomain: 3,
19838
+ * destinationDomain: 26,
19839
+ * isTestnet: false,
19840
+ * })
19841
+ * // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
19842
+ * ```
19843
+ */ function buildFeeQuoteUrl(params) {
19844
+ const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
19845
+ assertDomain(sourceDomain, 'sourceDomain');
19846
+ assertDomain(destinationDomain, 'destinationDomain');
19847
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
19848
+ return new URL(`${QUOTE_BURN_USDC_PATH}/${String(sourceDomain)}/${String(destinationDomain)}`, resolvedBaseUrl).toString();
19849
+ }
19850
+
19851
+ /**
19852
+ * Assert that a quote's per-item fee amounts sum to its `feeTotalAmount`.
19853
+ *
19854
+ * A defensive integrity check on the Quote API response, enforced internally
19855
+ * by `fetchFeeQuote`. It does not compare against the destination-side
19856
+ * `feeExecuted`, which is expected to be zero for prepaid forward-only burns.
19857
+ *
19858
+ * @param quote - The signed fee quote to check.
19859
+ * @returns Nothing.
19860
+ * @throws {@link KitError} When the item amounts do not sum to `feeTotalAmount`.
19861
+ * @internal
19862
+ */ function assertFeeItemsSumToTotal(quote) {
19863
+ const itemsTotal = quote.items.reduce((sum, item)=>sum + BigInt(item.amount), 0n);
19864
+ const declaredTotal = BigInt(quote.feeTotalAmount);
19865
+ if (itemsTotal !== declaredTotal) {
19866
+ throw new KitError({
19867
+ ...InputError.VALIDATION_FAILED,
19868
+ recoverability: 'FATAL',
19869
+ message: `Quote API getFeeQuote failed: fee items sum ` + `(${itemsTotal.toString()}) does not equal feeTotalAmount ` + `(${declaredTotal.toString()})`,
19870
+ cause: {
19871
+ trace: {
19872
+ itemsTotal: itemsTotal.toString(),
19873
+ feeTotalAmount: quote.feeTotalAmount
19874
+ }
19875
+ }
19876
+ });
19877
+ }
19878
+ }
19879
+
19880
+ /**
19881
+ * Determine whether a Quote API error represents a disabled source chain.
19882
+ *
19883
+ * @param error - The error thrown by the HTTP layer.
19884
+ * @returns `true` when the error contains the `SERVICE_NOT_ENABLED` marker.
19885
+ * @internal
19886
+ */ function isServiceNotEnabled(error) {
19887
+ const body = typeof error === 'object' && error !== null && 'responseBody' in error ? error.responseBody : undefined;
19888
+ if (typeof body === 'object' && body !== null) {
19889
+ const fields = body;
19890
+ const candidates = [
19891
+ fields['errorCode'],
19892
+ fields['code'],
19893
+ fields['message'],
19894
+ fields['externalMessage'],
19895
+ fields['error']
19896
+ ];
19897
+ if (candidates.some((value)=>typeof value === 'string' && value.toUpperCase().includes('SERVICE_NOT_ENABLED'))) {
19898
+ return true;
19899
+ }
19900
+ }
19901
+ return getErrorMessage(error).toUpperCase().includes('SERVICE_NOT_ENABLED');
19902
+ }
19903
+
19904
+ const SERVICE$1 = 'Quote API';
19905
+ const OPERATION$1 = 'getFeeQuote';
19906
+ /**
19907
+ * Serialize request items for the wire body.
19908
+ *
19909
+ * `PRE_FINALITY` is emitted with no `params` key, and a `FORWARD` item only
19910
+ * carries the binding fields that are present.
19911
+ *
19912
+ * @param requests - The request items to serialize.
19913
+ * @returns The serialized request items.
19914
+ * @internal
19915
+ */ function serializeRequests(requests) {
19916
+ return requests.map((request)=>{
19917
+ if (request.type === 'PRE_FINALITY') {
19918
+ return {
19919
+ type: 'PRE_FINALITY'
19920
+ };
19921
+ }
19922
+ const params = request.params;
19923
+ if (params === undefined) {
19924
+ return {
19925
+ type: 'FORWARD'
19926
+ };
19927
+ }
19928
+ const forwardParams = {};
19929
+ if (params.hookData !== undefined) {
19930
+ forwardParams.hookData = params.hookData;
19931
+ }
19932
+ if (params.destinationCaller !== undefined) {
19933
+ forwardParams.destinationCaller = params.destinationCaller;
19934
+ }
19935
+ return {
19936
+ type: 'FORWARD',
19937
+ params: forwardParams
19938
+ };
19939
+ });
19940
+ }
19941
+ /**
19942
+ * Fetch a signed fee quote from Circle's Quote API for a CCTP v2 USDC burn.
19943
+ *
19944
+ * Validates inputs, POSTs to
19945
+ * `/v2/quote/burn/usdc/{sourceDomain}/{destinationDomain}` with a single
19946
+ * attempt (the signed quote is short-lived), and returns the typed quote. A
19947
+ * disabled source chain (`503 SERVICE_NOT_ENABLED`) surfaces as a fatal,
19948
+ * non-retryable error; other failures are mapped to a {@link KitError} via the
19949
+ * shared API error parser.
19950
+ *
19951
+ * @param params - The domains, amount, request items, and environment.
19952
+ * @returns The signed fee quote.
19953
+ * @throws {@link KitError} On invalid input, a disabled source chain, an HTTP
19954
+ * error, or an invalid response shape.
19955
+ *
19956
+ * @example
19957
+ * ```typescript
19958
+ * import { fetchFeeQuote } from '@circle-fin/provider-fee-v1'
19959
+ *
19960
+ * const quote = await fetchFeeQuote({
19961
+ * sourceDomain: 3,
19962
+ * destinationDomain: 26,
19963
+ * amount: '1000000',
19964
+ * requests: [{ type: 'FORWARD' }, { type: 'PRE_FINALITY' }],
19965
+ * isTestnet: false,
19966
+ * })
19967
+ * console.log(quote.feeTotalAmount, quote.expiry)
19968
+ * ```
19969
+ */ async function fetchFeeQuote(params) {
19970
+ const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
19971
+ const parsed = fetchFeeQuoteInputSchema.safeParse({
19972
+ sourceDomain,
19973
+ destinationDomain,
19974
+ amount,
19975
+ feeToken,
19976
+ requests,
19977
+ isTestnet,
19978
+ baseUrl,
19979
+ config
19980
+ });
19981
+ if (!parsed.success) {
19982
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
19983
+ throw new KitError({
19984
+ ...InputError.VALIDATION_FAILED,
19985
+ recoverability: 'FATAL',
19986
+ message: `${SERVICE$1} ${OPERATION$1} failed: ${detail}`,
19987
+ cause: {
19988
+ trace: parsed.error.issues
19989
+ }
19990
+ });
19991
+ }
19992
+ const url = baseUrl === undefined ? buildFeeQuoteUrl({
19993
+ sourceDomain,
19994
+ destinationDomain,
19995
+ isTestnet
19996
+ }) : buildFeeQuoteUrl({
19997
+ sourceDomain,
19998
+ destinationDomain,
19999
+ isTestnet,
20000
+ baseUrl
20001
+ });
20002
+ const body = {
20003
+ amount,
20004
+ feeToken: feeToken ?? NATIVE_FEE_TOKEN,
20005
+ requests: serializeRequests(requests)
20006
+ };
20007
+ const pollingConfig = {
20008
+ ...FEE_QUOTE_DEFAULT_CONFIG,
20009
+ ...config
20010
+ };
20011
+ let quote;
20012
+ try {
20013
+ quote = await pollApiPost(url, body, isSignedFeeQuote, pollingConfig);
20014
+ } catch (error) {
20015
+ // Only one service-specific code (SERVICE_NOT_ENABLED) needs bespoke
20016
+ // mapping, so it is detected inline rather than via a dedicated
20017
+ // `parseFeeQuoteApiError` parser; everything else flows through the shared
20018
+ // `parseApiError`. Promote to a parser if more coded errors appear.
20019
+ if (isServiceNotEnabled(error)) {
20020
+ throw new KitError({
20021
+ ...InputError.UNSUPPORTED_ROUTE,
20022
+ recoverability: 'FATAL',
20023
+ message: `${SERVICE$1} ${OPERATION$1} failed: source chain not enabled for fee ` + `quotes (SERVICE_NOT_ENABLED)`,
20024
+ cause: {
20025
+ trace: error
20026
+ }
20027
+ });
20028
+ }
20029
+ throw parseApiError(error, {
20030
+ service: SERVICE$1,
20031
+ operation: OPERATION$1
20032
+ });
20033
+ }
20034
+ // Defense-in-depth: a self-consistent quote's per-item fees sum to the
20035
+ // declared total. Enforced here so callers cannot forget the check.
20036
+ assertFeeItemsSumToTotal(quote);
20037
+ return quote;
20038
+ }
20039
+
20040
+ const SERVICE = 'Quote API';
20041
+ const OPERATION = 'validateQuote';
20042
+ /**
20043
+ * Validate a signed quote against a complete source-chain contract call.
20044
+ *
20045
+ * @param params - The source domain, ABI signature, call arguments, and environment.
20046
+ * @returns The claimability, binding checks, and authoritative expiry status.
20047
+ * @throws {@link KitError} When input, transport, or response validation fails.
20048
+ *
20049
+ * @example
20050
+ * ```typescript
20051
+ * import { validateQuote } from '@circle-fin/provider-fee-v1'
20052
+ *
20053
+ * const result = await validateQuote({
20054
+ * sourceDomain: 3,
20055
+ * // The exact function + arguments, in ABI order, that will be burned on-chain.
20056
+ * abiSignature:
20057
+ * 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))',
20058
+ * args: [
20059
+ * '1000000',
20060
+ * '26',
20061
+ * '0x0000000000000000000000001111111111111111111111111111111111111111',
20062
+ * '0x2222222222222222222222222222222222222222',
20063
+ * '0x0000000000000000000000000000000000000000000000000000000000000000',
20064
+ * '0x636374702d666f72776172640000000000000000000000000000000000000000',
20065
+ * ['0x01abcd', '0x3333333333333333333333333333333333333333'],
20066
+ * ],
20067
+ * isTestnet: false,
20068
+ * })
20069
+ * console.log(result.claimable, result.expiry.secondsRemaining)
20070
+ * ```
20071
+ */ async function validateQuote(params) {
20072
+ const parsed = validateQuoteInputSchema.safeParse(params);
20073
+ if (!parsed.success) {
20074
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
20075
+ throw new KitError({
20076
+ ...InputError.VALIDATION_FAILED,
20077
+ recoverability: 'FATAL',
20078
+ message: `${SERVICE} ${OPERATION} failed: ${detail}`,
20079
+ cause: {
20080
+ trace: parsed.error.issues
20081
+ }
20082
+ });
20083
+ }
20084
+ const { sourceDomain, abiSignature, args, isTestnet, baseUrl } = parsed.data;
20085
+ // `config` was validated by the schema above; spread the caller's original,
20086
+ // precisely typed `Partial<ApiPollingConfig>` so the merged polling config
20087
+ // stays assignable under `exactOptionalPropertyTypes`.
20088
+ const pollingConfig = {
20089
+ ...FEE_QUOTE_DEFAULT_CONFIG,
20090
+ ...params.config
20091
+ };
20092
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
20093
+ const url = new URL(`${QUOTE_VALIDATE_USDC_PATH}/${String(sourceDomain)}`, resolvedBaseUrl).toString();
20094
+ try {
20095
+ return await pollApiPost(url, {
20096
+ abiSignature,
20097
+ args
20098
+ }, isValidateQuoteResult, pollingConfig);
20099
+ } catch (error) {
20100
+ if (isServiceNotEnabled(error)) {
20101
+ throw new KitError({
20102
+ ...InputError.UNSUPPORTED_ROUTE,
20103
+ recoverability: 'FATAL',
20104
+ message: `${SERVICE} ${OPERATION} failed: source chain not enabled for ` + `quote validation (SERVICE_NOT_ENABLED)`,
20105
+ cause: {
20106
+ trace: error
20107
+ }
20108
+ });
20109
+ }
20110
+ throw parseApiError(error, {
20111
+ service: SERVICE,
20112
+ operation: OPERATION
20113
+ });
20114
+ }
20115
+ }
20116
+
20117
+ /** Refresh timestamp quotes this many seconds before submission. */ const QUOTE_EXPIRY_SAFETY_SECONDS = 30;
20118
+ /** ABI signature validated by the Quote API before source-chain submission. */ const BURN_WITH_FEES_ABI_SIGNATURE = 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))';
20119
+ /** Unrestricted CCTP destination caller used by the forwarding relayer. */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
20120
+ function isQuoteNearEstimatedExpiry(quote) {
20121
+ const currentSeconds = Math.floor(Date.now() / 1_000);
20122
+ const expiresAt = quote.expiry.mode === 'TIMESTAMP' ? quote.expiry.expiresAt : quote.expiry.blockEstimatedAt;
20123
+ // A BLOCK_NUMBER quote may omit the advisory `blockEstimatedAt` estimate; when
20124
+ // it is absent, skip this wall-clock pre-check and defer to the authoritative
20125
+ // source-chain-tip validation performed downstream.
20126
+ if (expiresAt === undefined) {
20127
+ return false;
20128
+ }
20129
+ return expiresAt <= currentSeconds + QUOTE_EXPIRY_SAFETY_SECONDS;
20130
+ }
20131
+ function assertSourceFeeRoute(params) {
20132
+ const { source, destination, config } = params;
20133
+ if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
20134
+ throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
20135
+ }
20136
+ const useForwarder = destination.useForwarder;
20137
+ if (useForwarder !== true) {
20138
+ throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
20139
+ }
20140
+ if (config.customFee !== undefined) {
20141
+ throw createValidationFailedError$1('config.customFee', config.customFee, "Custom fees are not supported with feePayment: 'source'. Remove the " + 'per-call customFee or the kit-level custom fee policy for this route.');
20142
+ }
20143
+ }
20144
+ function buildQuoteBinding(params) {
20145
+ assertSourceFeeRoute(params);
20146
+ const mintRecipient = params.destination.recipientAddress ?? params.destination.address;
20147
+ const hookData = buildForwardingHookData();
20148
+ const requests = [
20149
+ {
20150
+ type: 'FORWARD',
20151
+ params: {
20152
+ hookData,
20153
+ destinationCaller: ZERO_ADDRESS$1
20154
+ }
20155
+ }
20156
+ ];
20157
+ if ((params.config.transferSpeed ?? exports.TransferSpeed.FAST) === exports.TransferSpeed.FAST) {
20158
+ requests.push({
20159
+ type: 'PRE_FINALITY'
20160
+ });
20161
+ }
20162
+ return {
20163
+ sourceDomain: params.source.chain.cctp.domain,
20164
+ destinationDomain: params.destination.chain.cctp.domain,
20165
+ isTestnet: params.source.chain.isTestnet,
20166
+ amount: params.amount,
20167
+ mintRecipient,
20168
+ hookData,
20169
+ destinationCaller: ZERO_ADDRESS$1,
20170
+ feeToken: params.source.chain.usdcAddress,
20171
+ requests
20172
+ };
20173
+ }
20174
+ async function fetchBoundQuote(binding) {
20175
+ try {
20176
+ const quote = await fetchFeeQuote({
20177
+ sourceDomain: binding.sourceDomain,
20178
+ destinationDomain: binding.destinationDomain,
20179
+ amount: binding.amount,
20180
+ feeToken: binding.feeToken,
20181
+ requests: binding.requests,
20182
+ isTestnet: binding.isTestnet
20183
+ });
20184
+ if (quote.feeToken.toLowerCase() !== binding.feeToken.toLowerCase()) {
20185
+ throw createValidationFailedError$1('feeToken', quote.feeToken, 'Fee Service must return source-chain USDC for source-fee bridging');
20186
+ }
20187
+ return quote;
20188
+ } catch (error) {
20189
+ if (isRateLimitError(error)) {
20190
+ throw new KitError({
20191
+ ...RateLimitError.RATE_LIMIT_EXCEEDED,
20192
+ recoverability: 'RETRYABLE',
20193
+ message: 'Fee Service rate limit exceeded. Retry with caller-managed exponential backoff; Bridge Kit does not retry signed quote requests automatically.',
20194
+ cause: {
20195
+ trace: error
20196
+ }
20197
+ });
20198
+ }
20199
+ throw error;
20200
+ }
20201
+ }
20202
+ async function fetchSubmissionQuote(binding) {
20203
+ let quote = await fetchBoundQuote(binding);
20204
+ if (isQuoteNearEstimatedExpiry(quote)) {
20205
+ quote = await fetchBoundQuote(binding);
20206
+ }
20207
+ if (isQuoteNearEstimatedExpiry(quote)) {
20208
+ throw createValidationFailedError$1('quote', undefined, 'Fee Service returned a quote too close to expiry for safe submission');
20209
+ }
20210
+ return quote;
20211
+ }
20212
+ async function validateBoundQuote(binding, signedQuote, refundAddress) {
20213
+ return validateQuote({
20214
+ sourceDomain: binding.sourceDomain,
20215
+ abiSignature: BURN_WITH_FEES_ABI_SIGNATURE,
20216
+ args: [
20217
+ binding.amount,
20218
+ String(binding.destinationDomain),
20219
+ padAddressToBytes32(binding.mintRecipient),
20220
+ binding.feeToken,
20221
+ padAddressToBytes32(binding.destinationCaller),
20222
+ binding.hookData,
20223
+ [
20224
+ signedQuote,
20225
+ refundAddress
20226
+ ]
20227
+ ],
20228
+ isTestnet: binding.isTestnet
20229
+ });
20230
+ }
20231
+ function isValidationSafe(binding, signedQuote, validation, expectedQuote) {
20232
+ return validation.claimable && !validation.expiry.expired && validation.expiry.secondsRemaining > QUOTE_EXPIRY_SAFETY_SECONDS && validation.signedQuote.toLowerCase() === signedQuote.toLowerCase() && validation.feeToken.toLowerCase() === binding.feeToken.toLowerCase() && (expectedQuote === undefined || validation.feeTotalAmount === expectedQuote.feeTotalAmount && validation.nonce === expectedQuote.nonce);
20233
+ }
20234
+ function unsafeQuoteError(validation) {
20235
+ const detail = validation.failedChecks.length > 0 ? ` (${validation.failedChecks.join(', ')})` : '';
20236
+ return createValidationFailedError$1('quote', undefined, `The fee quote is not safe for submission${detail}; ` + 'call estimate again to obtain a valid quote');
20237
+ }
20238
+ function toExecutionFeeQuote(quote) {
20239
+ return {
20240
+ signedQuote: quote.signedQuote,
20241
+ feeToken: quote.feeToken,
20242
+ feeTotalAmount: quote.feeTotalAmount
20243
+ };
20244
+ }
20245
+ function toFeeItems(quote) {
20246
+ return quote.items.map((item)=>({
20247
+ type: item.type,
20248
+ amount: formatUnits(item.amount, 6),
20249
+ args: item.args,
20250
+ argsHash: item.argsHash
20251
+ }));
20252
+ }
20253
+ /**
20254
+ * Estimate a source-fee bridge using a source-denominated signed fee quote.
20255
+ *
20256
+ * @internal
20257
+ */ async function estimateSourceFeeBridge(params) {
20258
+ const binding = buildQuoteBinding(params);
20259
+ const quote = await fetchSubmissionQuote(binding);
20260
+ const feeTotal = formatUnits(quote.feeTotalAmount, 6);
20261
+ return {
20262
+ token: 'USDC',
20263
+ amount: formatUnits(params.amount, 6),
20264
+ source: {
20265
+ address: params.source.address,
20266
+ chain: params.source.chain.chain
20267
+ },
20268
+ destination: {
20269
+ address: params.destination.address,
20270
+ chain: params.destination.chain.chain,
20271
+ ...params.destination.recipientAddress !== undefined && {
20272
+ recipientAddress: params.destination.recipientAddress
20273
+ }
20274
+ },
20275
+ gasFees: [],
20276
+ fees: quote.items.map((item)=>({
20277
+ type: item.type === 'FORWARD' ? 'forwarder' : 'provider',
20278
+ token: 'USDC',
20279
+ amount: formatUnits(item.amount, 6)
20280
+ })),
20281
+ amountReceived: formatUnits(params.amount, 6),
20282
+ feeTotal,
20283
+ feeItems: toFeeItems(quote),
20284
+ totalDebit: formatUnits((BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString(), 6),
20285
+ quoteExpiry: quote.expiry,
20286
+ quote: quote.signedQuote
20287
+ };
20288
+ }
20289
+ async function readAllowance(params, delegate) {
20290
+ const operationContext = {
20291
+ chain: params.source.chain,
20292
+ address: params.source.address
20293
+ };
20294
+ const prepared = await params.source.adapter.prepareAction('usdc.allowance', {
20295
+ walletAddress: params.source.address,
20296
+ delegate
20297
+ }, operationContext);
20298
+ return BigInt(String(await prepared.execute()));
20299
+ }
20300
+ async function executeAndConfirm(request, params, provider) {
20301
+ const txHash = await request.execute();
20302
+ const data = await provider.waitForTransaction(params.source.adapter, txHash, params.source.chain);
20303
+ return {
20304
+ txHash,
20305
+ data
20306
+ };
20307
+ }
20308
+ async function prepareAndPreflight(params, binding, quote, provider) {
20309
+ assertSourceFeeRoute(params);
20310
+ const totalDebit = (BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString();
20311
+ const operationContext = {
20312
+ chain: params.source.chain,
20313
+ address: params.source.address
20314
+ };
20315
+ await validateBalanceForTransaction({
20316
+ adapter: params.source.adapter,
20317
+ amount: totalDebit,
20318
+ token: 'USDC',
20319
+ tokenAddress: params.source.chain.usdcAddress,
20320
+ operationContext
20321
+ });
20322
+ const prepared = await provider.burnWithFees({
20323
+ source: params.source,
20324
+ destinationChain: params.destination.chain,
20325
+ amount: params.amount,
20326
+ mintRecipient: binding.mintRecipient,
20327
+ destinationCaller: binding.destinationCaller,
20328
+ hookData: binding.hookData,
20329
+ claim: {
20330
+ signedQuote: quote.signedQuote,
20331
+ refundAddress: params.source.address
20332
+ },
20333
+ feeToken: quote.feeToken,
20334
+ feeTotalAmount: quote.feeTotalAmount
20335
+ });
20336
+ const wrapper = resolveCCTPV2ContractAddress(params.source.chain, 'tokenMessengerWithFees');
20337
+ const allowance = await readAllowance(params, wrapper);
20338
+ if (allowance >= BigInt(totalDebit)) {
20339
+ return {
20340
+ prepared,
20341
+ approveStep: {
20342
+ name: 'approve',
20343
+ state: 'noop'
20344
+ }
20345
+ };
20346
+ }
20347
+ let lastApproval;
20348
+ for (const approval of prepared.approvals){
20349
+ lastApproval = await executeAndConfirm(approval, params, provider);
20350
+ }
20351
+ return {
20352
+ prepared,
20353
+ approveStep: {
20354
+ name: 'approve',
20355
+ state: 'success',
20356
+ data: lastApproval?.data,
20357
+ ...lastApproval?.txHash !== undefined && {
20358
+ txHash: lastApproval.txHash,
20359
+ explorerUrl: buildExplorerUrl(params.source.chain, lastApproval.txHash)
20360
+ }
20361
+ }
20362
+ };
20363
+ }
20364
+ async function resolveExecutionQuote(binding, suppliedQuote, refundAddress) {
20365
+ if (suppliedQuote === undefined) {
20366
+ const fetchedQuote = await fetchSubmissionQuote(binding);
20367
+ return {
20368
+ quote: toExecutionFeeQuote(fetchedQuote),
20369
+ fetchedQuote
20370
+ };
20371
+ }
20372
+ const validation = await validateBoundQuote(binding, suppliedQuote, refundAddress);
20373
+ if (!isValidationSafe(binding, suppliedQuote, validation)) {
20374
+ throw unsafeQuoteError(validation);
20375
+ }
20376
+ return {
20377
+ quote: toExecutionFeeQuote(validation),
20378
+ fetchedQuote: undefined
20379
+ };
20380
+ }
20381
+ /**
20382
+ * Execute a source-fee bridge while preserving receive-exact semantics.
20383
+ *
20384
+ * @internal
20385
+ */ async function executeSourceFeeBridge(rawParams, params, provider) {
20386
+ // Narrows `params` to the CCTP v2 route type for the rest of this function.
20387
+ // buildQuoteBinding asserts too, but that narrows its own scope, not this one.
20388
+ assertSourceFeeRoute(params);
20389
+ const binding = buildQuoteBinding(params);
20390
+ const suppliedQuote = rawParams.quote;
20391
+ let { quote, fetchedQuote } = await resolveExecutionQuote(binding, suppliedQuote, params.source.address);
20392
+ let { prepared, approveStep } = await prepareAndPreflight(params, binding, quote, provider);
20393
+ // Validate against the current source-chain tip after approval confirmation.
20394
+ // BLOCK_NUMBER expiries cannot be checked safely with wall-clock time alone.
20395
+ let validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
20396
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
20397
+ if (suppliedQuote === undefined) {
20398
+ fetchedQuote = await fetchSubmissionQuote(binding);
20399
+ quote = toExecutionFeeQuote(fetchedQuote);
20400
+ const refreshedPreparation = await prepareAndPreflight(params, binding, quote, provider);
20401
+ prepared = refreshedPreparation.prepared;
20402
+ if (refreshedPreparation.approveStep.state !== 'noop') {
20403
+ approveStep = refreshedPreparation.approveStep;
20404
+ }
20405
+ validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
20406
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
20407
+ throw unsafeQuoteError(validation);
20408
+ }
20409
+ } else {
20410
+ throw unsafeQuoteError(validation);
20411
+ }
20412
+ }
20413
+ // Surface the same step events as the standard bridge() path so
20414
+ // kit.on('approve'|'burn'|'mint', ...) handlers fire for source-fee bridges.
20415
+ if (approveStep.state !== 'noop') {
20416
+ provider.emitBridgeStep('approve', approveStep);
20417
+ }
20418
+ const burn = await executeAndConfirm(prepared.burn, params, provider);
20419
+ const burnStep = {
20420
+ name: 'burn',
20421
+ state: 'success',
20422
+ txHash: burn.txHash,
20423
+ data: burn.data,
20424
+ explorerUrl: buildExplorerUrl(params.source.chain, burn.txHash)
20425
+ };
20426
+ provider.emitBridgeStep('burn', burnStep);
20427
+ const resultBase = {
20428
+ amount: params.amount,
20429
+ token: 'USDC',
20430
+ config: params.config,
20431
+ provider: provider.name,
20432
+ source: {
20433
+ address: params.source.address,
20434
+ chain: params.source.chain
20435
+ },
20436
+ destination: {
20437
+ address: params.destination.address,
20438
+ chain: params.destination.chain,
20439
+ ...params.destination.recipientAddress !== undefined && {
20440
+ recipientAddress: params.destination.recipientAddress
20441
+ },
20442
+ useForwarder: true
20443
+ }
20444
+ };
20445
+ const attestation = await provider.fetchRelayerMint(params.source, burn.txHash);
20446
+ const forwardTxHash = attestation.forwardTxHash;
20447
+ // The burn already moved funds. If the relayer confirms without a
20448
+ // destination hash, surface an error-state result that preserves the burn
20449
+ // step (so `retry()` can resume the mint) instead of throwing and discarding
20450
+ // the completed burn.
20451
+ if (typeof forwardTxHash !== 'string' || forwardTxHash.trim() === '') {
20452
+ const mintStep = {
20453
+ name: 'mint',
20454
+ state: 'error',
20455
+ forwarded: true,
20456
+ errorCategory: 'failed_offchain',
20457
+ errorMessage: 'Relayer confirmation did not include a destination transaction hash'
20458
+ };
20459
+ provider.emitBridgeStep('mint', mintStep);
20460
+ return {
20461
+ ...resultBase,
20462
+ state: 'error',
20463
+ steps: [
20464
+ approveStep,
20465
+ burnStep,
20466
+ mintStep
20467
+ ]
20468
+ };
20469
+ }
20470
+ const mintStep = {
20471
+ name: 'mint',
20472
+ state: 'success',
20473
+ forwarded: true,
20474
+ txHash: forwardTxHash,
20475
+ explorerUrl: buildExplorerUrl(params.destination.chain, forwardTxHash)
20476
+ };
20477
+ provider.emitBridgeStep('mint', mintStep);
20478
+ return {
20479
+ ...resultBase,
20480
+ state: 'success',
20481
+ steps: [
20482
+ approveStep,
20483
+ burnStep,
20484
+ mintStep
20485
+ ]
20486
+ };
20487
+ }
20488
+
19136
20489
  /** SDK name used in telemetry payloads. */ const SDK_NAME$3 = resolveKitSdkName(pkg$5.name);
19137
20490
  /**
19138
20491
  * Pick the most-relevant `txHash` to attach to an error telemetry payload.
@@ -19360,11 +20713,18 @@ function assertCCTPV2Config(config) {
19360
20713
  this.validateNetworkCompatibility(resolvedParams);
19361
20714
  // Merge the custom fee config into the resolved params
19362
20715
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
19363
- // Find a provider that supports this route
19364
- const provider = this.findProviderForRoute(finalResolvedParams);
19365
- // Execute the transfer using the provider
19366
- // Format the bridge result into human-readable string values for the user
19367
- const result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
20716
+ let result;
20717
+ // Execute the explicit source-fee path without changing legacy
20718
+ // useForwarder behavior for callers that did not opt in.
20719
+ if (params.config?.feePayment === 'source') {
20720
+ const sourceFeeProvider = this.findSourceFeeProvider(finalResolvedParams);
20721
+ result = formatBridgeResult(await executeSourceFeeBridge(params, finalResolvedParams, sourceFeeProvider), 'to-human-readable');
20722
+ } else {
20723
+ // Find a provider that supports this route
20724
+ const provider = this.findProviderForRoute(finalResolvedParams);
20725
+ // Execute the transfer using the provider and format the result.
20726
+ result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
20727
+ }
19368
20728
  // Emit error telemetry when the provider returns an error state
19369
20729
  // (provider records step failures in the result instead of throwing).
19370
20730
  if (result.state === 'error') {
@@ -19487,42 +20847,7 @@ function assertCCTPV2Config(config) {
19487
20847
  tokenIn: result.token
19488
20848
  });
19489
20849
  }
19490
- /**
19491
- * Estimate the cost and fees for a cross-chain USDC bridge operation.
19492
- *
19493
- * This method calculates the expected gas fees and protocol costs for bridging
19494
- * without actually executing the transaction. It performs the same validation
19495
- * as the bridge method but stops before execution.
19496
- *
19497
- * @param params - The bridge parameters for cost estimation, including optional invocation metadata
19498
- * @returns Promise resolving to detailed cost breakdown including gas estimates
19499
- * @throws {KitError} When the parameters are invalid.
19500
- * @throws {UnsupportedRouteError} When the route is not supported.
19501
- *
19502
- * @example
19503
- * ```typescript
19504
- * // Basic usage
19505
- * const estimate = await kit.estimate({
19506
- * from: { adapter: adapter, chain: 'Ethereum' },
19507
- * to: { adapter: adapter, chain: 'Base' },
19508
- * amount: '10.50',
19509
- * token: 'USDC'
19510
- * })
19511
- * console.log('Estimated cost:', estimate.totalCost)
19512
- *
19513
- * // With custom invocation metadata
19514
- * const estimate = await kit.estimate({
19515
- * from: { adapter: adapter, chain: 'Ethereum' },
19516
- * to: { adapter: adapter, chain: 'Base' },
19517
- * amount: '10.50',
19518
- * token: 'USDC',
19519
- * invocationMeta: {
19520
- * traceId: 'custom-trace-id',
19521
- * callers: [{ type: 'app', name: 'MyDApp', version: '1.0.0' }],
19522
- * },
19523
- * })
19524
- * ```
19525
- */ async estimate(params) {
20850
+ async estimate(params) {
19526
20851
  return withErrorTelemetry(async ()=>{
19527
20852
  // First validate the parameters
19528
20853
  assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
@@ -19532,6 +20857,10 @@ function assertCCTPV2Config(config) {
19532
20857
  this.validateNetworkCompatibility(resolvedParams);
19533
20858
  // Merge the custom fee config into the resolved params
19534
20859
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
20860
+ if (params.config?.feePayment === 'source') {
20861
+ this.findSourceFeeProvider(finalResolvedParams);
20862
+ return estimateSourceFeeBridge(finalResolvedParams);
20863
+ }
19535
20864
  // Find a provider that supports this route
19536
20865
  const provider = this.findProviderForRoute(finalResolvedParams);
19537
20866
  // Estimate the transfer using the provider and format amounts to human-readable strings
@@ -19580,6 +20909,9 @@ function assertCCTPV2Config(config) {
19580
20909
  * // Get only chains that support forwarding
19581
20910
  * const forwarderChains = kit.getSupportedChains({ forwarderSupported: true })
19582
20911
  *
20912
+ * // Get only chains that can pay fees on the source chain (receive-exact)
20913
+ * const sourceFeeChains = kit.getSupportedChains({ sourceFeeSupported: true })
20914
+ *
19583
20915
  * console.log('Supported chains:')
19584
20916
  * allChains.forEach(chain => {
19585
20917
  * console.log(`- ${chain.name} (${chain.type})`)
@@ -19628,6 +20960,10 @@ function assertCCTPV2Config(config) {
19628
20960
  return options.forwarderSupported ? fs.source || fs.destination : !fs.source && !fs.destination;
19629
20961
  });
19630
20962
  }
20963
+ // Apply source-paid ("receive-exact") fee support filter if provided
20964
+ if (options?.sourceFeeSupported !== undefined) {
20965
+ chains = chains.filter((chain)=>hasSourceFeeSupport(chain) === options.sourceFeeSupported);
20966
+ }
19631
20967
  return chains;
19632
20968
  }
19633
20969
  /**
@@ -19662,6 +20998,20 @@ function assertCCTPV2Config(config) {
19662
20998
  return provider;
19663
20999
  }
19664
21000
  /**
21001
+ * Find the default CCTP v2 provider for a source-fee forwarding route.
21002
+ *
21003
+ * @param params - The resolved provider parameters.
21004
+ * @returns The CCTP v2 provider that supports the forwarded route.
21005
+ * @throws {UnsupportedRouteError} When no source-fee provider supports the route.
21006
+ * @internal
21007
+ */ findSourceFeeProvider(params) {
21008
+ const provider = this.providers.find((candidate)=>candidate instanceof CCTPV2BridgingProvider && candidate.supportsRoute(params.source.chain, params.destination.chain, params.token, true));
21009
+ if (!(provider instanceof CCTPV2BridgingProvider)) {
21010
+ throw createUnsupportedRouteError(params.source.chain.name, params.destination.chain.name);
21011
+ }
21012
+ return provider;
21013
+ }
21014
+ /**
19665
21015
  * Merge custom fee configuration into provider parameters.
19666
21016
  *
19667
21017
  * Prioritizes any custom fee configuration already present on the
@@ -19801,7 +21151,7 @@ function assertCCTPV2Config(config) {
19801
21151
  registerKit(`${pkg$5.name}/${pkg$5.version}`);
19802
21152
 
19803
21153
  var name$3 = "@circle-fin/swap-kit";
19804
- var version$3 = "1.5.2";
21154
+ var version$3 = "1.6.0";
19805
21155
  var pkg$3 = {
19806
21156
  name: name$3,
19807
21157
  version: version$3};
@@ -19815,7 +21165,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
19815
21165
  * Catches obviously malformed addresses at parse time; chain-specific validation
19816
21166
  * is performed in buildServiceParams.
19817
21167
  */ const destinationAddressSchema = zod.z.union([
19818
- evmAddressSchema,
21168
+ evmAddressSchema$1,
19819
21169
  solanaAddressSchema
19820
21170
  ]);
19821
21171
  /**
@@ -19861,9 +21211,16 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
19861
21211
  message: 'stopLimit must be greater than 0'
19862
21212
  }).optional(),
19863
21213
  customFee: serviceSwapCustomFeeSchema.optional(),
21214
+ apiKey: zod.z.string({
21215
+ invalid_type_error: 'apiKey must be a string'
21216
+ })// Tolerate '' so the `process.env.CIRCLE_API_KEY ?? ''` idiom falls back to
21217
+ // kitKey via resolveApiKey instead of being rejected here.
21218
+ .optional(),
19864
21219
  kitKey: zod.z.string({
19865
21220
  invalid_type_error: 'kitKey must be a string'
19866
- }).min(1, 'kitKey must be a non-empty string').optional(),
21221
+ })// Tolerate '' so an unset kit-key env var yields the permissionless path,
21222
+ // matching buildServiceParams (which already omits an empty credential).
21223
+ .optional(),
19867
21224
  provider: zod.z.string({
19868
21225
  invalid_type_error: 'provider must be a string'
19869
21226
  }).min(1, 'provider must be a non-empty string').optional(),
@@ -20028,23 +21385,49 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20028
21385
  fees: zod.z.array(serviceSwapFeeSchema).optional()
20029
21386
  }).passthrough();
20030
21387
 
20031
- /** Placeholder used wherever a kit key is scrubbed from logs, traces, or errors. */ const REDACTED_KIT_KEY = '[redacted]';
21388
+ /** Placeholder used wherever a credential is scrubbed from logs, traces, or errors. */ const REDACTED_CREDENTIAL = '[redacted]';
21389
+ /**
21390
+ * Config fields that carry a Stablecoin Service credential.
21391
+ *
21392
+ * `apiKey` is the supported field; `kitKey` is the deprecated alias. Redaction
21393
+ * and credential resolution both read this list so a newly added field is
21394
+ * scrubbed everywhere it needs to be.
21395
+ */ const CREDENTIAL_CONFIG_FIELDS = [
21396
+ 'apiKey',
21397
+ 'kitKey'
21398
+ ];
20032
21399
 
21400
+ /**
21401
+ * Replaces any credential field present on a config with a redaction marker.
21402
+ *
21403
+ * Covers `apiKey` and the deprecated `kitKey`, so adding a credential field
21404
+ * without extending this list cannot silently leak it into a trace. Returns the
21405
+ * config unchanged when it carries no credential.
21406
+ */ const redactCredentialFields = (config)=>{
21407
+ const present = CREDENTIAL_CONFIG_FIELDS.filter((field)=>field in config);
21408
+ if (present.length === 0) {
21409
+ return config;
21410
+ }
21411
+ return present.reduce((acc, field)=>({
21412
+ ...acc,
21413
+ [field]: REDACTED_CREDENTIAL
21414
+ }), {
21415
+ ...config
21416
+ });
21417
+ };
20033
21418
  /**
20034
21419
  * Builds a redacted projection of swap params that is safe to embed in error
20035
21420
  * traces (which may be logged downstream). Allow-lists only non-sensitive
20036
21421
  * fields and drops `from` entirely — `from.adapter` can carry signer/private-key
20037
- * material for signer-backed (viem/ethers) flows — and redacts `config.kitKey`.
21422
+ * material for signer-backed (viem/ethers) flows — and redacts every credential
21423
+ * field on `config`.
20038
21424
  */ const redactSwapParamsForTrace = (params)=>{
20039
21425
  if (typeof params !== 'object' || params === null) {
20040
21426
  return params;
20041
21427
  }
20042
21428
  const record = params;
20043
21429
  const config = record['config'];
20044
- const redactedConfig = typeof config === 'object' && config !== null && 'kitKey' in config ? {
20045
- ...config,
20046
- kitKey: REDACTED_KIT_KEY
20047
- } : config;
21430
+ const redactedConfig = typeof config === 'object' && config !== null ? redactCredentialFields(config) : config;
20048
21431
  return {
20049
21432
  tokenIn: record['tokenIn'],
20050
21433
  tokenOut: record['tokenOut'],
@@ -20128,6 +21511,41 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20128
21511
  *
20129
21512
  * @internal
20130
21513
  */ const MAX_RATE_ADDRESSES_PER_REQUEST = 100;
21514
+ /**
21515
+ * Environment prefixes carried by Circle platform API keys.
21516
+ *
21517
+ * A Circle API key is `<ENV>_API_KEY:<keyId>:<keySecret>`, where `<ENV>` is one
21518
+ * of these prefixes.
21519
+ *
21520
+ * @internal
21521
+ */ const API_KEY_ENV_PREFIXES = [
21522
+ 'TEST',
21523
+ 'LIVE',
21524
+ 'SAND',
21525
+ 'SANDBOX',
21526
+ 'SMOK',
21527
+ 'PROD',
21528
+ 'STAG',
21529
+ 'DEV'
21530
+ ];
21531
+ /**
21532
+ * Accepted credential formats for Stablecoin Service authentication.
21533
+ *
21534
+ * Matches a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
21535
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`). API keys are the
21536
+ * recommended credential; kit keys remain accepted as the legacy path.
21537
+ *
21538
+ * @remarks
21539
+ * This is a local pre-flight check, not the authority — the Stablecoin Service
21540
+ * validates the credential and answers 401 when it rejects one. The prefix list
21541
+ * is therefore deliberately permissive: a valid key carrying a prefix this SDK
21542
+ * has not been taught about should reach the service and be judged there rather
21543
+ * than be refused locally, since refusing locally is indistinguishable from an
21544
+ * outage to the caller. Kept as the single source of truth so the pattern is
21545
+ * not restated per call site.
21546
+ *
21547
+ * @internal
21548
+ */ const API_KEY_PATTERN = new RegExp(`^(?:KIT_KEY|(?:${API_KEY_ENV_PREFIXES.join('|')})_API_KEY)` + ':[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$');
20131
21549
 
20132
21550
  /**
20133
21551
  * Zod schema for validating stop limits.
@@ -20196,13 +21614,14 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20196
21614
  /**
20197
21615
  * Zod schema for validating API keys.
20198
21616
  *
20199
- * Validates that the API key is a valid API key format.
21617
+ * Accepts a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
21618
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`).
20200
21619
  *
20201
21620
  * @example
20202
21621
  * ```typescript
20203
21622
  * import { apiKeySchema } from '@core/service-client'
20204
21623
  *
20205
- * const result = apiKeySchema.safeParse('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
21624
+ * const result = apiKeySchema.safeParse('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
20206
21625
  * if (!result.success) {
20207
21626
  * console.error('Invalid API key format:', result.error.issues)
20208
21627
  * }
@@ -20210,7 +21629,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20210
21629
  */ const apiKeySchema = zod.z.string({
20211
21630
  required_error: 'API key is required',
20212
21631
  invalid_type_error: 'Invalid API key format'
20213
- }).regex(/^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/, 'Invalid API key format');
21632
+ }).regex(API_KEY_PATTERN, 'Invalid API key format');
20214
21633
  /**
20215
21634
  * Zod schema for platform fees configuration.
20216
21635
  *
@@ -20734,8 +22153,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20734
22153
  * Validates that an API key is properly formatted for Circle Stablecoin Service.
20735
22154
  *
20736
22155
  * This function performs validation on API keys to ensure they conform to the
20737
- * expected format before making API requests. The key must follow the structure:
20738
- * `KIT_KEY:keyId:keySecret`
22156
+ * expected format before making API requests. The key must follow the structure
22157
+ * `<ENV>_API_KEY:keyId:keySecret`, or the legacy `KIT_KEY:keyId:keySecret`.
20739
22158
  *
20740
22159
  * @param apiKey - The API key to validate (can be any type for graceful handling).
20741
22160
  * @returns True if the API key is valid, false otherwise.
@@ -20743,7 +22162,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20743
22162
  * @remarks
20744
22163
  * This function handles invalid inputs gracefully by returning false rather than
20745
22164
  * throwing errors. It validates:
20746
- * - The key starts with the `KIT_KEY:` prefix
22165
+ * - The key carries a recognized prefix — `<ENV>_API_KEY:` for a Circle platform
22166
+ * API key, or `KIT_KEY:` for the legacy kit key
20747
22167
  * - Contains exactly three colon-separated parts
20748
22168
  * - keyId and keySecret contain only valid characters (alphanumeric, `-`, `_`, `.`)
20749
22169
  * - keyId and keySecret are non-empty
@@ -20751,40 +22171,80 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20751
22171
  *
20752
22172
  * Valid characters in keyId and keySecret: `a-z`, `A-Z`, `0-9`, `-`, `_`, `.`
20753
22173
  *
22174
+ * Format is checked locally only so an obviously malformed credential fails
22175
+ * before a request is made. The Stablecoin Service remains the authority on
22176
+ * whether a well-formed key is actually accepted.
22177
+ *
20754
22178
  * @example
20755
22179
  * ```typescript
20756
22180
  * import { isValidApiKey } from '@core/service-client'
20757
22181
  *
20758
22182
  * // Valid API key
20759
- * const isValid = isValidApiKey('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
22183
+ * const isValid = isValidApiKey('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
20760
22184
  * console.log(isValid) // true
20761
22185
  *
22186
+ * // Legacy kit keys remain valid
22187
+ * console.log(isValidApiKey('KIT_KEY:my-key-id:my-key-secret')) // true
22188
+ *
20762
22189
  * ```
20763
22190
  */ const isValidApiKey = (apiKey)=>{
20764
22191
  // Handle invalid input types
20765
22192
  if (typeof apiKey !== 'string') {
20766
22193
  return false;
20767
22194
  }
20768
- const apiKeyPattern = /^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/;
20769
22195
  // Validate without trimming - any whitespace will cause validation to fail
20770
- return apiKeyPattern.test(apiKey);
22196
+ return API_KEY_PATTERN.test(apiKey);
20771
22197
  };
20772
22198
  /**
20773
- * Assert that a Stablecoin Service kit key is not being supplied from a browser.
22199
+ * Resolve the credential to authenticate a Stablecoin Service request with.
22200
+ *
22201
+ * `apiKey` is the supported field; `kitKey` is the deprecated alias kept for
22202
+ * existing integrations. When both are supplied `apiKey` wins, so a caller
22203
+ * migrating field-by-field cannot be silently pinned to a stale credential.
20774
22204
  *
20775
- * The kit key (`KIT_KEY:<id>:<secret>`) is a server-only secret. Every
20776
- * Stablecoin Service request that attaches an `Authorization: Bearer` header
20777
- * funnels through this package, so calling this guard before that header is
20778
- * built prevents the secret from being sent from and thus bundled into — a
20779
- * client application. In Node.js the check is a no-op, preserving the
20780
- * legitimate "hold the kit key on the server, forward the prepared transaction
20781
- * to the client" flow. When no kit key is supplied the permissionless (keyless)
20782
- * client path remains fully allowed.
22205
+ * An empty-string value is treated as absent on either field. Without this, the
22206
+ * common `process.env.CIRCLE_API_KEY ?? ''` idiom (which yields `''` when the
22207
+ * variable is unset) would either shadow a working `kitKey` or, on a bare
22208
+ * `kitKey: ''`, reach downstream validation as an invalid credential instead of
22209
+ * falling through to the permissionless path.
22210
+ *
22211
+ * @param source - Object carrying either credential field, or neither.
22212
+ * @returns The credential to use, or `undefined` for the permissionless
22213
+ * (keyless) path.
22214
+ *
22215
+ * @example
22216
+ * ```typescript
22217
+ * import { resolveApiKey } from '@core/service-client'
20783
22218
  *
20784
- * @param apiKey - The inline kit key for the request, or `undefined` when none
20785
- * was supplied (permissionless mode).
22219
+ * resolveApiKey({ apiKey: 'TEST_API_KEY:id:secret' }) // 'TEST_API_KEY:id:secret'
22220
+ * resolveApiKey({ kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
22221
+ * resolveApiKey({ apiKey: '', kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
22222
+ * resolveApiKey({ kitKey: '' }) // undefined
22223
+ * resolveApiKey({}) // undefined
22224
+ * ```
22225
+ */ const resolveApiKey = (source)=>{
22226
+ // Treat an empty-string value as absent on either field so the
22227
+ // `env ?? ''` idiom falls through to the next credential (or permissionless).
22228
+ const normalize = (value)=>value !== undefined && value !== '' ? value : undefined;
22229
+ return normalize(source.apiKey) ?? normalize(source.kitKey);
22230
+ };
22231
+ /**
22232
+ * Assert that a Stablecoin Service credential is not supplied from a browser.
22233
+ *
22234
+ * An API key (`<ENV>_API_KEY:<id>:<secret>`) and a legacy kit key
22235
+ * (`KIT_KEY:<id>:<secret>`) are both server-only secrets. Every Stablecoin
22236
+ * Service request that attaches an `Authorization: Bearer` header funnels
22237
+ * through this package, so calling this guard before that header is built
22238
+ * prevents the secret from being sent from — and thus bundled into — a client
22239
+ * application. In Node.js the check is a no-op, preserving the legitimate "hold
22240
+ * the credential on the server, forward the prepared transaction to the client"
22241
+ * flow. When no credential is supplied the permissionless (keyless) client path
22242
+ * remains fully allowed.
22243
+ *
22244
+ * @param apiKey - The inline credential for the request, or `undefined` when
22245
+ * none was supplied (permissionless mode).
20786
22246
  * @returns Nothing.
20787
- * @throws KitError with VALIDATION_FAILED when a kit key is supplied while
22247
+ * @throws KitError with VALIDATION_FAILED when a credential is supplied while
20788
22248
  * running in a browser environment. The secret value is never echoed.
20789
22249
  *
20790
22250
  * @example
@@ -20792,10 +22252,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20792
22252
  * import { assertBrowserSafeApiKey } from '@core/service-client'
20793
22253
  *
20794
22254
  * // Server (Node.js): no-op, request proceeds with the Authorization header.
20795
- * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
22255
+ * assertBrowserSafeApiKey('TEST_API_KEY:my-id:my-secret')
20796
22256
  *
20797
22257
  * // Browser: throws to stop the secret from leaking into the client bundle.
20798
- * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
22258
+ * assertBrowserSafeApiKey('TEST_API_KEY:my-id:my-secret')
20799
22259
  *
20800
22260
  * // Browser, permissionless: allowed.
20801
22261
  * assertBrowserSafeApiKey(undefined)
@@ -20805,7 +22265,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20805
22265
  return;
20806
22266
  }
20807
22267
  if (isBrowserEnvironment()) {
20808
- throw createValidationFailedError$1('kitKey', '[redacted]', 'kitKey must not be provided in a browser environment — it is a server-only secret. Run kit operations that use a kit key on your server, or omit kitKey to use the permissionless (keyless) client path');
22268
+ throw createValidationFailedError$1('apiKey', '[redacted]', 'apiKey must not be provided in a browser environment — it is a server-only secret. Run kit operations that use an API key on your server, or omit apiKey (and the legacy kitKey) to use the permissionless (keyless) client path');
20809
22269
  }
20810
22270
  };
20811
22271
 
@@ -20817,7 +22277,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20817
22277
  *
20818
22278
  * @remarks
20819
22279
  * - Input validation leverages Zod schemas to surface actionable KitErrors.
20820
- * - API key format must match `KIT_KEY:<keyId>:<keySecret>`
22280
+ * - API key format must match `<ENV>_API_KEY:<keyId>:<keySecret>`, or the
22281
+ * legacy `KIT_KEY:<keyId>:<keySecret>`
20821
22282
  *
20822
22283
  * @param params - The create transaction request parameters with API key.
20823
22284
  * @returns A promise that resolves to a typed {@link CreateSwapResponse}.
@@ -20846,7 +22307,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20846
22307
  * recipientAddress: '0x050928e1f9086b20539c767c96909663ff7fbdf2',
20847
22308
  * },
20848
22309
  * },
20849
- * apiKey: 'KIT_KEY:example-key:example-secret',
22310
+ * apiKey: 'TEST_API_KEY:example-key:example-secret',
20850
22311
  * })
20851
22312
  *
20852
22313
  * console.log(`Transaction target: ${transaction.transaction.target}`)
@@ -20865,13 +22326,13 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20865
22326
  const url = new URL('/v1/stablecoinKits/swap', STABLECOIN_SERVICE_BASE_URL).toString();
20866
22327
  // Remove the API key from the request body
20867
22328
  const { apiKey, ...requestBody } = validatedParams;
20868
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22329
+ // Never let a server-only credential leave a browser (no-op in Node.js).
20869
22330
  assertBrowserSafeApiKey(apiKey);
20870
22331
  const effectiveConfig = {
20871
22332
  ...DEFAULT_CONFIG$1,
20872
22333
  headers: {
20873
22334
  ...DEFAULT_CONFIG$1.headers,
20874
- // Permissionless mode: no Authorization header when the kit key is absent.
22335
+ // Permissionless mode: no Authorization header when the credential is absent.
20875
22336
  ...apiKey !== undefined && {
20876
22337
  Authorization: `Bearer ${apiKey}`
20877
22338
  }
@@ -21021,7 +22482,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21021
22482
  }
21022
22483
  // Use validated data
21023
22484
  const validatedParams = result.data;
21024
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22485
+ // Never let a server-only credential leave a browser (no-op in Node.js).
21025
22486
  assertBrowserSafeApiKey(validatedParams.apiKey);
21026
22487
  // Build the API URL
21027
22488
  const url = buildQuoteUrl(validatedParams);
@@ -21030,7 +22491,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21030
22491
  ...DEFAULT_CONFIG$1,
21031
22492
  headers: {
21032
22493
  ...DEFAULT_CONFIG$1.headers,
21033
- // Permissionless mode: no Authorization header when the kit key is absent.
22494
+ // Permissionless mode: no Authorization header when the credential is absent.
21034
22495
  ...validatedParams.apiKey !== undefined && {
21035
22496
  Authorization: `Bearer ${validatedParams.apiKey}`
21036
22497
  }
@@ -21095,14 +22556,14 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21095
22556
  toChain: result.data.toChain
21096
22557
  }
21097
22558
  };
21098
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22559
+ // Never let a server-only credential leave a browser (no-op in Node.js).
21099
22560
  assertBrowserSafeApiKey(validatedParams.apiKey);
21100
22561
  const url = buildSwapStatusUrl(validatedParams);
21101
22562
  const effectiveConfig = {
21102
22563
  ...DEFAULT_CONFIG$1,
21103
22564
  headers: {
21104
22565
  ...DEFAULT_CONFIG$1.headers,
21105
- // Permissionless mode: no Authorization header when the kit key is absent.
22566
+ // Permissionless mode: no Authorization header when the credential is absent.
21106
22567
  ...validatedParams.apiKey !== undefined && {
21107
22568
  Authorization: `Bearer ${validatedParams.apiKey}`
21108
22569
  }
@@ -21128,7 +22589,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21128
22589
  * const url = buildTokenRatesUrl({
21129
22590
  * chain: 'Ethereum',
21130
22591
  * addresses: ['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
21131
- * apiKey: 'KIT_KEY:key:secret',
22592
+ * apiKey: 'TEST_API_KEY:key:secret',
21132
22593
  * })
21133
22594
  * // => 'https://api.circle.com/v1/stablecoinKits/rates?chain=Ethereum&addresses=...'
21134
22595
  * ```
@@ -21201,14 +22662,14 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21201
22662
  addresses: result.data.addresses
21202
22663
  }
21203
22664
  };
21204
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22665
+ // Never let a server-only credential leave a browser (no-op in Node.js).
21205
22666
  assertBrowserSafeApiKey(validatedParams.apiKey);
21206
22667
  const url = buildTokenRatesUrl(validatedParams);
21207
22668
  const effectiveConfig = {
21208
22669
  ...DEFAULT_CONFIG$1,
21209
22670
  headers: {
21210
22671
  ...DEFAULT_CONFIG$1.headers,
21211
- // Permissionless mode: no Authorization header when the kit key is absent.
22672
+ // Permissionless mode: no Authorization header when the credential is absent.
21212
22673
  ...validatedParams.apiKey !== undefined && {
21213
22674
  Authorization: `Bearer ${validatedParams.apiKey}`
21214
22675
  }
@@ -23186,7 +24647,7 @@ const abiParameterSchema = zod.z.object({
23186
24647
  */ zod.z.object({
23187
24648
  type: zod.z.literal('evm'),
23188
24649
  abi: abiSchema,
23189
- address: evmAddressSchema,
24650
+ address: evmAddressSchema$1,
23190
24651
  functionName: zod.z.string({
23191
24652
  required_error: 'Function name is required',
23192
24653
  invalid_type_error: 'Function name must be a string'
@@ -23223,7 +24684,7 @@ const abiParameterSchema = zod.z.object({
23223
24684
  * }
23224
24685
  * ```
23225
24686
  */ zod.z.object({
23226
- address: evmAddressSchema,
24687
+ address: evmAddressSchema$1,
23227
24688
  value: zod.z.bigint({
23228
24689
  required_error: 'Value is required for native transfers',
23229
24690
  invalid_type_error: 'Value must be a bigint'
@@ -24457,6 +25918,23 @@ async function signUnits(group, signingUnits) {
24457
25918
  }
24458
25919
  return signedSets;
24459
25920
  }
25921
+ /**
25922
+ * Fail loudly if a signing group carries an intent it does not own.
25923
+ *
25924
+ * Every intent in a group is signed by `group.address`, so that signer must own
25925
+ * each intent's `spec.sourceSigner`. If distinct spend sources ever collapse
25926
+ * into one group again (DEVX-3638), this throws before signing instead of
25927
+ * silently producing a wrong-signer signature. specs are bytes32 and
25928
+ * `group.address` is a native address, so normalize both before comparing.
25929
+ */ function assertGroupSignerOwnsIntents(group) {
25930
+ if (group.address == null || group.address === '') return;
25931
+ const groupSigner = convertAddress(group.address, 'bytes32').toLowerCase();
25932
+ for (const intent of group.intents){
25933
+ if (intent.spec.sourceSigner.toLowerCase() !== groupSigner) {
25934
+ throw createValidationFailedError$1('group.address', group.address, `Signing group for ${group.address} carries an intent whose ` + `sourceSigner is ${intent.spec.sourceSigner}; a group must be signed ` + 'by the wallet that owns every intent it carries. This indicates ' + 'distinct spend sources collapsed into a single signing group.');
25935
+ }
25936
+ }
25937
+ }
24460
25938
  /**
24461
25939
  * Sign an EVM adapter group.
24462
25940
  *
@@ -24499,22 +25977,42 @@ async function signUnits(group, signingUnits) {
24499
25977
  // Validate the collection before doing bytecode reads or asking a wallet
24500
25978
  // to sign. evmSigningData owns the canonical BurnIntent validation.
24501
25979
  validateGroupIntents(group.intents);
25980
+ // Guard against a wrong-signer collapse: the group's signer must own every
25981
+ // intent it carries.
25982
+ assertGroupSignerOwnsIntents(group);
24502
25983
  const chainsByDomain = collectChainsByDomain(group);
24503
25984
  const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
24504
25985
  const signingUnits = createSigningUnits(group, signerTypeByDomain);
24505
25986
  return await signUnits(group, signingUnits);
24506
25987
  }
24507
25988
 
25989
+ // One adapter instance can serve several signer addresses (e.g. a single
25990
+ // dev-controlled adapter used for multiple wallets), so the group map must be
25991
+ // keyed on `(adapter, sourceSigner)` rather than the adapter reference alone.
25992
+ // Keying on the adapter alone collapsed two same-chain sources into one group,
25993
+ // signing the second wallet's intent with the first wallet (DEVX-3638 /
25994
+ // DEVX-3194). A Map needs a primitive key, so give each adapter instance a
25995
+ // stable token and combine it with the (case-normalized) signer address.
25996
+ const adapterTokens = new WeakMap();
25997
+ let nextAdapterToken = 0;
25998
+ function groupKey(alloc) {
25999
+ let token = adapterTokens.get(alloc.adapter);
26000
+ if (token === undefined) {
26001
+ token = nextAdapterToken++;
26002
+ adapterTokens.set(alloc.adapter, token);
26003
+ }
26004
+ return `${String(token)}:${alloc.sourceSigner.toLowerCase()}`;
26005
+ }
24508
26006
  /**
24509
26007
  * Add an EVM intent into the batched EVM group map.
24510
26008
  *
24511
- * On EVM, all intents for the same adapter are batched into a single
24512
- * group. The signing step uses `chainsByDomain` to preserve EOA batching
26009
+ * On EVM, all intents for the same adapter **and signer** are batched into a
26010
+ * single group. The signing step uses `chainsByDomain` to preserve EOA batching
24513
26011
  * while signing ERC-1271 intents separately on their source chains.
24514
26012
  *
24515
26013
  * @param intent - The burn intent to group.
24516
26014
  * @param alloc - The allocation that resolved to this intent.
24517
- * @param evmGroups - Mutable map of adapter → EVM group.
26015
+ * @param evmGroups - Mutable map of `(adapter, sourceSigner)` key → EVM group.
24518
26016
  *
24519
26017
  * @example
24520
26018
  * ```typescript
@@ -24524,12 +26022,13 @@ async function signUnits(group, signingUnits) {
24524
26022
  * groupEvmIntents(intent, alloc, groups)
24525
26023
  * ```
24526
26024
  */ function groupEvmIntents(intent, alloc, evmGroups) {
24527
- const existing = evmGroups.get(alloc.adapter);
26025
+ const key = groupKey(alloc);
26026
+ const existing = evmGroups.get(key);
24528
26027
  if (existing) {
24529
26028
  existing.intents.push(intent);
24530
26029
  existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
24531
26030
  } else {
24532
- evmGroups.set(alloc.adapter, {
26031
+ evmGroups.set(key, {
24533
26032
  adapter: alloc.adapter,
24534
26033
  chain: alloc.chain,
24535
26034
  intents: [
@@ -24622,7 +26121,7 @@ zod.z.object({
24622
26121
  signature: evmSignatureSchema,
24623
26122
  tokenInputs: zod.z.array(zod.z.object({
24624
26123
  permitType: zod.z.nativeEnum(PermitType),
24625
- token: evmAddressSchema,
26124
+ token: evmAddressSchema$1,
24626
26125
  amount: zod.z.bigint().refine((value)=>value >= 0n, {
24627
26126
  message: 'amount must be a non-negative bigint'
24628
26127
  }),
@@ -25215,7 +26714,7 @@ function writeBytes32(buffer, hex, offset) {
25215
26714
  });
25216
26715
 
25217
26716
  /**
25218
- * Assert that the kit key is valid for swap operations.
26717
+ * Assert that the credential is valid for swap operations.
25219
26718
  *
25220
26719
  * @remarks
25221
26720
  * This validation is called when swap operations are executed, not at construction time.
@@ -25223,34 +26722,35 @@ function writeBytes32(buffer, hex, offset) {
25223
26722
  * at usage time rather than construction time.
25224
26723
  *
25225
26724
  * Validates:
25226
- * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
25227
- * An absent or empty kit key is permitted (permissionless mode) — the swap
25228
- * service now treats the key as optional.
26725
+ * - The credential matches an accepted format when provided — a Circle API key
26726
+ * (`<ENV>_API_KEY:id:secret`) or a legacy kit key (`KIT_KEY:id:secret`).
26727
+ * An absent or empty credential is permitted (permissionless mode) — the swap
26728
+ * service treats the credential as optional.
25229
26729
  *
25230
- * @param kitKey - The inline kit key from the swap operation config (optional)
25231
- * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
25232
- * match the KIT_KEY:<keyId>:<keySecret> format
26730
+ * @param apiKey - The inline credential from the swap operation config (optional)
26731
+ * @throws KitError with VALIDATION_FAILED if a credential is provided but does
26732
+ * not match an accepted format
25233
26733
  *
25234
26734
  * @example
25235
26735
  * ```typescript
25236
- * import { assertKitKey } from './assertKitKey'
26736
+ * import { assertApiKey } from './assertApiKey'
25237
26737
  *
25238
- * const kitKey = 'KIT_KEY:9d1bd085-bad2-4a2b-ae67-5814b85167c9:secretvalue'
26738
+ * const apiKey = 'TEST_API_KEY:9d1bd085-bad2-4a2b-ae67-5814b85167c9:secretvalue'
25239
26739
  *
25240
- * // Validate kit key before use
25241
- * assertKitKey(kitKey)
26740
+ * // Validate the credential before use
26741
+ * assertApiKey(apiKey)
25242
26742
  * ```
25243
- */ function assertKitKey(kitKey) {
25244
- // Permissionless mode: the swap service treats the kit key as optional, so an
25245
- // absent (or empty) key is valid. Only validate the format when a key is
26743
+ */ function assertApiKey(apiKey) {
26744
+ // Permissionless mode: the swap service treats the credential as optional, so
26745
+ // an absent (or empty) value is valid. Only validate the format when one is
25246
26746
  // actually provided.
25247
- if (!kitKey) {
26747
+ if (!apiKey) {
25248
26748
  return;
25249
26749
  }
25250
- const apiKeyResult = apiKeySchema.safeParse(kitKey);
26750
+ const apiKeyResult = apiKeySchema.safeParse(apiKey);
25251
26751
  if (!apiKeyResult.success) {
25252
26752
  const errors = apiKeyResult.error.issues.map((issue)=>issue.message).join(', ');
25253
- throw createValidationFailedError$1('kitKey', REDACTED_KIT_KEY, `Invalid API key format. ${errors}. Expected format: KIT_KEY:<keyId>:<keySecret>. Get your free Kit Key at: https://developers.circle.com/w3s/keys#kit-keys`);
26753
+ throw createValidationFailedError$1('apiKey', REDACTED_CREDENTIAL, `Invalid API key format. ${errors}. Expected format: <ENV>_API_KEY:<keyId>:<keySecret> (for example TEST_API_KEY:... or LIVE_API_KEY:...). Legacy KIT_KEY:<keyId>:<keySecret> values are still accepted. Get your key at: https://developers.circle.com/w3s/keys`);
25254
26754
  }
25255
26755
  }
25256
26756
 
@@ -25442,7 +26942,7 @@ const TOKEN_REGISTRY$4 = createTokenRegistry();
25442
26942
  }
25443
26943
  // Validate recipient address format matches the chain type
25444
26944
  if (chain.type === 'evm') {
25445
- const result = evmAddressSchema.safeParse(recipientAddress);
26945
+ const result = evmAddressSchema$1.safeParse(recipientAddress);
25446
26946
  if (!result.success) {
25447
26947
  throw createInvalidAddressError(recipientAddress, chain.name, '0x-prefixed 42-character hex string (0x + 40 hex characters)');
25448
26948
  }
@@ -25504,7 +27004,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25504
27004
  * to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
25505
27005
  * config: {
25506
27006
  * slippageBps: 300,
25507
- * kitKey: 'KIT_KEY:...:...' // Required inline kit key
27007
+ * apiKey: 'TEST_API_KEY:...:...' // Inline credential (optional)
25508
27008
  * }
25509
27009
  * }
25510
27010
  *
@@ -25542,18 +27042,17 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25542
27042
  validateResolvedAddress(resolvedTokenInAddress, chain);
25543
27043
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
25544
27044
  validateResolvedAddress(to, destinationChain);
25545
- const kitKey = config?.kitKey;
25546
- // Validate the kit key format when one is provided (permissionless otherwise).
25547
- assertKitKey(kitKey);
27045
+ const apiKey = resolveApiKey(config ?? {});
27046
+ // Validate the credential format when one is provided (permissionless
27047
+ // otherwise).
27048
+ assertApiKey(apiKey);
25548
27049
  // Validate custom fee configuration if present
25549
27050
  const customFee = config?.customFee;
25550
27051
  if (customFee !== undefined) {
25551
27052
  const feePayoutChain = resolveFeePayoutChain(tokenIn, tokenOut, chain, destinationChain);
25552
27053
  validateCustomFeeConfig(customFee, feePayoutChain);
25553
27054
  }
25554
- // Build CreateSwapParams for service client
25555
- // Use object spread to conditionally include optional properties
25556
- // Note: Service client expects 'apiKey' field, so we map kitKey → apiKey
27055
+ // Build CreateSwapParams for service client, spreading optional properties.
25557
27056
  const chainName = chain.chain // The API expects the Blockchain enum value format like "World_Chain"
25558
27057
  ;
25559
27058
  const destinationChainName = destinationChain.chain;
@@ -25588,9 +27087,9 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25588
27087
  }
25589
27088
  }
25590
27089
  },
25591
- // Map kitKey → apiKey for the service client; omitted in permissionless mode.
25592
- ...kitKey ? {
25593
- apiKey: kitKey
27090
+ // Omitted in permissionless mode.
27091
+ ...apiKey ? {
27092
+ apiKey
25594
27093
  } : {}
25595
27094
  };
25596
27095
  }
@@ -27494,9 +28993,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
27494
28993
  * ```typescript
27495
28994
  * import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
27496
28995
  *
27497
- * // Create provider with developer fee (1 USDC)
28996
+ * // Create provider with developer fee (1 USDC).
28997
+ * // Credentials are supplied per operation via `config.apiKey`, not here.
27498
28998
  * const provider = new StablecoinServiceSwapProvider({
27499
- * kitKey: process.env.STABLECOIN_KIT_API_KEY,
27500
28999
  * customFee: {
27501
29000
  * amount: '1000000', // 1 USDC in base units (6 decimals)
27502
29001
  * recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
@@ -28158,9 +29657,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
28158
29657
  * import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
28159
29658
  * import { Ethereum } from '@core/chains'
28160
29659
  *
28161
- * const provider = new StablecoinServiceSwapProvider({
28162
- * kitKey: process.env.STABLECOIN_KIT_API_KEY
28163
- * })
29660
+ * const provider = new StablecoinServiceSwapProvider()
28164
29661
  *
28165
29662
  * const adapter = createAdapterFromPrivateKey({
28166
29663
  * privateKey: process.env.PRIVATE_KEY
@@ -28654,7 +30151,7 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
28654
30151
  /**
28655
30152
  * Fee recipient address (required).
28656
30153
  * Must be a valid EVM address or Solana address.
28657
- */ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
30154
+ */ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
28658
30155
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
28659
30156
  })
28660
30157
  }).strict();
@@ -28666,7 +30163,8 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
28666
30163
  * - slippageBps: Optional positive number for slippage tolerance
28667
30164
  * - stopLimit: Optional decimal string for minimum output
28668
30165
  * - customFee: Optional fee configuration
28669
- * - kitKey: Optional string identifier
30166
+ * - apiKey: Optional credential string
30167
+ * - kitKey: Optional credential string (deprecated alias for apiKey)
28670
30168
  */ const swapConfigSchema = zod.z.object({
28671
30169
  allowanceStrategy: allowanceStrategySchema$1.optional(),
28672
30170
  slippageBps: zod.z.number().int().min(0).optional(),
@@ -28676,11 +30174,12 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
28676
30174
  attributeName: 'stopLimit'
28677
30175
  })(zod.z.string())).optional(),
28678
30176
  customFee: swapCustomFeeSchema.optional(),
30177
+ apiKey: zod.z.string().optional(),
28679
30178
  kitKey: zod.z.string().optional()
28680
30179
  });
28681
30180
  const swapDestinationSchema = zod.z.object({
28682
30181
  chain: optionalSwapChainIdentifierField,
28683
- recipientAddress: zod.z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
30182
+ recipientAddress: zod.z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
28684
30183
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
28685
30184
  }).optional()
28686
30185
  }).strict();
@@ -30787,7 +32286,8 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
30787
32286
  /**
30788
32287
  * Helper to format the config section of a swap result, transforming fee amounts.
30789
32288
  *
30790
- * Strips sensitive fields like kitKey from the config before returning.
32289
+ * Strips the credential fields (`apiKey` and the deprecated `kitKey`) from the
32290
+ * config before returning, so a result is safe to log or serialize.
30791
32291
  *
30792
32292
  * @param config - The swap config from the provider.
30793
32293
  * @param outputTokenTransform - The amount transformer for output-token-denominated values (fees and amountOut).
@@ -30796,11 +32296,11 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
30796
32296
  if (!config) {
30797
32297
  return undefined;
30798
32298
  }
30799
- // Remove kitKey from the config
30800
- const cloneConfig = {
30801
- ...config
30802
- };
30803
- delete cloneConfig.kitKey;
32299
+ // Rebuild the config without any credential field. Filtering entries rather
32300
+ // than deleting them avoids reading the deprecated `kitKey` property
32301
+ // directly, and keeps the credential list in one place.
32302
+ const credentialFields = CREDENTIAL_CONFIG_FIELDS;
32303
+ const cloneConfig = Object.fromEntries(Object.entries(config).filter(([key])=>!credentialFields.includes(key)));
30804
32304
  if (Object.keys(cloneConfig).length === 0) {
30805
32305
  return undefined;
30806
32306
  }
@@ -31001,9 +32501,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
31001
32501
  try {
31002
32502
  // Step 1: Build quote params directly (no need for buildServiceParams)
31003
32503
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
31004
- // The kit key is optional (permissionless mode); when absent the quote is
31005
- // fetched without an Authorization header.
31006
- const kitKey = params.config?.kitKey;
32504
+ // The credential is optional (permissionless mode); when absent the quote
32505
+ // is fetched without an Authorization header.
32506
+ const apiKey = resolveApiKey(params.config ?? {});
31007
32507
  // Resolve token aliases to addresses for the quote API
31008
32508
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
31009
32509
  const chain = params.from.chain;
@@ -31028,8 +32528,8 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
31028
32528
  ...params.config?.slippageBps !== undefined && {
31029
32529
  slippageBps: params.config.slippageBps
31030
32530
  },
31031
- ...kitKey ? {
31032
- apiKey: kitKey
32531
+ ...apiKey ? {
32532
+ apiKey
31033
32533
  } : {}
31034
32534
  };
31035
32535
  // Step 2: Get quote from service
@@ -31433,7 +32933,7 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31433
32933
  * the service returns `receivingTokenDecimals`. When decimals are missing
31434
32934
  * the field is omitted entirely (never raw base units).
31435
32935
  *
31436
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
32936
+ * @param params - `txHash`, `chainIn`, optional `chainOut`, and `apiKey`.
31437
32937
  * @returns A single snapshot of the swap's status.
31438
32938
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
31439
32939
  *
@@ -31449,7 +32949,7 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31449
32949
  * txHash: result.txHash,
31450
32950
  * chainIn: result.chainIn,
31451
32951
  * chainOut: result.chainOut,
31452
- * kitKey: process.env.KIT_KEY ?? '',
32952
+ * apiKey: process.env.CIRCLE_API_KEY,
31453
32953
  * })
31454
32954
  * while (status.progress.status === 'PENDING') {
31455
32955
  * await new Promise((r) => setTimeout(r, 3_000))
@@ -31457,7 +32957,7 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31457
32957
  * txHash: result.txHash,
31458
32958
  * chainIn: result.chainIn,
31459
32959
  * chainOut: result.chainOut,
31460
- * kitKey: process.env.KIT_KEY ?? '',
32960
+ * apiKey: process.env.CIRCLE_API_KEY,
31461
32961
  * })
31462
32962
  * }
31463
32963
  *
@@ -31475,14 +32975,15 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31475
32975
  const chainIn = normalizeChain(params.chainIn, 'chainIn');
31476
32976
  const chainOut = params.chainOut === undefined ? undefined : normalizeChain(params.chainOut, 'chainOut');
31477
32977
  const isCrossChain = chainOut !== undefined && chainOut !== chainIn;
32978
+ const apiKey = resolveApiKey(params);
31478
32979
  const request = {
31479
32980
  txHash: params.txHash,
31480
32981
  chain: chainIn,
31481
32982
  ...isCrossChain && {
31482
32983
  toChain: chainOut
31483
32984
  },
31484
- ...params.kitKey ? {
31485
- apiKey: params.kitKey
32985
+ ...apiKey ? {
32986
+ apiKey
31486
32987
  } : {}
31487
32988
  };
31488
32989
  let raw = await getSwapStatus$2(request);
@@ -31561,9 +33062,9 @@ const isResultShape = (params)=>'result' in params;
31561
33062
  *
31562
33063
  * Two parameter shapes are accepted:
31563
33064
  *
31564
- * - Pass the {@link SwapResult} directly: `{ result, kitKey, ... }`.
33065
+ * - Pass the {@link SwapResult} directly: `{ result, apiKey, ... }`.
31565
33066
  * Cleanest when chaining off `kit.swap()`.
31566
- * - Pass discrete fields: `{ txHash, chainIn, chainOut?, kitKey, ... }`.
33067
+ * - Pass discrete fields: `{ txHash, chainIn, chainOut?, apiKey, ... }`.
31567
33068
  * Useful for resuming an in-flight swap from a persisted record.
31568
33069
  *
31569
33070
  * @param params - Wait configuration. See {@link WaitForSwapParams}.
@@ -31583,7 +33084,7 @@ const isResultShape = (params)=>'result' in params;
31583
33084
  *
31584
33085
  * const final = await waitForSwap({
31585
33086
  * result,
31586
- * kitKey: process.env.KIT_KEY ?? '',
33087
+ * apiKey: process.env.CIRCLE_API_KEY,
31587
33088
  * onProgress: (snapshot) => {
31588
33089
  * console.log(snapshot.progress.substatus ?? snapshot.progress.status)
31589
33090
  * },
@@ -31601,7 +33102,7 @@ const isResultShape = (params)=>'result' in params;
31601
33102
  * txHash: '0x…',
31602
33103
  * chainIn: 'Ethereum',
31603
33104
  * chainOut: 'Base',
31604
- * kitKey: process.env.KIT_KEY ?? '',
33105
+ * apiKey: process.env.CIRCLE_API_KEY,
31605
33106
  * })
31606
33107
  * ```
31607
33108
  */ async function waitForSwap$1(params) {
@@ -31617,14 +33118,15 @@ const isResultShape = (params)=>'result' in params;
31617
33118
  const chainOut = isResultShape(params) ? params.result.chainOut : params.chainOut;
31618
33119
  // Delegate parameter validation (txHash, chainIn, chainOut) to
31619
33120
  // getSwapStatus so the same rules apply to both helpers.
33121
+ const apiKey = resolveApiKey(params);
31620
33122
  const statusParams = {
31621
33123
  txHash,
31622
33124
  chainIn,
31623
33125
  ...chainOut !== undefined && {
31624
33126
  chainOut
31625
33127
  },
31626
- ...params.kitKey ? {
31627
- kitKey: params.kitKey
33128
+ ...apiKey ? {
33129
+ apiKey
31628
33130
  } : {}
31629
33131
  };
31630
33132
  const deadline = Date.now() + timeoutMs;
@@ -31748,7 +33250,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31748
33250
  *
31749
33251
  * @param context - SwapKit context (provides the `TokenRegistry` used for
31750
33252
  * symbol resolution).
31751
- * @param params - `chain`, optional `tokens`, and `kitKey`.
33253
+ * @param params - `chain`, optional `tokens`, and `apiKey`.
31752
33254
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
31753
33255
  * @throws \{KitError\} If `chain` cannot be resolved, `tokens` exceeds 100
31754
33256
  * entries, or any entry is neither a registered symbol nor a
@@ -31763,7 +33265,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31763
33265
  * const { rates } = await getTokenRates(context, {
31764
33266
  * chain: Blockchain.Ethereum,
31765
33267
  * tokens: ['USDC', 'EURC', 'NATIVE'],
31766
- * kitKey: process.env.KIT_KEY ?? '',
33268
+ * apiKey: process.env.CIRCLE_API_KEY,
31767
33269
  * })
31768
33270
  *
31769
33271
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -31784,10 +33286,11 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31784
33286
  // valid Blockchain enum string, so `getChainByEnum` is guaranteed to resolve.
31785
33287
  const chainDef = getChainByEnum(chain);
31786
33288
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
33289
+ const apiKey = resolveApiKey(params);
31787
33290
  return getTokenRates$2({
31788
33291
  chain,
31789
- ...params.kitKey ? {
31790
- apiKey: params.kitKey
33292
+ ...apiKey ? {
33293
+ apiKey
31791
33294
  } : {},
31792
33295
  ...resolvedAddresses !== undefined && {
31793
33296
  addresses: resolvedAddresses
@@ -32418,7 +33921,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32418
33921
  * is formatted as a human-readable decimal string; when decimals are
32419
33922
  * missing the field is omitted entirely (never raw base units).
32420
33923
  *
32421
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
33924
+ * @param params - `txHash`, `chainIn`, optional `chainOut`, and `apiKey`.
32422
33925
  * @returns A snapshot of the swap's status at the time of the call.
32423
33926
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
32424
33927
  *
@@ -32431,7 +33934,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32431
33934
  * txHash: result.txHash,
32432
33935
  * chainIn: result.chainIn,
32433
33936
  * chainOut: result.chainOut,
32434
- * kitKey: process.env.KIT_KEY ?? '',
33937
+ * apiKey: process.env.CIRCLE_API_KEY,
32435
33938
  * })
32436
33939
  * console.log(status.progress.status, status.progress.substatus)
32437
33940
  * ```
@@ -32443,7 +33946,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32443
33946
  * txHash: result.txHash,
32444
33947
  * chainIn: result.chainIn,
32445
33948
  * chainOut: result.chainOut,
32446
- * kitKey: process.env.KIT_KEY ?? '',
33949
+ * apiKey: process.env.CIRCLE_API_KEY,
32447
33950
  * })
32448
33951
  * while (status.progress.status === 'PENDING') {
32449
33952
  * await new Promise((r) => setTimeout(r, 3_000))
@@ -32451,7 +33954,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32451
33954
  * txHash: result.txHash,
32452
33955
  * chainIn: result.chainIn,
32453
33956
  * chainOut: result.chainOut,
32454
- * kitKey: process.env.KIT_KEY ?? '',
33957
+ * apiKey: process.env.CIRCLE_API_KEY,
32455
33958
  * })
32456
33959
  * }
32457
33960
  *
@@ -32482,7 +33985,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32482
33985
  * 5xx, etc.) surface unchanged so the caller can decide whether to
32483
33986
  * retry the whole wait. The wait itself only emits a timeout error.
32484
33987
  *
32485
- * @param params - Wait configuration including identifiers, kit key,
33988
+ * @param params - Wait configuration including identifiers, API key,
32486
33989
  * optional `timeoutMs`, and an optional `onProgress`
32487
33990
  * callback fired on every successful poll.
32488
33991
  * @returns The first terminal {@link SwapStatusResult} observed.
@@ -32501,7 +34004,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32501
34004
  *
32502
34005
  * const final = await kit.waitForSwap({
32503
34006
  * result,
32504
- * kitKey: process.env.KIT_KEY ?? '',
34007
+ * apiKey: process.env.CIRCLE_API_KEY,
32505
34008
  * onProgress: (snap) => console.log(snap.progress.substatus ?? snap.progress.status),
32506
34009
  * })
32507
34010
  *
@@ -32519,7 +34022,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32519
34022
  * txHash: persisted.txHash,
32520
34023
  * chainIn: persisted.chainIn,
32521
34024
  * chainOut: persisted.chainOut,
32522
- * kitKey: process.env.KIT_KEY ?? '',
34025
+ * apiKey: process.env.CIRCLE_API_KEY,
32523
34026
  * })
32524
34027
  * ```
32525
34028
  */ async waitForSwap(params) {
@@ -32553,7 +34056,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32553
34056
  * not cache a rate for the native asset on that chain, the response simply
32554
34057
  * omits the address.
32555
34058
  *
32556
- * @param params - `chain`, optional `tokens`, and `kitKey`.
34059
+ * @param params - `chain`, optional `tokens`, and `apiKey`.
32557
34060
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
32558
34061
  * @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
32559
34062
  * entries, or any entry is neither a registered symbol nor a
@@ -32565,7 +34068,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32565
34068
  * const { rates } = await kit.getTokenRates({
32566
34069
  * chain: 'Ethereum',
32567
34070
  * tokens: ['USDC', 'EURC', 'NATIVE'],
32568
- * kitKey: process.env.KIT_KEY ?? '',
34071
+ * apiKey: process.env.CIRCLE_API_KEY,
32569
34072
  * })
32570
34073
  *
32571
34074
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -32579,7 +34082,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32579
34082
  * chain: 'Ethereum',
32580
34083
  * // 'USDC' (registry symbol) + raw USDT address
32581
34084
  * tokens: ['USDC', '0xdAC17F958D2ee523a2206206994597C13D831ec7'],
32582
- * kitKey: process.env.KIT_KEY ?? '',
34085
+ * apiKey: process.env.CIRCLE_API_KEY,
32583
34086
  * })
32584
34087
  * ```
32585
34088
  *
@@ -32588,7 +34091,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32588
34091
  * ```typescript
32589
34092
  * const { rates } = await kit.getTokenRates({
32590
34093
  * chain: 'Base',
32591
- * kitKey: process.env.KIT_KEY ?? '',
34094
+ * apiKey: process.env.CIRCLE_API_KEY,
32592
34095
  * })
32593
34096
  * ```
32594
34097
  */ async getTokenRates(params) {
@@ -32950,7 +34453,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32950
34453
  };
32951
34454
 
32952
34455
  var name$2 = "@circle-fin/earn-kit";
32953
- var version$2 = "1.5.1";
34456
+ var version$2 = "1.6.0";
32954
34457
  var pkg$2 = {
32955
34458
  name: name$2,
32956
34459
  version: version$2};
@@ -33166,7 +34669,7 @@ function toSdkChain(chain) {
33166
34669
  *
33167
34670
  * @internal
33168
34671
  */ function assertHexAddress(field, value, message = `${field} must be a 0x-prefixed 20-byte hex address`) {
33169
- const result = evmAddressSchema.safeParse(value);
34672
+ const result = evmAddressSchema$1.safeParse(value);
33170
34673
  if (!result.success) {
33171
34674
  throw createValidationFailedError$1(field, value, message);
33172
34675
  }
@@ -34852,7 +36355,7 @@ const EARN_OPERATIONS = new Set([
34852
36355
  // Store the original service params non-enumerably. `retry()` reads them by
34853
36356
  // direct property access (`trace.params`), but JSON.stringify, console.log,
34854
36357
  // and telemetry serializers skip non-enumerable properties — so a
34855
- // permissioned call's `config.kitKey` (a `KIT_KEY:<id>:<secret>` value) and
36358
+ // permissioned call's `config.apiKey` (a credential value) and
34856
36359
  // the in-memory adapter never leak through a logged error. defineProperty
34857
36360
  // also redefines any enumerable `params` an upstream trace may have carried.
34858
36361
  Object.defineProperty(trace, 'params', {
@@ -34923,7 +36426,7 @@ function isNonNegativeBigIntLike(value) {
34923
36426
  }
34924
36427
  }
34925
36428
  const hexSignatureSchema = evmSignatureSchema;
34926
- const hexAddressSchema = evmAddressSchema;
36429
+ const hexAddressSchema = evmAddressSchema$1;
34927
36430
  // '0x' prefix + 32 bytes * 2 hex chars.
34928
36431
  const BYTES32_HEX_LENGTH = 66;
34929
36432
  const bridgeFeeTokenSchema = hexAddressSchema;
@@ -35892,7 +37395,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
35892
37395
  }
35893
37396
 
35894
37397
  var name$1 = "@circle-fin/provider-earn-service";
35895
- var version$1 = "1.4.1";
37398
+ var version$1 = "1.5.0";
35896
37399
  var pkg$1 = {
35897
37400
  name: name$1,
35898
37401
  version: version$1};
@@ -35954,10 +37457,12 @@ var pkg$1 = {
35954
37457
  *
35955
37458
  * @internal
35956
37459
  */ function buildConfig(serviceConfig) {
35957
- // The kit key is a server-only secret. Reject it in the browser so it cannot
35958
- // leak into a client bundle (no-op in Node.js). Keyless usage stays allowed.
35959
- if (serviceConfig?.kitKey !== undefined && isBrowserEnvironment()) {
35960
- throw createValidationFailedError$1('kitKey', '[redacted]', 'kitKey must not be provided in a browser environment — it is a server-only secret. Run EarnKit operations that use a kit key on your server, or omit kitKey to use the permissionless (keyless) client path');
37460
+ const apiKey = resolveApiKey(serviceConfig ?? {});
37461
+ // The credential is a server-only secret. Reject it in the browser so it
37462
+ // cannot leak into a client bundle (no-op in Node.js). Keyless usage stays
37463
+ // allowed.
37464
+ if (apiKey !== undefined && isBrowserEnvironment()) {
37465
+ throw createValidationFailedError$1('apiKey', '[redacted]', 'apiKey must not be provided in a browser environment — it is a server-only secret. Run EarnKit operations that use an API key on your server, or omit apiKey (and the legacy kitKey) to use the permissionless (keyless) client path');
35961
37466
  }
35962
37467
  const baseUrl = serviceConfig?.baseUrl ?? EARN_SERVICE_BASE_URL;
35963
37468
  // The API CORS policy does not allow this custom header. Keep the existing
@@ -35966,7 +37471,7 @@ var pkg$1 = {
35966
37471
  const sdkVersionHeader = isNodeEnvironment() ? {
35967
37472
  [SDK_VERSION_HEADER]: resolveSdkVersionHeader()
35968
37473
  } : {};
35969
- if (serviceConfig?.kitKey === undefined) {
37474
+ if (apiKey === undefined) {
35970
37475
  return {
35971
37476
  pollingConfig: {
35972
37477
  ...DEFAULT_CONFIG,
@@ -35978,11 +37483,11 @@ var pkg$1 = {
35978
37483
  baseUrl
35979
37484
  };
35980
37485
  }
35981
- if (!isValidApiKey(serviceConfig.kitKey)) {
37486
+ if (!isValidApiKey(apiKey)) {
35982
37487
  throw new KitError({
35983
37488
  ...InputError.VALIDATION_FAILED,
35984
37489
  recoverability: 'FATAL',
35985
- message: 'Invalid kitKey format. Expected KIT_KEY:<keyId>:<keySecret>.'
37490
+ message: 'Invalid apiKey format. Expected <ENV>_API_KEY:<keyId>:<keySecret> (for example TEST_API_KEY:...), or a legacy KIT_KEY:<keyId>:<keySecret>.'
35986
37491
  });
35987
37492
  }
35988
37493
  return {
@@ -35991,7 +37496,7 @@ var pkg$1 = {
35991
37496
  headers: {
35992
37497
  ...DEFAULT_CONFIG.headers,
35993
37498
  ...sdkVersionHeader,
35994
- Authorization: `Bearer ${serviceConfig.kitKey}`
37499
+ Authorization: `Bearer ${apiKey}`
35995
37500
  }
35996
37501
  },
35997
37502
  baseUrl
@@ -37308,9 +38813,9 @@ function finishElapsedWait(lastStatus, lastError) {
37308
38813
  * // Permissionless mode
37309
38814
  * const provider = new EarnServiceProvider()
37310
38815
  *
37311
- * // Permissioned mode with Kit Key
38816
+ * // Permissioned mode with an API key
37312
38817
  * const provider = new EarnServiceProvider({
37313
- * kitKey: 'KIT_KEY:keyId:keySecret',
38818
+ * apiKey: 'TEST_API_KEY:keyId:keySecret',
37314
38819
  * })
37315
38820
  *
37316
38821
  * // Custom base URL for testing
@@ -38537,7 +40042,7 @@ function findProvider(context, chain, operation = 'earn') {
38537
40042
  * fast instead of round-tripping to the service.
38538
40043
  *
38539
40044
  * @internal
38540
- */ const recipientEvmAddressSchema = evmAddressSchema.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
40045
+ */ const recipientEvmAddressSchema = evmAddressSchema$1.refine((address)=>!ZERO_EVM_ADDRESS_REGEX.test(address), 'address must not be the zero address').refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
38541
40046
  /**
38542
40047
  * Schema for the adapter context within earn operations.
38543
40048
  *
@@ -38569,19 +40074,39 @@ const sourceAdapterContextSchema = zod.z.object({
38569
40074
  /**
38570
40075
  * Schema for the EarnConfig options.
38571
40076
  *
38572
- * Validate the optional Kit Key field using the standard `apiKeySchema`
38573
- * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
38574
- * operates in permissionless mode. `baseUrl` overrides the Earn Service
38575
- * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
38576
- * batched execution. Both are forwarded to the provider, so this `.strict()`
38577
- * schema must accept them or a valid config object is rejected.
40077
+ * Validate the *resolved* credential using the standard `apiKeySchema` format
40078
+ * (`<ENV>_API_KEY:<keyId>:<keySecret>`, or a legacy
40079
+ * `KIT_KEY:<keyId>:<keySecret>`). `apiKey` takes precedence over the deprecated
40080
+ * `kitKey`, so a malformed `kitKey` that is being ignored must not fail a config
40081
+ * that supplies a valid `apiKey` (and vice versa) only the credential that
40082
+ * would actually be sent is format-checked. When neither is supplied the SDK
40083
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service endpoint
40084
+ * (e.g. staging); `batchTransactions: false` opts out of atomic batched
40085
+ * execution. All are forwarded to the provider, so this `.strict()` schema must
40086
+ * accept them or a valid config object is rejected.
38578
40087
  *
38579
40088
  * @internal
38580
40089
  */ const earnConfigSchema = zod.z.object({
38581
- kitKey: apiKeySchema.optional(),
40090
+ apiKey: zod.z.string().optional(),
40091
+ kitKey: zod.z.string().optional(),
38582
40092
  baseUrl: zod.z.string().optional(),
38583
40093
  batchTransactions: zod.z.boolean().optional()
38584
- }).strict();
40094
+ }).strict().superRefine((config, ctx)=>{
40095
+ const credential = resolveApiKey(config);
40096
+ if (credential === undefined) {
40097
+ return;
40098
+ }
40099
+ const result = apiKeySchema.safeParse(credential);
40100
+ if (!result.success) {
40101
+ ctx.addIssue({
40102
+ code: zod.z.ZodIssueCode.custom,
40103
+ path: [
40104
+ credential === config.apiKey ? 'apiKey' : 'kitKey'
40105
+ ],
40106
+ message: result.error.issues[0]?.message ?? 'Invalid API key format'
40107
+ });
40108
+ }
40109
+ });
38585
40110
  /**
38586
40111
  * Canonical decimal form: a leading digit with no leading zeros (a single
38587
40112
  * '0' is only allowed immediately before the decimal point). Rejects the
@@ -38653,7 +40178,7 @@ const sourceAdapterContextSchema = zod.z.object({
38653
40178
  * currently supports EVM vault addresses on Arc Testnet.
38654
40179
  *
38655
40180
  * @internal
38656
- */ const vaultAddressSchema = evmAddressSchema.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
40181
+ */ const vaultAddressSchema = evmAddressSchema$1.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
38657
40182
  /**
38658
40183
  * Validation schema for VaultQuery.
38659
40184
  *
@@ -38928,7 +40453,7 @@ const sameChainGetDepositQuoteParamsSchema = zod.z.object({
38928
40453
  const crossChainGetDepositQuoteParamsSchema = zod.z.object({
38929
40454
  from: sourceAdapterContextSchema,
38930
40455
  chain: earnBridgeDestinationChainIdentifierSchema,
38931
- address: evmAddressSchema,
40456
+ address: evmAddressSchema$1,
38932
40457
  vaultAddress: vaultAddressSchema,
38933
40458
  amount: amountSchema$1,
38934
40459
  transferSpeed: zod.z.enum([
@@ -41244,7 +42769,7 @@ const tokens = createTokenRegistry();
41244
42769
  *
41245
42770
  * @param context - AppKit context.
41246
42771
  * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
41247
- * `kitKey`.
42772
+ * `apiKey`.
41248
42773
  * @returns A snapshot of the swap's status at the time of the call.
41249
42774
  *
41250
42775
  * @example
@@ -41258,7 +42783,7 @@ const tokens = createTokenRegistry();
41258
42783
  * txHash: result.txHash,
41259
42784
  * chainIn: result.chainIn,
41260
42785
  * chainOut: result.chainOut,
41261
- * kitKey: process.env.KIT_KEY ?? '',
42786
+ * apiKey: process.env.CIRCLE_API_KEY,
41262
42787
  * })
41263
42788
  *
41264
42789
  * console.log(status.progress.status, status.progress.substatus)
@@ -41281,7 +42806,7 @@ const tokens = createTokenRegistry();
41281
42806
  * `timeoutMs` elapses.
41282
42807
  *
41283
42808
  * @param context - AppKit context.
41284
- * @param params - Wait configuration, including identifiers, kit key,
42809
+ * @param params - Wait configuration, including identifiers, API key,
41285
42810
  * optional `timeoutMs`, and an optional `onProgress`
41286
42811
  * callback.
41287
42812
  * @returns The first terminal {@link SwapStatusResult} observed.
@@ -41297,7 +42822,7 @@ const tokens = createTokenRegistry();
41297
42822
  * txHash: result.txHash,
41298
42823
  * chainIn: result.chainIn,
41299
42824
  * chainOut: result.chainOut,
41300
- * kitKey: process.env.KIT_KEY ?? '',
42825
+ * apiKey: process.env.CIRCLE_API_KEY,
41301
42826
  * onProgress: (snap) => console.log(snap.progress.status),
41302
42827
  * })
41303
42828
  *
@@ -41333,7 +42858,7 @@ const tokens = createTokenRegistry();
41333
42858
  * addresses before indexing into `result.rates[chain]`.
41334
42859
  *
41335
42860
  * @param context - AppKit context.
41336
- * @param params - `chain`, plus optional `tokens` and `kitKey`.
42861
+ * @param params - `chain`, plus optional `tokens` and `apiKey`.
41337
42862
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
41338
42863
  *
41339
42864
  * @example
@@ -41344,7 +42869,7 @@ const tokens = createTokenRegistry();
41344
42869
  * const { rates } = await kit.getTokenRates({
41345
42870
  * chain: 'Ethereum',
41346
42871
  * tokens: ['USDC'],
41347
- * kitKey: process.env.KIT_KEY ?? '',
42872
+ * apiKey: process.env.CIRCLE_API_KEY,
41348
42873
  * })
41349
42874
  *
41350
42875
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -41355,6 +42880,43 @@ const tokens = createTokenRegistry();
41355
42880
  return kit.getTokenRates(params);
41356
42881
  };
41357
42882
 
42883
+ const BRIDGE_FILTER_KEYS = new Set([
42884
+ 'chainType',
42885
+ 'isTestnet',
42886
+ 'forwarderSupported',
42887
+ 'sourceFeeSupported'
42888
+ ]);
42889
+ function assertBridgeSupportedChainsOptions(options) {
42890
+ if (options === undefined) {
42891
+ return;
42892
+ }
42893
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) {
42894
+ throw createValidationFailedError$1('options', options, 'Bridge chain filters must be an object');
42895
+ }
42896
+ const entries = Object.entries(options);
42897
+ if (entries.length === 0 || entries.some(([key])=>!BRIDGE_FILTER_KEYS.has(key))) {
42898
+ throw createValidationFailedError$1('options', options, 'Bridge chain filters must contain only supported filter properties');
42899
+ }
42900
+ const record = options;
42901
+ for (const key of [
42902
+ 'isTestnet',
42903
+ 'forwarderSupported',
42904
+ 'sourceFeeSupported'
42905
+ ]){
42906
+ if (record[key] !== undefined && typeof record[key] !== 'boolean') {
42907
+ throw createValidationFailedError$1(`options.${key}`, record[key], 'Bridge chain boolean filters must be true or false');
42908
+ }
42909
+ }
42910
+ const chainType = record['chainType'];
42911
+ if (chainType !== undefined) {
42912
+ const chainTypes = Array.isArray(chainType) ? chainType : [
42913
+ chainType
42914
+ ];
42915
+ if (chainTypes.length === 0 || chainTypes.some((value)=>value !== 'evm' && value !== 'solana')) {
42916
+ throw createValidationFailedError$1('options.chainType', chainType, "Bridge chain type must be 'evm', 'solana', or an array of those values");
42917
+ }
42918
+ }
42919
+ }
41358
42920
  /**
41359
42921
  * Get chains supported by AppKit operations.
41360
42922
  *
@@ -41369,6 +42931,7 @@ const tokens = createTokenRegistry();
41369
42931
  * @param context - AppKit context containing fee configuration.
41370
42932
  * @param operationType - Optional operation type to filter chains ('bridge' | 'swap' | 'earn' | 'unifiedBalance').
41371
42933
  * @param unifiedBalance - Persistent AppKitUnifiedBalance namespace instance.
42934
+ * @param bridgeOptions - Optional Bridge Kit chain filters. Requires `operationType: 'bridge'`.
41372
42935
  * @returns Array of unique chain definitions supporting the specified operation(s)
41373
42936
  *
41374
42937
  * @example Get all supported chains via AppKit
@@ -41387,6 +42950,13 @@ const tokens = createTokenRegistry();
41387
42950
  * console.log('Chains supporting bridge:', bridgeChains.map(c => c.name))
41388
42951
  * ```
41389
42952
  *
42953
+ * @example Get chains that support source-paid bridge fees
42954
+ * ```typescript
42955
+ * const sourceFeeChains = kit.getSupportedChains('bridge', {
42956
+ * sourceFeeSupported: true,
42957
+ * })
42958
+ * ```
42959
+ *
41390
42960
  * @example Get swap-specific chains
41391
42961
  * ```typescript
41392
42962
  * const swapChains = kit.getSupportedChains('swap')
@@ -41407,7 +42977,8 @@ const tokens = createTokenRegistry();
41407
42977
  * const supportsEthBridge = bridgeChains.some(c => c.chain === Ethereum.chain)
41408
42978
  * console.log(`Ethereum supports bridge: ${supportsEthBridge}`)
41409
42979
  * ```
41410
- */ const getSupportedChains$2 = (context, operationType, unifiedBalance)=>{
42980
+ */ const getSupportedChains$2 = (context, operationType, unifiedBalance, bridgeOptions)=>{
42981
+ assertBridgeSupportedChainsOptions(bridgeOptions);
41411
42982
  // Runtime validation when operationType is provided
41412
42983
  if (operationType !== undefined && operationType !== 'bridge' && operationType !== 'swap' && operationType !== 'earn' && operationType !== 'unifiedBalance') {
41413
42984
  throw new KitError({
@@ -41416,10 +42987,13 @@ const tokens = createTokenRegistry();
41416
42987
  message: `Invalid operationType: "${String(operationType)}". Expected 'bridge', 'swap', 'earn', or 'unifiedBalance'.`
41417
42988
  });
41418
42989
  }
42990
+ if (bridgeOptions !== undefined && operationType !== 'bridge') {
42991
+ throw createValidationFailedError$1('options', bridgeOptions, "Bridge chain filters require operationType 'bridge'");
42992
+ }
41419
42993
  // Return bridge-specific chains
41420
42994
  if (operationType === 'bridge') {
41421
42995
  const bridgeKit = createBridgeKit(context);
41422
- return bridgeKit.getSupportedChains();
42996
+ return bridgeKit.getSupportedChains(bridgeOptions);
41423
42997
  }
41424
42998
  // Return swap-specific chains
41425
42999
  if (operationType === 'swap') {
@@ -41757,7 +43331,7 @@ async function deposit$2(context, params) {
41757
43331
  }
41758
43332
 
41759
43333
  var name = "@circle-fin/unified-balance-kit";
41760
- var version = "1.4.1";
43334
+ var version = "1.5.0";
41761
43335
  var pkg = {
41762
43336
  name: name,
41763
43337
  version: version};
@@ -42590,6 +44164,8 @@ const gatewayInfoSchema = zod.z.object({
42590
44164
  * Fetch Gateway info from the `/v1/info` endpoint.
42591
44165
  *
42592
44166
  * @param isTestnet - Whether to query the testnet or mainnet API.
44167
+ * @param config - Optional polling config overrides, including custom
44168
+ * `headers` forwarded with the request.
42593
44169
  * @returns Parsed Gateway info including per-domain processed heights.
42594
44170
  * @throws KitError on HTTP failure or invalid response.
42595
44171
  *
@@ -42598,11 +44174,11 @@ const gatewayInfoSchema = zod.z.object({
42598
44174
  * const info = await fetchGatewayInfo(true)
42599
44175
  * console.log(info.domains.map(d => `${d.chain}: ${d.processedHeight}`))
42600
44176
  * ```
42601
- */ async function fetchGatewayInfo(isTestnet) {
44177
+ */ async function fetchGatewayInfo(isTestnet, config) {
42602
44178
  const apiBaseUrl = getGatewayApiBaseUrl(isTestnet);
42603
44179
  const url = `${apiBaseUrl}/v1/info`;
42604
44180
  try {
42605
- return await pollApiGet(url, assertGatewayInfoResponse);
44181
+ return await pollApiGet(url, assertGatewayInfoResponse, config);
42606
44182
  } catch (error) {
42607
44183
  if (error instanceof KitError) {
42608
44184
  throw error;
@@ -42628,6 +44204,8 @@ const gatewayInfoSchema = zod.z.object({
42628
44204
  *
42629
44205
  * @param isTestnet - Whether to query the testnet or mainnet API.
42630
44206
  * @param domain - The Gateway domain number (e.g. 0 = Ethereum, 6 = Base).
44207
+ * @param config - Optional polling config overrides, including custom
44208
+ * `headers` forwarded with the `/v1/info` request.
42631
44209
  * @returns The processed height as a bigint.
42632
44210
  * @throws KitError when the domain is not found in the response.
42633
44211
  *
@@ -42636,10 +44214,10 @@ const gatewayInfoSchema = zod.z.object({
42636
44214
  * const height = await getProcessedHeight(true, 6) // Base Sepolia
42637
44215
  * console.log(`Gateway processed up to block ${height}`)
42638
44216
  * ```
42639
- */ async function getProcessedHeight(isTestnet, domain) {
44217
+ */ async function getProcessedHeight(isTestnet, domain, config) {
42640
44218
  // No caching in v1 — /v1/info is cheap and delegate status checks
42641
44219
  // are infrequent. Revisit if rate-limit or latency issues appear.
42642
- const info = await fetchGatewayInfo(isTestnet);
44220
+ const info = await fetchGatewayInfo(isTestnet, config);
42643
44221
  const entry = info.domains.find((d)=>d.domain === domain);
42644
44222
  if (!entry) {
42645
44223
  throw new KitError({
@@ -43105,12 +44683,22 @@ function throwNetworkMismatch(expected, actual) {
43105
44683
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
43106
44684
  * @returns Array of groups, each with adapter, chain, and intents to sign.
43107
44685
  */ function groupIntentsByAdapter(intents, allocations) {
44686
+ // Keyed by `(adapter, sourceSigner)` (see groupEvmIntents), not by adapter
44687
+ // alone: two spend sources on the same chain — including two addresses behind
44688
+ // one shared adapter — must stay in separate signing groups.
43108
44689
  const evmGroups = new Map();
43109
44690
  const solanaGroups = [];
43110
44691
  for (const intent of intents){
43111
- const alloc = allocations.find((a)=>a.chain.gateway.domain === intent.spec.sourceDomain);
44692
+ // Match each intent back to its source allocation by BOTH Gateway domain
44693
+ // and signer. Two spend sources on the same chain share a domain, so
44694
+ // matching on domain alone always returned the FIRST allocation — folding
44695
+ // the second wallet's intent into the first wallet's signing group so it
44696
+ // was signed by the wrong wallet (DEVX-3638). specs carry sourceSigner as
44697
+ // bytes32 and allocations carry native addresses, so normalize both.
44698
+ const intentSigner = intent.spec.sourceSigner.toLowerCase();
44699
+ const alloc = allocations.find((a)=>a.chain.gateway.domain === intent.spec.sourceDomain && convertAddress(a.sourceSigner, 'bytes32').toLowerCase() === intentSigner);
43112
44700
  if (!alloc) {
43113
- throw createValidationFailedError$1('intent.spec.sourceDomain', intent.spec.sourceDomain, `No allocation found for sourceDomain ${String(intent.spec.sourceDomain)}; ` + 'estimate response contains an intent that does not match any allocation');
44701
+ throw createValidationFailedError$1('intent.spec.sourceDomain', intent.spec.sourceDomain, `No allocation found for sourceDomain ${String(intent.spec.sourceDomain)} and sourceSigner ${String(intent.spec.sourceSigner)}; ` + 'estimate response contains an intent that does not match any allocation');
43114
44702
  }
43115
44703
  if (alloc.chain.type === 'solana') {
43116
44704
  solanaGroups.push(groupSolanaIntents(intent, alloc));
@@ -45011,12 +46599,12 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45011
46599
  * exceeds the 5-minute timeout without reaching a terminal status.
45012
46600
  * @throws KitError Propagated from {@link pollApiGet} if the HTTP
45013
46601
  * request itself fails or returns a non-OK response.
45014
- */ async function pollForwarderCompletion(apiBaseUrl, transferId) {
46602
+ */ async function pollForwarderCompletion(apiBaseUrl, transferId, requestConfig) {
45015
46603
  const startTime = Date.now();
45016
46604
  while(Date.now() - startTime < FORWARDER_POLL_TIMEOUT_MS){
45017
46605
  let status;
45018
46606
  try {
45019
- status = await pollApiGet(`${apiBaseUrl}/v1/transfer/${transferId}`, assertGatewayTransferDetails);
46607
+ status = await pollApiGet(`${apiBaseUrl}/v1/transfer/${transferId}`, assertGatewayTransferDetails, requestConfig);
45020
46608
  } catch (error) {
45021
46609
  throwGatewayApiError(error);
45022
46610
  }
@@ -45134,7 +46722,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45134
46722
  * @param params - Spend parameters (source(s) and token).
45135
46723
  * @param destChain - Resolved destination chain (used for network type).
45136
46724
  * @returns Confirmed balances tagged with their originating source index.
45137
- */ async function fetchChainBalances(params, destChain) {
46725
+ */ async function fetchChainBalances(params, destChain, requestConfig) {
45138
46726
  const rawSources = Array.isArray(params.from) ? params.from : [
45139
46727
  params.from
45140
46728
  ];
@@ -45163,7 +46751,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45163
46751
  token: params.token,
45164
46752
  sources: querySource,
45165
46753
  networkType
45166
- });
46754
+ }, requestConfig);
45167
46755
  }));
45168
46756
  const chainBalances = [];
45169
46757
  for(let i = 0; i < balanceResults.length; i++){
@@ -45236,9 +46824,9 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45236
46824
  * @param recipientAddress - Resolved recipient address.
45237
46825
  * @param useForwarder - Whether the Forwarding Service path is active.
45238
46826
  * @returns Allocations, intents, and (auto-allocation only) confirmed balances.
45239
- */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
46827
+ */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder, requestConfig) {
45240
46828
  if (params.amountIn) {
45241
- const chainBalances = await fetchChainBalances(params, destChain);
46829
+ const chainBalances = await fetchChainBalances(params, destChain, requestConfig);
45242
46830
  const { allocations, intents } = await buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, params.amountIn, chainBalances);
45243
46831
  return {
45244
46832
  allocations,
@@ -45261,11 +46849,11 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45261
46849
  * @param useForwarder - Whether the Forwarding Service path is active.
45262
46850
  * @param allocations - Normalised allocations (forwarded to error handler).
45263
46851
  * @returns Estimate entries and optional forwarding fee.
45264
- */ async function fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations) {
46852
+ */ async function fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations, requestConfig) {
45265
46853
  if (useForwarder) {
45266
46854
  let fwdResponse;
45267
46855
  try {
45268
- fwdResponse = await pollApiPost(`${apiBaseUrl}/v1/estimate?enableForwarder=true`, estimateBody, assertGatewayForwarderEstimateResponse);
46856
+ fwdResponse = await pollApiPost(`${apiBaseUrl}/v1/estimate?enableForwarder=true`, estimateBody, assertGatewayForwarderEstimateResponse, requestConfig);
45269
46857
  } catch (error) {
45270
46858
  throwGatewayApiError(error, allocations);
45271
46859
  }
@@ -45276,7 +46864,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45276
46864
  }
45277
46865
  let entries;
45278
46866
  try {
45279
- entries = await pollApiPost(`${apiBaseUrl}/v1/estimate`, estimateBody, assertGatewayEstimateResponse);
46867
+ entries = await pollApiPost(`${apiBaseUrl}/v1/estimate`, estimateBody, assertGatewayEstimateResponse, requestConfig);
45280
46868
  } catch (error) {
45281
46869
  throwGatewayApiError(error, allocations);
45282
46870
  }
@@ -45347,14 +46935,14 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45347
46935
  * @param destChain - Resolved destination chain with Gateway v1 config.
45348
46936
  * @param useForwarder - Whether the Forwarding Service path is active.
45349
46937
  * @returns Estimated intents (with real maxFee) and optional forwarding fee.
45350
- */ async function validateAndEstimate(allocations, intents, destChain, useForwarder) {
46938
+ */ async function validateAndEstimate(allocations, intents, destChain, useForwarder, requestConfig) {
45351
46939
  assertNetworkCompatibility(allocations, destChain);
45352
46940
  if (useForwarder) {
45353
46941
  assertForwarderRouteSupport(destChain, allocations);
45354
46942
  }
45355
46943
  const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
45356
46944
  const estimateBody = buildEstimateRequestBody(intents);
45357
- const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
46945
+ const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations, requestConfig);
45358
46946
  const estimatedIntents = parseEstimateResponse(entries, intents);
45359
46947
  return {
45360
46948
  estimatedIntents,
@@ -45413,7 +47001,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45413
47001
  }
45414
47002
  throw err;
45415
47003
  }
45416
- const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder);
47004
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder, opts.requestConfig);
45417
47005
  const stillOverdrawn = findOverdrawnChains(estimatedIntents, corrected.allocations, opts.chainBalances);
45418
47006
  if (stillOverdrawn.length === 0) {
45419
47007
  return {
@@ -45447,12 +47035,12 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45447
47035
  * @param recipientAddress - Resolved recipient address.
45448
47036
  * @param useForwarder - Whether the Forwarding Service path is active.
45449
47037
  * @returns Allocations, estimated intents, and optional forwarding fee.
45450
- */ async function resolveValidatedIntents(params, destChain, recipientAddress, useForwarder) {
47038
+ */ async function resolveValidatedIntents(params, destChain, recipientAddress, useForwarder, requestConfig) {
45451
47039
  if (useForwarder) {
45452
47040
  assertForwarderRouteSupport(destChain);
45453
47041
  }
45454
- const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
45455
- const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder);
47042
+ const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder, requestConfig);
47043
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder, requestConfig);
45456
47044
  if (params.amountIn && chainBalances) {
45457
47045
  const overdrawn = findOverdrawnChains(estimatedIntents, allocations, chainBalances);
45458
47046
  if (overdrawn.length > 0) {
@@ -45465,7 +47053,10 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45465
47053
  chainBalances,
45466
47054
  estimatedIntents,
45467
47055
  allocations,
45468
- overdrawn
47056
+ overdrawn,
47057
+ ...requestConfig ? {
47058
+ requestConfig
47059
+ } : {}
45469
47060
  });
45470
47061
  }
45471
47062
  }
@@ -45509,10 +47100,10 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45509
47100
  attestationSignature: transferResponse.signature
45510
47101
  };
45511
47102
  }
45512
- async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, steps) {
47103
+ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, steps, requestConfig) {
45513
47104
  const chainName = destChain.name;
45514
47105
  const recipientAddress = await resolveRecipientAddress(params.to);
45515
- const { allocations, estimatedIntents, forwardingFee: estimateFwdFee } = await resolveValidatedIntents(params, destChain, recipientAddress, useForwarder);
47106
+ const { allocations, estimatedIntents, forwardingFee: estimateFwdFee } = await resolveValidatedIntents(params, destChain, recipientAddress, useForwarder, requestConfig);
45516
47107
  pushStep(steps, dispatcher, 'gateway.spend.step.buildBurnIntents', chainName, {
45517
47108
  name: 'buildBurnIntents',
45518
47109
  state: 'success',
@@ -45549,7 +47140,7 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
45549
47140
  const forwarderParam = useForwarder ? '?enableForwarder=true' : '';
45550
47141
  let transferResponse;
45551
47142
  try {
45552
- transferResponse = await pollApiPost(`${transferApiBaseUrl}/v1/transfer${forwarderParam}`, transferBody, assertGatewayTransferResponse);
47143
+ transferResponse = await pollApiPost(`${transferApiBaseUrl}/v1/transfer${forwarderParam}`, transferBody, assertGatewayTransferResponse, requestConfig);
45553
47144
  } catch (error) {
45554
47145
  throwGatewayApiError(error, allocations);
45555
47146
  }
@@ -45629,10 +47220,11 @@ function updateFeesWithActual(fees, completionStatus) {
45629
47220
  amount: actual
45630
47221
  } : f);
45631
47222
  }
45632
- async function resolveDestinationTxHash(params, destChain, forwarder, transferId, attestation, attestationSignature, fees) {
47223
+ async function resolveDestinationTxHash(opts) {
47224
+ const { params, destChain, forwarder, transferId, attestation, attestationSignature, fees, requestConfig } = opts;
45633
47225
  if (forwarder && transferId) {
45634
47226
  const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
45635
- const completionStatus = await pollForwarderCompletion(apiBaseUrl, transferId);
47227
+ const completionStatus = await pollForwarderCompletion(apiBaseUrl, transferId, requestConfig);
45636
47228
  if (!completionStatus.transactionHash) {
45637
47229
  throw new KitError({
45638
47230
  ...OnchainError.TRANSACTION_REVERTED,
@@ -45715,9 +47307,20 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45715
47307
  /**
45716
47308
  * Execute the mint step, recording success/error as a spend step.
45717
47309
  */ async function executeMintStep(opts) {
45718
- const { params, destChain, forwarder, transferId, attestation, attestationSignature, fees, steps, dispatcher } = opts;
47310
+ const { params, destChain, forwarder, transferId, attestation, attestationSignature, fees, steps, dispatcher, requestConfig } = opts;
45719
47311
  try {
45720
- const mintResult = await resolveDestinationTxHash(params, destChain, forwarder, transferId, attestation, attestationSignature, fees);
47312
+ const mintResult = await resolveDestinationTxHash({
47313
+ params,
47314
+ destChain,
47315
+ forwarder,
47316
+ transferId,
47317
+ attestation,
47318
+ attestationSignature,
47319
+ fees,
47320
+ ...requestConfig ? {
47321
+ requestConfig
47322
+ } : {}
47323
+ });
45721
47324
  pushStep(steps, dispatcher, 'gateway.spend.step.mint', destChain.name, {
45722
47325
  name: 'mint',
45723
47326
  state: 'success',
@@ -45772,7 +47375,8 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45772
47375
  assertSpendAddresses(params, destChain);
45773
47376
  const forwarder = isForwarderEnabled(params.to);
45774
47377
  const steps = [];
45775
- const context = params.config?.retry ? await resolveRetryContext(params) : await resolveNormalContext(params, destChain, forwarder, dispatcher, steps);
47378
+ const requestConfig = options?.requestConfig;
47379
+ const context = params.config?.retry ? await resolveRetryContext(params) : await resolveNormalContext(params, destChain, forwarder, dispatcher, steps, requestConfig);
45776
47380
  const fees = appendForwarderFee(context.fees, context.forwardingFee);
45777
47381
  const mintResult = await executeMintStep({
45778
47382
  params,
@@ -45783,7 +47387,10 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45783
47387
  attestationSignature: context.attestationSignature,
45784
47388
  fees,
45785
47389
  steps,
45786
- dispatcher
47390
+ dispatcher,
47391
+ ...requestConfig ? {
47392
+ requestConfig
47393
+ } : {}
45787
47394
  });
45788
47395
  // A throwing `onBroadcast` must never strand the caller after a
45789
47396
  // successful mint — the chain has moved. `safeInvokeCallback` swallows
@@ -45800,14 +47407,14 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45800
47407
  /**
45801
47408
  * Resolve the spend context via the normal (non-retry) path.
45802
47409
  * Validates `from`, then delegates to `runSpendNormalPath`.
45803
- */ async function resolveNormalContext(params, destChain, forwarder, dispatcher, steps) {
47410
+ */ async function resolveNormalContext(params, destChain, forwarder, dispatcher, steps, requestConfig) {
45804
47411
  if (!params.from) {
45805
47412
  throw createValidationFailedError$1('from', params.from, 'Source (from) is required when config.retry is not provided.');
45806
47413
  }
45807
47414
  return runSpendNormalPath({
45808
47415
  ...params,
45809
47416
  from: params.from
45810
- }, destChain, forwarder, dispatcher, steps);
47417
+ }, destChain, forwarder, dispatcher, steps, requestConfig);
45811
47418
  }
45812
47419
  /**
45813
47420
  * Estimate fees for a Gateway spend without executing it.
@@ -45830,13 +47437,13 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45830
47437
  * })
45831
47438
  * fees.forEach((f) => console.log(f.type, f.amount, f.allocations))
45832
47439
  * ```
45833
- */ async function estimateSpend$1(params) {
47440
+ */ async function estimateSpend$1(params, requestConfig) {
45834
47441
  const destChain = resolveChainIdentifier(params.to.chain);
45835
47442
  assertGatewayV1(destChain);
45836
47443
  assertSpendAddresses(params, destChain);
45837
47444
  const forwarder = isForwarderEnabled(params.to);
45838
47445
  const recipientAddress = await resolveRecipientAddress(params.to);
45839
- const { allocations, estimatedIntents, forwardingFee } = await resolveValidatedIntents(params, destChain, recipientAddress, forwarder);
47446
+ const { allocations, estimatedIntents, forwardingFee } = await resolveValidatedIntents(params, destChain, recipientAddress, forwarder, requestConfig);
45840
47447
  const fees = computeEstimateFees(estimatedIntents, allocations, params.config?.customFee);
45841
47448
  if (forwardingFee) {
45842
47449
  fees.push({
@@ -45960,6 +47567,9 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45960
47567
  * the delegate is `'ready'` (finalized); otherwise it is `'pending'`.
45961
47568
  *
45962
47569
  * @param params - The adapter context (`from`), delegate address, and token.
47570
+ * @param requestConfig - Optional polling config overrides, including custom
47571
+ * `headers` forwarded with the `/v1/info` request used on the EVM finality
47572
+ * check.
45963
47573
  * @returns Promise resolving to `'none'`, `'pending'`, or `'ready'`.
45964
47574
  * @throws KitError if the chain is unsupported or `/v1/info` fails.
45965
47575
  *
@@ -45979,7 +47589,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45979
47589
  * const status = await getDelegateStatus(params)
45980
47590
  * if (status === 'ready') { // safe to spend }
45981
47591
  * ```
45982
- */ async function getDelegateStatus$1(params) {
47592
+ */ async function getDelegateStatus$1(params, requestConfig) {
45983
47593
  const { from, delegateAddress } = params;
45984
47594
  const { adapter } = from;
45985
47595
  const chain = resolveChainIdentifier(from.chain);
@@ -46019,7 +47629,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
46019
47629
  return String(finalizedResult).toLowerCase() === 'true' ? 'ready' : 'pending';
46020
47630
  }
46021
47631
  // EVM: use processedHeight from /v1/info
46022
- const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain);
47632
+ const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain, requestConfig);
46023
47633
  // If the RPC node lags Gateway's indexer view, the historical read at
46024
47634
  // processedHeight may throw a block-range error. This is safe to treat
46025
47635
  // as 'pending' because processedHeight comes from Gateway's /v1/info
@@ -46237,6 +47847,35 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46237
47847
  dispatchEvent(dispatcher, opName, 'failed', chain, extractErrorInfo(error));
46238
47848
  }
46239
47849
 
47850
+ // ---------------------------------------------------------------------------
47851
+ // Config validation
47852
+ // ---------------------------------------------------------------------------
47853
+ function isPlainObject(value) {
47854
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
47855
+ return false;
47856
+ }
47857
+ const prototype = Object.getPrototypeOf(value);
47858
+ return prototype === Object.prototype || prototype === null;
47859
+ }
47860
+ function assertHeadersConfig(headers) {
47861
+ if (headers === undefined) {
47862
+ return;
47863
+ }
47864
+ if (!isPlainObject(headers)) {
47865
+ throw createValidationFailedError$1('config.headers', headers, 'config.headers must be a plain object with string header values when provided');
47866
+ }
47867
+ for (const [name, value] of Object.entries(headers)){
47868
+ if (typeof value !== 'string') {
47869
+ throw createValidationFailedError$1(`config.headers.${name}`, value, 'header values must be strings');
47870
+ }
47871
+ }
47872
+ }
47873
+ function assertGatewayV1Config(config) {
47874
+ if (!isPlainObject(config)) {
47875
+ throw createValidationFailedError$1('config', config, 'config must be a plain object when provided');
47876
+ }
47877
+ assertHeadersConfig(config['headers']);
47878
+ }
46240
47879
  // ---------------------------------------------------------------------------
46241
47880
  // Factory
46242
47881
  // ---------------------------------------------------------------------------
@@ -46246,7 +47885,12 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46246
47885
  * Returns a plain object that implements {@link IGatewayProvider} by
46247
47886
  * delegating to the standalone functions in `operations/`.
46248
47887
  *
47888
+ * @param config - Optional provider configuration. Supply `headers` to
47889
+ * forward custom HTTP headers with every Gateway API request. See
47890
+ * {@link GatewayV1Config}.
46249
47891
  * @returns An {@link IGatewayProvider} instance.
47892
+ * @throws Throws when `config` or `config.headers` is not a plain object, or a
47893
+ * header value is not a string.
46250
47894
  *
46251
47895
  * @example
46252
47896
  * ```typescript
@@ -46255,7 +47899,23 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46255
47899
  * const provider = createGatewayV1Provider()
46256
47900
  * const chains = provider.getSupportedChains()
46257
47901
  * ```
46258
- */ function createGatewayV1Provider() {
47902
+ *
47903
+ * @example
47904
+ * ```typescript
47905
+ * // Forward a custom header on every Gateway API request.
47906
+ * const provider = createGatewayV1Provider({
47907
+ * headers: { 'X-Access-Key': process.env.GATEWAY_ACCESS_KEY! },
47908
+ * })
47909
+ * ```
47910
+ */ function createGatewayV1Provider(config = {}) {
47911
+ assertGatewayV1Config(config);
47912
+ // Captured once and threaded to every Gateway API call the operations make,
47913
+ // so a configured header reaches balances, estimate, transfer, forwarder
47914
+ // status, and /v1/info alike. Left undefined when no headers are configured
47915
+ // so the operations keep their existing defaults untouched.
47916
+ const requestConfig = config.headers ? {
47917
+ headers: config.headers
47918
+ } : undefined;
46259
47919
  /**
46260
47920
  * Execute an operation with started/succeeded/failed event dispatch.
46261
47921
  */ async function withEvents(opName, chain, startedData, params, fn) {
@@ -46286,21 +47946,25 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46286
47946
  }, params, depositFor$1),
46287
47947
  spend: async (params, options)=>{
46288
47948
  const destChainName = extractChainInfo(params.to.chain).name;
47949
+ const mergedOptions = requestConfig ? {
47950
+ ...options,
47951
+ requestConfig
47952
+ } : options;
46289
47953
  return withEvents('spend', destChainName, {
46290
47954
  destinationChain: destChainName,
46291
47955
  token: params.token ?? 'USDC'
46292
- }, params, async (p)=>spend$1(p, provider.actionDispatcher, options));
47956
+ }, params, async (p)=>spend$1(p, provider.actionDispatcher, mergedOptions));
46293
47957
  },
46294
47958
  estimateSpend: async (params)=>{
46295
47959
  const destChainName = extractChainInfo(params.to.chain).name;
46296
47960
  return withEvents('estimateSpend', destChainName, {
46297
47961
  destinationChain: destChainName,
46298
47962
  token: params.token ?? 'USDC'
46299
- }, params, estimateSpend$1);
47963
+ }, params, async (p)=>estimateSpend$1(p, requestConfig));
46300
47964
  },
46301
47965
  getBalances: async (params)=>withEvents('getBalances', 'aggregate', {
46302
47966
  token: params.token ?? 'USDC'
46303
- }, params, getBalances$1),
47967
+ }, params, async (p)=>getBalances$1(p, requestConfig)),
46304
47968
  addDelegate: async (params)=>withEvents('addDelegate', extractChainInfo(params.from.chain).name, {
46305
47969
  delegateAddress: params.delegateAddress
46306
47970
  }, params, addDelegate$1),
@@ -46318,7 +47982,9 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46318
47982
  // in withEvents. getSupportedChains is synchronous. getDelegateStatus is a
46319
47983
  // lightweight read that consumers treat as a utility check, unlike
46320
47984
  // getBalances which involves API polling and benefits from lifecycle events.
46321
- getDelegateStatus: getDelegateStatus$1,
47985
+ // getDelegateStatus is wrapped to forward the provider's request config to
47986
+ // its /v1/info call; getSupportedChains makes no API call.
47987
+ getDelegateStatus: async (params)=>getDelegateStatus$1(params, requestConfig),
46322
47988
  getSupportedChains: getSupportedChains$1
46323
47989
  };
46324
47990
  return provider;
@@ -46328,6 +47994,7 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46328
47994
  * The default providers that will be used in addition to the providers
46329
47995
  * provided to the createUnifiedBalanceKitContext factory function.
46330
47996
  *
47997
+ * @param config - Optional configuration forwarded to the default provider.
46331
47998
  * @returns A readonly tuple with one element: the default Gateway v1 provider
46332
47999
  * instance created via {@link createGatewayV1Provider}.
46333
48000
  * @internal
@@ -46339,8 +48006,10 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46339
48006
  * const { providers } = createUnifiedBalanceKitContext()
46340
48007
  * // providers[0] is the default Gateway v1 instance from getDefaultProviders()
46341
48008
  * ```
46342
- */ const getDefaultProviders = ()=>[
46343
- createGatewayV1Provider()
48009
+ */ const getDefaultProviders = (config = {})=>[
48010
+ createGatewayV1Provider(config.headers ? {
48011
+ headers: config.headers
48012
+ } : {})
46344
48013
  ];
46345
48014
  function createUnifiedBalanceKitContext(config = {}) {
46346
48015
  // Runtime guard: `excludeDefaultProviders: true` with no `providers` is
@@ -46354,7 +48023,10 @@ function createUnifiedBalanceKitContext(config = {}) {
46354
48023
  // Initialize default providers — either the canonical singleton tuple or
46355
48024
  // an empty list when the caller has opted out. Typed loosely here; each
46356
48025
  // public overload narrows the return type for callers.
46357
- const defaultProviders = config.excludeDefaultProviders === true ? [] : getDefaultProviders();
48026
+ const defaultProvidersConfig = config.headers ? {
48027
+ headers: config.headers
48028
+ } : {};
48029
+ const defaultProviders = config.excludeDefaultProviders === true ? [] : getDefaultProviders(defaultProvidersConfig);
46358
48030
  const providers = [
46359
48031
  ...defaultProviders,
46360
48032
  ...config.providers ?? []
@@ -47045,29 +48717,65 @@ function sumAllocationsFromSources(sources) {
47045
48717
  }
47046
48718
  });
47047
48719
 
48720
+ function normalizeAddressIdentity(value) {
48721
+ if (typeof value !== 'string') return undefined;
48722
+ try {
48723
+ return convertAddress(value, 'bytes32').toLowerCase();
48724
+ } catch {
48725
+ // Address validation below owns the structured error for invalid input.
48726
+ return value;
48727
+ }
48728
+ }
47048
48729
  /**
47049
- * Reject `from` arrays containing the same adapter reference more than once.
47050
- * Must run on the raw input (before Zod parsing) because `z.object()` creates
47051
- * new wrapper objects and identity (`===`) is lost.
48730
+ * Reject `from` arrays that describe the same spend source more than once.
48731
+ *
48732
+ * One adapter instance may legitimately be reused across distinct Gateway
48733
+ * source accounts. When `sourceAccount` is omitted, the signer address
48734
+ * identifies the source. Must run on the raw input (before Zod parsing)
48735
+ * because `z.object()` creates new wrapper objects and adapter identity
48736
+ * (`===`) is lost.
47052
48737
  *
47053
48738
  * @internal
47054
- */ function assertNoDuplicateAdapters(params) {
48739
+ */ function assertNoDuplicateSpendSources(params) {
47055
48740
  if (params == null || typeof params !== 'object') return;
47056
48741
  const from = params['from'];
47057
48742
  if (!Array.isArray(from) || from.length < 2) return;
47058
- const seen = new Set();
48743
+ const seen = new Map();
47059
48744
  for (const source of from){
47060
48745
  if (source == null || typeof source !== 'object') continue;
47061
- const adapter = source['adapter'];
48746
+ const record = source;
48747
+ const adapter = record['adapter'];
47062
48748
  if (adapter == null) continue;
47063
- if (seen.has(adapter)) {
48749
+ const sourceAccount = normalizeAddressIdentity(record['sourceAccount']) ?? normalizeAddressIdentity(record['address']);
48750
+ let sourceAccounts = seen.get(adapter);
48751
+ if (sourceAccounts == null) {
48752
+ sourceAccounts = new Set();
48753
+ seen.set(adapter, sourceAccounts);
48754
+ }
48755
+ if (sourceAccounts.has(sourceAccount)) {
47064
48756
  throw new KitError({
47065
48757
  ...InputError.VALIDATION_FAILED,
47066
48758
  recoverability: 'FATAL',
47067
- message: 'Duplicate adapter instance detected in from array. ' + 'Each source must use a distinct adapter object.'
48759
+ message: 'Duplicate spend source detected in from array. The same adapter ' + 'and Gateway source account are reused more than once.'
47068
48760
  });
47069
48761
  }
47070
- seen.add(adapter);
48762
+ sourceAccounts.add(sourceAccount);
48763
+ }
48764
+ }
48765
+ function assertSourceAddressContexts(from) {
48766
+ const sources = Array.isArray(from) ? from : [
48767
+ from
48768
+ ];
48769
+ for (const raw of sources){
48770
+ if (raw == null || typeof raw !== 'object') continue;
48771
+ const source = raw;
48772
+ const addressContext = source.adapter?.capabilities?.addressContext;
48773
+ if (addressContext === 'developer-controlled' && (typeof source.address !== 'string' || source.address === '')) {
48774
+ throw createValidationFailedError$1('address', source.address, 'Address is required for developer-controlled adapters.');
48775
+ }
48776
+ if (addressContext === 'user-controlled' && source.address !== undefined) {
48777
+ throw createValidationFailedError$1('address', source.address, 'Address should not be provided for user-controlled adapters. ' + 'The address is automatically resolved from the connected wallet.');
48778
+ }
47071
48779
  }
47072
48780
  }
47073
48781
  /**
@@ -47081,14 +48789,17 @@ function sumAllocationsFromSources(sources) {
47081
48789
  * @throws \{KitError\} When validation fails; error code is
47082
48790
  * {@link InputError.VALIDATION_FAILED}.
47083
48791
  */ function assertSpendParams(params) {
47084
- assertNoDuplicateAdapters(params);
48792
+ assertNoDuplicateSpendSources(params);
48793
+ if (params != null && typeof params === 'object' && 'from' in params) {
48794
+ assertSourceAddressContexts(params['from']);
48795
+ }
47085
48796
  const result = spendParamsSchema.safeParse(params);
47086
48797
  if (!result.success) {
47087
48798
  throw convertZodErrorToStructured(result.error, params);
47088
48799
  }
47089
48800
  const data = result.data;
47090
48801
  // Validate recipientAddress against the destination chain
47091
- if (data.to.recipientAddress) {
48802
+ if (data.to.recipientAddress !== undefined && data.to.recipientAddress !== '') {
47092
48803
  const toChain = data.to.chain;
47093
48804
  if (!isValidAddressForChain(data.to.recipientAddress, toChain)) {
47094
48805
  const chainInfo = extractChainInfo(toChain);
@@ -47096,7 +48807,7 @@ function sumAllocationsFromSources(sources) {
47096
48807
  }
47097
48808
  }
47098
48809
  // Validate sourceAccount against each source's allocation chain
47099
- if (data.from) {
48810
+ if (data.from !== undefined) {
47100
48811
  assertSourceAccountAddresses(data.from);
47101
48812
  }
47102
48813
  }
@@ -47111,11 +48822,15 @@ function assertSourceAccountAddresses(from) {
47111
48822
  from
47112
48823
  ];
47113
48824
  for (const raw of sources){
47114
- if (!raw || typeof raw !== 'object' || !('sourceAccount' in raw)) continue;
48825
+ if (raw === null || typeof raw !== 'object' || !('sourceAccount' in raw)) {
48826
+ continue;
48827
+ }
47115
48828
  const source = raw;
47116
- if (!source.sourceAccount) continue;
48829
+ if (source.sourceAccount === undefined || source.sourceAccount === '') {
48830
+ continue;
48831
+ }
47117
48832
  const sourceChain = getSourceChainFromAllocations(source.allocations);
47118
- if (!sourceChain) continue;
48833
+ if (sourceChain === undefined) continue;
47119
48834
  if (!isValidAddressForChain(source.sourceAccount, sourceChain)) {
47120
48835
  const chainInfo = extractChainInfo(sourceChain);
47121
48836
  throw createInvalidAddressError(source.sourceAccount, String(chainInfo.name), chainInfo.expectedAddressFormat);
@@ -49315,6 +51030,11 @@ function assertAppKitCustomFeePolicyScope(operation) {
49315
51030
  assertAppKitCustomFeePolicy(config.customFeePolicy);
49316
51031
  }
49317
51032
  const unifiedBalance = new AppKitUnifiedBalance({
51033
+ // Top-level headers act as the shared default; an explicit
51034
+ // `unifiedBalance.headers` overrides them by coming later in the spread.
51035
+ ...config.headers != null && {
51036
+ headers: config.headers
51037
+ },
49318
51038
  ...config.unifiedBalance,
49319
51039
  ...config.disableErrorReporting != null && {
49320
51040
  disableErrorReporting: config.disableErrorReporting
@@ -49417,30 +51137,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49417
51137
  */ async retryBridge(result, retryContext) {
49418
51138
  return retryBridge(this.context, result, retryContext);
49419
51139
  }
49420
- /**
49421
- * Estimate the bridge operation.
49422
- *
49423
- * Calculates gas costs, protocol fees, and optional custom fees for a
49424
- * cross-chain bridge transfer without executing the transaction. Useful
49425
- * for displaying cost estimates to users before they confirm a transfer.
49426
- *
49427
- * @param params - Bridge parameters containing source, destination, amount, and token
49428
- * @returns Promise resolving to the estimate result with fee information
49429
- * @throws If bridge parameters are invalid
49430
- * @throws If the bridge route is not supported
49431
- *
49432
- * @example
49433
- * ```typescript
49434
- * const estimate = await kit.estimateBridge({
49435
- * from: { adapter: sourceAdapter, chain: 'Ethereum' },
49436
- * to: { adapter: destAdapter, chain: 'Polygon' },
49437
- * amount: '100.50',
49438
- * token: 'USDC'
49439
- * })
49440
- *
49441
- * console.log('Estimated fees:', estimate.fees)
49442
- * ```
49443
- */ async estimateBridge(params) {
51140
+ async estimateBridge(params) {
49444
51141
  return estimateBridge(this.context, params);
49445
51142
  }
49446
51143
  /**
@@ -49556,7 +51253,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49556
51253
  * config: {
49557
51254
  * slippageBps: 300, // 3% slippage
49558
51255
  * allowanceStrategy: 'permit',
49559
- * kitKey: 'KIT_KEY:id:secret'
51256
+ * apiKey: 'TEST_API_KEY:id:secret'
49560
51257
  * }
49561
51258
  * })
49562
51259
  *
@@ -49585,7 +51282,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49585
51282
  * amountIn: '100.50',
49586
51283
  * config: {
49587
51284
  * slippageBps: 300,
49588
- * kitKey: 'KIT_KEY:id:secret'
51285
+ * apiKey: 'TEST_API_KEY:id:secret'
49589
51286
  * }
49590
51287
  * })
49591
51288
  *
@@ -49609,7 +51306,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49609
51306
  * not write the polling loop yourself.
49610
51307
  *
49611
51308
  * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
49612
- * `kitKey`.
51309
+ * `apiKey`.
49613
51310
  * @returns A snapshot of the swap's status at the time of the call.
49614
51311
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
49615
51312
  *
@@ -49622,7 +51319,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49622
51319
  * txHash: result.txHash,
49623
51320
  * chainIn: result.chainIn,
49624
51321
  * chainOut: result.chainOut,
49625
- * kitKey: process.env.KIT_KEY ?? '',
51322
+ * apiKey: process.env.CIRCLE_API_KEY,
49626
51323
  * })
49627
51324
  *
49628
51325
  * console.log(status.progress.status, status.progress.substatus)
@@ -49635,7 +51332,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49635
51332
  * txHash: result.txHash,
49636
51333
  * chainIn: result.chainIn,
49637
51334
  * chainOut: result.chainOut,
49638
- * kitKey: process.env.KIT_KEY ?? '',
51335
+ * apiKey: process.env.CIRCLE_API_KEY,
49639
51336
  * })
49640
51337
  * while (status.progress.status === 'PENDING') {
49641
51338
  * await new Promise((r) => setTimeout(r, 3_000))
@@ -49643,7 +51340,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49643
51340
  * txHash: result.txHash,
49644
51341
  * chainIn: result.chainIn,
49645
51342
  * chainOut: result.chainOut,
49646
- * kitKey: process.env.KIT_KEY ?? '',
51343
+ * apiKey: process.env.CIRCLE_API_KEY,
49647
51344
  * })
49648
51345
  * }
49649
51346
  * ```
@@ -49662,7 +51359,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49662
51359
  * expires. Timeouts surface as a RETRYABLE `KitError` so callers can
49663
51360
  * re-invoke with the same `txHash`.
49664
51361
  *
49665
- * @param params - Wait configuration: identifiers, kit key, and optional
51362
+ * @param params - Wait configuration: identifiers, API key, and optional
49666
51363
  * `timeoutMs` / `onProgress`.
49667
51364
  * @returns The first terminal {@link SwapStatusResult} observed.
49668
51365
  * @throws \{KitError\} (`NETWORK.TIMEOUT`, RETRYABLE) when the wait
@@ -49675,7 +51372,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49675
51372
  *
49676
51373
  * const final = await kit.waitForSwap({
49677
51374
  * result,
49678
- * kitKey: process.env.KIT_KEY ?? '',
51375
+ * apiKey: process.env.CIRCLE_API_KEY,
49679
51376
  * onProgress: (snap) => console.log(snap.progress.status),
49680
51377
  * })
49681
51378
  *
@@ -49691,7 +51388,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49691
51388
  * txHash: persisted.txHash,
49692
51389
  * chainIn: persisted.chainIn,
49693
51390
  * chainOut: persisted.chainOut,
49694
- * kitKey: process.env.KIT_KEY ?? '',
51391
+ * apiKey: process.env.CIRCLE_API_KEY,
49695
51392
  * })
49696
51393
  * ```
49697
51394
  */ async waitForSwap(params) {
@@ -49723,7 +51420,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49723
51420
  * translates to the chain's native sentinel address — `0xEee…` for EVM,
49724
51421
  * `1111…` for Solana — before querying the service.
49725
51422
  *
49726
- * @param params - `chain`, plus optional `tokens` and `kitKey`.
51423
+ * @param params - `chain`, plus optional `tokens` and `apiKey`.
49727
51424
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
49728
51425
  * @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
49729
51426
  * entries, or any entry is neither a registered symbol nor a
@@ -49734,7 +51431,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49734
51431
  * const { rates } = await kit.getTokenRates({
49735
51432
  * chain: 'Ethereum',
49736
51433
  * tokens: ['USDC', 'EURC'],
49737
- * kitKey: process.env.KIT_KEY ?? '',
51434
+ * apiKey: process.env.CIRCLE_API_KEY,
49738
51435
  * })
49739
51436
  *
49740
51437
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -49743,38 +51440,8 @@ function assertAppKitCustomFeePolicyScope(operation) {
49743
51440
  */ async getTokenRates(params) {
49744
51441
  return getTokenRates(this.context, params);
49745
51442
  }
49746
- /**
49747
- * Get chains supported by AppKit operations.
49748
- *
49749
- * Returns blockchain networks that support specific stablecoin operations.
49750
- * When no operation type is specified, returns all chains supporting any
49751
- * operation (bridge, swap, earn, or unified balance).
49752
- *
49753
- * @param operationType - Optional operation type to filter chains ('bridge' | 'swap' | 'earn' | 'unifiedBalance')
49754
- * @returns Array of unique chain definitions supporting the specified operation(s)
49755
- *
49756
- * @example Get all supported chains
49757
- * ```typescript
49758
- * import { AppKit } from '@circle-fin/app-kit'
49759
- *
49760
- * const kit = new AppKit()
49761
- * const allChains = kit.getSupportedChains()
49762
- *
49763
- * console.log(`Total supported chains: ${allChains.length}`)
49764
- * allChains.forEach(chain => {
49765
- * console.log(`- ${chain.name} (${chain.type})`)
49766
- * })
49767
- * ```
49768
- *
49769
- * @example Get bridge-specific chains
49770
- * ```typescript
49771
- * const kit = new AppKit()
49772
- * const bridgeChains = kit.getSupportedChains('bridge')
49773
- *
49774
- * console.log('Chains supporting bridge:', bridgeChains.map(c => c.name))
49775
- * ```
49776
- */ getSupportedChains(operationType) {
49777
- return getSupportedChains$2(this.context, operationType, this.unifiedBalance);
51443
+ getSupportedChains(operationType, options) {
51444
+ return getSupportedChains$2(this.context, operationType, this.unifiedBalance, options);
49778
51445
  }
49779
51446
  /**
49780
51447
  * Set operation-scoped custom fee policies.