@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.mjs CHANGED
@@ -30,9 +30,9 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
30
30
  import { z } from 'zod';
31
31
  import pino from 'pino';
32
32
  import { parseUnits as parseUnits$1, formatUnits as formatUnits$1 } from '@ethersproject/units';
33
- import { hexlify, hexZeroPad } from '@ethersproject/bytes';
33
+ import { hexlify, hexZeroPad, concat } from '@ethersproject/bytes';
34
34
  import '@ethersproject/abi';
35
- import { getAddress } from '@ethersproject/address';
35
+ import { getAddress, isAddress } from '@ethersproject/address';
36
36
  import bs58 from 'bs58';
37
37
  import { PublicKey } from '@solana/web3.js';
38
38
  import 'bn.js';
@@ -4064,6 +4064,8 @@ function getOptionalString(value) {
4064
4064
  Blockchain["Optimism_Sepolia"] = "Optimism_Sepolia";
4065
4065
  Blockchain["Pharos"] = "Pharos";
4066
4066
  Blockchain["Pharos_Testnet"] = "Pharos_Testnet";
4067
+ Blockchain["Plasma"] = "Plasma";
4068
+ Blockchain["Plasma_Testnet"] = "Plasma_Testnet";
4067
4069
  Blockchain["Polkadot_Asset_Hub"] = "Polkadot_Asset_Hub";
4068
4070
  Blockchain["Polkadot_Westmint"] = "Polkadot_Westmint";
4069
4071
  Blockchain["Plume"] = "Plume";
@@ -4133,6 +4135,7 @@ var BridgeChain;
4133
4135
  BridgeChain["Morph"] = "Morph";
4134
4136
  BridgeChain["Optimism"] = "Optimism";
4135
4137
  BridgeChain["Pharos"] = "Pharos";
4138
+ BridgeChain["Plasma"] = "Plasma";
4136
4139
  BridgeChain["Plume"] = "Plume";
4137
4140
  BridgeChain["Polygon"] = "Polygon";
4138
4141
  BridgeChain["Sei"] = "Sei";
@@ -4159,6 +4162,7 @@ var BridgeChain;
4159
4162
  BridgeChain["Morph_Testnet"] = "Morph_Testnet";
4160
4163
  BridgeChain["Optimism_Sepolia"] = "Optimism_Sepolia";
4161
4164
  BridgeChain["Pharos_Testnet"] = "Pharos_Testnet";
4165
+ BridgeChain["Plasma_Testnet"] = "Plasma_Testnet";
4162
4166
  BridgeChain["Plume_Testnet"] = "Plume_Testnet";
4163
4167
  BridgeChain["Polygon_Amoy_Testnet"] = "Polygon_Amoy_Testnet";
4164
4168
  BridgeChain["Sei_Testnet"] = "Sei_Testnet";
@@ -4616,6 +4620,8 @@ var EarnChain;
4616
4620
  * This program handles minting operations for Gateway transactions
4617
4621
  * on Solana devnet.
4618
4622
  */ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
4623
+ /** TokenMessengerWithFees address shared by enabled EVM mainnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
4624
+ /** TokenMessengerWithFees address shared by enabled EVM testnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
4619
4625
  /**
4620
4626
  * Circle's fee recipient address for EVM chains.
4621
4627
  *
@@ -4663,6 +4669,7 @@ var EarnChain;
4663
4669
  v2: {
4664
4670
  type: 'split',
4665
4671
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4672
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4666
4673
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4667
4674
  confirmations: 1,
4668
4675
  fastConfirmations: 1
@@ -4730,6 +4737,7 @@ var EarnChain;
4730
4737
  v2: {
4731
4738
  type: 'split',
4732
4739
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4740
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4733
4741
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4734
4742
  confirmations: 65,
4735
4743
  fastConfirmations: 1
@@ -4794,6 +4802,7 @@ var EarnChain;
4794
4802
  v2: {
4795
4803
  type: 'split',
4796
4804
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4805
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4797
4806
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4798
4807
  confirmations: 65,
4799
4808
  fastConfirmations: 1
@@ -4858,6 +4867,7 @@ var EarnChain;
4858
4867
  v2: {
4859
4868
  type: 'split',
4860
4869
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4870
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4861
4871
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4862
4872
  confirmations: 1,
4863
4873
  fastConfirmations: 1
@@ -4919,6 +4929,7 @@ var EarnChain;
4919
4929
  v2: {
4920
4930
  type: 'split',
4921
4931
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
4932
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4922
4933
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
4923
4934
  confirmations: 1,
4924
4935
  fastConfirmations: 1
@@ -4986,6 +4997,7 @@ var EarnChain;
4986
4997
  v2: {
4987
4998
  type: 'split',
4988
4999
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5000
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4989
5001
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4990
5002
  confirmations: 65,
4991
5003
  fastConfirmations: 1
@@ -5050,6 +5062,7 @@ var EarnChain;
5050
5062
  v2: {
5051
5063
  type: 'split',
5052
5064
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5065
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5053
5066
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5054
5067
  confirmations: 65,
5055
5068
  fastConfirmations: 1
@@ -5160,6 +5173,7 @@ var EarnChain;
5160
5173
  v2: {
5161
5174
  type: 'split',
5162
5175
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5176
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5163
5177
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5164
5178
  confirmations: 65,
5165
5179
  fastConfirmations: 1
@@ -5204,6 +5218,7 @@ var EarnChain;
5204
5218
  v2: {
5205
5219
  type: 'split',
5206
5220
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5221
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5207
5222
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5208
5223
  confirmations: 65,
5209
5224
  fastConfirmations: 1
@@ -5435,6 +5450,7 @@ var EarnChain;
5435
5450
  v2: {
5436
5451
  type: 'split',
5437
5452
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5453
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5438
5454
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5439
5455
  confirmations: 65,
5440
5456
  fastConfirmations: 2
@@ -5499,6 +5515,7 @@ var EarnChain;
5499
5515
  v2: {
5500
5516
  type: 'split',
5501
5517
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5518
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5502
5519
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5503
5520
  confirmations: 65,
5504
5521
  fastConfirmations: 2
@@ -5609,6 +5626,7 @@ var EarnChain;
5609
5626
  v2: {
5610
5627
  type: 'split',
5611
5628
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5629
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5612
5630
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5613
5631
  confirmations: 1,
5614
5632
  fastConfirmations: 1
@@ -5668,6 +5686,7 @@ var EarnChain;
5668
5686
  v2: {
5669
5687
  type: 'split',
5670
5688
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5689
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5671
5690
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5672
5691
  confirmations: 1,
5673
5692
  fastConfirmations: 1
@@ -5822,6 +5841,7 @@ var EarnChain;
5822
5841
  v2: {
5823
5842
  type: 'split',
5824
5843
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5844
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5825
5845
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5826
5846
  confirmations: 65,
5827
5847
  fastConfirmations: 1
@@ -5869,6 +5889,7 @@ var EarnChain;
5869
5889
  v2: {
5870
5890
  type: 'split',
5871
5891
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5892
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5872
5893
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5873
5894
  confirmations: 65,
5874
5895
  fastConfirmations: 1
@@ -5913,6 +5934,7 @@ var EarnChain;
5913
5934
  v2: {
5914
5935
  type: 'split',
5915
5936
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5937
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5916
5938
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5917
5939
  confirmations: 1,
5918
5940
  fastConfirmations: 1
@@ -5958,6 +5980,7 @@ var EarnChain;
5958
5980
  v2: {
5959
5981
  type: 'split',
5960
5982
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
5983
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5961
5984
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
5962
5985
  confirmations: 1,
5963
5986
  fastConfirmations: 1
@@ -6004,6 +6027,7 @@ var EarnChain;
6004
6027
  v2: {
6005
6028
  type: 'split',
6006
6029
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6030
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6007
6031
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6008
6032
  confirmations: 1,
6009
6033
  fastConfirmations: 1
@@ -6317,6 +6341,7 @@ var EarnChain;
6317
6341
  v2: {
6318
6342
  type: 'split',
6319
6343
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6344
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6320
6345
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6321
6346
  confirmations: 65,
6322
6347
  fastConfirmations: 1
@@ -6381,6 +6406,7 @@ var EarnChain;
6381
6406
  v2: {
6382
6407
  type: 'split',
6383
6408
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
6409
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6384
6410
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
6385
6411
  confirmations: 65,
6386
6412
  fastConfirmations: 1
@@ -6501,6 +6527,98 @@ var EarnChain;
6501
6527
  }
6502
6528
  });
6503
6529
 
6530
+ /**
6531
+ * Plasma Mainnet chain definition
6532
+ * @remarks
6533
+ * This represents the official production network for the Plasma blockchain.
6534
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
6535
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
6536
+ */ const Plasma = defineChain({
6537
+ type: 'evm',
6538
+ chain: Blockchain.Plasma,
6539
+ name: 'Plasma',
6540
+ title: 'Plasma Mainnet',
6541
+ nativeCurrency: {
6542
+ name: 'Plasma',
6543
+ symbol: 'XPL',
6544
+ decimals: 18
6545
+ },
6546
+ chainId: 9745,
6547
+ isTestnet: false,
6548
+ explorerUrl: 'https://plasmascan.to/tx/{hash}',
6549
+ rpcEndpoints: [
6550
+ 'https://rpc.plasma.to'
6551
+ ],
6552
+ eurcAddress: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
6553
+ usdcAddress: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
6554
+ usdtAddress: null,
6555
+ cctp: {
6556
+ domain: 33,
6557
+ contracts: {
6558
+ v2: {
6559
+ type: 'split',
6560
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6561
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6562
+ confirmations: 3,
6563
+ fastConfirmations: 1
6564
+ }
6565
+ },
6566
+ forwarderSupported: {
6567
+ source: false,
6568
+ destination: false
6569
+ }
6570
+ },
6571
+ kitContracts: {
6572
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
6573
+ }
6574
+ });
6575
+
6576
+ /**
6577
+ * Plasma Testnet chain definition
6578
+ * @remarks
6579
+ * This represents the official test network for the Plasma blockchain.
6580
+ * Plasma is an EVM-equivalent Layer 1 blockchain purpose-built for global
6581
+ * stablecoin payments, with deterministic BFT finality (PlasmaBFT/Fast-HotStuff).
6582
+ */ const PlasmaTestnet = defineChain({
6583
+ type: 'evm',
6584
+ chain: Blockchain.Plasma_Testnet,
6585
+ name: 'Plasma Testnet',
6586
+ title: 'Plasma Testnet',
6587
+ nativeCurrency: {
6588
+ name: 'Plasma',
6589
+ symbol: 'XPL',
6590
+ decimals: 18
6591
+ },
6592
+ chainId: 9746,
6593
+ isTestnet: true,
6594
+ explorerUrl: 'https://testnet.plasmascan.to/tx/{hash}',
6595
+ rpcEndpoints: [
6596
+ 'https://testnet-rpc.plasma.to'
6597
+ ],
6598
+ eurcAddress: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330',
6599
+ usdcAddress: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
6600
+ usdtAddress: null,
6601
+ cctp: {
6602
+ domain: 33,
6603
+ contracts: {
6604
+ v2: {
6605
+ type: 'split',
6606
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6607
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6608
+ confirmations: 3,
6609
+ fastConfirmations: 1
6610
+ }
6611
+ },
6612
+ forwarderSupported: {
6613
+ source: false,
6614
+ destination: false
6615
+ }
6616
+ },
6617
+ kitContracts: {
6618
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
6619
+ }
6620
+ });
6621
+
6504
6622
  /**
6505
6623
  * Plume Mainnet chain definition
6506
6624
  * @remarks
@@ -6532,6 +6650,7 @@ var EarnChain;
6532
6650
  v2: {
6533
6651
  type: 'split',
6534
6652
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6653
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6535
6654
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6536
6655
  confirmations: 65,
6537
6656
  fastConfirmations: 1
@@ -6578,6 +6697,7 @@ var EarnChain;
6578
6697
  v2: {
6579
6698
  type: 'split',
6580
6699
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6700
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6581
6701
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6582
6702
  confirmations: 65,
6583
6703
  fastConfirmations: 1
@@ -6679,6 +6799,7 @@ var EarnChain;
6679
6799
  v2: {
6680
6800
  type: 'split',
6681
6801
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6802
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6682
6803
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6683
6804
  confirmations: 33,
6684
6805
  fastConfirmations: 13
@@ -6744,6 +6865,7 @@ var EarnChain;
6744
6865
  v2: {
6745
6866
  type: 'split',
6746
6867
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6868
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6747
6869
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6748
6870
  confirmations: 33,
6749
6871
  fastConfirmations: 13
@@ -6803,6 +6925,7 @@ var EarnChain;
6803
6925
  v2: {
6804
6926
  type: 'split',
6805
6927
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6928
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6806
6929
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6807
6930
  confirmations: 1,
6808
6931
  fastConfirmations: 1
@@ -6862,6 +6985,7 @@ var EarnChain;
6862
6985
  v2: {
6863
6986
  type: 'split',
6864
6987
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6988
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6865
6989
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6866
6990
  confirmations: 1,
6867
6991
  fastConfirmations: 1
@@ -6919,6 +7043,7 @@ var EarnChain;
6919
7043
  v2: {
6920
7044
  type: 'split',
6921
7045
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7046
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
6922
7047
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6923
7048
  confirmations: 1,
6924
7049
  fastConfirmations: 1
@@ -6977,6 +7102,7 @@ var EarnChain;
6977
7102
  v2: {
6978
7103
  type: 'split',
6979
7104
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7105
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
6980
7106
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6981
7107
  confirmations: 1,
6982
7108
  fastConfirmations: 1
@@ -7292,6 +7418,7 @@ var EarnChain;
7292
7418
  v2: {
7293
7419
  type: 'split',
7294
7420
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7421
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
7295
7422
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7296
7423
  confirmations: 65,
7297
7424
  fastConfirmations: 1
@@ -7356,6 +7483,7 @@ var EarnChain;
7356
7483
  v2: {
7357
7484
  type: 'split',
7358
7485
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7486
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
7359
7487
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
7360
7488
  confirmations: 65,
7361
7489
  fastConfirmations: 1
@@ -7413,6 +7541,7 @@ var EarnChain;
7413
7541
  v2: {
7414
7542
  type: 'split',
7415
7543
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cF5d',
7544
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
7416
7545
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7417
7546
  confirmations: 65,
7418
7547
  fastConfirmations: 1
@@ -7472,6 +7601,7 @@ var EarnChain;
7472
7601
  v2: {
7473
7602
  type: 'split',
7474
7603
  tokenMessenger: '0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa',
7604
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
7475
7605
  messageTransmitter: '0xe737e5cebeeba77efe34d4aa090756590b1ce275',
7476
7606
  confirmations: 65,
7477
7607
  fastConfirmations: 1
@@ -7532,6 +7662,7 @@ var EarnChain;
7532
7662
  v2: {
7533
7663
  type: 'split',
7534
7664
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7665
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
7535
7666
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7536
7667
  confirmations: 3,
7537
7668
  fastConfirmations: 3
@@ -7577,6 +7708,7 @@ var EarnChain;
7577
7708
  v2: {
7578
7709
  type: 'split',
7579
7710
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7711
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
7580
7712
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
7581
7713
  confirmations: 3,
7582
7714
  fastConfirmations: 1
@@ -7787,6 +7919,8 @@ var Chains = /*#__PURE__*/Object.freeze({
7787
7919
  OptimismSepolia: OptimismSepolia,
7788
7920
  Pharos: Pharos,
7789
7921
  PharosTestnet: PharosTestnet,
7922
+ Plasma: Plasma,
7923
+ PlasmaTestnet: PlasmaTestnet,
7790
7924
  Plume: Plume,
7791
7925
  PlumeTestnet: PlumeTestnet,
7792
7926
  PolkadotAssetHub: PolkadotAssetHub,
@@ -7836,6 +7970,34 @@ var Chains = /*#__PURE__*/Object.freeze({
7836
7970
  return chain.cctp?.contracts.v2 !== undefined;
7837
7971
  }
7838
7972
 
7973
+ /**
7974
+ * Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
7975
+ *
7976
+ * A chain supports source-paid fees when its CCTP v2 configuration carries a
7977
+ * deployed `TokenMessengerWithFees` wrapper address. Bridge Kit routes
7978
+ * `feePayment: 'source'` transfers through this wrapper via
7979
+ * `depositForBurnWithHookAndFees`, so a chain without the wrapper cannot be a
7980
+ * source for receive-exact bridging.
7981
+ *
7982
+ * @param chain - The chain definition to check.
7983
+ * @returns `true` when the chain has a `tokenMessengerWithFees` wrapper
7984
+ * configured, `false` otherwise.
7985
+ *
7986
+ * @example
7987
+ * ```typescript
7988
+ * import { Chains, hasSourceFeeSupport } from '@core/chains'
7989
+ *
7990
+ * hasSourceFeeSupport(Chains.Optimism) // true
7991
+ * hasSourceFeeSupport(Chains.Solana) // false
7992
+ * ```
7993
+ */ function hasSourceFeeSupport(chain) {
7994
+ if (!isCCTPV2Supported(chain)) {
7995
+ return false;
7996
+ }
7997
+ const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
7998
+ return typeof wrapper === 'string' && wrapper.length > 0;
7999
+ }
8000
+
7839
8001
  /**
7840
8002
  * Check if a chain supports a specific type of custom smart contract logic.
7841
8003
  *
@@ -10163,6 +10325,7 @@ function parseOrThrow(value, schema, context) {
10163
10325
  [Blockchain.Noble]: 'uusdc',
10164
10326
  [Blockchain.Optimism]: '0x0b2c639c533813f4aa9d7837caf62653d097ff85',
10165
10327
  [Blockchain.Pharos]: '0xC879C018dB60520F4355C26eD1a6D572cdAC1815',
10328
+ [Blockchain.Plasma]: '0x2d661C89D812261039AF9764eceaAee884f5F67F',
10166
10329
  [Blockchain.Plume]: '0x222365EF19F7947e5484218551B56bb3965Aa7aF',
10167
10330
  [Blockchain.Polkadot_Asset_Hub]: '1337',
10168
10331
  [Blockchain.Polygon]: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359',
@@ -10199,6 +10362,7 @@ function parseOrThrow(value, schema, context) {
10199
10362
  [Blockchain.Noble_Testnet]: 'uusdc',
10200
10363
  [Blockchain.Optimism_Sepolia]: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
10201
10364
  [Blockchain.Pharos_Testnet]: '0xcfC8330f4BCAB529c625D12781b1C19466A9Fc8B',
10365
+ [Blockchain.Plasma_Testnet]: '0xE67Fb267022cBA8064Dd388CC2FED724F3120D9D',
10202
10366
  [Blockchain.Plume_Testnet]: '0xcB5f30e335672893c7eb944B374c196392C19D18',
10203
10367
  [Blockchain.Polkadot_Westmint]: '31337',
10204
10368
  [Blockchain.Polygon_Amoy_Testnet]: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -10261,6 +10425,7 @@ function parseOrThrow(value, schema, context) {
10261
10425
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
10262
10426
  [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
10263
10427
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
10428
+ [Blockchain.Plasma]: '0x3EE196E78d4d4248b849B8E1C7F44C5457FAFD2C',
10264
10429
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
10265
10430
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
10266
10431
  // =========================================================================
@@ -10269,7 +10434,8 @@ function parseOrThrow(value, schema, context) {
10269
10434
  [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
10270
10435
  [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
10271
10436
  [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
10272
- [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
10437
+ [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4',
10438
+ [Blockchain.Plasma_Testnet]: '0x98AfA0F93Dd993B736399f9074eDcEBD1985A330'
10273
10439
  }
10274
10440
  };
10275
10441
 
@@ -11222,6 +11388,16 @@ function parseOrThrow(value, schema, context) {
11222
11388
  *
11223
11389
  * Set to 0 when no additional Circle-reserved data is needed.
11224
11390
  */ const CCTP_FORWARD_PAYLOAD_LENGTH = 0;
11391
+ /**
11392
+ * Length in bytes of a Solana owner (ed25519 / PDA) public key.
11393
+ */ const SOLANA_PUBKEY_LENGTH = 32;
11394
+ /**
11395
+ * Byte length of the Solana ATA-creation forwarding payload appended after the
11396
+ * `cctp-forward` frame: `createAta` (1 byte) + `ataOwner` (32 bytes).
11397
+ *
11398
+ * Circle's Orbit relayer decodes exactly this many bytes; see
11399
+ * {@link buildSolanaAtaForwardingHookData}.
11400
+ */ const SOLANA_ATA_FORWARD_PAYLOAD_LENGTH = 1 + SOLANA_PUBKEY_LENGTH;
11225
11401
  /**
11226
11402
  * Build the hookData bytes for CCTP forwarding.
11227
11403
  *
@@ -11278,6 +11454,108 @@ function buildForwardingHookData() {
11278
11454
  cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
11279
11455
  return cachedHookDataHex;
11280
11456
  }
11457
+ /**
11458
+ * Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
11459
+ * create the recipient's Associated Token Account (ATA) before minting on Solana.
11460
+ *
11461
+ * When an EVM→Solana bridge is forwarded, the destination mint targets the
11462
+ * recipient's USDC ATA — which does not exist for a fresh wallet. This frame
11463
+ * tells the relayer to prepend an idempotent `createAssociatedTokenAccount`
11464
+ * instruction (the relayer pays the rent) so the mint always succeeds.
11465
+ *
11466
+ * Unlike {@link buildForwardingHookData} (an empty version-0 frame), this emits
11467
+ * a version-0 frame whose 32-bit `dataLength` is set to
11468
+ * {@link SOLANA_ATA_FORWARD_PAYLOAD_LENGTH} (33), followed by the payload the
11469
+ * relayer decodes:
11470
+ * - Byte 0: `createAta` flag, always `1`
11471
+ * - Bytes 1-32: the recipient's 32-byte Solana owner public key (`ataOwner`)
11472
+ *
11473
+ * @remarks
11474
+ * `ataOwner` is the recipient's *wallet* public key, not the derived ATA. The
11475
+ * relayer re-derives the ATA from `ataOwner` and the USDC mint and requires it
11476
+ * to equal the burn's `mintRecipient`, so callers must pass the same owner used
11477
+ * to derive `mintRecipient`. The all-zero key is reserved as "absent owner" and
11478
+ * is rejected.
11479
+ *
11480
+ * @param ataOwner - The recipient's 32-byte Solana owner public key.
11481
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by the 33-byte
11482
+ * Solana ATA payload.
11483
+ * @throws {KitError} If `ataOwner` is not exactly 32 bytes, or is the all-zero
11484
+ * key (INPUT_VALIDATION_FAILED).
11485
+ *
11486
+ * @example
11487
+ * ```typescript
11488
+ * import { PublicKey } from '@solana/web3.js'
11489
+ * import { buildSolanaAtaForwardingHookData } from '@core/utils'
11490
+ *
11491
+ * const owner = new PublicKey('9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM')
11492
+ * const hookData = buildSolanaAtaForwardingHookData(owner.toBytes())
11493
+ *
11494
+ * // Use with the forwarded depositForBurnWithHook action so the relayer
11495
+ * // creates the recipient ATA before minting.
11496
+ * await adapter.prepareAction('cctp.v2.depositForBurnWithHook', {
11497
+ * amount: BigInt('1000000'),
11498
+ * mintRecipient: '0x...',
11499
+ * maxFee: BigInt('50000'),
11500
+ * minFinalityThreshold: 1000,
11501
+ * fromChain: ethereum,
11502
+ * toChain: solana,
11503
+ * hookData,
11504
+ * })
11505
+ * ```
11506
+ */ function buildSolanaAtaForwardingHookData(ataOwner) {
11507
+ if (!(ataOwner instanceof Uint8Array) || ataOwner.length !== SOLANA_PUBKEY_LENGTH) {
11508
+ throw createValidationFailedError$1('ataOwner', ataOwner, `Expected a ${String(SOLANA_PUBKEY_LENGTH)}-byte Solana owner public key`);
11509
+ }
11510
+ if (ataOwner.every((byte)=>byte === 0)) {
11511
+ throw createValidationFailedError$1('ataOwner', ataOwner, 'Expected a non-zero Solana owner public key; the all-zero key is reserved as "absent owner"');
11512
+ }
11513
+ // Inner payload: createAta(1) + ataOwner(32).
11514
+ const payload = new Uint8Array(SOLANA_ATA_FORWARD_PAYLOAD_LENGTH);
11515
+ payload[0] = 1 // createAta = true
11516
+ ;
11517
+ payload.set(ataOwner, 1);
11518
+ // 32-byte header: 24-byte magic + uint32 version(0) + uint32 dataLength(33).
11519
+ // The relayer reads dataLength from the v0 frame to slice the inner payload,
11520
+ // so it MUST reflect the appended byte count (unlike the GenericExecutor path).
11521
+ const frame = new Uint8Array(32);
11522
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
11523
+ const view = new DataView(frame.buffer);
11524
+ view.setUint32(24, CCTP_FORWARD_VERSION, false) // big-endian, 0
11525
+ ;
11526
+ view.setUint32(28, SOLANA_ATA_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 33
11527
+ ;
11528
+ return hexlify(concat([
11529
+ frame,
11530
+ payload
11531
+ ]));
11532
+ }
11533
+
11534
+ /**
11535
+ * Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
11536
+ *
11537
+ * Mirrors viem's `pad(address, size 32)` and CCTP's `mintRecipient`
11538
+ * convention. Solana addresses are already 32 bytes and need no padding.
11539
+ *
11540
+ * @param address - A 0x-prefixed 20-byte EVM address.
11541
+ * @returns The address left-zero-padded to a 0x-prefixed 32-byte hex string.
11542
+ * @throws {KitError} If `address` is not a valid EVM address (INPUT_VALIDATION_FAILED).
11543
+ *
11544
+ * @example
11545
+ * ```typescript
11546
+ * import { padAddressToBytes32 } from '@core/utils'
11547
+ *
11548
+ * padAddressToBytes32('0x75275Aff2D01699D922f045b69ed291311209738')
11549
+ * // '0x00000000000000000000000075275aff2d01699d922f045b69ed291311209738'
11550
+ * ```
11551
+ */ function padAddressToBytes32(address) {
11552
+ if (!isAddress(address)) {
11553
+ throw createValidationFailedError$1('address', address, 'Expected a valid 20-byte EVM address');
11554
+ }
11555
+ // bytes32 is raw bytes, not a checksummed address — emit lowercase so it
11556
+ // matches ABI-decoded output.
11557
+ return hexZeroPad(getAddress(address), 32).toLowerCase();
11558
+ }
11281
11559
 
11282
11560
  /**
11283
11561
  * Configuration for {@link retryAsync}.
@@ -11866,7 +12144,7 @@ function resolveOptions(options) {
11866
12144
  }
11867
12145
 
11868
12146
  var name$4 = "@circle-fin/bridge-kit";
11869
- var version$5 = "1.13.0";
12147
+ var version$5 = "1.14.0";
11870
12148
  var pkg$5 = {
11871
12149
  name: name$4,
11872
12150
  version: version$5};
@@ -12316,7 +12594,7 @@ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
12316
12594
  * const result = evmAddressSchema.safeParse(validAddress)
12317
12595
  * console.log(result.success) // true
12318
12596
  * ```
12319
- */ const evmAddressSchema = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
12597
+ */ const evmAddressSchema$1 = hexStringSchema.refine((value)=>value.length === 42, 'EVM address must be exactly 42 characters long (0x + 40 hex characters)').transform((value)=>value);
12320
12598
  /**
12321
12599
  * Schema for validating transaction hashes.
12322
12600
  *
@@ -13005,6 +13283,10 @@ var TransferSpeed;
13005
13283
  token: z.literal('USDC').optional(),
13006
13284
  config: z.object({
13007
13285
  transferSpeed: z.nativeEnum(TransferSpeed).optional(),
13286
+ feePayment: z.enum([
13287
+ 'source',
13288
+ 'destination'
13289
+ ]).optional(),
13008
13290
  maxFee: z.string().min(1, 'Required').pipe(createDecimalStringValidator({
13009
13291
  allowZero: true,
13010
13292
  regexMessage: MAX_FEE_FORMAT_ERROR_MESSAGE,
@@ -13012,7 +13294,8 @@ var TransferSpeed;
13012
13294
  maxDecimals: 6
13013
13295
  })(z.string())).optional(),
13014
13296
  customFee: customFeeSchema.optional()
13015
- }).optional()
13297
+ }).optional(),
13298
+ quote: z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex').optional()
13016
13299
  });
13017
13300
 
13018
13301
  /**
@@ -14226,16 +14509,73 @@ var TransferSpeed;
14226
14509
  };
14227
14510
  }
14228
14511
 
14512
+ /**
14513
+ * Dispatch a bridge step event through the provider's action dispatcher.
14514
+ *
14515
+ * Constructs the appropriate action payload and dispatches it to any registered
14516
+ * event listeners. Handles type-safe dispatching for different step types.
14517
+ * When provided, traceId from the invocation context is included for end-to-end correlation.
14518
+ *
14519
+ * @param name - The step name (approve, burn, fetchAttestation, or mint).
14520
+ * @param step - The completed bridge step containing transaction details and explorerUrl.
14521
+ * @param provider - The CCTP v2 provider with action dispatcher.
14522
+ * @param invocation - Optional invocation context containing traceId for correlation.
14523
+ *
14524
+ * @example
14525
+ * ```typescript
14526
+ * const step: BridgeStep = {
14527
+ * name: 'burn',
14528
+ * state: 'success',
14529
+ * txHash: '0xabc...',
14530
+ * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
14531
+ * data: { ... }
14532
+ * }
14533
+ * dispatchStepEvent('burn', step, provider, invocationContext)
14534
+ * ```
14535
+ */ function dispatchStepEvent(name, step, provider, invocation) {
14536
+ if (!provider.actionDispatcher) {
14537
+ return;
14538
+ }
14539
+ // Extract traceId from invocation context if provided
14540
+ const traceId = invocation?.traceId;
14541
+ const actionValues = {
14542
+ protocol: 'cctp',
14543
+ version: 'v2',
14544
+ ...traceId !== undefined && {
14545
+ traceId
14546
+ },
14547
+ values: step
14548
+ };
14549
+ switch(name){
14550
+ case 'approve':
14551
+ case 'burn':
14552
+ case 'mint':
14553
+ provider.actionDispatcher.dispatch(name, {
14554
+ ...actionValues,
14555
+ method: name
14556
+ });
14557
+ break;
14558
+ case 'fetchAttestation':
14559
+ case 'reAttest':
14560
+ provider.actionDispatcher.dispatch(name, {
14561
+ ...actionValues,
14562
+ method: name,
14563
+ values: step
14564
+ });
14565
+ break;
14566
+ }
14567
+ }
14568
+
14229
14569
  /**
14230
14570
  * Base URL for Circle's IRIS API (mainnet/production).
14231
14571
  *
14232
14572
  * The IRIS API provides attestation services for CCTP cross-chain transfers.
14233
- */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
14573
+ */ const IRIS_API_BASE_URL$1 = 'https://iris-api.circle.com';
14234
14574
  /**
14235
14575
  * Base URL for Circle's IRIS API (testnet/sandbox).
14236
14576
  *
14237
14577
  * Used for development and testing on testnet chains.
14238
- */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
14578
+ */ const IRIS_API_SANDBOX_BASE_URL$1 = 'https://iris-api-sandbox.circle.com';
14239
14579
 
14240
14580
  /**
14241
14581
  * Type guard to validate the API response structure.
@@ -14278,7 +14618,7 @@ const isFastBurnFeeResponse = (data)=>{
14278
14618
  * @param isTestnet - Whether the request is for a testnet chain
14279
14619
  * @returns The complete API URL
14280
14620
  */ function buildFastBurnFeeUrl(sourceDomain, destinationDomain, isTestnet) {
14281
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
14621
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
14282
14622
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}`;
14283
14623
  }
14284
14624
  const FAST_TIER_FINALITY_THRESHOLD = 1000;
@@ -14489,6 +14829,77 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14489
14829
  }
14490
14830
  };
14491
14831
 
14832
+ /**
14833
+ * Build the forwarding `hookData` for a forwarded (Orbit-relayed) CCTP v2 burn,
14834
+ * tailored to the destination chain.
14835
+ *
14836
+ * For EVM destinations the recipient already holds ERC-20 USDC directly, so the
14837
+ * empty version-0 `cctp-forward` frame is sufficient. For Solana destinations
14838
+ * USDC is held in an Associated Token Account (ATA) that may not exist for a
14839
+ * fresh wallet, so this emits a frame carrying `createAta` + `ataOwner` that
14840
+ * instructs the relayer to create the recipient ATA (idempotently, at the
14841
+ * relayer's expense) before minting.
14842
+ *
14843
+ * `@solana/web3.js` is imported lazily so EVM-only consumers never load Solana
14844
+ * code, mirroring {@link getMintRecipientAccount}.
14845
+ *
14846
+ * @param chainType - The destination blockchain type ('evm' or 'solana').
14847
+ * @param ownerAddress - The recipient's wallet address on the destination chain
14848
+ * (base58 for Solana). Must be the same owner used to derive `mintRecipient`.
14849
+ * @returns A 0x-prefixed hookData hex string for the forwarded burn.
14850
+ * @throws {KitError} If `chainType` is neither 'evm' nor 'solana', if
14851
+ * `@solana/web3.js` cannot be loaded, or if `ownerAddress` is not a valid
14852
+ * Solana public key (all FATAL).
14853
+ *
14854
+ * @example
14855
+ * ```typescript
14856
+ * import { getForwarderHookData } from './getForwarderHookData'
14857
+ *
14858
+ * // EVM: empty forwarding frame
14859
+ * const evmHook = await getForwarderHookData('evm', '0x742d35Cc...')
14860
+ *
14861
+ * // Solana: frame instructing the relayer to create the recipient ATA
14862
+ * const solanaHook = await getForwarderHookData(
14863
+ * 'solana',
14864
+ * '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
14865
+ * )
14866
+ * ```
14867
+ */ const getForwarderHookData = async (/** The destination blockchain type - determines the hookData shape */ chainType, /** The recipient's wallet address (hex for EVM, base58 for Solana) */ ownerAddress)=>{
14868
+ if (chainType === 'evm') {
14869
+ // EVM: the recipient holds USDC directly; no ATA setup is needed.
14870
+ return buildForwardingHookData();
14871
+ }
14872
+ // Fail closed: only EVM and Solana forwarding destinations are supported.
14873
+ // Without this guard any future non-EVM chain type would silently fall
14874
+ // through to the Solana path and mis-encode hookData on a money-movement path.
14875
+ if (chainType !== 'solana') {
14876
+ throw new KitError({
14877
+ ...InputError.VALIDATION_FAILED,
14878
+ recoverability: 'FATAL',
14879
+ message: `Forwarded burns are not supported for destination chain type "${chainType}"`
14880
+ });
14881
+ }
14882
+ // Solana: encode the owner so the relayer creates the recipient ATA.
14883
+ // Resolve @solana/web3.js lazily so EVM-only consumers never load Solana code.
14884
+ const { PublicKey } = await import('@solana/web3.js').catch(()=>{
14885
+ throw new KitError({
14886
+ ...InputError.VALIDATION_FAILED,
14887
+ recoverability: 'FATAL',
14888
+ message: 'Failed to load @solana/web3.js. Please ensure it is installed: npm install @solana/web3.js'
14889
+ });
14890
+ });
14891
+ try {
14892
+ const owner = new PublicKey(ownerAddress);
14893
+ return buildSolanaAtaForwardingHookData(owner.toBytes());
14894
+ } catch (error) {
14895
+ throw new KitError({
14896
+ ...InputError.INVALID_ADDRESS,
14897
+ recoverability: 'FATAL',
14898
+ message: `Failed to build Solana forwarder hookData for recipient "${ownerAddress}": ${error instanceof Error ? error.message : String(error)}`
14899
+ });
14900
+ }
14901
+ };
14902
+
14492
14903
  /**
14493
14904
  * Validates and converts a fee value to bigint.
14494
14905
  *
@@ -14591,7 +15002,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14591
15002
 
14592
15003
  /**
14593
15004
  * The zero address, denoting a native-currency fee in a signed quote.
14594
- */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
15005
+ */ const ZERO_ADDRESS$2 = '0x0000000000000000000000000000000000000000';
14595
15006
  /**
14596
15007
  * Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
14597
15008
  *
@@ -14632,7 +15043,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14632
15043
  if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
14633
15044
  throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
14634
15045
  }
14635
- const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
15046
+ const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$2;
14636
15047
  const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
14637
15048
  if (isNativeFee) {
14638
15049
  return {
@@ -14770,7 +15181,7 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14770
15181
  * @param isTestnet - Whether the request is for a testnet chain
14771
15182
  * @returns The complete API URL with forward=true query parameter
14772
15183
  */ function buildForwardingFeeUrl(sourceDomain, destinationDomain, isTestnet) {
14773
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
15184
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
14774
15185
  return `${baseUrl}/v2/burn/USDC/fees/${sourceDomain.toString()}/${destinationDomain.toString()}?forward=true`;
14775
15186
  }
14776
15187
  /**
@@ -15831,7 +16242,7 @@ function hasPendingState(analysis, result) {
15831
16242
  * // => 'https://iris-api-sandbox.circle.com/v2/messages/1?transactionHash=0xdef...'
15832
16243
  * ```
15833
16244
  */ const buildIrisUrl = (sourceDomainId, transactionHash, isTestnet)=>{
15834
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
16245
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
15835
16246
  const url = new URL(`${baseUrl}/v2/messages/${String(sourceDomainId)}`);
15836
16247
  url.searchParams.set('transactionHash', transactionHash);
15837
16248
  return url.toString();
@@ -15999,7 +16410,7 @@ function hasPendingState(analysis, result) {
15999
16410
  * // => 'https://iris-api-sandbox.circle.com/v2/reattest/0xabc'
16000
16411
  * ```
16001
16412
  */ const buildReAttestUrl = (nonce, isTestnet)=>{
16002
- const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL;
16413
+ const baseUrl = isTestnet ? IRIS_API_SANDBOX_BASE_URL$1 : IRIS_API_BASE_URL$1;
16003
16414
  const url = new URL(`${baseUrl}/v2/reattest/${nonce}`);
16004
16415
  return url.toString();
16005
16416
  };
@@ -16454,7 +16865,8 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16454
16865
  * - `destinationChain` — present and supports CCTP v2
16455
16866
  * - source and destination chains must both be testnet or both mainnet
16456
16867
  * - source and destination chains must differ
16457
- * - `executor` non-empty string
16868
+ * - destination — either `executor`, or both `mintRecipient` and
16869
+ * `destinationCaller`; not both
16458
16870
  * - `amount` — bigint or non-empty string coercible to bigint
16459
16871
  * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
16460
16872
  * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
@@ -16496,10 +16908,17 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16496
16908
  if (source.chain.name === dest.name) {
16497
16909
  throw createUnsupportedRouteError(source.chain.name, dest.name);
16498
16910
  }
16499
- // executor
16911
+ // Destination: GenericExecutor shorthand or explicit recipient + caller.
16500
16912
  const executor = p['executor'];
16501
- if (typeof executor !== 'string' || executor === '') {
16502
- throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
16913
+ const mintRecipient = p['mintRecipient'];
16914
+ const destinationCaller = p['destinationCaller'];
16915
+ const hasExecutor = typeof executor === 'string' && executor !== '';
16916
+ const hasDirectDestination = typeof mintRecipient === 'string' && mintRecipient !== '' && typeof destinationCaller === 'string' && destinationCaller !== '';
16917
+ if (!hasExecutor && !hasDirectDestination) {
16918
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor, or both mintRecipient and destinationCaller');
16919
+ }
16920
+ if (hasExecutor && hasDirectDestination) {
16921
+ throw createValidationFailedError$1('destination', undefined, 'Provide executor or direct destination fields, not both');
16503
16922
  }
16504
16923
  // amount
16505
16924
  const rawAmount = p['amount'];
@@ -16522,7 +16941,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16522
16941
  throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
16523
16942
  }
16524
16943
  // feeToken
16525
- if (!evmAddressSchema.safeParse(p['feeToken']).success) {
16944
+ if (!evmAddressSchema$1.safeParse(p['feeToken']).success) {
16526
16945
  throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
16527
16946
  }
16528
16947
  // claim
@@ -16534,7 +16953,7 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
16534
16953
  if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
16535
16954
  throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
16536
16955
  }
16537
- if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
16956
+ if (!evmAddressSchema$1.safeParse(claim['refundAddress']).success) {
16538
16957
  throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
16539
16958
  }
16540
16959
  // hookData (optional)
@@ -16916,63 +17335,6 @@ const mockAttestationMessage = {
16916
17335
  });
16917
17336
  }
16918
17337
 
16919
- /**
16920
- * Dispatch a bridge step event through the provider's action dispatcher.
16921
- *
16922
- * Constructs the appropriate action payload and dispatches it to any registered
16923
- * event listeners. Handles type-safe dispatching for different step types.
16924
- * When provided, traceId from the invocation context is included for end-to-end correlation.
16925
- *
16926
- * @param name - The step name (approve, burn, fetchAttestation, or mint).
16927
- * @param step - The completed bridge step containing transaction details and explorerUrl.
16928
- * @param provider - The CCTP v2 provider with action dispatcher.
16929
- * @param invocation - Optional invocation context containing traceId for correlation.
16930
- *
16931
- * @example
16932
- * ```typescript
16933
- * const step: BridgeStep = {
16934
- * name: 'burn',
16935
- * state: 'success',
16936
- * txHash: '0xabc...',
16937
- * explorerUrl: 'https://sepolia.etherscan.io/tx/0xabc...',
16938
- * data: { ... }
16939
- * }
16940
- * dispatchStepEvent('burn', step, provider, invocationContext)
16941
- * ```
16942
- */ function dispatchStepEvent(name, step, provider, invocation) {
16943
- if (!provider.actionDispatcher) {
16944
- return;
16945
- }
16946
- // Extract traceId from invocation context if provided
16947
- const traceId = invocation?.traceId;
16948
- const actionValues = {
16949
- protocol: 'cctp',
16950
- version: 'v2',
16951
- ...traceId !== undefined && {
16952
- traceId
16953
- },
16954
- values: step
16955
- };
16956
- switch(name){
16957
- case 'approve':
16958
- case 'burn':
16959
- case 'mint':
16960
- provider.actionDispatcher.dispatch(name, {
16961
- ...actionValues,
16962
- method: name
16963
- });
16964
- break;
16965
- case 'fetchAttestation':
16966
- case 'reAttest':
16967
- provider.actionDispatcher.dispatch(name, {
16968
- ...actionValues,
16969
- method: name,
16970
- values: step
16971
- });
16972
- break;
16973
- }
16974
- }
16975
-
16976
17338
  /**
16977
17339
  * Check whether the source adapter supports EIP-5792 atomic batching and
16978
17340
  * the consumer has not explicitly opted out via `config.batchTransactions`.
@@ -17219,7 +17581,7 @@ const mockAttestationMessage = {
17219
17581
  return step;
17220
17582
  }
17221
17583
 
17222
- var version$4 = "1.11.0";
17584
+ var version$4 = "1.12.0";
17223
17585
  var pkg$4 = {
17224
17586
  version: version$4};
17225
17587
 
@@ -17925,18 +18287,18 @@ var pkg$4 = {
17925
18287
  }
17926
18288
  }
17927
18289
 
17928
- function isPlainObject(value) {
18290
+ function isPlainObject$1(value) {
17929
18291
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
17930
18292
  return false;
17931
18293
  }
17932
18294
  const prototype = Object.getPrototypeOf(value);
17933
18295
  return prototype === Object.prototype || prototype === null;
17934
18296
  }
17935
- function assertHeadersConfig(headers, field) {
18297
+ function assertHeadersConfig$1(headers, field) {
17936
18298
  if (headers === undefined) {
17937
18299
  return;
17938
18300
  }
17939
- if (!isPlainObject(headers)) {
18301
+ if (!isPlainObject$1(headers)) {
17940
18302
  throw createValidationFailedError$1(field, headers, `${field} must be a plain object with string header values when provided`);
17941
18303
  }
17942
18304
  for (const [name, value] of Object.entries(headers)){
@@ -17946,16 +18308,16 @@ function assertHeadersConfig(headers, field) {
17946
18308
  }
17947
18309
  }
17948
18310
  function assertCCTPV2Config(config) {
17949
- if (!isPlainObject(config)) {
18311
+ if (!isPlainObject$1(config)) {
17950
18312
  throw createValidationFailedError$1('config', config, 'config must be a plain object when provided');
17951
18313
  }
17952
- assertHeadersConfig(config['headers'], 'config.headers');
18314
+ assertHeadersConfig$1(config['headers'], 'config.headers');
17953
18315
  const attestation = config['attestation'];
17954
18316
  if (attestation !== undefined) {
17955
- if (!isPlainObject(attestation)) {
18317
+ if (!isPlainObject$1(attestation)) {
17956
18318
  throw createValidationFailedError$1('config.attestation', attestation, 'config.attestation must be a plain object when provided');
17957
18319
  }
17958
- assertHeadersConfig(attestation['headers'], 'config.attestation.headers');
18320
+ assertHeadersConfig$1(attestation['headers'], 'config.attestation.headers');
17959
18321
  }
17960
18322
  }
17961
18323
  /**
@@ -18030,6 +18392,34 @@ function assertCCTPV2Config(config) {
18030
18392
  this.config = config;
18031
18393
  }
18032
18394
  /**
18395
+ * Emit a bridge step event through the provider's registered action
18396
+ * dispatcher.
18397
+ *
18398
+ * Kit-level orchestration that drives the burn primitives directly instead
18399
+ * of {@link CCTPV2BridgingProvider.bridge} (for example the receive-exact
18400
+ * source-fee flow) uses this to surface the same `approve`/`burn`/`mint`
18401
+ * events as the standard bridge path. It is a no-op when no dispatcher is
18402
+ * registered.
18403
+ *
18404
+ * @param name - The step name (`approve`, `burn`, `mint`, ...).
18405
+ * @param step - The completed bridge step to broadcast.
18406
+ * @param invocation - Optional invocation context carrying a `traceId` for
18407
+ * end-to-end correlation.
18408
+ * @returns Nothing.
18409
+ *
18410
+ * @example
18411
+ * ```typescript
18412
+ * const provider = new CCTPV2BridgingProvider()
18413
+ * provider.emitBridgeStep('burn', {
18414
+ * name: 'burn',
18415
+ * state: 'success',
18416
+ * txHash: '0xabc...',
18417
+ * })
18418
+ * ```
18419
+ */ emitBridgeStep(name, step, invocation) {
18420
+ dispatchStepEvent(name, step, this, invocation);
18421
+ }
18422
+ /**
18033
18423
  * Resolves the effective polling configuration for an attestation request.
18034
18424
  *
18035
18425
  * Precedence (lowest to highest): provider `config.attestation`, then the
@@ -18855,8 +19245,11 @@ function assertCCTPV2Config(config) {
18855
19245
  // 2. Forwarder: Does the user want Circle's relayer to handle attestation/mint?
18856
19246
  const useCustomBurn = hasCustomContractSupport(source.chain, 'bridge');
18857
19247
  const useForwarder = destination.useForwarder === true;
18858
- // Build hookData once if forwarder is enabled (memoized internally)
18859
- const hookData = useForwarder ? buildForwardingHookData() : undefined;
19248
+ // Build hookData once if forwarder is enabled. EVM destinations get the
19249
+ // empty forwarding frame; Solana destinations get a frame instructing the
19250
+ // relayer to create the recipient's ATA (using the same owner that derived
19251
+ // `mintRecipient`) so the mint succeeds even for a fresh wallet.
19252
+ const hookData = useForwarder ? await getForwarderHookData(destination.chain.type, destinationAddressForMint) : undefined;
18860
19253
  if (useCustomBurn) {
18861
19254
  // Custom burn path: use bridge contract (with or without hook)
18862
19255
  const customBurnParams = {
@@ -18884,13 +19277,14 @@ function assertCCTPV2Config(config) {
18884
19277
  /**
18885
19278
  * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
18886
19279
  *
18887
- * Builds the source-chain `depositForBurnWithHookAndFees` call for the
18888
- * GenericExecutor FORWARD path: fees are collected up front on the source chain
18889
- * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
18890
- * the GenericExecutor, and the GE `hookData` is passed through unchanged.
19280
+ * Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
19281
+ * collected up front on the source chain against a signed quote. The
19282
+ * destination may use the GenericExecutor shorthand, or an explicit mint
19283
+ * recipient and destination caller for direct forwarding.
18891
19284
  *
18892
- * This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
18893
- * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
19285
+ * This is the low-level on-chain primitive behind the Unified Balance Kit
19286
+ * `fastCrossChainDeposit` and the Bridge Kit source-fee
19287
+ * (`feePayment: 'source'`) flow. The `hookData` and signed-quote
18894
19288
  * `claim` are produced elsewhere and passed in here:
18895
19289
  * - `hookData`: `buildForwardingHookDataWithPayload(version,
18896
19290
  * buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
@@ -18907,32 +19301,48 @@ function assertCCTPV2Config(config) {
18907
19301
  * approval covers both; the redundant second approval is skipped.
18908
19302
  *
18909
19303
  * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18910
- * @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
19304
+ * @param params - The burn amount, destination, hook data, signed quote, and fee.
18911
19305
  * @returns The prepared approvals, the prepared burn, and the resolved fee plan.
18912
19306
  * @throws {KitError} If the wallet context is invalid, `destinationChain` does not
18913
- * support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
18914
- * a bigint or a numeric string coercible to bigint, the hookData lacks a
18915
- * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
18916
- * context cannot be resolved.
19307
+ * support CCTP v2, the destination fields are missing, `amount` or
19308
+ * `feeTotalAmount` is not a bigint or a numeric string coercible to bigint,
19309
+ * the hook data lacks a `cctp-forward` frame (guaranteed
19310
+ * `ForwardFeeWithoutHook`), or the operation context cannot be resolved.
18917
19311
  *
18918
19312
  * @example
18919
19313
  * ```typescript
19314
+ * import {
19315
+ * CCTPV2BridgingProvider,
19316
+ * type BurnWithFeesParams,
19317
+ * } from '@circle-fin/provider-cctp-v2'
19318
+ *
19319
+ * declare const source: BurnWithFeesParams['source']
19320
+ * declare const destinationChain: BurnWithFeesParams['destinationChain']
19321
+ * declare const recipient: string
19322
+ * declare const hookData: string
19323
+ * declare const claim: BurnWithFeesParams['claim']
19324
+ *
19325
+ * const provider = new CCTPV2BridgingProvider()
18920
19326
  * const { approvals, burn } = await provider.burnWithFees({
18921
19327
  * source,
18922
- * destinationChain: Arc,
19328
+ * destinationChain,
18923
19329
  * amount: 1_000_000n,
18924
- * executor: genericExecutorAddress,
18925
- * hookData: geForwardHookData,
18926
- * claim: { signedQuote: '0x01...', refundAddress: userAddress },
18927
- * feeToken: '0x0000000000000000000000000000000000000000', // native
18928
- * feeTotalAmount: 3_500_000n,
19330
+ * mintRecipient: recipient,
19331
+ * destinationCaller: '0x0000000000000000000000000000000000000000',
19332
+ * hookData,
19333
+ * claim,
19334
+ * feeToken: source.chain.usdcAddress,
19335
+ * feeTotalAmount: 10_000n,
18929
19336
  * })
18930
19337
  * for (const approval of approvals) await approval.execute()
18931
19338
  * const txHash = await burn.execute()
18932
19339
  * ```
18933
19340
  */ async burnWithFees(params) {
18934
19341
  assertBurnWithFeesParams(params);
18935
- const { source, destinationChain, executor, hookData, claim, feeToken } = params;
19342
+ const { source, destinationChain, hookData, claim, feeToken } = params;
19343
+ const hasExecutor = 'executor' in params && params.executor !== undefined;
19344
+ const mintRecipient = hasExecutor ? params.executor : params.mintRecipient;
19345
+ const destinationCaller = hasExecutor ? params.executor : params.destinationCaller;
18936
19346
  const amount = BigInt(params.amount);
18937
19347
  const feeTotalAmount = BigInt(params.feeTotalAmount);
18938
19348
  // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
@@ -18963,13 +19373,14 @@ function assertCCTPV2Config(config) {
18963
19373
  delegate: wrapperAddress,
18964
19374
  amount: approval.amount
18965
19375
  }, context)));
18966
- // Build the burn: mintRecipient AND destinationCaller are both the executor.
19376
+ // Build the burn with either the GenericExecutor shorthand or the explicit
19377
+ // direct-forwarding recipient and caller.
18967
19378
  const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18968
19379
  fromChain: source.chain,
18969
19380
  toChain: destinationChain,
18970
19381
  amount,
18971
- mintRecipient: executor,
18972
- destinationCaller: executor,
19382
+ mintRecipient,
19383
+ destinationCaller,
18973
19384
  hookData,
18974
19385
  claim,
18975
19386
  feeToken,
@@ -19126,6 +19537,948 @@ function assertCCTPV2Config(config) {
19126
19537
  ]
19127
19538
  ];
19128
19539
 
19540
+ /**
19541
+ * Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
19542
+ */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
19543
+ /**
19544
+ * Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
19545
+ */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
19546
+ /**
19547
+ * Native fee-token sentinel (the zero address).
19548
+ *
19549
+ * When `feeToken` is the zero address the quote prices fees in the source
19550
+ * chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
19551
+ * address instead to denominate fees in USDC.
19552
+ */ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
19553
+ /**
19554
+ * API path prefix for the CCTP v2 USDC burn quote endpoint.
19555
+ *
19556
+ * The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
19557
+ * `usdc` is a fixed literal, not a token parameter.
19558
+ */ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
19559
+ /**
19560
+ * API path prefix for the CCTP v2 USDC quote validate endpoint.
19561
+ *
19562
+ * The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
19563
+ * `POST { abiSignature, args }` body and returns whether the signed quote is
19564
+ * currently claimable together with its authoritative expiry status.
19565
+ */ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
19566
+ /**
19567
+ * Default polling configuration for Quote API calls.
19568
+ *
19569
+ * A signed quote is short-lived (typically ~2 minutes, varying per chain) and
19570
+ * a feature-flag-disabled source chain returns a
19571
+ * permanent `503 SERVICE_NOT_ENABLED`, so retrying buys little and risks
19572
+ * outliving the quote. The client therefore makes a single attempt
19573
+ * (`maxRetries: 1`) with a 15s timeout, mirroring the reference
19574
+ * implementation; callers refresh by requesting a new quote rather than
19575
+ * relying on transport retries.
19576
+ *
19577
+ * No `headers` are set here: `pollApiWithValidation` always injects
19578
+ * `Content-Type: application/json` and adds `User-Agent` in Node. Browser
19579
+ * requests omit a user-agent header to avoid a CORS preflight, so duplicating
19580
+ * either header here would be dead configuration.
19581
+ */ const FEE_QUOTE_DEFAULT_CONFIG = {
19582
+ timeout: 15_000,
19583
+ maxRetries: 1,
19584
+ retryDelay: 200
19585
+ };
19586
+
19587
+ /** Decimal string in token minor units, constrained to be strictly positive. */ const positiveAmountSchema = z.string().regex(/^\d+$/, 'must be a non-negative integer string')// Re-check the digit shape here: zod still runs this refinement when the
19588
+ // regex check above fails ("dirty"), so guard BigInt() against throwing on a
19589
+ // non-numeric value before comparing.
19590
+ .refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
19591
+ /**
19592
+ * A 20-byte EVM address in `0x` hex.
19593
+ *
19594
+ * The MVP prepaid-`FORWARD` `burn/usdc` path targets EVM contracts
19595
+ * (`TokenMessengerWithFees` / `GenericExecutor`), so `feeToken` and
19596
+ * `destinationCaller` are constrained to EVM addresses by design. This is an
19597
+ * intentional scope limit, not a permanent one: it can be widened to other
19598
+ * address formats as the fee service expands to more chains.
19599
+ */ const evmAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'must be a 20-byte 0x address');
19600
+ /** Even-length `0x` hex (the empty `0x` is allowed). */ const hexSchema = z.string().regex(/^0x([a-fA-F0-9]{2})*$/, 'must be even-length 0x hex');
19601
+ /** Non-empty, even-length `0x` hex. */ const nonEmptyHexSchema = z.string().regex(/^0x([a-fA-F0-9]{2})+$/, 'must be non-empty 0x hex');
19602
+ /** A 32-byte `0x` hex hash. */ const bytes32Schema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, 'must be a 32-byte 0x hash');
19603
+ /** Decimal string in minor units, allowing zero. */ const numericStringSchema$1 = z.string().regex(/^\d+$/, 'must be a non-negative integer string');
19604
+ /** An `https:` URL, used for the optional base-URL override. */ const httpsUrlSchema = z.string().refine((value)=>{
19605
+ try {
19606
+ return new URL(value).protocol === 'https:';
19607
+ } catch {
19608
+ return false;
19609
+ }
19610
+ }, 'must be an https URL');
19611
+ const forwardParamsSchema = z.object({
19612
+ hookData: hexSchema.optional(),
19613
+ destinationCaller: evmAddressSchema.optional()
19614
+ }).strict();
19615
+ const forwardRequestSchema = z.object({
19616
+ type: z.literal('FORWARD'),
19617
+ params: forwardParamsSchema.optional()
19618
+ }).strict();
19619
+ const preFinalityRequestSchema = z.object({
19620
+ type: z.literal('PRE_FINALITY')
19621
+ }).strict();
19622
+ /** A single quote request item (`FORWARD` or `PRE_FINALITY`). */ const feeQuoteRequestSchema = z.discriminatedUnion('type', [
19623
+ forwardRequestSchema,
19624
+ preFinalityRequestSchema
19625
+ ]);
19626
+ /** A non-empty list of quote request items with unique types. */ const feeQuoteRequestsSchema = 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');
19627
+ /**
19628
+ * A structured `Partial<ApiPollingConfig>` polling override.
19629
+ *
19630
+ * Validates the field types callers actually set, so a plain-JS caller passing
19631
+ * `{ timeout: 'soon' }` is rejected at the boundary rather than failing opaquely
19632
+ * inside the transport. Unknown keys pass through so a future `ApiPollingConfig`
19633
+ * field is forwarded rather than silently dropped.
19634
+ */ const apiPollingConfigSchema = z.object({
19635
+ timeout: z.number().int().positive().optional(),
19636
+ maxRetries: z.number().int().nonnegative().optional(),
19637
+ retryDelay: z.number().int().nonnegative().optional(),
19638
+ backoff: z.enum([
19639
+ 'fixed',
19640
+ 'exponential'
19641
+ ]).optional(),
19642
+ maxRetryDelayMs: z.number().int().positive().optional(),
19643
+ headers: z.record(z.string()).optional()
19644
+ }).passthrough();
19645
+ /**
19646
+ * The validatable input for {@link fetchFeeQuote}.
19647
+ *
19648
+ * This is the single source of truth for input validation, including the CCTP
19649
+ * domains and the `isTestnet` environment flag. Validating `isTestnet` at
19650
+ * runtime matters because a plain-JS caller who omits it would otherwise leave
19651
+ * it `undefined`, which is falsy and silently selects the production base URL.
19652
+ * (`buildFeeQuoteUrl` independently re-validates the domains for standalone
19653
+ * callers.)
19654
+ */ const fetchFeeQuoteInputSchema = z.object({
19655
+ sourceDomain: z.number().int().nonnegative(),
19656
+ destinationDomain: z.number().int().nonnegative(),
19657
+ amount: positiveAmountSchema,
19658
+ feeToken: evmAddressSchema.optional(),
19659
+ requests: feeQuoteRequestsSchema,
19660
+ isTestnet: z.boolean(),
19661
+ baseUrl: httpsUrlSchema.optional(),
19662
+ config: apiPollingConfigSchema.optional()
19663
+ }).strict();
19664
+ const feeQuoteItemSchema = z.object({
19665
+ type: z.string().min(1),
19666
+ amount: numericStringSchema$1,
19667
+ args: z.array(z.string()),
19668
+ argsHash: bytes32Schema
19669
+ }).passthrough();
19670
+ const exchangeRatesSchema = z.object({
19671
+ feeTokenUsd: z.string(),
19672
+ destinationTokenUsd: z.string()
19673
+ }).passthrough();
19674
+ const metadataSchema = z.object({
19675
+ destinationGasPrice: z.string().optional(),
19676
+ exchangeRates: exchangeRatesSchema.optional()
19677
+ }).passthrough();
19678
+ /** The `expiry` object the Quote API nests the quote TTL under. */ const expirySchema = z.discriminatedUnion('mode', [
19679
+ z.object({
19680
+ mode: z.literal('TIMESTAMP'),
19681
+ expiresAt: z.number().int().nonnegative()
19682
+ }).passthrough(),
19683
+ z.object({
19684
+ mode: z.literal('BLOCK_NUMBER'),
19685
+ expiresAtBlock: z.number().int().nonnegative(),
19686
+ blockEstimatedAt: z.number().int().nonnegative().optional()
19687
+ }).passthrough()
19688
+ ]);
19689
+ /** Schema for a signed fee quote returned by the Quote API. */ const signedFeeQuoteSchema = z.object({
19690
+ // The runtime YAML spec maps signedQuote to a looser `hex` (which allows
19691
+ // an empty `0x`); we keep the stricter non-empty form. Do not relax
19692
+ // without a reason.
19693
+ signedQuote: nonEmptyHexSchema,
19694
+ issuedAt: z.number().int().nonnegative(),
19695
+ // The API returns a mode-specific timestamp or source-block deadline.
19696
+ expiry: expirySchema,
19697
+ feeTotalAmount: numericStringSchema$1,
19698
+ feeToken: evmAddressSchema,
19699
+ nonce: numericStringSchema$1,
19700
+ items: z.array(feeQuoteItemSchema),
19701
+ metadata: metadataSchema.optional()
19702
+ }).passthrough();
19703
+ /**
19704
+ * Validate that an unknown value is a signed fee quote.
19705
+ *
19706
+ * @param value - The unknown value to validate.
19707
+ * @returns `true` when the value matches the signed-quote response shape.
19708
+ *
19709
+ * @example
19710
+ * ```typescript
19711
+ * import { isSignedFeeQuote } from '@circle-fin/provider-fee-v1'
19712
+ *
19713
+ * declare const payload: unknown
19714
+ * if (isSignedFeeQuote(payload)) {
19715
+ * console.log(payload.feeTotalAmount)
19716
+ * }
19717
+ * ```
19718
+ */ function isSignedFeeQuote(value) {
19719
+ return signedFeeQuoteSchema.safeParse(value).success;
19720
+ }
19721
+ /** Validate input to {@link validateQuote}. */ const validateQuoteInputSchema = z.object({
19722
+ sourceDomain: z.number().int().nonnegative(),
19723
+ abiSignature: z.string().min(1),
19724
+ args: z.array(z.union([
19725
+ z.string(),
19726
+ z.array(z.string())
19727
+ ])),
19728
+ isTestnet: z.boolean(),
19729
+ baseUrl: httpsUrlSchema.optional(),
19730
+ config: apiPollingConfigSchema.optional()
19731
+ }).strict();
19732
+ const quoteExpiryStatusSchema = z.discriminatedUnion('mode', [
19733
+ z.object({
19734
+ mode: z.literal('TIMESTAMP'),
19735
+ expired: z.boolean(),
19736
+ secondsRemaining: z.number().int().nonnegative(),
19737
+ expiresAt: z.number().int().nonnegative()
19738
+ }).passthrough(),
19739
+ z.object({
19740
+ mode: z.literal('BLOCK_NUMBER'),
19741
+ expired: z.boolean(),
19742
+ secondsRemaining: z.number().int().nonnegative(),
19743
+ expiresAtBlock: z.number().int().nonnegative(),
19744
+ blockEstimatedAt: z.number().int().nonnegative().optional()
19745
+ }).passthrough()
19746
+ ]);
19747
+ const validateQuoteItemSchema = z.object({
19748
+ type: z.string().min(1),
19749
+ argsMatch: z.boolean(),
19750
+ amount: numericStringSchema$1.optional(),
19751
+ args: z.array(z.string()).optional(),
19752
+ argsHash: bytes32Schema.optional(),
19753
+ computedArgsHash: bytes32Schema.optional()
19754
+ }).passthrough();
19755
+ /**
19756
+ * Schema for a response from the Quote API validation endpoint.
19757
+ *
19758
+ * The endpoint takes the source domain as a URL path parameter and does not
19759
+ * return it in the response body, so `sourceDomain` is intentionally not part
19760
+ * of this schema.
19761
+ */ const validateQuoteResultSchema = z.object({
19762
+ signedQuote: nonEmptyHexSchema,
19763
+ expiry: quoteExpiryStatusSchema,
19764
+ feeTotalAmount: numericStringSchema$1,
19765
+ feeToken: evmAddressSchema,
19766
+ nonce: numericStringSchema$1,
19767
+ claimable: z.boolean(),
19768
+ // Preserve newly introduced server-side reasons as opaque strings rather
19769
+ // than rejecting the entire safety response before the SDK is updated.
19770
+ failedChecks: z.array(z.string().min(1)),
19771
+ items: z.array(validateQuoteItemSchema)
19772
+ }).passthrough();
19773
+ /**
19774
+ * Validate a Quote API validation response.
19775
+ *
19776
+ * @param value - The unknown response value.
19777
+ * @returns `true` when the value has the expected validation response shape.
19778
+ *
19779
+ * @example
19780
+ * ```typescript
19781
+ * import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
19782
+ *
19783
+ * declare const response: unknown
19784
+ * if (isValidateQuoteResult(response)) {
19785
+ * console.log(response.claimable)
19786
+ * }
19787
+ * ```
19788
+ */ function isValidateQuoteResult(value) {
19789
+ return validateQuoteResultSchema.safeParse(value).success;
19790
+ }
19791
+
19792
+ /**
19793
+ * Validate that a CCTP domain id is a non-negative integer.
19794
+ *
19795
+ * @param value - The domain id to validate.
19796
+ * @param label - The parameter name, used in the error message.
19797
+ * @returns Nothing.
19798
+ * @throws {@link KitError} When the value is not a non-negative integer.
19799
+ * @internal
19800
+ */ function assertDomain(value, label) {
19801
+ if (!Number.isInteger(value) || value < 0) {
19802
+ throw new KitError({
19803
+ ...InputError.VALIDATION_FAILED,
19804
+ recoverability: 'FATAL',
19805
+ message: `Quote API getFeeQuote failed: ${label} must be a ` + `non-negative integer, received ${String(value)}`,
19806
+ cause: {
19807
+ trace: {
19808
+ [label]: value
19809
+ }
19810
+ }
19811
+ });
19812
+ }
19813
+ }
19814
+ /**
19815
+ * Build the Quote API URL for a CCTP v2 USDC burn quote.
19816
+ *
19817
+ * Resolves the environment base URL (or an explicit `baseUrl` override) and
19818
+ * appends the burn/usdc path with the source and destination CCTP domains.
19819
+ * `usdc` is a fixed path literal, not a token parameter.
19820
+ *
19821
+ * @param params - The domains and environment selector.
19822
+ * @returns The fully-qualified Quote API URL.
19823
+ * @throws {@link KitError} When either domain is not a non-negative integer.
19824
+ *
19825
+ * @example
19826
+ * ```typescript
19827
+ * import { buildFeeQuoteUrl } from '@circle-fin/provider-fee-v1'
19828
+ *
19829
+ * const url = buildFeeQuoteUrl({
19830
+ * sourceDomain: 3,
19831
+ * destinationDomain: 26,
19832
+ * isTestnet: false,
19833
+ * })
19834
+ * // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
19835
+ * ```
19836
+ */ function buildFeeQuoteUrl(params) {
19837
+ const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
19838
+ assertDomain(sourceDomain, 'sourceDomain');
19839
+ assertDomain(destinationDomain, 'destinationDomain');
19840
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
19841
+ return new URL(`${QUOTE_BURN_USDC_PATH}/${String(sourceDomain)}/${String(destinationDomain)}`, resolvedBaseUrl).toString();
19842
+ }
19843
+
19844
+ /**
19845
+ * Assert that a quote's per-item fee amounts sum to its `feeTotalAmount`.
19846
+ *
19847
+ * A defensive integrity check on the Quote API response, enforced internally
19848
+ * by `fetchFeeQuote`. It does not compare against the destination-side
19849
+ * `feeExecuted`, which is expected to be zero for prepaid forward-only burns.
19850
+ *
19851
+ * @param quote - The signed fee quote to check.
19852
+ * @returns Nothing.
19853
+ * @throws {@link KitError} When the item amounts do not sum to `feeTotalAmount`.
19854
+ * @internal
19855
+ */ function assertFeeItemsSumToTotal(quote) {
19856
+ const itemsTotal = quote.items.reduce((sum, item)=>sum + BigInt(item.amount), 0n);
19857
+ const declaredTotal = BigInt(quote.feeTotalAmount);
19858
+ if (itemsTotal !== declaredTotal) {
19859
+ throw new KitError({
19860
+ ...InputError.VALIDATION_FAILED,
19861
+ recoverability: 'FATAL',
19862
+ message: `Quote API getFeeQuote failed: fee items sum ` + `(${itemsTotal.toString()}) does not equal feeTotalAmount ` + `(${declaredTotal.toString()})`,
19863
+ cause: {
19864
+ trace: {
19865
+ itemsTotal: itemsTotal.toString(),
19866
+ feeTotalAmount: quote.feeTotalAmount
19867
+ }
19868
+ }
19869
+ });
19870
+ }
19871
+ }
19872
+
19873
+ /**
19874
+ * Determine whether a Quote API error represents a disabled source chain.
19875
+ *
19876
+ * @param error - The error thrown by the HTTP layer.
19877
+ * @returns `true` when the error contains the `SERVICE_NOT_ENABLED` marker.
19878
+ * @internal
19879
+ */ function isServiceNotEnabled(error) {
19880
+ const body = typeof error === 'object' && error !== null && 'responseBody' in error ? error.responseBody : undefined;
19881
+ if (typeof body === 'object' && body !== null) {
19882
+ const fields = body;
19883
+ const candidates = [
19884
+ fields['errorCode'],
19885
+ fields['code'],
19886
+ fields['message'],
19887
+ fields['externalMessage'],
19888
+ fields['error']
19889
+ ];
19890
+ if (candidates.some((value)=>typeof value === 'string' && value.toUpperCase().includes('SERVICE_NOT_ENABLED'))) {
19891
+ return true;
19892
+ }
19893
+ }
19894
+ return getErrorMessage(error).toUpperCase().includes('SERVICE_NOT_ENABLED');
19895
+ }
19896
+
19897
+ const SERVICE$1 = 'Quote API';
19898
+ const OPERATION$1 = 'getFeeQuote';
19899
+ /**
19900
+ * Serialize request items for the wire body.
19901
+ *
19902
+ * `PRE_FINALITY` is emitted with no `params` key, and a `FORWARD` item only
19903
+ * carries the binding fields that are present.
19904
+ *
19905
+ * @param requests - The request items to serialize.
19906
+ * @returns The serialized request items.
19907
+ * @internal
19908
+ */ function serializeRequests(requests) {
19909
+ return requests.map((request)=>{
19910
+ if (request.type === 'PRE_FINALITY') {
19911
+ return {
19912
+ type: 'PRE_FINALITY'
19913
+ };
19914
+ }
19915
+ const params = request.params;
19916
+ if (params === undefined) {
19917
+ return {
19918
+ type: 'FORWARD'
19919
+ };
19920
+ }
19921
+ const forwardParams = {};
19922
+ if (params.hookData !== undefined) {
19923
+ forwardParams.hookData = params.hookData;
19924
+ }
19925
+ if (params.destinationCaller !== undefined) {
19926
+ forwardParams.destinationCaller = params.destinationCaller;
19927
+ }
19928
+ return {
19929
+ type: 'FORWARD',
19930
+ params: forwardParams
19931
+ };
19932
+ });
19933
+ }
19934
+ /**
19935
+ * Fetch a signed fee quote from Circle's Quote API for a CCTP v2 USDC burn.
19936
+ *
19937
+ * Validates inputs, POSTs to
19938
+ * `/v2/quote/burn/usdc/{sourceDomain}/{destinationDomain}` with a single
19939
+ * attempt (the signed quote is short-lived), and returns the typed quote. A
19940
+ * disabled source chain (`503 SERVICE_NOT_ENABLED`) surfaces as a fatal,
19941
+ * non-retryable error; other failures are mapped to a {@link KitError} via the
19942
+ * shared API error parser.
19943
+ *
19944
+ * @param params - The domains, amount, request items, and environment.
19945
+ * @returns The signed fee quote.
19946
+ * @throws {@link KitError} On invalid input, a disabled source chain, an HTTP
19947
+ * error, or an invalid response shape.
19948
+ *
19949
+ * @example
19950
+ * ```typescript
19951
+ * import { fetchFeeQuote } from '@circle-fin/provider-fee-v1'
19952
+ *
19953
+ * const quote = await fetchFeeQuote({
19954
+ * sourceDomain: 3,
19955
+ * destinationDomain: 26,
19956
+ * amount: '1000000',
19957
+ * requests: [{ type: 'FORWARD' }, { type: 'PRE_FINALITY' }],
19958
+ * isTestnet: false,
19959
+ * })
19960
+ * console.log(quote.feeTotalAmount, quote.expiry)
19961
+ * ```
19962
+ */ async function fetchFeeQuote(params) {
19963
+ const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
19964
+ const parsed = fetchFeeQuoteInputSchema.safeParse({
19965
+ sourceDomain,
19966
+ destinationDomain,
19967
+ amount,
19968
+ feeToken,
19969
+ requests,
19970
+ isTestnet,
19971
+ baseUrl,
19972
+ config
19973
+ });
19974
+ if (!parsed.success) {
19975
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
19976
+ throw new KitError({
19977
+ ...InputError.VALIDATION_FAILED,
19978
+ recoverability: 'FATAL',
19979
+ message: `${SERVICE$1} ${OPERATION$1} failed: ${detail}`,
19980
+ cause: {
19981
+ trace: parsed.error.issues
19982
+ }
19983
+ });
19984
+ }
19985
+ const url = baseUrl === undefined ? buildFeeQuoteUrl({
19986
+ sourceDomain,
19987
+ destinationDomain,
19988
+ isTestnet
19989
+ }) : buildFeeQuoteUrl({
19990
+ sourceDomain,
19991
+ destinationDomain,
19992
+ isTestnet,
19993
+ baseUrl
19994
+ });
19995
+ const body = {
19996
+ amount,
19997
+ feeToken: feeToken ?? NATIVE_FEE_TOKEN,
19998
+ requests: serializeRequests(requests)
19999
+ };
20000
+ const pollingConfig = {
20001
+ ...FEE_QUOTE_DEFAULT_CONFIG,
20002
+ ...config
20003
+ };
20004
+ let quote;
20005
+ try {
20006
+ quote = await pollApiPost(url, body, isSignedFeeQuote, pollingConfig);
20007
+ } catch (error) {
20008
+ // Only one service-specific code (SERVICE_NOT_ENABLED) needs bespoke
20009
+ // mapping, so it is detected inline rather than via a dedicated
20010
+ // `parseFeeQuoteApiError` parser; everything else flows through the shared
20011
+ // `parseApiError`. Promote to a parser if more coded errors appear.
20012
+ if (isServiceNotEnabled(error)) {
20013
+ throw new KitError({
20014
+ ...InputError.UNSUPPORTED_ROUTE,
20015
+ recoverability: 'FATAL',
20016
+ message: `${SERVICE$1} ${OPERATION$1} failed: source chain not enabled for fee ` + `quotes (SERVICE_NOT_ENABLED)`,
20017
+ cause: {
20018
+ trace: error
20019
+ }
20020
+ });
20021
+ }
20022
+ throw parseApiError(error, {
20023
+ service: SERVICE$1,
20024
+ operation: OPERATION$1
20025
+ });
20026
+ }
20027
+ // Defense-in-depth: a self-consistent quote's per-item fees sum to the
20028
+ // declared total. Enforced here so callers cannot forget the check.
20029
+ assertFeeItemsSumToTotal(quote);
20030
+ return quote;
20031
+ }
20032
+
20033
+ const SERVICE = 'Quote API';
20034
+ const OPERATION = 'validateQuote';
20035
+ /**
20036
+ * Validate a signed quote against a complete source-chain contract call.
20037
+ *
20038
+ * @param params - The source domain, ABI signature, call arguments, and environment.
20039
+ * @returns The claimability, binding checks, and authoritative expiry status.
20040
+ * @throws {@link KitError} When input, transport, or response validation fails.
20041
+ *
20042
+ * @example
20043
+ * ```typescript
20044
+ * import { validateQuote } from '@circle-fin/provider-fee-v1'
20045
+ *
20046
+ * const result = await validateQuote({
20047
+ * sourceDomain: 3,
20048
+ * // The exact function + arguments, in ABI order, that will be burned on-chain.
20049
+ * abiSignature:
20050
+ * 'depositForBurnWithHookAndFees(uint256,uint32,bytes32,address,bytes32,bytes,(bytes,address))',
20051
+ * args: [
20052
+ * '1000000',
20053
+ * '26',
20054
+ * '0x0000000000000000000000001111111111111111111111111111111111111111',
20055
+ * '0x2222222222222222222222222222222222222222',
20056
+ * '0x0000000000000000000000000000000000000000000000000000000000000000',
20057
+ * '0x636374702d666f72776172640000000000000000000000000000000000000000',
20058
+ * ['0x01abcd', '0x3333333333333333333333333333333333333333'],
20059
+ * ],
20060
+ * isTestnet: false,
20061
+ * })
20062
+ * console.log(result.claimable, result.expiry.secondsRemaining)
20063
+ * ```
20064
+ */ async function validateQuote(params) {
20065
+ const parsed = validateQuoteInputSchema.safeParse(params);
20066
+ if (!parsed.success) {
20067
+ const detail = parsed.error.issues.map((issue)=>`${issue.path.join('.')}: ${issue.message}`).join('; ');
20068
+ throw new KitError({
20069
+ ...InputError.VALIDATION_FAILED,
20070
+ recoverability: 'FATAL',
20071
+ message: `${SERVICE} ${OPERATION} failed: ${detail}`,
20072
+ cause: {
20073
+ trace: parsed.error.issues
20074
+ }
20075
+ });
20076
+ }
20077
+ const { sourceDomain, abiSignature, args, isTestnet, baseUrl } = parsed.data;
20078
+ // `config` was validated by the schema above; spread the caller's original,
20079
+ // precisely typed `Partial<ApiPollingConfig>` so the merged polling config
20080
+ // stays assignable under `exactOptionalPropertyTypes`.
20081
+ const pollingConfig = {
20082
+ ...FEE_QUOTE_DEFAULT_CONFIG,
20083
+ ...params.config
20084
+ };
20085
+ const resolvedBaseUrl = baseUrl ?? (isTestnet ? IRIS_API_SANDBOX_BASE_URL : IRIS_API_BASE_URL);
20086
+ const url = new URL(`${QUOTE_VALIDATE_USDC_PATH}/${String(sourceDomain)}`, resolvedBaseUrl).toString();
20087
+ try {
20088
+ return await pollApiPost(url, {
20089
+ abiSignature,
20090
+ args
20091
+ }, isValidateQuoteResult, pollingConfig);
20092
+ } catch (error) {
20093
+ if (isServiceNotEnabled(error)) {
20094
+ throw new KitError({
20095
+ ...InputError.UNSUPPORTED_ROUTE,
20096
+ recoverability: 'FATAL',
20097
+ message: `${SERVICE} ${OPERATION} failed: source chain not enabled for ` + `quote validation (SERVICE_NOT_ENABLED)`,
20098
+ cause: {
20099
+ trace: error
20100
+ }
20101
+ });
20102
+ }
20103
+ throw parseApiError(error, {
20104
+ service: SERVICE,
20105
+ operation: OPERATION
20106
+ });
20107
+ }
20108
+ }
20109
+
20110
+ /** Refresh timestamp quotes this many seconds before submission. */ const QUOTE_EXPIRY_SAFETY_SECONDS = 30;
20111
+ /** 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))';
20112
+ /** Unrestricted CCTP destination caller used by the forwarding relayer. */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
20113
+ function isQuoteNearEstimatedExpiry(quote) {
20114
+ const currentSeconds = Math.floor(Date.now() / 1_000);
20115
+ const expiresAt = quote.expiry.mode === 'TIMESTAMP' ? quote.expiry.expiresAt : quote.expiry.blockEstimatedAt;
20116
+ // A BLOCK_NUMBER quote may omit the advisory `blockEstimatedAt` estimate; when
20117
+ // it is absent, skip this wall-clock pre-check and defer to the authoritative
20118
+ // source-chain-tip validation performed downstream.
20119
+ if (expiresAt === undefined) {
20120
+ return false;
20121
+ }
20122
+ return expiresAt <= currentSeconds + QUOTE_EXPIRY_SAFETY_SECONDS;
20123
+ }
20124
+ function assertSourceFeeRoute(params) {
20125
+ const { source, destination, config } = params;
20126
+ if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
20127
+ throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
20128
+ }
20129
+ const useForwarder = destination.useForwarder;
20130
+ if (useForwarder !== true) {
20131
+ throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
20132
+ }
20133
+ if (config.customFee !== undefined) {
20134
+ 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.');
20135
+ }
20136
+ }
20137
+ function buildQuoteBinding(params) {
20138
+ assertSourceFeeRoute(params);
20139
+ const mintRecipient = params.destination.recipientAddress ?? params.destination.address;
20140
+ const hookData = buildForwardingHookData();
20141
+ const requests = [
20142
+ {
20143
+ type: 'FORWARD',
20144
+ params: {
20145
+ hookData,
20146
+ destinationCaller: ZERO_ADDRESS$1
20147
+ }
20148
+ }
20149
+ ];
20150
+ if ((params.config.transferSpeed ?? TransferSpeed.FAST) === TransferSpeed.FAST) {
20151
+ requests.push({
20152
+ type: 'PRE_FINALITY'
20153
+ });
20154
+ }
20155
+ return {
20156
+ sourceDomain: params.source.chain.cctp.domain,
20157
+ destinationDomain: params.destination.chain.cctp.domain,
20158
+ isTestnet: params.source.chain.isTestnet,
20159
+ amount: params.amount,
20160
+ mintRecipient,
20161
+ hookData,
20162
+ destinationCaller: ZERO_ADDRESS$1,
20163
+ feeToken: params.source.chain.usdcAddress,
20164
+ requests
20165
+ };
20166
+ }
20167
+ async function fetchBoundQuote(binding) {
20168
+ try {
20169
+ const quote = await fetchFeeQuote({
20170
+ sourceDomain: binding.sourceDomain,
20171
+ destinationDomain: binding.destinationDomain,
20172
+ amount: binding.amount,
20173
+ feeToken: binding.feeToken,
20174
+ requests: binding.requests,
20175
+ isTestnet: binding.isTestnet
20176
+ });
20177
+ if (quote.feeToken.toLowerCase() !== binding.feeToken.toLowerCase()) {
20178
+ throw createValidationFailedError$1('feeToken', quote.feeToken, 'Fee Service must return source-chain USDC for source-fee bridging');
20179
+ }
20180
+ return quote;
20181
+ } catch (error) {
20182
+ if (isRateLimitError(error)) {
20183
+ throw new KitError({
20184
+ ...RateLimitError.RATE_LIMIT_EXCEEDED,
20185
+ recoverability: 'RETRYABLE',
20186
+ message: 'Fee Service rate limit exceeded. Retry with caller-managed exponential backoff; Bridge Kit does not retry signed quote requests automatically.',
20187
+ cause: {
20188
+ trace: error
20189
+ }
20190
+ });
20191
+ }
20192
+ throw error;
20193
+ }
20194
+ }
20195
+ async function fetchSubmissionQuote(binding) {
20196
+ let quote = await fetchBoundQuote(binding);
20197
+ if (isQuoteNearEstimatedExpiry(quote)) {
20198
+ quote = await fetchBoundQuote(binding);
20199
+ }
20200
+ if (isQuoteNearEstimatedExpiry(quote)) {
20201
+ throw createValidationFailedError$1('quote', undefined, 'Fee Service returned a quote too close to expiry for safe submission');
20202
+ }
20203
+ return quote;
20204
+ }
20205
+ async function validateBoundQuote(binding, signedQuote, refundAddress) {
20206
+ return validateQuote({
20207
+ sourceDomain: binding.sourceDomain,
20208
+ abiSignature: BURN_WITH_FEES_ABI_SIGNATURE,
20209
+ args: [
20210
+ binding.amount,
20211
+ String(binding.destinationDomain),
20212
+ padAddressToBytes32(binding.mintRecipient),
20213
+ binding.feeToken,
20214
+ padAddressToBytes32(binding.destinationCaller),
20215
+ binding.hookData,
20216
+ [
20217
+ signedQuote,
20218
+ refundAddress
20219
+ ]
20220
+ ],
20221
+ isTestnet: binding.isTestnet
20222
+ });
20223
+ }
20224
+ function isValidationSafe(binding, signedQuote, validation, expectedQuote) {
20225
+ 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);
20226
+ }
20227
+ function unsafeQuoteError(validation) {
20228
+ const detail = validation.failedChecks.length > 0 ? ` (${validation.failedChecks.join(', ')})` : '';
20229
+ return createValidationFailedError$1('quote', undefined, `The fee quote is not safe for submission${detail}; ` + 'call estimate again to obtain a valid quote');
20230
+ }
20231
+ function toExecutionFeeQuote(quote) {
20232
+ return {
20233
+ signedQuote: quote.signedQuote,
20234
+ feeToken: quote.feeToken,
20235
+ feeTotalAmount: quote.feeTotalAmount
20236
+ };
20237
+ }
20238
+ function toFeeItems(quote) {
20239
+ return quote.items.map((item)=>({
20240
+ type: item.type,
20241
+ amount: formatUnits(item.amount, 6),
20242
+ args: item.args,
20243
+ argsHash: item.argsHash
20244
+ }));
20245
+ }
20246
+ /**
20247
+ * Estimate a source-fee bridge using a source-denominated signed fee quote.
20248
+ *
20249
+ * @internal
20250
+ */ async function estimateSourceFeeBridge(params) {
20251
+ const binding = buildQuoteBinding(params);
20252
+ const quote = await fetchSubmissionQuote(binding);
20253
+ const feeTotal = formatUnits(quote.feeTotalAmount, 6);
20254
+ return {
20255
+ token: 'USDC',
20256
+ amount: formatUnits(params.amount, 6),
20257
+ source: {
20258
+ address: params.source.address,
20259
+ chain: params.source.chain.chain
20260
+ },
20261
+ destination: {
20262
+ address: params.destination.address,
20263
+ chain: params.destination.chain.chain,
20264
+ ...params.destination.recipientAddress !== undefined && {
20265
+ recipientAddress: params.destination.recipientAddress
20266
+ }
20267
+ },
20268
+ gasFees: [],
20269
+ fees: quote.items.map((item)=>({
20270
+ type: item.type === 'FORWARD' ? 'forwarder' : 'provider',
20271
+ token: 'USDC',
20272
+ amount: formatUnits(item.amount, 6)
20273
+ })),
20274
+ amountReceived: formatUnits(params.amount, 6),
20275
+ feeTotal,
20276
+ feeItems: toFeeItems(quote),
20277
+ totalDebit: formatUnits((BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString(), 6),
20278
+ quoteExpiry: quote.expiry,
20279
+ quote: quote.signedQuote
20280
+ };
20281
+ }
20282
+ async function readAllowance(params, delegate) {
20283
+ const operationContext = {
20284
+ chain: params.source.chain,
20285
+ address: params.source.address
20286
+ };
20287
+ const prepared = await params.source.adapter.prepareAction('usdc.allowance', {
20288
+ walletAddress: params.source.address,
20289
+ delegate
20290
+ }, operationContext);
20291
+ return BigInt(String(await prepared.execute()));
20292
+ }
20293
+ async function executeAndConfirm(request, params, provider) {
20294
+ const txHash = await request.execute();
20295
+ const data = await provider.waitForTransaction(params.source.adapter, txHash, params.source.chain);
20296
+ return {
20297
+ txHash,
20298
+ data
20299
+ };
20300
+ }
20301
+ async function prepareAndPreflight(params, binding, quote, provider) {
20302
+ assertSourceFeeRoute(params);
20303
+ const totalDebit = (BigInt(params.amount) + BigInt(quote.feeTotalAmount)).toString();
20304
+ const operationContext = {
20305
+ chain: params.source.chain,
20306
+ address: params.source.address
20307
+ };
20308
+ await validateBalanceForTransaction({
20309
+ adapter: params.source.adapter,
20310
+ amount: totalDebit,
20311
+ token: 'USDC',
20312
+ tokenAddress: params.source.chain.usdcAddress,
20313
+ operationContext
20314
+ });
20315
+ const prepared = await provider.burnWithFees({
20316
+ source: params.source,
20317
+ destinationChain: params.destination.chain,
20318
+ amount: params.amount,
20319
+ mintRecipient: binding.mintRecipient,
20320
+ destinationCaller: binding.destinationCaller,
20321
+ hookData: binding.hookData,
20322
+ claim: {
20323
+ signedQuote: quote.signedQuote,
20324
+ refundAddress: params.source.address
20325
+ },
20326
+ feeToken: quote.feeToken,
20327
+ feeTotalAmount: quote.feeTotalAmount
20328
+ });
20329
+ const wrapper = resolveCCTPV2ContractAddress(params.source.chain, 'tokenMessengerWithFees');
20330
+ const allowance = await readAllowance(params, wrapper);
20331
+ if (allowance >= BigInt(totalDebit)) {
20332
+ return {
20333
+ prepared,
20334
+ approveStep: {
20335
+ name: 'approve',
20336
+ state: 'noop'
20337
+ }
20338
+ };
20339
+ }
20340
+ let lastApproval;
20341
+ for (const approval of prepared.approvals){
20342
+ lastApproval = await executeAndConfirm(approval, params, provider);
20343
+ }
20344
+ return {
20345
+ prepared,
20346
+ approveStep: {
20347
+ name: 'approve',
20348
+ state: 'success',
20349
+ data: lastApproval?.data,
20350
+ ...lastApproval?.txHash !== undefined && {
20351
+ txHash: lastApproval.txHash,
20352
+ explorerUrl: buildExplorerUrl(params.source.chain, lastApproval.txHash)
20353
+ }
20354
+ }
20355
+ };
20356
+ }
20357
+ async function resolveExecutionQuote(binding, suppliedQuote, refundAddress) {
20358
+ if (suppliedQuote === undefined) {
20359
+ const fetchedQuote = await fetchSubmissionQuote(binding);
20360
+ return {
20361
+ quote: toExecutionFeeQuote(fetchedQuote),
20362
+ fetchedQuote
20363
+ };
20364
+ }
20365
+ const validation = await validateBoundQuote(binding, suppliedQuote, refundAddress);
20366
+ if (!isValidationSafe(binding, suppliedQuote, validation)) {
20367
+ throw unsafeQuoteError(validation);
20368
+ }
20369
+ return {
20370
+ quote: toExecutionFeeQuote(validation),
20371
+ fetchedQuote: undefined
20372
+ };
20373
+ }
20374
+ /**
20375
+ * Execute a source-fee bridge while preserving receive-exact semantics.
20376
+ *
20377
+ * @internal
20378
+ */ async function executeSourceFeeBridge(rawParams, params, provider) {
20379
+ // Narrows `params` to the CCTP v2 route type for the rest of this function.
20380
+ // buildQuoteBinding asserts too, but that narrows its own scope, not this one.
20381
+ assertSourceFeeRoute(params);
20382
+ const binding = buildQuoteBinding(params);
20383
+ const suppliedQuote = rawParams.quote;
20384
+ let { quote, fetchedQuote } = await resolveExecutionQuote(binding, suppliedQuote, params.source.address);
20385
+ let { prepared, approveStep } = await prepareAndPreflight(params, binding, quote, provider);
20386
+ // Validate against the current source-chain tip after approval confirmation.
20387
+ // BLOCK_NUMBER expiries cannot be checked safely with wall-clock time alone.
20388
+ let validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
20389
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
20390
+ if (suppliedQuote === undefined) {
20391
+ fetchedQuote = await fetchSubmissionQuote(binding);
20392
+ quote = toExecutionFeeQuote(fetchedQuote);
20393
+ const refreshedPreparation = await prepareAndPreflight(params, binding, quote, provider);
20394
+ prepared = refreshedPreparation.prepared;
20395
+ if (refreshedPreparation.approveStep.state !== 'noop') {
20396
+ approveStep = refreshedPreparation.approveStep;
20397
+ }
20398
+ validation = await validateBoundQuote(binding, quote.signedQuote, params.source.address);
20399
+ if (!isValidationSafe(binding, quote.signedQuote, validation, fetchedQuote)) {
20400
+ throw unsafeQuoteError(validation);
20401
+ }
20402
+ } else {
20403
+ throw unsafeQuoteError(validation);
20404
+ }
20405
+ }
20406
+ // Surface the same step events as the standard bridge() path so
20407
+ // kit.on('approve'|'burn'|'mint', ...) handlers fire for source-fee bridges.
20408
+ if (approveStep.state !== 'noop') {
20409
+ provider.emitBridgeStep('approve', approveStep);
20410
+ }
20411
+ const burn = await executeAndConfirm(prepared.burn, params, provider);
20412
+ const burnStep = {
20413
+ name: 'burn',
20414
+ state: 'success',
20415
+ txHash: burn.txHash,
20416
+ data: burn.data,
20417
+ explorerUrl: buildExplorerUrl(params.source.chain, burn.txHash)
20418
+ };
20419
+ provider.emitBridgeStep('burn', burnStep);
20420
+ const resultBase = {
20421
+ amount: params.amount,
20422
+ token: 'USDC',
20423
+ config: params.config,
20424
+ provider: provider.name,
20425
+ source: {
20426
+ address: params.source.address,
20427
+ chain: params.source.chain
20428
+ },
20429
+ destination: {
20430
+ address: params.destination.address,
20431
+ chain: params.destination.chain,
20432
+ ...params.destination.recipientAddress !== undefined && {
20433
+ recipientAddress: params.destination.recipientAddress
20434
+ },
20435
+ useForwarder: true
20436
+ }
20437
+ };
20438
+ const attestation = await provider.fetchRelayerMint(params.source, burn.txHash);
20439
+ const forwardTxHash = attestation.forwardTxHash;
20440
+ // The burn already moved funds. If the relayer confirms without a
20441
+ // destination hash, surface an error-state result that preserves the burn
20442
+ // step (so `retry()` can resume the mint) instead of throwing and discarding
20443
+ // the completed burn.
20444
+ if (typeof forwardTxHash !== 'string' || forwardTxHash.trim() === '') {
20445
+ const mintStep = {
20446
+ name: 'mint',
20447
+ state: 'error',
20448
+ forwarded: true,
20449
+ errorCategory: 'failed_offchain',
20450
+ errorMessage: 'Relayer confirmation did not include a destination transaction hash'
20451
+ };
20452
+ provider.emitBridgeStep('mint', mintStep);
20453
+ return {
20454
+ ...resultBase,
20455
+ state: 'error',
20456
+ steps: [
20457
+ approveStep,
20458
+ burnStep,
20459
+ mintStep
20460
+ ]
20461
+ };
20462
+ }
20463
+ const mintStep = {
20464
+ name: 'mint',
20465
+ state: 'success',
20466
+ forwarded: true,
20467
+ txHash: forwardTxHash,
20468
+ explorerUrl: buildExplorerUrl(params.destination.chain, forwardTxHash)
20469
+ };
20470
+ provider.emitBridgeStep('mint', mintStep);
20471
+ return {
20472
+ ...resultBase,
20473
+ state: 'success',
20474
+ steps: [
20475
+ approveStep,
20476
+ burnStep,
20477
+ mintStep
20478
+ ]
20479
+ };
20480
+ }
20481
+
19129
20482
  /** SDK name used in telemetry payloads. */ const SDK_NAME$3 = resolveKitSdkName(pkg$5.name);
19130
20483
  /**
19131
20484
  * Pick the most-relevant `txHash` to attach to an error telemetry payload.
@@ -19353,11 +20706,18 @@ function assertCCTPV2Config(config) {
19353
20706
  this.validateNetworkCompatibility(resolvedParams);
19354
20707
  // Merge the custom fee config into the resolved params
19355
20708
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
19356
- // Find a provider that supports this route
19357
- const provider = this.findProviderForRoute(finalResolvedParams);
19358
- // Execute the transfer using the provider
19359
- // Format the bridge result into human-readable string values for the user
19360
- const result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
20709
+ let result;
20710
+ // Execute the explicit source-fee path without changing legacy
20711
+ // useForwarder behavior for callers that did not opt in.
20712
+ if (params.config?.feePayment === 'source') {
20713
+ const sourceFeeProvider = this.findSourceFeeProvider(finalResolvedParams);
20714
+ result = formatBridgeResult(await executeSourceFeeBridge(params, finalResolvedParams, sourceFeeProvider), 'to-human-readable');
20715
+ } else {
20716
+ // Find a provider that supports this route
20717
+ const provider = this.findProviderForRoute(finalResolvedParams);
20718
+ // Execute the transfer using the provider and format the result.
20719
+ result = formatBridgeResult(await provider.bridge(finalResolvedParams), 'to-human-readable');
20720
+ }
19361
20721
  // Emit error telemetry when the provider returns an error state
19362
20722
  // (provider records step failures in the result instead of throwing).
19363
20723
  if (result.state === 'error') {
@@ -19480,42 +20840,7 @@ function assertCCTPV2Config(config) {
19480
20840
  tokenIn: result.token
19481
20841
  });
19482
20842
  }
19483
- /**
19484
- * Estimate the cost and fees for a cross-chain USDC bridge operation.
19485
- *
19486
- * This method calculates the expected gas fees and protocol costs for bridging
19487
- * without actually executing the transaction. It performs the same validation
19488
- * as the bridge method but stops before execution.
19489
- *
19490
- * @param params - The bridge parameters for cost estimation, including optional invocation metadata
19491
- * @returns Promise resolving to detailed cost breakdown including gas estimates
19492
- * @throws {KitError} When the parameters are invalid.
19493
- * @throws {UnsupportedRouteError} When the route is not supported.
19494
- *
19495
- * @example
19496
- * ```typescript
19497
- * // Basic usage
19498
- * const estimate = await kit.estimate({
19499
- * from: { adapter: adapter, chain: 'Ethereum' },
19500
- * to: { adapter: adapter, chain: 'Base' },
19501
- * amount: '10.50',
19502
- * token: 'USDC'
19503
- * })
19504
- * console.log('Estimated cost:', estimate.totalCost)
19505
- *
19506
- * // With custom invocation metadata
19507
- * const estimate = await kit.estimate({
19508
- * from: { adapter: adapter, chain: 'Ethereum' },
19509
- * to: { adapter: adapter, chain: 'Base' },
19510
- * amount: '10.50',
19511
- * token: 'USDC',
19512
- * invocationMeta: {
19513
- * traceId: 'custom-trace-id',
19514
- * callers: [{ type: 'app', name: 'MyDApp', version: '1.0.0' }],
19515
- * },
19516
- * })
19517
- * ```
19518
- */ async estimate(params) {
20843
+ async estimate(params) {
19519
20844
  return withErrorTelemetry(async ()=>{
19520
20845
  // First validate the parameters
19521
20846
  assertBridgeParams(params, bridgeParamsWithChainIdentifierSchema);
@@ -19525,6 +20850,10 @@ function assertCCTPV2Config(config) {
19525
20850
  this.validateNetworkCompatibility(resolvedParams);
19526
20851
  // Merge the custom fee config into the resolved params
19527
20852
  const finalResolvedParams = await this.mergeCustomFeeConfig(resolvedParams);
20853
+ if (params.config?.feePayment === 'source') {
20854
+ this.findSourceFeeProvider(finalResolvedParams);
20855
+ return estimateSourceFeeBridge(finalResolvedParams);
20856
+ }
19528
20857
  // Find a provider that supports this route
19529
20858
  const provider = this.findProviderForRoute(finalResolvedParams);
19530
20859
  // Estimate the transfer using the provider and format amounts to human-readable strings
@@ -19573,6 +20902,9 @@ function assertCCTPV2Config(config) {
19573
20902
  * // Get only chains that support forwarding
19574
20903
  * const forwarderChains = kit.getSupportedChains({ forwarderSupported: true })
19575
20904
  *
20905
+ * // Get only chains that can pay fees on the source chain (receive-exact)
20906
+ * const sourceFeeChains = kit.getSupportedChains({ sourceFeeSupported: true })
20907
+ *
19576
20908
  * console.log('Supported chains:')
19577
20909
  * allChains.forEach(chain => {
19578
20910
  * console.log(`- ${chain.name} (${chain.type})`)
@@ -19621,6 +20953,10 @@ function assertCCTPV2Config(config) {
19621
20953
  return options.forwarderSupported ? fs.source || fs.destination : !fs.source && !fs.destination;
19622
20954
  });
19623
20955
  }
20956
+ // Apply source-paid ("receive-exact") fee support filter if provided
20957
+ if (options?.sourceFeeSupported !== undefined) {
20958
+ chains = chains.filter((chain)=>hasSourceFeeSupport(chain) === options.sourceFeeSupported);
20959
+ }
19624
20960
  return chains;
19625
20961
  }
19626
20962
  /**
@@ -19655,6 +20991,20 @@ function assertCCTPV2Config(config) {
19655
20991
  return provider;
19656
20992
  }
19657
20993
  /**
20994
+ * Find the default CCTP v2 provider for a source-fee forwarding route.
20995
+ *
20996
+ * @param params - The resolved provider parameters.
20997
+ * @returns The CCTP v2 provider that supports the forwarded route.
20998
+ * @throws {UnsupportedRouteError} When no source-fee provider supports the route.
20999
+ * @internal
21000
+ */ findSourceFeeProvider(params) {
21001
+ const provider = this.providers.find((candidate)=>candidate instanceof CCTPV2BridgingProvider && candidate.supportsRoute(params.source.chain, params.destination.chain, params.token, true));
21002
+ if (!(provider instanceof CCTPV2BridgingProvider)) {
21003
+ throw createUnsupportedRouteError(params.source.chain.name, params.destination.chain.name);
21004
+ }
21005
+ return provider;
21006
+ }
21007
+ /**
19658
21008
  * Merge custom fee configuration into provider parameters.
19659
21009
  *
19660
21010
  * Prioritizes any custom fee configuration already present on the
@@ -19794,7 +21144,7 @@ function assertCCTPV2Config(config) {
19794
21144
  registerKit(`${pkg$5.name}/${pkg$5.version}`);
19795
21145
 
19796
21146
  var name$3 = "@circle-fin/swap-kit";
19797
- var version$3 = "1.5.2";
21147
+ var version$3 = "1.6.0";
19798
21148
  var pkg$3 = {
19799
21149
  name: name$3,
19800
21150
  version: version$3};
@@ -19808,7 +21158,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
19808
21158
  * Catches obviously malformed addresses at parse time; chain-specific validation
19809
21159
  * is performed in buildServiceParams.
19810
21160
  */ const destinationAddressSchema = z.union([
19811
- evmAddressSchema,
21161
+ evmAddressSchema$1,
19812
21162
  solanaAddressSchema
19813
21163
  ]);
19814
21164
  /**
@@ -19854,9 +21204,16 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
19854
21204
  message: 'stopLimit must be greater than 0'
19855
21205
  }).optional(),
19856
21206
  customFee: serviceSwapCustomFeeSchema.optional(),
21207
+ apiKey: z.string({
21208
+ invalid_type_error: 'apiKey must be a string'
21209
+ })// Tolerate '' so the `process.env.CIRCLE_API_KEY ?? ''` idiom falls back to
21210
+ // kitKey via resolveApiKey instead of being rejected here.
21211
+ .optional(),
19857
21212
  kitKey: z.string({
19858
21213
  invalid_type_error: 'kitKey must be a string'
19859
- }).min(1, 'kitKey must be a non-empty string').optional(),
21214
+ })// Tolerate '' so an unset kit-key env var yields the permissionless path,
21215
+ // matching buildServiceParams (which already omits an empty credential).
21216
+ .optional(),
19860
21217
  provider: z.string({
19861
21218
  invalid_type_error: 'provider must be a string'
19862
21219
  }).min(1, 'provider must be a non-empty string').optional(),
@@ -20021,23 +21378,49 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20021
21378
  fees: z.array(serviceSwapFeeSchema).optional()
20022
21379
  }).passthrough();
20023
21380
 
20024
- /** Placeholder used wherever a kit key is scrubbed from logs, traces, or errors. */ const REDACTED_KIT_KEY = '[redacted]';
21381
+ /** Placeholder used wherever a credential is scrubbed from logs, traces, or errors. */ const REDACTED_CREDENTIAL = '[redacted]';
21382
+ /**
21383
+ * Config fields that carry a Stablecoin Service credential.
21384
+ *
21385
+ * `apiKey` is the supported field; `kitKey` is the deprecated alias. Redaction
21386
+ * and credential resolution both read this list so a newly added field is
21387
+ * scrubbed everywhere it needs to be.
21388
+ */ const CREDENTIAL_CONFIG_FIELDS = [
21389
+ 'apiKey',
21390
+ 'kitKey'
21391
+ ];
20025
21392
 
21393
+ /**
21394
+ * Replaces any credential field present on a config with a redaction marker.
21395
+ *
21396
+ * Covers `apiKey` and the deprecated `kitKey`, so adding a credential field
21397
+ * without extending this list cannot silently leak it into a trace. Returns the
21398
+ * config unchanged when it carries no credential.
21399
+ */ const redactCredentialFields = (config)=>{
21400
+ const present = CREDENTIAL_CONFIG_FIELDS.filter((field)=>field in config);
21401
+ if (present.length === 0) {
21402
+ return config;
21403
+ }
21404
+ return present.reduce((acc, field)=>({
21405
+ ...acc,
21406
+ [field]: REDACTED_CREDENTIAL
21407
+ }), {
21408
+ ...config
21409
+ });
21410
+ };
20026
21411
  /**
20027
21412
  * Builds a redacted projection of swap params that is safe to embed in error
20028
21413
  * traces (which may be logged downstream). Allow-lists only non-sensitive
20029
21414
  * fields and drops `from` entirely — `from.adapter` can carry signer/private-key
20030
- * material for signer-backed (viem/ethers) flows — and redacts `config.kitKey`.
21415
+ * material for signer-backed (viem/ethers) flows — and redacts every credential
21416
+ * field on `config`.
20031
21417
  */ const redactSwapParamsForTrace = (params)=>{
20032
21418
  if (typeof params !== 'object' || params === null) {
20033
21419
  return params;
20034
21420
  }
20035
21421
  const record = params;
20036
21422
  const config = record['config'];
20037
- const redactedConfig = typeof config === 'object' && config !== null && 'kitKey' in config ? {
20038
- ...config,
20039
- kitKey: REDACTED_KIT_KEY
20040
- } : config;
21423
+ const redactedConfig = typeof config === 'object' && config !== null ? redactCredentialFields(config) : config;
20041
21424
  return {
20042
21425
  tokenIn: record['tokenIn'],
20043
21426
  tokenOut: record['tokenOut'],
@@ -20121,6 +21504,41 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20121
21504
  *
20122
21505
  * @internal
20123
21506
  */ const MAX_RATE_ADDRESSES_PER_REQUEST = 100;
21507
+ /**
21508
+ * Environment prefixes carried by Circle platform API keys.
21509
+ *
21510
+ * A Circle API key is `<ENV>_API_KEY:<keyId>:<keySecret>`, where `<ENV>` is one
21511
+ * of these prefixes.
21512
+ *
21513
+ * @internal
21514
+ */ const API_KEY_ENV_PREFIXES = [
21515
+ 'TEST',
21516
+ 'LIVE',
21517
+ 'SAND',
21518
+ 'SANDBOX',
21519
+ 'SMOK',
21520
+ 'PROD',
21521
+ 'STAG',
21522
+ 'DEV'
21523
+ ];
21524
+ /**
21525
+ * Accepted credential formats for Stablecoin Service authentication.
21526
+ *
21527
+ * Matches a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
21528
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`). API keys are the
21529
+ * recommended credential; kit keys remain accepted as the legacy path.
21530
+ *
21531
+ * @remarks
21532
+ * This is a local pre-flight check, not the authority — the Stablecoin Service
21533
+ * validates the credential and answers 401 when it rejects one. The prefix list
21534
+ * is therefore deliberately permissive: a valid key carrying a prefix this SDK
21535
+ * has not been taught about should reach the service and be judged there rather
21536
+ * than be refused locally, since refusing locally is indistinguishable from an
21537
+ * outage to the caller. Kept as the single source of truth so the pattern is
21538
+ * not restated per call site.
21539
+ *
21540
+ * @internal
21541
+ */ const API_KEY_PATTERN = new RegExp(`^(?:KIT_KEY|(?:${API_KEY_ENV_PREFIXES.join('|')})_API_KEY)` + ':[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$');
20124
21542
 
20125
21543
  /**
20126
21544
  * Zod schema for validating stop limits.
@@ -20189,13 +21607,14 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20189
21607
  /**
20190
21608
  * Zod schema for validating API keys.
20191
21609
  *
20192
- * Validates that the API key is a valid API key format.
21610
+ * Accepts a Circle platform API key (`<ENV>_API_KEY:<keyId>:<keySecret>`) and
21611
+ * the legacy kit key (`KIT_KEY:<keyId>:<keySecret>`).
20193
21612
  *
20194
21613
  * @example
20195
21614
  * ```typescript
20196
21615
  * import { apiKeySchema } from '@core/service-client'
20197
21616
  *
20198
- * const result = apiKeySchema.safeParse('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
21617
+ * const result = apiKeySchema.safeParse('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
20199
21618
  * if (!result.success) {
20200
21619
  * console.error('Invalid API key format:', result.error.issues)
20201
21620
  * }
@@ -20203,7 +21622,7 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
20203
21622
  */ const apiKeySchema = z.string({
20204
21623
  required_error: 'API key is required',
20205
21624
  invalid_type_error: 'Invalid API key format'
20206
- }).regex(/^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/, 'Invalid API key format');
21625
+ }).regex(API_KEY_PATTERN, 'Invalid API key format');
20207
21626
  /**
20208
21627
  * Zod schema for platform fees configuration.
20209
21628
  *
@@ -20727,8 +22146,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20727
22146
  * Validates that an API key is properly formatted for Circle Stablecoin Service.
20728
22147
  *
20729
22148
  * This function performs validation on API keys to ensure they conform to the
20730
- * expected format before making API requests. The key must follow the structure:
20731
- * `KIT_KEY:keyId:keySecret`
22149
+ * expected format before making API requests. The key must follow the structure
22150
+ * `<ENV>_API_KEY:keyId:keySecret`, or the legacy `KIT_KEY:keyId:keySecret`.
20732
22151
  *
20733
22152
  * @param apiKey - The API key to validate (can be any type for graceful handling).
20734
22153
  * @returns True if the API key is valid, false otherwise.
@@ -20736,7 +22155,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20736
22155
  * @remarks
20737
22156
  * This function handles invalid inputs gracefully by returning false rather than
20738
22157
  * throwing errors. It validates:
20739
- * - The key starts with the `KIT_KEY:` prefix
22158
+ * - The key carries a recognized prefix — `<ENV>_API_KEY:` for a Circle platform
22159
+ * API key, or `KIT_KEY:` for the legacy kit key
20740
22160
  * - Contains exactly three colon-separated parts
20741
22161
  * - keyId and keySecret contain only valid characters (alphanumeric, `-`, `_`, `.`)
20742
22162
  * - keyId and keySecret are non-empty
@@ -20744,40 +22164,80 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20744
22164
  *
20745
22165
  * Valid characters in keyId and keySecret: `a-z`, `A-Z`, `0-9`, `-`, `_`, `.`
20746
22166
  *
22167
+ * Format is checked locally only so an obviously malformed credential fails
22168
+ * before a request is made. The Stablecoin Service remains the authority on
22169
+ * whether a well-formed key is actually accepted.
22170
+ *
20747
22171
  * @example
20748
22172
  * ```typescript
20749
22173
  * import { isValidApiKey } from '@core/service-client'
20750
22174
  *
20751
22175
  * // Valid API key
20752
- * const isValid = isValidApiKey('KIT_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
22176
+ * const isValid = isValidApiKey('TEST_API_KEY:e84d2546d4e321b2ff427dc988c89503:f84d2548d4e322b2ff427fc989c87503')
20753
22177
  * console.log(isValid) // true
20754
22178
  *
22179
+ * // Legacy kit keys remain valid
22180
+ * console.log(isValidApiKey('KIT_KEY:my-key-id:my-key-secret')) // true
22181
+ *
20755
22182
  * ```
20756
22183
  */ const isValidApiKey = (apiKey)=>{
20757
22184
  // Handle invalid input types
20758
22185
  if (typeof apiKey !== 'string') {
20759
22186
  return false;
20760
22187
  }
20761
- const apiKeyPattern = /^KIT_KEY:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/;
20762
22188
  // Validate without trimming - any whitespace will cause validation to fail
20763
- return apiKeyPattern.test(apiKey);
22189
+ return API_KEY_PATTERN.test(apiKey);
20764
22190
  };
20765
22191
  /**
20766
- * Assert that a Stablecoin Service kit key is not being supplied from a browser.
22192
+ * Resolve the credential to authenticate a Stablecoin Service request with.
22193
+ *
22194
+ * `apiKey` is the supported field; `kitKey` is the deprecated alias kept for
22195
+ * existing integrations. When both are supplied `apiKey` wins, so a caller
22196
+ * migrating field-by-field cannot be silently pinned to a stale credential.
20767
22197
  *
20768
- * The kit key (`KIT_KEY:<id>:<secret>`) is a server-only secret. Every
20769
- * Stablecoin Service request that attaches an `Authorization: Bearer` header
20770
- * funnels through this package, so calling this guard before that header is
20771
- * built prevents the secret from being sent from and thus bundled into — a
20772
- * client application. In Node.js the check is a no-op, preserving the
20773
- * legitimate "hold the kit key on the server, forward the prepared transaction
20774
- * to the client" flow. When no kit key is supplied the permissionless (keyless)
20775
- * client path remains fully allowed.
22198
+ * An empty-string value is treated as absent on either field. Without this, the
22199
+ * common `process.env.CIRCLE_API_KEY ?? ''` idiom (which yields `''` when the
22200
+ * variable is unset) would either shadow a working `kitKey` or, on a bare
22201
+ * `kitKey: ''`, reach downstream validation as an invalid credential instead of
22202
+ * falling through to the permissionless path.
22203
+ *
22204
+ * @param source - Object carrying either credential field, or neither.
22205
+ * @returns The credential to use, or `undefined` for the permissionless
22206
+ * (keyless) path.
22207
+ *
22208
+ * @example
22209
+ * ```typescript
22210
+ * import { resolveApiKey } from '@core/service-client'
20776
22211
  *
20777
- * @param apiKey - The inline kit key for the request, or `undefined` when none
20778
- * was supplied (permissionless mode).
22212
+ * resolveApiKey({ apiKey: 'TEST_API_KEY:id:secret' }) // 'TEST_API_KEY:id:secret'
22213
+ * resolveApiKey({ kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
22214
+ * resolveApiKey({ apiKey: '', kitKey: 'KIT_KEY:id:secret' }) // 'KIT_KEY:id:secret'
22215
+ * resolveApiKey({ kitKey: '' }) // undefined
22216
+ * resolveApiKey({}) // undefined
22217
+ * ```
22218
+ */ const resolveApiKey = (source)=>{
22219
+ // Treat an empty-string value as absent on either field so the
22220
+ // `env ?? ''` idiom falls through to the next credential (or permissionless).
22221
+ const normalize = (value)=>value !== undefined && value !== '' ? value : undefined;
22222
+ return normalize(source.apiKey) ?? normalize(source.kitKey);
22223
+ };
22224
+ /**
22225
+ * Assert that a Stablecoin Service credential is not supplied from a browser.
22226
+ *
22227
+ * An API key (`<ENV>_API_KEY:<id>:<secret>`) and a legacy kit key
22228
+ * (`KIT_KEY:<id>:<secret>`) are both server-only secrets. Every Stablecoin
22229
+ * Service request that attaches an `Authorization: Bearer` header funnels
22230
+ * through this package, so calling this guard before that header is built
22231
+ * prevents the secret from being sent from — and thus bundled into — a client
22232
+ * application. In Node.js the check is a no-op, preserving the legitimate "hold
22233
+ * the credential on the server, forward the prepared transaction to the client"
22234
+ * flow. When no credential is supplied the permissionless (keyless) client path
22235
+ * remains fully allowed.
22236
+ *
22237
+ * @param apiKey - The inline credential for the request, or `undefined` when
22238
+ * none was supplied (permissionless mode).
20779
22239
  * @returns Nothing.
20780
- * @throws KitError with VALIDATION_FAILED when a kit key is supplied while
22240
+ * @throws KitError with VALIDATION_FAILED when a credential is supplied while
20781
22241
  * running in a browser environment. The secret value is never echoed.
20782
22242
  *
20783
22243
  * @example
@@ -20785,10 +22245,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20785
22245
  * import { assertBrowserSafeApiKey } from '@core/service-client'
20786
22246
  *
20787
22247
  * // Server (Node.js): no-op, request proceeds with the Authorization header.
20788
- * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
22248
+ * assertBrowserSafeApiKey('TEST_API_KEY:my-id:my-secret')
20789
22249
  *
20790
22250
  * // Browser: throws to stop the secret from leaking into the client bundle.
20791
- * assertBrowserSafeApiKey('KIT_KEY:my-id:my-secret')
22251
+ * assertBrowserSafeApiKey('TEST_API_KEY:my-id:my-secret')
20792
22252
  *
20793
22253
  * // Browser, permissionless: allowed.
20794
22254
  * assertBrowserSafeApiKey(undefined)
@@ -20798,7 +22258,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20798
22258
  return;
20799
22259
  }
20800
22260
  if (isBrowserEnvironment()) {
20801
- 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');
22261
+ 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');
20802
22262
  }
20803
22263
  };
20804
22264
 
@@ -20810,7 +22270,8 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20810
22270
  *
20811
22271
  * @remarks
20812
22272
  * - Input validation leverages Zod schemas to surface actionable KitErrors.
20813
- * - API key format must match `KIT_KEY:<keyId>:<keySecret>`
22273
+ * - API key format must match `<ENV>_API_KEY:<keyId>:<keySecret>`, or the
22274
+ * legacy `KIT_KEY:<keyId>:<keySecret>`
20814
22275
  *
20815
22276
  * @param params - The create transaction request parameters with API key.
20816
22277
  * @returns A promise that resolves to a typed {@link CreateSwapResponse}.
@@ -20839,7 +22300,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20839
22300
  * recipientAddress: '0x050928e1f9086b20539c767c96909663ff7fbdf2',
20840
22301
  * },
20841
22302
  * },
20842
- * apiKey: 'KIT_KEY:example-key:example-secret',
22303
+ * apiKey: 'TEST_API_KEY:example-key:example-secret',
20843
22304
  * })
20844
22305
  *
20845
22306
  * console.log(`Transaction target: ${transaction.transaction.target}`)
@@ -20858,13 +22319,13 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20858
22319
  const url = new URL('/v1/stablecoinKits/swap', STABLECOIN_SERVICE_BASE_URL).toString();
20859
22320
  // Remove the API key from the request body
20860
22321
  const { apiKey, ...requestBody } = validatedParams;
20861
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22322
+ // Never let a server-only credential leave a browser (no-op in Node.js).
20862
22323
  assertBrowserSafeApiKey(apiKey);
20863
22324
  const effectiveConfig = {
20864
22325
  ...DEFAULT_CONFIG$1,
20865
22326
  headers: {
20866
22327
  ...DEFAULT_CONFIG$1.headers,
20867
- // Permissionless mode: no Authorization header when the kit key is absent.
22328
+ // Permissionless mode: no Authorization header when the credential is absent.
20868
22329
  ...apiKey !== undefined && {
20869
22330
  Authorization: `Bearer ${apiKey}`
20870
22331
  }
@@ -21014,7 +22475,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21014
22475
  }
21015
22476
  // Use validated data
21016
22477
  const validatedParams = result.data;
21017
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22478
+ // Never let a server-only credential leave a browser (no-op in Node.js).
21018
22479
  assertBrowserSafeApiKey(validatedParams.apiKey);
21019
22480
  // Build the API URL
21020
22481
  const url = buildQuoteUrl(validatedParams);
@@ -21023,7 +22484,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21023
22484
  ...DEFAULT_CONFIG$1,
21024
22485
  headers: {
21025
22486
  ...DEFAULT_CONFIG$1.headers,
21026
- // Permissionless mode: no Authorization header when the kit key is absent.
22487
+ // Permissionless mode: no Authorization header when the credential is absent.
21027
22488
  ...validatedParams.apiKey !== undefined && {
21028
22489
  Authorization: `Bearer ${validatedParams.apiKey}`
21029
22490
  }
@@ -21088,14 +22549,14 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21088
22549
  toChain: result.data.toChain
21089
22550
  }
21090
22551
  };
21091
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22552
+ // Never let a server-only credential leave a browser (no-op in Node.js).
21092
22553
  assertBrowserSafeApiKey(validatedParams.apiKey);
21093
22554
  const url = buildSwapStatusUrl(validatedParams);
21094
22555
  const effectiveConfig = {
21095
22556
  ...DEFAULT_CONFIG$1,
21096
22557
  headers: {
21097
22558
  ...DEFAULT_CONFIG$1.headers,
21098
- // Permissionless mode: no Authorization header when the kit key is absent.
22559
+ // Permissionless mode: no Authorization header when the credential is absent.
21099
22560
  ...validatedParams.apiKey !== undefined && {
21100
22561
  Authorization: `Bearer ${validatedParams.apiKey}`
21101
22562
  }
@@ -21121,7 +22582,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21121
22582
  * const url = buildTokenRatesUrl({
21122
22583
  * chain: 'Ethereum',
21123
22584
  * addresses: ['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
21124
- * apiKey: 'KIT_KEY:key:secret',
22585
+ * apiKey: 'TEST_API_KEY:key:secret',
21125
22586
  * })
21126
22587
  * // => 'https://api.circle.com/v1/stablecoinKits/rates?chain=Ethereum&addresses=...'
21127
22588
  * ```
@@ -21194,14 +22655,14 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
21194
22655
  addresses: result.data.addresses
21195
22656
  }
21196
22657
  };
21197
- // Never let a server-only kit key leave a browser (no-op in Node.js).
22658
+ // Never let a server-only credential leave a browser (no-op in Node.js).
21198
22659
  assertBrowserSafeApiKey(validatedParams.apiKey);
21199
22660
  const url = buildTokenRatesUrl(validatedParams);
21200
22661
  const effectiveConfig = {
21201
22662
  ...DEFAULT_CONFIG$1,
21202
22663
  headers: {
21203
22664
  ...DEFAULT_CONFIG$1.headers,
21204
- // Permissionless mode: no Authorization header when the kit key is absent.
22665
+ // Permissionless mode: no Authorization header when the credential is absent.
21205
22666
  ...validatedParams.apiKey !== undefined && {
21206
22667
  Authorization: `Bearer ${validatedParams.apiKey}`
21207
22668
  }
@@ -23179,7 +24640,7 @@ const abiParameterSchema = z.object({
23179
24640
  */ z.object({
23180
24641
  type: z.literal('evm'),
23181
24642
  abi: abiSchema,
23182
- address: evmAddressSchema,
24643
+ address: evmAddressSchema$1,
23183
24644
  functionName: z.string({
23184
24645
  required_error: 'Function name is required',
23185
24646
  invalid_type_error: 'Function name must be a string'
@@ -23216,7 +24677,7 @@ const abiParameterSchema = z.object({
23216
24677
  * }
23217
24678
  * ```
23218
24679
  */ z.object({
23219
- address: evmAddressSchema,
24680
+ address: evmAddressSchema$1,
23220
24681
  value: z.bigint({
23221
24682
  required_error: 'Value is required for native transfers',
23222
24683
  invalid_type_error: 'Value must be a bigint'
@@ -24450,6 +25911,23 @@ async function signUnits(group, signingUnits) {
24450
25911
  }
24451
25912
  return signedSets;
24452
25913
  }
25914
+ /**
25915
+ * Fail loudly if a signing group carries an intent it does not own.
25916
+ *
25917
+ * Every intent in a group is signed by `group.address`, so that signer must own
25918
+ * each intent's `spec.sourceSigner`. If distinct spend sources ever collapse
25919
+ * into one group again (DEVX-3638), this throws before signing instead of
25920
+ * silently producing a wrong-signer signature. specs are bytes32 and
25921
+ * `group.address` is a native address, so normalize both before comparing.
25922
+ */ function assertGroupSignerOwnsIntents(group) {
25923
+ if (group.address == null || group.address === '') return;
25924
+ const groupSigner = convertAddress(group.address, 'bytes32').toLowerCase();
25925
+ for (const intent of group.intents){
25926
+ if (intent.spec.sourceSigner.toLowerCase() !== groupSigner) {
25927
+ 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.');
25928
+ }
25929
+ }
25930
+ }
24453
25931
  /**
24454
25932
  * Sign an EVM adapter group.
24455
25933
  *
@@ -24492,22 +25970,42 @@ async function signUnits(group, signingUnits) {
24492
25970
  // Validate the collection before doing bytecode reads or asking a wallet
24493
25971
  // to sign. evmSigningData owns the canonical BurnIntent validation.
24494
25972
  validateGroupIntents(group.intents);
25973
+ // Guard against a wrong-signer collapse: the group's signer must own every
25974
+ // intent it carries.
25975
+ assertGroupSignerOwnsIntents(group);
24495
25976
  const chainsByDomain = collectChainsByDomain(group);
24496
25977
  const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
24497
25978
  const signingUnits = createSigningUnits(group, signerTypeByDomain);
24498
25979
  return await signUnits(group, signingUnits);
24499
25980
  }
24500
25981
 
25982
+ // One adapter instance can serve several signer addresses (e.g. a single
25983
+ // dev-controlled adapter used for multiple wallets), so the group map must be
25984
+ // keyed on `(adapter, sourceSigner)` rather than the adapter reference alone.
25985
+ // Keying on the adapter alone collapsed two same-chain sources into one group,
25986
+ // signing the second wallet's intent with the first wallet (DEVX-3638 /
25987
+ // DEVX-3194). A Map needs a primitive key, so give each adapter instance a
25988
+ // stable token and combine it with the (case-normalized) signer address.
25989
+ const adapterTokens = new WeakMap();
25990
+ let nextAdapterToken = 0;
25991
+ function groupKey(alloc) {
25992
+ let token = adapterTokens.get(alloc.adapter);
25993
+ if (token === undefined) {
25994
+ token = nextAdapterToken++;
25995
+ adapterTokens.set(alloc.adapter, token);
25996
+ }
25997
+ return `${String(token)}:${alloc.sourceSigner.toLowerCase()}`;
25998
+ }
24501
25999
  /**
24502
26000
  * Add an EVM intent into the batched EVM group map.
24503
26001
  *
24504
- * On EVM, all intents for the same adapter are batched into a single
24505
- * group. The signing step uses `chainsByDomain` to preserve EOA batching
26002
+ * On EVM, all intents for the same adapter **and signer** are batched into a
26003
+ * single group. The signing step uses `chainsByDomain` to preserve EOA batching
24506
26004
  * while signing ERC-1271 intents separately on their source chains.
24507
26005
  *
24508
26006
  * @param intent - The burn intent to group.
24509
26007
  * @param alloc - The allocation that resolved to this intent.
24510
- * @param evmGroups - Mutable map of adapter → EVM group.
26008
+ * @param evmGroups - Mutable map of `(adapter, sourceSigner)` key → EVM group.
24511
26009
  *
24512
26010
  * @example
24513
26011
  * ```typescript
@@ -24517,12 +26015,13 @@ async function signUnits(group, signingUnits) {
24517
26015
  * groupEvmIntents(intent, alloc, groups)
24518
26016
  * ```
24519
26017
  */ function groupEvmIntents(intent, alloc, evmGroups) {
24520
- const existing = evmGroups.get(alloc.adapter);
26018
+ const key = groupKey(alloc);
26019
+ const existing = evmGroups.get(key);
24521
26020
  if (existing) {
24522
26021
  existing.intents.push(intent);
24523
26022
  existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
24524
26023
  } else {
24525
- evmGroups.set(alloc.adapter, {
26024
+ evmGroups.set(key, {
24526
26025
  adapter: alloc.adapter,
24527
26026
  chain: alloc.chain,
24528
26027
  intents: [
@@ -24615,7 +26114,7 @@ z.object({
24615
26114
  signature: evmSignatureSchema,
24616
26115
  tokenInputs: z.array(z.object({
24617
26116
  permitType: z.nativeEnum(PermitType),
24618
- token: evmAddressSchema,
26117
+ token: evmAddressSchema$1,
24619
26118
  amount: z.bigint().refine((value)=>value >= 0n, {
24620
26119
  message: 'amount must be a non-negative bigint'
24621
26120
  }),
@@ -25208,7 +26707,7 @@ function writeBytes32(buffer, hex, offset) {
25208
26707
  });
25209
26708
 
25210
26709
  /**
25211
- * Assert that the kit key is valid for swap operations.
26710
+ * Assert that the credential is valid for swap operations.
25212
26711
  *
25213
26712
  * @remarks
25214
26713
  * This validation is called when swap operations are executed, not at construction time.
@@ -25216,34 +26715,35 @@ function writeBytes32(buffer, hex, offset) {
25216
26715
  * at usage time rather than construction time.
25217
26716
  *
25218
26717
  * Validates:
25219
- * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
25220
- * An absent or empty kit key is permitted (permissionless mode) — the swap
25221
- * service now treats the key as optional.
26718
+ * - The credential matches an accepted format when provided — a Circle API key
26719
+ * (`<ENV>_API_KEY:id:secret`) or a legacy kit key (`KIT_KEY:id:secret`).
26720
+ * An absent or empty credential is permitted (permissionless mode) — the swap
26721
+ * service treats the credential as optional.
25222
26722
  *
25223
- * @param kitKey - The inline kit key from the swap operation config (optional)
25224
- * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
25225
- * match the KIT_KEY:<keyId>:<keySecret> format
26723
+ * @param apiKey - The inline credential from the swap operation config (optional)
26724
+ * @throws KitError with VALIDATION_FAILED if a credential is provided but does
26725
+ * not match an accepted format
25226
26726
  *
25227
26727
  * @example
25228
26728
  * ```typescript
25229
- * import { assertKitKey } from './assertKitKey'
26729
+ * import { assertApiKey } from './assertApiKey'
25230
26730
  *
25231
- * const kitKey = 'KIT_KEY:9d1bd085-bad2-4a2b-ae67-5814b85167c9:secretvalue'
26731
+ * const apiKey = 'TEST_API_KEY:9d1bd085-bad2-4a2b-ae67-5814b85167c9:secretvalue'
25232
26732
  *
25233
- * // Validate kit key before use
25234
- * assertKitKey(kitKey)
26733
+ * // Validate the credential before use
26734
+ * assertApiKey(apiKey)
25235
26735
  * ```
25236
- */ function assertKitKey(kitKey) {
25237
- // Permissionless mode: the swap service treats the kit key as optional, so an
25238
- // absent (or empty) key is valid. Only validate the format when a key is
26736
+ */ function assertApiKey(apiKey) {
26737
+ // Permissionless mode: the swap service treats the credential as optional, so
26738
+ // an absent (or empty) value is valid. Only validate the format when one is
25239
26739
  // actually provided.
25240
- if (!kitKey) {
26740
+ if (!apiKey) {
25241
26741
  return;
25242
26742
  }
25243
- const apiKeyResult = apiKeySchema.safeParse(kitKey);
26743
+ const apiKeyResult = apiKeySchema.safeParse(apiKey);
25244
26744
  if (!apiKeyResult.success) {
25245
26745
  const errors = apiKeyResult.error.issues.map((issue)=>issue.message).join(', ');
25246
- 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`);
26746
+ 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`);
25247
26747
  }
25248
26748
  }
25249
26749
 
@@ -25435,7 +26935,7 @@ const TOKEN_REGISTRY$4 = createTokenRegistry();
25435
26935
  }
25436
26936
  // Validate recipient address format matches the chain type
25437
26937
  if (chain.type === 'evm') {
25438
- const result = evmAddressSchema.safeParse(recipientAddress);
26938
+ const result = evmAddressSchema$1.safeParse(recipientAddress);
25439
26939
  if (!result.success) {
25440
26940
  throw createInvalidAddressError(recipientAddress, chain.name, '0x-prefixed 42-character hex string (0x + 40 hex characters)');
25441
26941
  }
@@ -25497,7 +26997,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25497
26997
  * to: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
25498
26998
  * config: {
25499
26999
  * slippageBps: 300,
25500
- * kitKey: 'KIT_KEY:...:...' // Required inline kit key
27000
+ * apiKey: 'TEST_API_KEY:...:...' // Inline credential (optional)
25501
27001
  * }
25502
27002
  * }
25503
27003
  *
@@ -25535,18 +27035,17 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25535
27035
  validateResolvedAddress(resolvedTokenInAddress, chain);
25536
27036
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
25537
27037
  validateResolvedAddress(to, destinationChain);
25538
- const kitKey = config?.kitKey;
25539
- // Validate the kit key format when one is provided (permissionless otherwise).
25540
- assertKitKey(kitKey);
27038
+ const apiKey = resolveApiKey(config ?? {});
27039
+ // Validate the credential format when one is provided (permissionless
27040
+ // otherwise).
27041
+ assertApiKey(apiKey);
25541
27042
  // Validate custom fee configuration if present
25542
27043
  const customFee = config?.customFee;
25543
27044
  if (customFee !== undefined) {
25544
27045
  const feePayoutChain = resolveFeePayoutChain(tokenIn, tokenOut, chain, destinationChain);
25545
27046
  validateCustomFeeConfig(customFee, feePayoutChain);
25546
27047
  }
25547
- // Build CreateSwapParams for service client
25548
- // Use object spread to conditionally include optional properties
25549
- // Note: Service client expects 'apiKey' field, so we map kitKey → apiKey
27048
+ // Build CreateSwapParams for service client, spreading optional properties.
25550
27049
  const chainName = chain.chain // The API expects the Blockchain enum value format like "World_Chain"
25551
27050
  ;
25552
27051
  const destinationChainName = destinationChain.chain;
@@ -25581,9 +27080,9 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25581
27080
  }
25582
27081
  }
25583
27082
  },
25584
- // Map kitKey → apiKey for the service client; omitted in permissionless mode.
25585
- ...kitKey ? {
25586
- apiKey: kitKey
27083
+ // Omitted in permissionless mode.
27084
+ ...apiKey ? {
27085
+ apiKey
25587
27086
  } : {}
25588
27087
  };
25589
27088
  }
@@ -27487,9 +28986,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
27487
28986
  * ```typescript
27488
28987
  * import { StablecoinServiceSwapProvider } from '@circle-fin/provider-stablecoin-service-swap'
27489
28988
  *
27490
- * // Create provider with developer fee (1 USDC)
28989
+ * // Create provider with developer fee (1 USDC).
28990
+ * // Credentials are supplied per operation via `config.apiKey`, not here.
27491
28991
  * const provider = new StablecoinServiceSwapProvider({
27492
- * kitKey: process.env.STABLECOIN_KIT_API_KEY,
27493
28992
  * customFee: {
27494
28993
  * amount: '1000000', // 1 USDC in base units (6 decimals)
27495
28994
  * recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
@@ -28151,9 +29650,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
28151
29650
  * import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
28152
29651
  * import { Ethereum } from '@core/chains'
28153
29652
  *
28154
- * const provider = new StablecoinServiceSwapProvider({
28155
- * kitKey: process.env.STABLECOIN_KIT_API_KEY
28156
- * })
29653
+ * const provider = new StablecoinServiceSwapProvider()
28157
29654
  *
28158
29655
  * const adapter = createAdapterFromPrivateKey({
28159
29656
  * privateKey: process.env.PRIVATE_KEY
@@ -28647,7 +30144,7 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
28647
30144
  /**
28648
30145
  * Fee recipient address (required).
28649
30146
  * Must be a valid EVM address or Solana address.
28650
- */ recipientAddress: z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
30147
+ */ recipientAddress: z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
28651
30148
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
28652
30149
  })
28653
30150
  }).strict();
@@ -28659,7 +30156,8 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
28659
30156
  * - slippageBps: Optional positive number for slippage tolerance
28660
30157
  * - stopLimit: Optional decimal string for minimum output
28661
30158
  * - customFee: Optional fee configuration
28662
- * - kitKey: Optional string identifier
30159
+ * - apiKey: Optional credential string
30160
+ * - kitKey: Optional credential string (deprecated alias for apiKey)
28663
30161
  */ const swapConfigSchema = z.object({
28664
30162
  allowanceStrategy: allowanceStrategySchema$1.optional(),
28665
30163
  slippageBps: z.number().int().min(0).optional(),
@@ -28669,11 +30167,12 @@ const optionalSwapChainIdentifierField = swapChainIdentifierField.optional();
28669
30167
  attributeName: 'stopLimit'
28670
30168
  })(z.string())).optional(),
28671
30169
  customFee: swapCustomFeeSchema.optional(),
30170
+ apiKey: z.string().optional(),
28672
30171
  kitKey: z.string().optional()
28673
30172
  });
28674
30173
  const swapDestinationSchema = z.object({
28675
30174
  chain: optionalSwapChainIdentifierField,
28676
- recipientAddress: z.string().refine((value)=>evmAddressSchema.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
30175
+ recipientAddress: z.string().refine((value)=>evmAddressSchema$1.safeParse(value).success || solanaAddressSchema.safeParse(value).success, {
28677
30176
  message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)'
28678
30177
  }).optional()
28679
30178
  }).strict();
@@ -30780,7 +32279,8 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
30780
32279
  /**
30781
32280
  * Helper to format the config section of a swap result, transforming fee amounts.
30782
32281
  *
30783
- * Strips sensitive fields like kitKey from the config before returning.
32282
+ * Strips the credential fields (`apiKey` and the deprecated `kitKey`) from the
32283
+ * config before returning, so a result is safe to log or serialize.
30784
32284
  *
30785
32285
  * @param config - The swap config from the provider.
30786
32286
  * @param outputTokenTransform - The amount transformer for output-token-denominated values (fees and amountOut).
@@ -30789,11 +32289,11 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
30789
32289
  if (!config) {
30790
32290
  return undefined;
30791
32291
  }
30792
- // Remove kitKey from the config
30793
- const cloneConfig = {
30794
- ...config
30795
- };
30796
- delete cloneConfig.kitKey;
32292
+ // Rebuild the config without any credential field. Filtering entries rather
32293
+ // than deleting them avoids reading the deprecated `kitKey` property
32294
+ // directly, and keeps the credential list in one place.
32295
+ const credentialFields = CREDENTIAL_CONFIG_FIELDS;
32296
+ const cloneConfig = Object.fromEntries(Object.entries(config).filter(([key])=>!credentialFields.includes(key)));
30797
32297
  if (Object.keys(cloneConfig).length === 0) {
30798
32298
  return undefined;
30799
32299
  }
@@ -30994,9 +32494,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
30994
32494
  try {
30995
32495
  // Step 1: Build quote params directly (no need for buildServiceParams)
30996
32496
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
30997
- // The kit key is optional (permissionless mode); when absent the quote is
30998
- // fetched without an Authorization header.
30999
- const kitKey = params.config?.kitKey;
32497
+ // The credential is optional (permissionless mode); when absent the quote
32498
+ // is fetched without an Authorization header.
32499
+ const apiKey = resolveApiKey(params.config ?? {});
31000
32500
  // Resolve token aliases to addresses for the quote API
31001
32501
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
31002
32502
  const chain = params.from.chain;
@@ -31021,8 +32521,8 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
31021
32521
  ...params.config?.slippageBps !== undefined && {
31022
32522
  slippageBps: params.config.slippageBps
31023
32523
  },
31024
- ...kitKey ? {
31025
- apiKey: kitKey
32524
+ ...apiKey ? {
32525
+ apiKey
31026
32526
  } : {}
31027
32527
  };
31028
32528
  // Step 2: Get quote from service
@@ -31426,7 +32926,7 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31426
32926
  * the service returns `receivingTokenDecimals`. When decimals are missing
31427
32927
  * the field is omitted entirely (never raw base units).
31428
32928
  *
31429
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
32929
+ * @param params - `txHash`, `chainIn`, optional `chainOut`, and `apiKey`.
31430
32930
  * @returns A single snapshot of the swap's status.
31431
32931
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
31432
32932
  *
@@ -31442,7 +32942,7 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31442
32942
  * txHash: result.txHash,
31443
32943
  * chainIn: result.chainIn,
31444
32944
  * chainOut: result.chainOut,
31445
- * kitKey: process.env.KIT_KEY ?? '',
32945
+ * apiKey: process.env.CIRCLE_API_KEY,
31446
32946
  * })
31447
32947
  * while (status.progress.status === 'PENDING') {
31448
32948
  * await new Promise((r) => setTimeout(r, 3_000))
@@ -31450,7 +32950,7 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31450
32950
  * txHash: result.txHash,
31451
32951
  * chainIn: result.chainIn,
31452
32952
  * chainOut: result.chainOut,
31453
- * kitKey: process.env.KIT_KEY ?? '',
32953
+ * apiKey: process.env.CIRCLE_API_KEY,
31454
32954
  * })
31455
32955
  * }
31456
32956
  *
@@ -31468,14 +32968,15 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
31468
32968
  const chainIn = normalizeChain(params.chainIn, 'chainIn');
31469
32969
  const chainOut = params.chainOut === undefined ? undefined : normalizeChain(params.chainOut, 'chainOut');
31470
32970
  const isCrossChain = chainOut !== undefined && chainOut !== chainIn;
32971
+ const apiKey = resolveApiKey(params);
31471
32972
  const request = {
31472
32973
  txHash: params.txHash,
31473
32974
  chain: chainIn,
31474
32975
  ...isCrossChain && {
31475
32976
  toChain: chainOut
31476
32977
  },
31477
- ...params.kitKey ? {
31478
- apiKey: params.kitKey
32978
+ ...apiKey ? {
32979
+ apiKey
31479
32980
  } : {}
31480
32981
  };
31481
32982
  let raw = await getSwapStatus$2(request);
@@ -31554,9 +33055,9 @@ const isResultShape = (params)=>'result' in params;
31554
33055
  *
31555
33056
  * Two parameter shapes are accepted:
31556
33057
  *
31557
- * - Pass the {@link SwapResult} directly: `{ result, kitKey, ... }`.
33058
+ * - Pass the {@link SwapResult} directly: `{ result, apiKey, ... }`.
31558
33059
  * Cleanest when chaining off `kit.swap()`.
31559
- * - Pass discrete fields: `{ txHash, chainIn, chainOut?, kitKey, ... }`.
33060
+ * - Pass discrete fields: `{ txHash, chainIn, chainOut?, apiKey, ... }`.
31560
33061
  * Useful for resuming an in-flight swap from a persisted record.
31561
33062
  *
31562
33063
  * @param params - Wait configuration. See {@link WaitForSwapParams}.
@@ -31576,7 +33077,7 @@ const isResultShape = (params)=>'result' in params;
31576
33077
  *
31577
33078
  * const final = await waitForSwap({
31578
33079
  * result,
31579
- * kitKey: process.env.KIT_KEY ?? '',
33080
+ * apiKey: process.env.CIRCLE_API_KEY,
31580
33081
  * onProgress: (snapshot) => {
31581
33082
  * console.log(snapshot.progress.substatus ?? snapshot.progress.status)
31582
33083
  * },
@@ -31594,7 +33095,7 @@ const isResultShape = (params)=>'result' in params;
31594
33095
  * txHash: '0x…',
31595
33096
  * chainIn: 'Ethereum',
31596
33097
  * chainOut: 'Base',
31597
- * kitKey: process.env.KIT_KEY ?? '',
33098
+ * apiKey: process.env.CIRCLE_API_KEY,
31598
33099
  * })
31599
33100
  * ```
31600
33101
  */ async function waitForSwap$1(params) {
@@ -31610,14 +33111,15 @@ const isResultShape = (params)=>'result' in params;
31610
33111
  const chainOut = isResultShape(params) ? params.result.chainOut : params.chainOut;
31611
33112
  // Delegate parameter validation (txHash, chainIn, chainOut) to
31612
33113
  // getSwapStatus so the same rules apply to both helpers.
33114
+ const apiKey = resolveApiKey(params);
31613
33115
  const statusParams = {
31614
33116
  txHash,
31615
33117
  chainIn,
31616
33118
  ...chainOut !== undefined && {
31617
33119
  chainOut
31618
33120
  },
31619
- ...params.kitKey ? {
31620
- kitKey: params.kitKey
33121
+ ...apiKey ? {
33122
+ apiKey
31621
33123
  } : {}
31622
33124
  };
31623
33125
  const deadline = Date.now() + timeoutMs;
@@ -31741,7 +33243,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31741
33243
  *
31742
33244
  * @param context - SwapKit context (provides the `TokenRegistry` used for
31743
33245
  * symbol resolution).
31744
- * @param params - `chain`, optional `tokens`, and `kitKey`.
33246
+ * @param params - `chain`, optional `tokens`, and `apiKey`.
31745
33247
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
31746
33248
  * @throws \{KitError\} If `chain` cannot be resolved, `tokens` exceeds 100
31747
33249
  * entries, or any entry is neither a registered symbol nor a
@@ -31756,7 +33258,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31756
33258
  * const { rates } = await getTokenRates(context, {
31757
33259
  * chain: Blockchain.Ethereum,
31758
33260
  * tokens: ['USDC', 'EURC', 'NATIVE'],
31759
- * kitKey: process.env.KIT_KEY ?? '',
33261
+ * apiKey: process.env.CIRCLE_API_KEY,
31760
33262
  * })
31761
33263
  *
31762
33264
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -31777,10 +33279,11 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31777
33279
  // valid Blockchain enum string, so `getChainByEnum` is guaranteed to resolve.
31778
33280
  const chainDef = getChainByEnum(chain);
31779
33281
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
33282
+ const apiKey = resolveApiKey(params);
31780
33283
  return getTokenRates$2({
31781
33284
  chain,
31782
- ...params.kitKey ? {
31783
- apiKey: params.kitKey
33285
+ ...apiKey ? {
33286
+ apiKey
31784
33287
  } : {},
31785
33288
  ...resolvedAddresses !== undefined && {
31786
33289
  addresses: resolvedAddresses
@@ -32411,7 +33914,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32411
33914
  * is formatted as a human-readable decimal string; when decimals are
32412
33915
  * missing the field is omitted entirely (never raw base units).
32413
33916
  *
32414
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
33917
+ * @param params - `txHash`, `chainIn`, optional `chainOut`, and `apiKey`.
32415
33918
  * @returns A snapshot of the swap's status at the time of the call.
32416
33919
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
32417
33920
  *
@@ -32424,7 +33927,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32424
33927
  * txHash: result.txHash,
32425
33928
  * chainIn: result.chainIn,
32426
33929
  * chainOut: result.chainOut,
32427
- * kitKey: process.env.KIT_KEY ?? '',
33930
+ * apiKey: process.env.CIRCLE_API_KEY,
32428
33931
  * })
32429
33932
  * console.log(status.progress.status, status.progress.substatus)
32430
33933
  * ```
@@ -32436,7 +33939,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32436
33939
  * txHash: result.txHash,
32437
33940
  * chainIn: result.chainIn,
32438
33941
  * chainOut: result.chainOut,
32439
- * kitKey: process.env.KIT_KEY ?? '',
33942
+ * apiKey: process.env.CIRCLE_API_KEY,
32440
33943
  * })
32441
33944
  * while (status.progress.status === 'PENDING') {
32442
33945
  * await new Promise((r) => setTimeout(r, 3_000))
@@ -32444,7 +33947,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32444
33947
  * txHash: result.txHash,
32445
33948
  * chainIn: result.chainIn,
32446
33949
  * chainOut: result.chainOut,
32447
- * kitKey: process.env.KIT_KEY ?? '',
33950
+ * apiKey: process.env.CIRCLE_API_KEY,
32448
33951
  * })
32449
33952
  * }
32450
33953
  *
@@ -32475,7 +33978,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32475
33978
  * 5xx, etc.) surface unchanged so the caller can decide whether to
32476
33979
  * retry the whole wait. The wait itself only emits a timeout error.
32477
33980
  *
32478
- * @param params - Wait configuration including identifiers, kit key,
33981
+ * @param params - Wait configuration including identifiers, API key,
32479
33982
  * optional `timeoutMs`, and an optional `onProgress`
32480
33983
  * callback fired on every successful poll.
32481
33984
  * @returns The first terminal {@link SwapStatusResult} observed.
@@ -32494,7 +33997,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32494
33997
  *
32495
33998
  * const final = await kit.waitForSwap({
32496
33999
  * result,
32497
- * kitKey: process.env.KIT_KEY ?? '',
34000
+ * apiKey: process.env.CIRCLE_API_KEY,
32498
34001
  * onProgress: (snap) => console.log(snap.progress.substatus ?? snap.progress.status),
32499
34002
  * })
32500
34003
  *
@@ -32512,7 +34015,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32512
34015
  * txHash: persisted.txHash,
32513
34016
  * chainIn: persisted.chainIn,
32514
34017
  * chainOut: persisted.chainOut,
32515
- * kitKey: process.env.KIT_KEY ?? '',
34018
+ * apiKey: process.env.CIRCLE_API_KEY,
32516
34019
  * })
32517
34020
  * ```
32518
34021
  */ async waitForSwap(params) {
@@ -32546,7 +34049,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32546
34049
  * not cache a rate for the native asset on that chain, the response simply
32547
34050
  * omits the address.
32548
34051
  *
32549
- * @param params - `chain`, optional `tokens`, and `kitKey`.
34052
+ * @param params - `chain`, optional `tokens`, and `apiKey`.
32550
34053
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
32551
34054
  * @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
32552
34055
  * entries, or any entry is neither a registered symbol nor a
@@ -32558,7 +34061,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32558
34061
  * const { rates } = await kit.getTokenRates({
32559
34062
  * chain: 'Ethereum',
32560
34063
  * tokens: ['USDC', 'EURC', 'NATIVE'],
32561
- * kitKey: process.env.KIT_KEY ?? '',
34064
+ * apiKey: process.env.CIRCLE_API_KEY,
32562
34065
  * })
32563
34066
  *
32564
34067
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -32572,7 +34075,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32572
34075
  * chain: 'Ethereum',
32573
34076
  * // 'USDC' (registry symbol) + raw USDT address
32574
34077
  * tokens: ['USDC', '0xdAC17F958D2ee523a2206206994597C13D831ec7'],
32575
- * kitKey: process.env.KIT_KEY ?? '',
34078
+ * apiKey: process.env.CIRCLE_API_KEY,
32576
34079
  * })
32577
34080
  * ```
32578
34081
  *
@@ -32581,7 +34084,7 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32581
34084
  * ```typescript
32582
34085
  * const { rates } = await kit.getTokenRates({
32583
34086
  * chain: 'Base',
32584
- * kitKey: process.env.KIT_KEY ?? '',
34087
+ * apiKey: process.env.CIRCLE_API_KEY,
32585
34088
  * })
32586
34089
  * ```
32587
34090
  */ async getTokenRates(params) {
@@ -32943,7 +34446,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32943
34446
  };
32944
34447
 
32945
34448
  var name$2 = "@circle-fin/earn-kit";
32946
- var version$2 = "1.5.1";
34449
+ var version$2 = "1.6.0";
32947
34450
  var pkg$2 = {
32948
34451
  name: name$2,
32949
34452
  version: version$2};
@@ -33159,7 +34662,7 @@ function toSdkChain(chain) {
33159
34662
  *
33160
34663
  * @internal
33161
34664
  */ function assertHexAddress(field, value, message = `${field} must be a 0x-prefixed 20-byte hex address`) {
33162
- const result = evmAddressSchema.safeParse(value);
34665
+ const result = evmAddressSchema$1.safeParse(value);
33163
34666
  if (!result.success) {
33164
34667
  throw createValidationFailedError$1(field, value, message);
33165
34668
  }
@@ -34845,7 +36348,7 @@ const EARN_OPERATIONS = new Set([
34845
36348
  // Store the original service params non-enumerably. `retry()` reads them by
34846
36349
  // direct property access (`trace.params`), but JSON.stringify, console.log,
34847
36350
  // and telemetry serializers skip non-enumerable properties — so a
34848
- // permissioned call's `config.kitKey` (a `KIT_KEY:<id>:<secret>` value) and
36351
+ // permissioned call's `config.apiKey` (a credential value) and
34849
36352
  // the in-memory adapter never leak through a logged error. defineProperty
34850
36353
  // also redefines any enumerable `params` an upstream trace may have carried.
34851
36354
  Object.defineProperty(trace, 'params', {
@@ -34916,7 +36419,7 @@ function isNonNegativeBigIntLike(value) {
34916
36419
  }
34917
36420
  }
34918
36421
  const hexSignatureSchema = evmSignatureSchema;
34919
- const hexAddressSchema = evmAddressSchema;
36422
+ const hexAddressSchema = evmAddressSchema$1;
34920
36423
  // '0x' prefix + 32 bytes * 2 hex chars.
34921
36424
  const BYTES32_HEX_LENGTH = 66;
34922
36425
  const bridgeFeeTokenSchema = hexAddressSchema;
@@ -35885,7 +37388,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
35885
37388
  }
35886
37389
 
35887
37390
  var name$1 = "@circle-fin/provider-earn-service";
35888
- var version$1 = "1.4.1";
37391
+ var version$1 = "1.5.0";
35889
37392
  var pkg$1 = {
35890
37393
  name: name$1,
35891
37394
  version: version$1};
@@ -35947,10 +37450,12 @@ var pkg$1 = {
35947
37450
  *
35948
37451
  * @internal
35949
37452
  */ function buildConfig(serviceConfig) {
35950
- // The kit key is a server-only secret. Reject it in the browser so it cannot
35951
- // leak into a client bundle (no-op in Node.js). Keyless usage stays allowed.
35952
- if (serviceConfig?.kitKey !== undefined && isBrowserEnvironment()) {
35953
- 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');
37453
+ const apiKey = resolveApiKey(serviceConfig ?? {});
37454
+ // The credential is a server-only secret. Reject it in the browser so it
37455
+ // cannot leak into a client bundle (no-op in Node.js). Keyless usage stays
37456
+ // allowed.
37457
+ if (apiKey !== undefined && isBrowserEnvironment()) {
37458
+ 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');
35954
37459
  }
35955
37460
  const baseUrl = serviceConfig?.baseUrl ?? EARN_SERVICE_BASE_URL;
35956
37461
  // The API CORS policy does not allow this custom header. Keep the existing
@@ -35959,7 +37464,7 @@ var pkg$1 = {
35959
37464
  const sdkVersionHeader = isNodeEnvironment() ? {
35960
37465
  [SDK_VERSION_HEADER]: resolveSdkVersionHeader()
35961
37466
  } : {};
35962
- if (serviceConfig?.kitKey === undefined) {
37467
+ if (apiKey === undefined) {
35963
37468
  return {
35964
37469
  pollingConfig: {
35965
37470
  ...DEFAULT_CONFIG,
@@ -35971,11 +37476,11 @@ var pkg$1 = {
35971
37476
  baseUrl
35972
37477
  };
35973
37478
  }
35974
- if (!isValidApiKey(serviceConfig.kitKey)) {
37479
+ if (!isValidApiKey(apiKey)) {
35975
37480
  throw new KitError({
35976
37481
  ...InputError.VALIDATION_FAILED,
35977
37482
  recoverability: 'FATAL',
35978
- message: 'Invalid kitKey format. Expected KIT_KEY:<keyId>:<keySecret>.'
37483
+ message: 'Invalid apiKey format. Expected <ENV>_API_KEY:<keyId>:<keySecret> (for example TEST_API_KEY:...), or a legacy KIT_KEY:<keyId>:<keySecret>.'
35979
37484
  });
35980
37485
  }
35981
37486
  return {
@@ -35984,7 +37489,7 @@ var pkg$1 = {
35984
37489
  headers: {
35985
37490
  ...DEFAULT_CONFIG.headers,
35986
37491
  ...sdkVersionHeader,
35987
- Authorization: `Bearer ${serviceConfig.kitKey}`
37492
+ Authorization: `Bearer ${apiKey}`
35988
37493
  }
35989
37494
  },
35990
37495
  baseUrl
@@ -37301,9 +38806,9 @@ function finishElapsedWait(lastStatus, lastError) {
37301
38806
  * // Permissionless mode
37302
38807
  * const provider = new EarnServiceProvider()
37303
38808
  *
37304
- * // Permissioned mode with Kit Key
38809
+ * // Permissioned mode with an API key
37305
38810
  * const provider = new EarnServiceProvider({
37306
- * kitKey: 'KIT_KEY:keyId:keySecret',
38811
+ * apiKey: 'TEST_API_KEY:keyId:keySecret',
37307
38812
  * })
37308
38813
  *
37309
38814
  * // Custom base URL for testing
@@ -38530,7 +40035,7 @@ function findProvider(context, chain, operation = 'earn') {
38530
40035
  * fast instead of round-tripping to the service.
38531
40036
  *
38532
40037
  * @internal
38533
- */ 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');
40038
+ */ 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');
38534
40039
  /**
38535
40040
  * Schema for the adapter context within earn operations.
38536
40041
  *
@@ -38562,19 +40067,39 @@ const sourceAdapterContextSchema = z.object({
38562
40067
  /**
38563
40068
  * Schema for the EarnConfig options.
38564
40069
  *
38565
- * Validate the optional Kit Key field using the standard `apiKeySchema`
38566
- * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
38567
- * operates in permissionless mode. `baseUrl` overrides the Earn Service
38568
- * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
38569
- * batched execution. Both are forwarded to the provider, so this `.strict()`
38570
- * schema must accept them or a valid config object is rejected.
40070
+ * Validate the *resolved* credential using the standard `apiKeySchema` format
40071
+ * (`<ENV>_API_KEY:<keyId>:<keySecret>`, or a legacy
40072
+ * `KIT_KEY:<keyId>:<keySecret>`). `apiKey` takes precedence over the deprecated
40073
+ * `kitKey`, so a malformed `kitKey` that is being ignored must not fail a config
40074
+ * that supplies a valid `apiKey` (and vice versa) only the credential that
40075
+ * would actually be sent is format-checked. When neither is supplied the SDK
40076
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service endpoint
40077
+ * (e.g. staging); `batchTransactions: false` opts out of atomic batched
40078
+ * execution. All are forwarded to the provider, so this `.strict()` schema must
40079
+ * accept them or a valid config object is rejected.
38571
40080
  *
38572
40081
  * @internal
38573
40082
  */ const earnConfigSchema = z.object({
38574
- kitKey: apiKeySchema.optional(),
40083
+ apiKey: z.string().optional(),
40084
+ kitKey: z.string().optional(),
38575
40085
  baseUrl: z.string().optional(),
38576
40086
  batchTransactions: z.boolean().optional()
38577
- }).strict();
40087
+ }).strict().superRefine((config, ctx)=>{
40088
+ const credential = resolveApiKey(config);
40089
+ if (credential === undefined) {
40090
+ return;
40091
+ }
40092
+ const result = apiKeySchema.safeParse(credential);
40093
+ if (!result.success) {
40094
+ ctx.addIssue({
40095
+ code: z.ZodIssueCode.custom,
40096
+ path: [
40097
+ credential === config.apiKey ? 'apiKey' : 'kitKey'
40098
+ ],
40099
+ message: result.error.issues[0]?.message ?? 'Invalid API key format'
40100
+ });
40101
+ }
40102
+ });
38578
40103
  /**
38579
40104
  * Canonical decimal form: a leading digit with no leading zeros (a single
38580
40105
  * '0' is only allowed immediately before the decimal point). Rejects the
@@ -38646,7 +40171,7 @@ const sourceAdapterContextSchema = z.object({
38646
40171
  * currently supports EVM vault addresses on Arc Testnet.
38647
40172
  *
38648
40173
  * @internal
38649
- */ const vaultAddressSchema = evmAddressSchema.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
40174
+ */ const vaultAddressSchema = evmAddressSchema$1.refine(isValidEip55Checksum, 'address has an invalid EIP-55 checksum');
38650
40175
  /**
38651
40176
  * Validation schema for VaultQuery.
38652
40177
  *
@@ -38921,7 +40446,7 @@ const sameChainGetDepositQuoteParamsSchema = z.object({
38921
40446
  const crossChainGetDepositQuoteParamsSchema = z.object({
38922
40447
  from: sourceAdapterContextSchema,
38923
40448
  chain: earnBridgeDestinationChainIdentifierSchema,
38924
- address: evmAddressSchema,
40449
+ address: evmAddressSchema$1,
38925
40450
  vaultAddress: vaultAddressSchema,
38926
40451
  amount: amountSchema$1,
38927
40452
  transferSpeed: z.enum([
@@ -41237,7 +42762,7 @@ const tokens = createTokenRegistry();
41237
42762
  *
41238
42763
  * @param context - AppKit context.
41239
42764
  * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
41240
- * `kitKey`.
42765
+ * `apiKey`.
41241
42766
  * @returns A snapshot of the swap's status at the time of the call.
41242
42767
  *
41243
42768
  * @example
@@ -41251,7 +42776,7 @@ const tokens = createTokenRegistry();
41251
42776
  * txHash: result.txHash,
41252
42777
  * chainIn: result.chainIn,
41253
42778
  * chainOut: result.chainOut,
41254
- * kitKey: process.env.KIT_KEY ?? '',
42779
+ * apiKey: process.env.CIRCLE_API_KEY,
41255
42780
  * })
41256
42781
  *
41257
42782
  * console.log(status.progress.status, status.progress.substatus)
@@ -41274,7 +42799,7 @@ const tokens = createTokenRegistry();
41274
42799
  * `timeoutMs` elapses.
41275
42800
  *
41276
42801
  * @param context - AppKit context.
41277
- * @param params - Wait configuration, including identifiers, kit key,
42802
+ * @param params - Wait configuration, including identifiers, API key,
41278
42803
  * optional `timeoutMs`, and an optional `onProgress`
41279
42804
  * callback.
41280
42805
  * @returns The first terminal {@link SwapStatusResult} observed.
@@ -41290,7 +42815,7 @@ const tokens = createTokenRegistry();
41290
42815
  * txHash: result.txHash,
41291
42816
  * chainIn: result.chainIn,
41292
42817
  * chainOut: result.chainOut,
41293
- * kitKey: process.env.KIT_KEY ?? '',
42818
+ * apiKey: process.env.CIRCLE_API_KEY,
41294
42819
  * onProgress: (snap) => console.log(snap.progress.status),
41295
42820
  * })
41296
42821
  *
@@ -41326,7 +42851,7 @@ const tokens = createTokenRegistry();
41326
42851
  * addresses before indexing into `result.rates[chain]`.
41327
42852
  *
41328
42853
  * @param context - AppKit context.
41329
- * @param params - `chain`, plus optional `tokens` and `kitKey`.
42854
+ * @param params - `chain`, plus optional `tokens` and `apiKey`.
41330
42855
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
41331
42856
  *
41332
42857
  * @example
@@ -41337,7 +42862,7 @@ const tokens = createTokenRegistry();
41337
42862
  * const { rates } = await kit.getTokenRates({
41338
42863
  * chain: 'Ethereum',
41339
42864
  * tokens: ['USDC'],
41340
- * kitKey: process.env.KIT_KEY ?? '',
42865
+ * apiKey: process.env.CIRCLE_API_KEY,
41341
42866
  * })
41342
42867
  *
41343
42868
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -41348,6 +42873,43 @@ const tokens = createTokenRegistry();
41348
42873
  return kit.getTokenRates(params);
41349
42874
  };
41350
42875
 
42876
+ const BRIDGE_FILTER_KEYS = new Set([
42877
+ 'chainType',
42878
+ 'isTestnet',
42879
+ 'forwarderSupported',
42880
+ 'sourceFeeSupported'
42881
+ ]);
42882
+ function assertBridgeSupportedChainsOptions(options) {
42883
+ if (options === undefined) {
42884
+ return;
42885
+ }
42886
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) {
42887
+ throw createValidationFailedError$1('options', options, 'Bridge chain filters must be an object');
42888
+ }
42889
+ const entries = Object.entries(options);
42890
+ if (entries.length === 0 || entries.some(([key])=>!BRIDGE_FILTER_KEYS.has(key))) {
42891
+ throw createValidationFailedError$1('options', options, 'Bridge chain filters must contain only supported filter properties');
42892
+ }
42893
+ const record = options;
42894
+ for (const key of [
42895
+ 'isTestnet',
42896
+ 'forwarderSupported',
42897
+ 'sourceFeeSupported'
42898
+ ]){
42899
+ if (record[key] !== undefined && typeof record[key] !== 'boolean') {
42900
+ throw createValidationFailedError$1(`options.${key}`, record[key], 'Bridge chain boolean filters must be true or false');
42901
+ }
42902
+ }
42903
+ const chainType = record['chainType'];
42904
+ if (chainType !== undefined) {
42905
+ const chainTypes = Array.isArray(chainType) ? chainType : [
42906
+ chainType
42907
+ ];
42908
+ if (chainTypes.length === 0 || chainTypes.some((value)=>value !== 'evm' && value !== 'solana')) {
42909
+ throw createValidationFailedError$1('options.chainType', chainType, "Bridge chain type must be 'evm', 'solana', or an array of those values");
42910
+ }
42911
+ }
42912
+ }
41351
42913
  /**
41352
42914
  * Get chains supported by AppKit operations.
41353
42915
  *
@@ -41362,6 +42924,7 @@ const tokens = createTokenRegistry();
41362
42924
  * @param context - AppKit context containing fee configuration.
41363
42925
  * @param operationType - Optional operation type to filter chains ('bridge' | 'swap' | 'earn' | 'unifiedBalance').
41364
42926
  * @param unifiedBalance - Persistent AppKitUnifiedBalance namespace instance.
42927
+ * @param bridgeOptions - Optional Bridge Kit chain filters. Requires `operationType: 'bridge'`.
41365
42928
  * @returns Array of unique chain definitions supporting the specified operation(s)
41366
42929
  *
41367
42930
  * @example Get all supported chains via AppKit
@@ -41380,6 +42943,13 @@ const tokens = createTokenRegistry();
41380
42943
  * console.log('Chains supporting bridge:', bridgeChains.map(c => c.name))
41381
42944
  * ```
41382
42945
  *
42946
+ * @example Get chains that support source-paid bridge fees
42947
+ * ```typescript
42948
+ * const sourceFeeChains = kit.getSupportedChains('bridge', {
42949
+ * sourceFeeSupported: true,
42950
+ * })
42951
+ * ```
42952
+ *
41383
42953
  * @example Get swap-specific chains
41384
42954
  * ```typescript
41385
42955
  * const swapChains = kit.getSupportedChains('swap')
@@ -41400,7 +42970,8 @@ const tokens = createTokenRegistry();
41400
42970
  * const supportsEthBridge = bridgeChains.some(c => c.chain === Ethereum.chain)
41401
42971
  * console.log(`Ethereum supports bridge: ${supportsEthBridge}`)
41402
42972
  * ```
41403
- */ const getSupportedChains$2 = (context, operationType, unifiedBalance)=>{
42973
+ */ const getSupportedChains$2 = (context, operationType, unifiedBalance, bridgeOptions)=>{
42974
+ assertBridgeSupportedChainsOptions(bridgeOptions);
41404
42975
  // Runtime validation when operationType is provided
41405
42976
  if (operationType !== undefined && operationType !== 'bridge' && operationType !== 'swap' && operationType !== 'earn' && operationType !== 'unifiedBalance') {
41406
42977
  throw new KitError({
@@ -41409,10 +42980,13 @@ const tokens = createTokenRegistry();
41409
42980
  message: `Invalid operationType: "${String(operationType)}". Expected 'bridge', 'swap', 'earn', or 'unifiedBalance'.`
41410
42981
  });
41411
42982
  }
42983
+ if (bridgeOptions !== undefined && operationType !== 'bridge') {
42984
+ throw createValidationFailedError$1('options', bridgeOptions, "Bridge chain filters require operationType 'bridge'");
42985
+ }
41412
42986
  // Return bridge-specific chains
41413
42987
  if (operationType === 'bridge') {
41414
42988
  const bridgeKit = createBridgeKit(context);
41415
- return bridgeKit.getSupportedChains();
42989
+ return bridgeKit.getSupportedChains(bridgeOptions);
41416
42990
  }
41417
42991
  // Return swap-specific chains
41418
42992
  if (operationType === 'swap') {
@@ -41750,7 +43324,7 @@ async function deposit$2(context, params) {
41750
43324
  }
41751
43325
 
41752
43326
  var name = "@circle-fin/unified-balance-kit";
41753
- var version = "1.4.1";
43327
+ var version = "1.5.0";
41754
43328
  var pkg = {
41755
43329
  name: name,
41756
43330
  version: version};
@@ -42583,6 +44157,8 @@ const gatewayInfoSchema = z.object({
42583
44157
  * Fetch Gateway info from the `/v1/info` endpoint.
42584
44158
  *
42585
44159
  * @param isTestnet - Whether to query the testnet or mainnet API.
44160
+ * @param config - Optional polling config overrides, including custom
44161
+ * `headers` forwarded with the request.
42586
44162
  * @returns Parsed Gateway info including per-domain processed heights.
42587
44163
  * @throws KitError on HTTP failure or invalid response.
42588
44164
  *
@@ -42591,11 +44167,11 @@ const gatewayInfoSchema = z.object({
42591
44167
  * const info = await fetchGatewayInfo(true)
42592
44168
  * console.log(info.domains.map(d => `${d.chain}: ${d.processedHeight}`))
42593
44169
  * ```
42594
- */ async function fetchGatewayInfo(isTestnet) {
44170
+ */ async function fetchGatewayInfo(isTestnet, config) {
42595
44171
  const apiBaseUrl = getGatewayApiBaseUrl(isTestnet);
42596
44172
  const url = `${apiBaseUrl}/v1/info`;
42597
44173
  try {
42598
- return await pollApiGet(url, assertGatewayInfoResponse);
44174
+ return await pollApiGet(url, assertGatewayInfoResponse, config);
42599
44175
  } catch (error) {
42600
44176
  if (error instanceof KitError) {
42601
44177
  throw error;
@@ -42621,6 +44197,8 @@ const gatewayInfoSchema = z.object({
42621
44197
  *
42622
44198
  * @param isTestnet - Whether to query the testnet or mainnet API.
42623
44199
  * @param domain - The Gateway domain number (e.g. 0 = Ethereum, 6 = Base).
44200
+ * @param config - Optional polling config overrides, including custom
44201
+ * `headers` forwarded with the `/v1/info` request.
42624
44202
  * @returns The processed height as a bigint.
42625
44203
  * @throws KitError when the domain is not found in the response.
42626
44204
  *
@@ -42629,10 +44207,10 @@ const gatewayInfoSchema = z.object({
42629
44207
  * const height = await getProcessedHeight(true, 6) // Base Sepolia
42630
44208
  * console.log(`Gateway processed up to block ${height}`)
42631
44209
  * ```
42632
- */ async function getProcessedHeight(isTestnet, domain) {
44210
+ */ async function getProcessedHeight(isTestnet, domain, config) {
42633
44211
  // No caching in v1 — /v1/info is cheap and delegate status checks
42634
44212
  // are infrequent. Revisit if rate-limit or latency issues appear.
42635
- const info = await fetchGatewayInfo(isTestnet);
44213
+ const info = await fetchGatewayInfo(isTestnet, config);
42636
44214
  const entry = info.domains.find((d)=>d.domain === domain);
42637
44215
  if (!entry) {
42638
44216
  throw new KitError({
@@ -43098,12 +44676,22 @@ function throwNetworkMismatch(expected, actual) {
43098
44676
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
43099
44677
  * @returns Array of groups, each with adapter, chain, and intents to sign.
43100
44678
  */ function groupIntentsByAdapter(intents, allocations) {
44679
+ // Keyed by `(adapter, sourceSigner)` (see groupEvmIntents), not by adapter
44680
+ // alone: two spend sources on the same chain — including two addresses behind
44681
+ // one shared adapter — must stay in separate signing groups.
43101
44682
  const evmGroups = new Map();
43102
44683
  const solanaGroups = [];
43103
44684
  for (const intent of intents){
43104
- const alloc = allocations.find((a)=>a.chain.gateway.domain === intent.spec.sourceDomain);
44685
+ // Match each intent back to its source allocation by BOTH Gateway domain
44686
+ // and signer. Two spend sources on the same chain share a domain, so
44687
+ // matching on domain alone always returned the FIRST allocation — folding
44688
+ // the second wallet's intent into the first wallet's signing group so it
44689
+ // was signed by the wrong wallet (DEVX-3638). specs carry sourceSigner as
44690
+ // bytes32 and allocations carry native addresses, so normalize both.
44691
+ const intentSigner = intent.spec.sourceSigner.toLowerCase();
44692
+ const alloc = allocations.find((a)=>a.chain.gateway.domain === intent.spec.sourceDomain && convertAddress(a.sourceSigner, 'bytes32').toLowerCase() === intentSigner);
43105
44693
  if (!alloc) {
43106
- 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');
44694
+ 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');
43107
44695
  }
43108
44696
  if (alloc.chain.type === 'solana') {
43109
44697
  solanaGroups.push(groupSolanaIntents(intent, alloc));
@@ -45004,12 +46592,12 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45004
46592
  * exceeds the 5-minute timeout without reaching a terminal status.
45005
46593
  * @throws KitError Propagated from {@link pollApiGet} if the HTTP
45006
46594
  * request itself fails or returns a non-OK response.
45007
- */ async function pollForwarderCompletion(apiBaseUrl, transferId) {
46595
+ */ async function pollForwarderCompletion(apiBaseUrl, transferId, requestConfig) {
45008
46596
  const startTime = Date.now();
45009
46597
  while(Date.now() - startTime < FORWARDER_POLL_TIMEOUT_MS){
45010
46598
  let status;
45011
46599
  try {
45012
- status = await pollApiGet(`${apiBaseUrl}/v1/transfer/${transferId}`, assertGatewayTransferDetails);
46600
+ status = await pollApiGet(`${apiBaseUrl}/v1/transfer/${transferId}`, assertGatewayTransferDetails, requestConfig);
45013
46601
  } catch (error) {
45014
46602
  throwGatewayApiError(error);
45015
46603
  }
@@ -45127,7 +46715,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45127
46715
  * @param params - Spend parameters (source(s) and token).
45128
46716
  * @param destChain - Resolved destination chain (used for network type).
45129
46717
  * @returns Confirmed balances tagged with their originating source index.
45130
- */ async function fetchChainBalances(params, destChain) {
46718
+ */ async function fetchChainBalances(params, destChain, requestConfig) {
45131
46719
  const rawSources = Array.isArray(params.from) ? params.from : [
45132
46720
  params.from
45133
46721
  ];
@@ -45156,7 +46744,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45156
46744
  token: params.token,
45157
46745
  sources: querySource,
45158
46746
  networkType
45159
- });
46747
+ }, requestConfig);
45160
46748
  }));
45161
46749
  const chainBalances = [];
45162
46750
  for(let i = 0; i < balanceResults.length; i++){
@@ -45229,9 +46817,9 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45229
46817
  * @param recipientAddress - Resolved recipient address.
45230
46818
  * @param useForwarder - Whether the Forwarding Service path is active.
45231
46819
  * @returns Allocations, intents, and (auto-allocation only) confirmed balances.
45232
- */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
46820
+ */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder, requestConfig) {
45233
46821
  if (params.amountIn) {
45234
- const chainBalances = await fetchChainBalances(params, destChain);
46822
+ const chainBalances = await fetchChainBalances(params, destChain, requestConfig);
45235
46823
  const { allocations, intents } = await buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, params.amountIn, chainBalances);
45236
46824
  return {
45237
46825
  allocations,
@@ -45254,11 +46842,11 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45254
46842
  * @param useForwarder - Whether the Forwarding Service path is active.
45255
46843
  * @param allocations - Normalised allocations (forwarded to error handler).
45256
46844
  * @returns Estimate entries and optional forwarding fee.
45257
- */ async function fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations) {
46845
+ */ async function fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations, requestConfig) {
45258
46846
  if (useForwarder) {
45259
46847
  let fwdResponse;
45260
46848
  try {
45261
- fwdResponse = await pollApiPost(`${apiBaseUrl}/v1/estimate?enableForwarder=true`, estimateBody, assertGatewayForwarderEstimateResponse);
46849
+ fwdResponse = await pollApiPost(`${apiBaseUrl}/v1/estimate?enableForwarder=true`, estimateBody, assertGatewayForwarderEstimateResponse, requestConfig);
45262
46850
  } catch (error) {
45263
46851
  throwGatewayApiError(error, allocations);
45264
46852
  }
@@ -45269,7 +46857,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45269
46857
  }
45270
46858
  let entries;
45271
46859
  try {
45272
- entries = await pollApiPost(`${apiBaseUrl}/v1/estimate`, estimateBody, assertGatewayEstimateResponse);
46860
+ entries = await pollApiPost(`${apiBaseUrl}/v1/estimate`, estimateBody, assertGatewayEstimateResponse, requestConfig);
45273
46861
  } catch (error) {
45274
46862
  throwGatewayApiError(error, allocations);
45275
46863
  }
@@ -45340,14 +46928,14 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45340
46928
  * @param destChain - Resolved destination chain with Gateway v1 config.
45341
46929
  * @param useForwarder - Whether the Forwarding Service path is active.
45342
46930
  * @returns Estimated intents (with real maxFee) and optional forwarding fee.
45343
- */ async function validateAndEstimate(allocations, intents, destChain, useForwarder) {
46931
+ */ async function validateAndEstimate(allocations, intents, destChain, useForwarder, requestConfig) {
45344
46932
  assertNetworkCompatibility(allocations, destChain);
45345
46933
  if (useForwarder) {
45346
46934
  assertForwarderRouteSupport(destChain, allocations);
45347
46935
  }
45348
46936
  const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
45349
46937
  const estimateBody = buildEstimateRequestBody(intents);
45350
- const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
46938
+ const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations, requestConfig);
45351
46939
  const estimatedIntents = parseEstimateResponse(entries, intents);
45352
46940
  return {
45353
46941
  estimatedIntents,
@@ -45406,7 +46994,7 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45406
46994
  }
45407
46995
  throw err;
45408
46996
  }
45409
- const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder);
46997
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder, opts.requestConfig);
45410
46998
  const stillOverdrawn = findOverdrawnChains(estimatedIntents, corrected.allocations, opts.chainBalances);
45411
46999
  if (stillOverdrawn.length === 0) {
45412
47000
  return {
@@ -45440,12 +47028,12 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45440
47028
  * @param recipientAddress - Resolved recipient address.
45441
47029
  * @param useForwarder - Whether the Forwarding Service path is active.
45442
47030
  * @returns Allocations, estimated intents, and optional forwarding fee.
45443
- */ async function resolveValidatedIntents(params, destChain, recipientAddress, useForwarder) {
47031
+ */ async function resolveValidatedIntents(params, destChain, recipientAddress, useForwarder, requestConfig) {
45444
47032
  if (useForwarder) {
45445
47033
  assertForwarderRouteSupport(destChain);
45446
47034
  }
45447
- const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
45448
- const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder);
47035
+ const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder, requestConfig);
47036
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder, requestConfig);
45449
47037
  if (params.amountIn && chainBalances) {
45450
47038
  const overdrawn = findOverdrawnChains(estimatedIntents, allocations, chainBalances);
45451
47039
  if (overdrawn.length > 0) {
@@ -45458,7 +47046,10 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45458
47046
  chainBalances,
45459
47047
  estimatedIntents,
45460
47048
  allocations,
45461
- overdrawn
47049
+ overdrawn,
47050
+ ...requestConfig ? {
47051
+ requestConfig
47052
+ } : {}
45462
47053
  });
45463
47054
  }
45464
47055
  }
@@ -45502,10 +47093,10 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
45502
47093
  attestationSignature: transferResponse.signature
45503
47094
  };
45504
47095
  }
45505
- async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, steps) {
47096
+ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, steps, requestConfig) {
45506
47097
  const chainName = destChain.name;
45507
47098
  const recipientAddress = await resolveRecipientAddress(params.to);
45508
- const { allocations, estimatedIntents, forwardingFee: estimateFwdFee } = await resolveValidatedIntents(params, destChain, recipientAddress, useForwarder);
47099
+ const { allocations, estimatedIntents, forwardingFee: estimateFwdFee } = await resolveValidatedIntents(params, destChain, recipientAddress, useForwarder, requestConfig);
45509
47100
  pushStep(steps, dispatcher, 'gateway.spend.step.buildBurnIntents', chainName, {
45510
47101
  name: 'buildBurnIntents',
45511
47102
  state: 'success',
@@ -45542,7 +47133,7 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
45542
47133
  const forwarderParam = useForwarder ? '?enableForwarder=true' : '';
45543
47134
  let transferResponse;
45544
47135
  try {
45545
- transferResponse = await pollApiPost(`${transferApiBaseUrl}/v1/transfer${forwarderParam}`, transferBody, assertGatewayTransferResponse);
47136
+ transferResponse = await pollApiPost(`${transferApiBaseUrl}/v1/transfer${forwarderParam}`, transferBody, assertGatewayTransferResponse, requestConfig);
45546
47137
  } catch (error) {
45547
47138
  throwGatewayApiError(error, allocations);
45548
47139
  }
@@ -45622,10 +47213,11 @@ function updateFeesWithActual(fees, completionStatus) {
45622
47213
  amount: actual
45623
47214
  } : f);
45624
47215
  }
45625
- async function resolveDestinationTxHash(params, destChain, forwarder, transferId, attestation, attestationSignature, fees) {
47216
+ async function resolveDestinationTxHash(opts) {
47217
+ const { params, destChain, forwarder, transferId, attestation, attestationSignature, fees, requestConfig } = opts;
45626
47218
  if (forwarder && transferId) {
45627
47219
  const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
45628
- const completionStatus = await pollForwarderCompletion(apiBaseUrl, transferId);
47220
+ const completionStatus = await pollForwarderCompletion(apiBaseUrl, transferId, requestConfig);
45629
47221
  if (!completionStatus.transactionHash) {
45630
47222
  throw new KitError({
45631
47223
  ...OnchainError.TRANSACTION_REVERTED,
@@ -45708,9 +47300,20 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45708
47300
  /**
45709
47301
  * Execute the mint step, recording success/error as a spend step.
45710
47302
  */ async function executeMintStep(opts) {
45711
- const { params, destChain, forwarder, transferId, attestation, attestationSignature, fees, steps, dispatcher } = opts;
47303
+ const { params, destChain, forwarder, transferId, attestation, attestationSignature, fees, steps, dispatcher, requestConfig } = opts;
45712
47304
  try {
45713
- const mintResult = await resolveDestinationTxHash(params, destChain, forwarder, transferId, attestation, attestationSignature, fees);
47305
+ const mintResult = await resolveDestinationTxHash({
47306
+ params,
47307
+ destChain,
47308
+ forwarder,
47309
+ transferId,
47310
+ attestation,
47311
+ attestationSignature,
47312
+ fees,
47313
+ ...requestConfig ? {
47314
+ requestConfig
47315
+ } : {}
47316
+ });
45714
47317
  pushStep(steps, dispatcher, 'gateway.spend.step.mint', destChain.name, {
45715
47318
  name: 'mint',
45716
47319
  state: 'success',
@@ -45765,7 +47368,8 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45765
47368
  assertSpendAddresses(params, destChain);
45766
47369
  const forwarder = isForwarderEnabled(params.to);
45767
47370
  const steps = [];
45768
- const context = params.config?.retry ? await resolveRetryContext(params) : await resolveNormalContext(params, destChain, forwarder, dispatcher, steps);
47371
+ const requestConfig = options?.requestConfig;
47372
+ const context = params.config?.retry ? await resolveRetryContext(params) : await resolveNormalContext(params, destChain, forwarder, dispatcher, steps, requestConfig);
45769
47373
  const fees = appendForwarderFee(context.fees, context.forwardingFee);
45770
47374
  const mintResult = await executeMintStep({
45771
47375
  params,
@@ -45776,7 +47380,10 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45776
47380
  attestationSignature: context.attestationSignature,
45777
47381
  fees,
45778
47382
  steps,
45779
- dispatcher
47383
+ dispatcher,
47384
+ ...requestConfig ? {
47385
+ requestConfig
47386
+ } : {}
45780
47387
  });
45781
47388
  // A throwing `onBroadcast` must never strand the caller after a
45782
47389
  // successful mint — the chain has moved. `safeInvokeCallback` swallows
@@ -45793,14 +47400,14 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45793
47400
  /**
45794
47401
  * Resolve the spend context via the normal (non-retry) path.
45795
47402
  * Validates `from`, then delegates to `runSpendNormalPath`.
45796
- */ async function resolveNormalContext(params, destChain, forwarder, dispatcher, steps) {
47403
+ */ async function resolveNormalContext(params, destChain, forwarder, dispatcher, steps, requestConfig) {
45797
47404
  if (!params.from) {
45798
47405
  throw createValidationFailedError$1('from', params.from, 'Source (from) is required when config.retry is not provided.');
45799
47406
  }
45800
47407
  return runSpendNormalPath({
45801
47408
  ...params,
45802
47409
  from: params.from
45803
- }, destChain, forwarder, dispatcher, steps);
47410
+ }, destChain, forwarder, dispatcher, steps, requestConfig);
45804
47411
  }
45805
47412
  /**
45806
47413
  * Estimate fees for a Gateway spend without executing it.
@@ -45823,13 +47430,13 @@ function buildSpendResult(recipientAddress, destChain, txHash, allocationResults
45823
47430
  * })
45824
47431
  * fees.forEach((f) => console.log(f.type, f.amount, f.allocations))
45825
47432
  * ```
45826
- */ async function estimateSpend$1(params) {
47433
+ */ async function estimateSpend$1(params, requestConfig) {
45827
47434
  const destChain = resolveChainIdentifier(params.to.chain);
45828
47435
  assertGatewayV1(destChain);
45829
47436
  assertSpendAddresses(params, destChain);
45830
47437
  const forwarder = isForwarderEnabled(params.to);
45831
47438
  const recipientAddress = await resolveRecipientAddress(params.to);
45832
- const { allocations, estimatedIntents, forwardingFee } = await resolveValidatedIntents(params, destChain, recipientAddress, forwarder);
47439
+ const { allocations, estimatedIntents, forwardingFee } = await resolveValidatedIntents(params, destChain, recipientAddress, forwarder, requestConfig);
45833
47440
  const fees = computeEstimateFees(estimatedIntents, allocations, params.config?.customFee);
45834
47441
  if (forwardingFee) {
45835
47442
  fees.push({
@@ -45953,6 +47560,9 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45953
47560
  * the delegate is `'ready'` (finalized); otherwise it is `'pending'`.
45954
47561
  *
45955
47562
  * @param params - The adapter context (`from`), delegate address, and token.
47563
+ * @param requestConfig - Optional polling config overrides, including custom
47564
+ * `headers` forwarded with the `/v1/info` request used on the EVM finality
47565
+ * check.
45956
47566
  * @returns Promise resolving to `'none'`, `'pending'`, or `'ready'`.
45957
47567
  * @throws KitError if the chain is unsupported or `/v1/info` fails.
45958
47568
  *
@@ -45972,7 +47582,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
45972
47582
  * const status = await getDelegateStatus(params)
45973
47583
  * if (status === 'ready') { // safe to spend }
45974
47584
  * ```
45975
- */ async function getDelegateStatus$1(params) {
47585
+ */ async function getDelegateStatus$1(params, requestConfig) {
45976
47586
  const { from, delegateAddress } = params;
45977
47587
  const { adapter } = from;
45978
47588
  const chain = resolveChainIdentifier(from.chain);
@@ -46012,7 +47622,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
46012
47622
  return String(finalizedResult).toLowerCase() === 'true' ? 'ready' : 'pending';
46013
47623
  }
46014
47624
  // EVM: use processedHeight from /v1/info
46015
- const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain);
47625
+ const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain, requestConfig);
46016
47626
  // If the RPC node lags Gateway's indexer view, the historical read at
46017
47627
  // processedHeight may throw a block-range error. This is safe to treat
46018
47628
  // as 'pending' because processedHeight comes from Gateway's /v1/info
@@ -46230,6 +47840,35 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46230
47840
  dispatchEvent(dispatcher, opName, 'failed', chain, extractErrorInfo(error));
46231
47841
  }
46232
47842
 
47843
+ // ---------------------------------------------------------------------------
47844
+ // Config validation
47845
+ // ---------------------------------------------------------------------------
47846
+ function isPlainObject(value) {
47847
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
47848
+ return false;
47849
+ }
47850
+ const prototype = Object.getPrototypeOf(value);
47851
+ return prototype === Object.prototype || prototype === null;
47852
+ }
47853
+ function assertHeadersConfig(headers) {
47854
+ if (headers === undefined) {
47855
+ return;
47856
+ }
47857
+ if (!isPlainObject(headers)) {
47858
+ throw createValidationFailedError$1('config.headers', headers, 'config.headers must be a plain object with string header values when provided');
47859
+ }
47860
+ for (const [name, value] of Object.entries(headers)){
47861
+ if (typeof value !== 'string') {
47862
+ throw createValidationFailedError$1(`config.headers.${name}`, value, 'header values must be strings');
47863
+ }
47864
+ }
47865
+ }
47866
+ function assertGatewayV1Config(config) {
47867
+ if (!isPlainObject(config)) {
47868
+ throw createValidationFailedError$1('config', config, 'config must be a plain object when provided');
47869
+ }
47870
+ assertHeadersConfig(config['headers']);
47871
+ }
46233
47872
  // ---------------------------------------------------------------------------
46234
47873
  // Factory
46235
47874
  // ---------------------------------------------------------------------------
@@ -46239,7 +47878,12 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46239
47878
  * Returns a plain object that implements {@link IGatewayProvider} by
46240
47879
  * delegating to the standalone functions in `operations/`.
46241
47880
  *
47881
+ * @param config - Optional provider configuration. Supply `headers` to
47882
+ * forward custom HTTP headers with every Gateway API request. See
47883
+ * {@link GatewayV1Config}.
46242
47884
  * @returns An {@link IGatewayProvider} instance.
47885
+ * @throws Throws when `config` or `config.headers` is not a plain object, or a
47886
+ * header value is not a string.
46243
47887
  *
46244
47888
  * @example
46245
47889
  * ```typescript
@@ -46248,7 +47892,23 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46248
47892
  * const provider = createGatewayV1Provider()
46249
47893
  * const chains = provider.getSupportedChains()
46250
47894
  * ```
46251
- */ function createGatewayV1Provider() {
47895
+ *
47896
+ * @example
47897
+ * ```typescript
47898
+ * // Forward a custom header on every Gateway API request.
47899
+ * const provider = createGatewayV1Provider({
47900
+ * headers: { 'X-Access-Key': process.env.GATEWAY_ACCESS_KEY! },
47901
+ * })
47902
+ * ```
47903
+ */ function createGatewayV1Provider(config = {}) {
47904
+ assertGatewayV1Config(config);
47905
+ // Captured once and threaded to every Gateway API call the operations make,
47906
+ // so a configured header reaches balances, estimate, transfer, forwarder
47907
+ // status, and /v1/info alike. Left undefined when no headers are configured
47908
+ // so the operations keep their existing defaults untouched.
47909
+ const requestConfig = config.headers ? {
47910
+ headers: config.headers
47911
+ } : undefined;
46252
47912
  /**
46253
47913
  * Execute an operation with started/succeeded/failed event dispatch.
46254
47914
  */ async function withEvents(opName, chain, startedData, params, fn) {
@@ -46279,21 +47939,25 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46279
47939
  }, params, depositFor$1),
46280
47940
  spend: async (params, options)=>{
46281
47941
  const destChainName = extractChainInfo(params.to.chain).name;
47942
+ const mergedOptions = requestConfig ? {
47943
+ ...options,
47944
+ requestConfig
47945
+ } : options;
46282
47946
  return withEvents('spend', destChainName, {
46283
47947
  destinationChain: destChainName,
46284
47948
  token: params.token ?? 'USDC'
46285
- }, params, async (p)=>spend$1(p, provider.actionDispatcher, options));
47949
+ }, params, async (p)=>spend$1(p, provider.actionDispatcher, mergedOptions));
46286
47950
  },
46287
47951
  estimateSpend: async (params)=>{
46288
47952
  const destChainName = extractChainInfo(params.to.chain).name;
46289
47953
  return withEvents('estimateSpend', destChainName, {
46290
47954
  destinationChain: destChainName,
46291
47955
  token: params.token ?? 'USDC'
46292
- }, params, estimateSpend$1);
47956
+ }, params, async (p)=>estimateSpend$1(p, requestConfig));
46293
47957
  },
46294
47958
  getBalances: async (params)=>withEvents('getBalances', 'aggregate', {
46295
47959
  token: params.token ?? 'USDC'
46296
- }, params, getBalances$1),
47960
+ }, params, async (p)=>getBalances$1(p, requestConfig)),
46297
47961
  addDelegate: async (params)=>withEvents('addDelegate', extractChainInfo(params.from.chain).name, {
46298
47962
  delegateAddress: params.delegateAddress
46299
47963
  }, params, addDelegate$1),
@@ -46311,7 +47975,9 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46311
47975
  // in withEvents. getSupportedChains is synchronous. getDelegateStatus is a
46312
47976
  // lightweight read that consumers treat as a utility check, unlike
46313
47977
  // getBalances which involves API polling and benefits from lifecycle events.
46314
- getDelegateStatus: getDelegateStatus$1,
47978
+ // getDelegateStatus is wrapped to forward the provider's request config to
47979
+ // its /v1/info call; getSupportedChains makes no API call.
47980
+ getDelegateStatus: async (params)=>getDelegateStatus$1(params, requestConfig),
46315
47981
  getSupportedChains: getSupportedChains$1
46316
47982
  };
46317
47983
  return provider;
@@ -46321,6 +47987,7 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46321
47987
  * The default providers that will be used in addition to the providers
46322
47988
  * provided to the createUnifiedBalanceKitContext factory function.
46323
47989
  *
47990
+ * @param config - Optional configuration forwarded to the default provider.
46324
47991
  * @returns A readonly tuple with one element: the default Gateway v1 provider
46325
47992
  * instance created via {@link createGatewayV1Provider}.
46326
47993
  * @internal
@@ -46332,8 +47999,10 @@ function dispatchEvent(dispatcher, opName, stage, chain, data) {
46332
47999
  * const { providers } = createUnifiedBalanceKitContext()
46333
48000
  * // providers[0] is the default Gateway v1 instance from getDefaultProviders()
46334
48001
  * ```
46335
- */ const getDefaultProviders = ()=>[
46336
- createGatewayV1Provider()
48002
+ */ const getDefaultProviders = (config = {})=>[
48003
+ createGatewayV1Provider(config.headers ? {
48004
+ headers: config.headers
48005
+ } : {})
46337
48006
  ];
46338
48007
  function createUnifiedBalanceKitContext(config = {}) {
46339
48008
  // Runtime guard: `excludeDefaultProviders: true` with no `providers` is
@@ -46347,7 +48016,10 @@ function createUnifiedBalanceKitContext(config = {}) {
46347
48016
  // Initialize default providers — either the canonical singleton tuple or
46348
48017
  // an empty list when the caller has opted out. Typed loosely here; each
46349
48018
  // public overload narrows the return type for callers.
46350
- const defaultProviders = config.excludeDefaultProviders === true ? [] : getDefaultProviders();
48019
+ const defaultProvidersConfig = config.headers ? {
48020
+ headers: config.headers
48021
+ } : {};
48022
+ const defaultProviders = config.excludeDefaultProviders === true ? [] : getDefaultProviders(defaultProvidersConfig);
46351
48023
  const providers = [
46352
48024
  ...defaultProviders,
46353
48025
  ...config.providers ?? []
@@ -47038,29 +48710,65 @@ function sumAllocationsFromSources(sources) {
47038
48710
  }
47039
48711
  });
47040
48712
 
48713
+ function normalizeAddressIdentity(value) {
48714
+ if (typeof value !== 'string') return undefined;
48715
+ try {
48716
+ return convertAddress(value, 'bytes32').toLowerCase();
48717
+ } catch {
48718
+ // Address validation below owns the structured error for invalid input.
48719
+ return value;
48720
+ }
48721
+ }
47041
48722
  /**
47042
- * Reject `from` arrays containing the same adapter reference more than once.
47043
- * Must run on the raw input (before Zod parsing) because `z.object()` creates
47044
- * new wrapper objects and identity (`===`) is lost.
48723
+ * Reject `from` arrays that describe the same spend source more than once.
48724
+ *
48725
+ * One adapter instance may legitimately be reused across distinct Gateway
48726
+ * source accounts. When `sourceAccount` is omitted, the signer address
48727
+ * identifies the source. Must run on the raw input (before Zod parsing)
48728
+ * because `z.object()` creates new wrapper objects and adapter identity
48729
+ * (`===`) is lost.
47045
48730
  *
47046
48731
  * @internal
47047
- */ function assertNoDuplicateAdapters(params) {
48732
+ */ function assertNoDuplicateSpendSources(params) {
47048
48733
  if (params == null || typeof params !== 'object') return;
47049
48734
  const from = params['from'];
47050
48735
  if (!Array.isArray(from) || from.length < 2) return;
47051
- const seen = new Set();
48736
+ const seen = new Map();
47052
48737
  for (const source of from){
47053
48738
  if (source == null || typeof source !== 'object') continue;
47054
- const adapter = source['adapter'];
48739
+ const record = source;
48740
+ const adapter = record['adapter'];
47055
48741
  if (adapter == null) continue;
47056
- if (seen.has(adapter)) {
48742
+ const sourceAccount = normalizeAddressIdentity(record['sourceAccount']) ?? normalizeAddressIdentity(record['address']);
48743
+ let sourceAccounts = seen.get(adapter);
48744
+ if (sourceAccounts == null) {
48745
+ sourceAccounts = new Set();
48746
+ seen.set(adapter, sourceAccounts);
48747
+ }
48748
+ if (sourceAccounts.has(sourceAccount)) {
47057
48749
  throw new KitError({
47058
48750
  ...InputError.VALIDATION_FAILED,
47059
48751
  recoverability: 'FATAL',
47060
- message: 'Duplicate adapter instance detected in from array. ' + 'Each source must use a distinct adapter object.'
48752
+ message: 'Duplicate spend source detected in from array. The same adapter ' + 'and Gateway source account are reused more than once.'
47061
48753
  });
47062
48754
  }
47063
- seen.add(adapter);
48755
+ sourceAccounts.add(sourceAccount);
48756
+ }
48757
+ }
48758
+ function assertSourceAddressContexts(from) {
48759
+ const sources = Array.isArray(from) ? from : [
48760
+ from
48761
+ ];
48762
+ for (const raw of sources){
48763
+ if (raw == null || typeof raw !== 'object') continue;
48764
+ const source = raw;
48765
+ const addressContext = source.adapter?.capabilities?.addressContext;
48766
+ if (addressContext === 'developer-controlled' && (typeof source.address !== 'string' || source.address === '')) {
48767
+ throw createValidationFailedError$1('address', source.address, 'Address is required for developer-controlled adapters.');
48768
+ }
48769
+ if (addressContext === 'user-controlled' && source.address !== undefined) {
48770
+ throw createValidationFailedError$1('address', source.address, 'Address should not be provided for user-controlled adapters. ' + 'The address is automatically resolved from the connected wallet.');
48771
+ }
47064
48772
  }
47065
48773
  }
47066
48774
  /**
@@ -47074,14 +48782,17 @@ function sumAllocationsFromSources(sources) {
47074
48782
  * @throws \{KitError\} When validation fails; error code is
47075
48783
  * {@link InputError.VALIDATION_FAILED}.
47076
48784
  */ function assertSpendParams(params) {
47077
- assertNoDuplicateAdapters(params);
48785
+ assertNoDuplicateSpendSources(params);
48786
+ if (params != null && typeof params === 'object' && 'from' in params) {
48787
+ assertSourceAddressContexts(params['from']);
48788
+ }
47078
48789
  const result = spendParamsSchema.safeParse(params);
47079
48790
  if (!result.success) {
47080
48791
  throw convertZodErrorToStructured(result.error, params);
47081
48792
  }
47082
48793
  const data = result.data;
47083
48794
  // Validate recipientAddress against the destination chain
47084
- if (data.to.recipientAddress) {
48795
+ if (data.to.recipientAddress !== undefined && data.to.recipientAddress !== '') {
47085
48796
  const toChain = data.to.chain;
47086
48797
  if (!isValidAddressForChain(data.to.recipientAddress, toChain)) {
47087
48798
  const chainInfo = extractChainInfo(toChain);
@@ -47089,7 +48800,7 @@ function sumAllocationsFromSources(sources) {
47089
48800
  }
47090
48801
  }
47091
48802
  // Validate sourceAccount against each source's allocation chain
47092
- if (data.from) {
48803
+ if (data.from !== undefined) {
47093
48804
  assertSourceAccountAddresses(data.from);
47094
48805
  }
47095
48806
  }
@@ -47104,11 +48815,15 @@ function assertSourceAccountAddresses(from) {
47104
48815
  from
47105
48816
  ];
47106
48817
  for (const raw of sources){
47107
- if (!raw || typeof raw !== 'object' || !('sourceAccount' in raw)) continue;
48818
+ if (raw === null || typeof raw !== 'object' || !('sourceAccount' in raw)) {
48819
+ continue;
48820
+ }
47108
48821
  const source = raw;
47109
- if (!source.sourceAccount) continue;
48822
+ if (source.sourceAccount === undefined || source.sourceAccount === '') {
48823
+ continue;
48824
+ }
47110
48825
  const sourceChain = getSourceChainFromAllocations(source.allocations);
47111
- if (!sourceChain) continue;
48826
+ if (sourceChain === undefined) continue;
47112
48827
  if (!isValidAddressForChain(source.sourceAccount, sourceChain)) {
47113
48828
  const chainInfo = extractChainInfo(sourceChain);
47114
48829
  throw createInvalidAddressError(source.sourceAccount, String(chainInfo.name), chainInfo.expectedAddressFormat);
@@ -49308,6 +51023,11 @@ function assertAppKitCustomFeePolicyScope(operation) {
49308
51023
  assertAppKitCustomFeePolicy(config.customFeePolicy);
49309
51024
  }
49310
51025
  const unifiedBalance = new AppKitUnifiedBalance({
51026
+ // Top-level headers act as the shared default; an explicit
51027
+ // `unifiedBalance.headers` overrides them by coming later in the spread.
51028
+ ...config.headers != null && {
51029
+ headers: config.headers
51030
+ },
49311
51031
  ...config.unifiedBalance,
49312
51032
  ...config.disableErrorReporting != null && {
49313
51033
  disableErrorReporting: config.disableErrorReporting
@@ -49410,30 +51130,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49410
51130
  */ async retryBridge(result, retryContext) {
49411
51131
  return retryBridge(this.context, result, retryContext);
49412
51132
  }
49413
- /**
49414
- * Estimate the bridge operation.
49415
- *
49416
- * Calculates gas costs, protocol fees, and optional custom fees for a
49417
- * cross-chain bridge transfer without executing the transaction. Useful
49418
- * for displaying cost estimates to users before they confirm a transfer.
49419
- *
49420
- * @param params - Bridge parameters containing source, destination, amount, and token
49421
- * @returns Promise resolving to the estimate result with fee information
49422
- * @throws If bridge parameters are invalid
49423
- * @throws If the bridge route is not supported
49424
- *
49425
- * @example
49426
- * ```typescript
49427
- * const estimate = await kit.estimateBridge({
49428
- * from: { adapter: sourceAdapter, chain: 'Ethereum' },
49429
- * to: { adapter: destAdapter, chain: 'Polygon' },
49430
- * amount: '100.50',
49431
- * token: 'USDC'
49432
- * })
49433
- *
49434
- * console.log('Estimated fees:', estimate.fees)
49435
- * ```
49436
- */ async estimateBridge(params) {
51133
+ async estimateBridge(params) {
49437
51134
  return estimateBridge(this.context, params);
49438
51135
  }
49439
51136
  /**
@@ -49549,7 +51246,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49549
51246
  * config: {
49550
51247
  * slippageBps: 300, // 3% slippage
49551
51248
  * allowanceStrategy: 'permit',
49552
- * kitKey: 'KIT_KEY:id:secret'
51249
+ * apiKey: 'TEST_API_KEY:id:secret'
49553
51250
  * }
49554
51251
  * })
49555
51252
  *
@@ -49578,7 +51275,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49578
51275
  * amountIn: '100.50',
49579
51276
  * config: {
49580
51277
  * slippageBps: 300,
49581
- * kitKey: 'KIT_KEY:id:secret'
51278
+ * apiKey: 'TEST_API_KEY:id:secret'
49582
51279
  * }
49583
51280
  * })
49584
51281
  *
@@ -49602,7 +51299,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49602
51299
  * not write the polling loop yourself.
49603
51300
  *
49604
51301
  * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
49605
- * `kitKey`.
51302
+ * `apiKey`.
49606
51303
  * @returns A snapshot of the swap's status at the time of the call.
49607
51304
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
49608
51305
  *
@@ -49615,7 +51312,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49615
51312
  * txHash: result.txHash,
49616
51313
  * chainIn: result.chainIn,
49617
51314
  * chainOut: result.chainOut,
49618
- * kitKey: process.env.KIT_KEY ?? '',
51315
+ * apiKey: process.env.CIRCLE_API_KEY,
49619
51316
  * })
49620
51317
  *
49621
51318
  * console.log(status.progress.status, status.progress.substatus)
@@ -49628,7 +51325,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49628
51325
  * txHash: result.txHash,
49629
51326
  * chainIn: result.chainIn,
49630
51327
  * chainOut: result.chainOut,
49631
- * kitKey: process.env.KIT_KEY ?? '',
51328
+ * apiKey: process.env.CIRCLE_API_KEY,
49632
51329
  * })
49633
51330
  * while (status.progress.status === 'PENDING') {
49634
51331
  * await new Promise((r) => setTimeout(r, 3_000))
@@ -49636,7 +51333,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49636
51333
  * txHash: result.txHash,
49637
51334
  * chainIn: result.chainIn,
49638
51335
  * chainOut: result.chainOut,
49639
- * kitKey: process.env.KIT_KEY ?? '',
51336
+ * apiKey: process.env.CIRCLE_API_KEY,
49640
51337
  * })
49641
51338
  * }
49642
51339
  * ```
@@ -49655,7 +51352,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49655
51352
  * expires. Timeouts surface as a RETRYABLE `KitError` so callers can
49656
51353
  * re-invoke with the same `txHash`.
49657
51354
  *
49658
- * @param params - Wait configuration: identifiers, kit key, and optional
51355
+ * @param params - Wait configuration: identifiers, API key, and optional
49659
51356
  * `timeoutMs` / `onProgress`.
49660
51357
  * @returns The first terminal {@link SwapStatusResult} observed.
49661
51358
  * @throws \{KitError\} (`NETWORK.TIMEOUT`, RETRYABLE) when the wait
@@ -49668,7 +51365,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49668
51365
  *
49669
51366
  * const final = await kit.waitForSwap({
49670
51367
  * result,
49671
- * kitKey: process.env.KIT_KEY ?? '',
51368
+ * apiKey: process.env.CIRCLE_API_KEY,
49672
51369
  * onProgress: (snap) => console.log(snap.progress.status),
49673
51370
  * })
49674
51371
  *
@@ -49684,7 +51381,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49684
51381
  * txHash: persisted.txHash,
49685
51382
  * chainIn: persisted.chainIn,
49686
51383
  * chainOut: persisted.chainOut,
49687
- * kitKey: process.env.KIT_KEY ?? '',
51384
+ * apiKey: process.env.CIRCLE_API_KEY,
49688
51385
  * })
49689
51386
  * ```
49690
51387
  */ async waitForSwap(params) {
@@ -49716,7 +51413,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49716
51413
  * translates to the chain's native sentinel address — `0xEee…` for EVM,
49717
51414
  * `1111…` for Solana — before querying the service.
49718
51415
  *
49719
- * @param params - `chain`, plus optional `tokens` and `kitKey`.
51416
+ * @param params - `chain`, plus optional `tokens` and `apiKey`.
49720
51417
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
49721
51418
  * @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
49722
51419
  * entries, or any entry is neither a registered symbol nor a
@@ -49727,7 +51424,7 @@ function assertAppKitCustomFeePolicyScope(operation) {
49727
51424
  * const { rates } = await kit.getTokenRates({
49728
51425
  * chain: 'Ethereum',
49729
51426
  * tokens: ['USDC', 'EURC'],
49730
- * kitKey: process.env.KIT_KEY ?? '',
51427
+ * apiKey: process.env.CIRCLE_API_KEY,
49731
51428
  * })
49732
51429
  *
49733
51430
  * const usdc = rates['Ethereum']?.['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']
@@ -49736,38 +51433,8 @@ function assertAppKitCustomFeePolicyScope(operation) {
49736
51433
  */ async getTokenRates(params) {
49737
51434
  return getTokenRates(this.context, params);
49738
51435
  }
49739
- /**
49740
- * Get chains supported by AppKit operations.
49741
- *
49742
- * Returns blockchain networks that support specific stablecoin operations.
49743
- * When no operation type is specified, returns all chains supporting any
49744
- * operation (bridge, swap, earn, or unified balance).
49745
- *
49746
- * @param operationType - Optional operation type to filter chains ('bridge' | 'swap' | 'earn' | 'unifiedBalance')
49747
- * @returns Array of unique chain definitions supporting the specified operation(s)
49748
- *
49749
- * @example Get all supported chains
49750
- * ```typescript
49751
- * import { AppKit } from '@circle-fin/app-kit'
49752
- *
49753
- * const kit = new AppKit()
49754
- * const allChains = kit.getSupportedChains()
49755
- *
49756
- * console.log(`Total supported chains: ${allChains.length}`)
49757
- * allChains.forEach(chain => {
49758
- * console.log(`- ${chain.name} (${chain.type})`)
49759
- * })
49760
- * ```
49761
- *
49762
- * @example Get bridge-specific chains
49763
- * ```typescript
49764
- * const kit = new AppKit()
49765
- * const bridgeChains = kit.getSupportedChains('bridge')
49766
- *
49767
- * console.log('Chains supporting bridge:', bridgeChains.map(c => c.name))
49768
- * ```
49769
- */ getSupportedChains(operationType) {
49770
- return getSupportedChains$2(this.context, operationType, this.unifiedBalance);
51436
+ getSupportedChains(operationType, options) {
51437
+ return getSupportedChains$2(this.context, operationType, this.unifiedBalance, options);
49771
51438
  }
49772
51439
  /**
49773
51440
  * Set operation-scoped custom fee policies.