@circle-fin/app-kit 1.13.0 → 1.14.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.
@@ -30,8 +30,8 @@ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
30
30
  import { z } from 'zod';
31
31
  import pino from 'pino';
32
32
  import { formatUnits as formatUnits$1, parseUnits as parseUnits$1 } from '@ethersproject/units';
33
- import { hexlify, hexZeroPad, concat } from '@ethersproject/bytes';
34
- import '@ethersproject/abi';
33
+ import { hexlify, hexZeroPad, isHexString, concat } from '@ethersproject/bytes';
34
+ import { Interface, defaultAbiCoder } from '@ethersproject/abi';
35
35
  import { getAddress, isAddress } from '@ethersproject/address';
36
36
  import bs58 from 'bs58';
37
37
  import { PublicKey } from '@solana/web3.js';
@@ -3658,8 +3658,45 @@ var EarnChain;
3658
3658
  * This program handles minting operations for Gateway transactions
3659
3659
  * on Solana devnet.
3660
3660
  */ const GATEWAY_MINTER_SOLANA_DEVNET = 'GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr';
3661
- /** TokenMessengerWithFees address shared by enabled EVM mainnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
3662
- /** TokenMessengerWithFees address shared by enabled EVM testnet sources. */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
3661
+ /**
3662
+ * The `TokenMessengerWithFees` proxy contract address for EVM mainnet networks
3663
+ * (all chains except Edge).
3664
+ *
3665
+ * Deployed at a CREATE3-derived address; identical across all mainnet EVM
3666
+ * source chains. Present on any chain that supports the prepaid FORWARD path
3667
+ * via `depositForBurnWithHookAndFees`.
3668
+ */ const TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET = '0x71f54F818671cD0D7ea140Da213e5C8b5C92a408';
3669
+ /**
3670
+ * The `TokenMessengerWithFees` proxy contract address for EVM testnet networks.
3671
+ *
3672
+ * Identical across all testnet EVM source chains. Present on any testnet chain
3673
+ * that supports the prepaid FORWARD path via `depositForBurnWithHookAndFees`.
3674
+ */ const TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET = '0x8745D906D67C346E5eb1aEEED38Eb87F34DF0C0A';
3675
+ /**
3676
+ * The `DepositForHandler` proxy contract address for EVM mainnet networks.
3677
+ *
3678
+ * The handler the GenericExecutor calls on a fast-deposit destination chain to
3679
+ * run a cross-chain deposit into the GatewayWallet. Deployed at the same
3680
+ * address across all mainnet EVM destination chains.
3681
+ */ const DEPOSIT_FOR_HANDLER_EVM_MAINNET = '0x16529813203f77E036576666336554a1210dce4D';
3682
+ /**
3683
+ * The `DepositForHandler` proxy contract address for EVM testnet networks.
3684
+ *
3685
+ * Identical across all testnet EVM destination chains.
3686
+ */ const DEPOSIT_FOR_HANDLER_EVM_TESTNET = '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48';
3687
+ /**
3688
+ * The `GenericExecutor` proxy contract address for EVM mainnet networks.
3689
+ *
3690
+ * The GenericExecutor is the `mintRecipient` and `destinationCaller` on the
3691
+ * destination chain for the CCTP v2 prepaid FORWARD path. It receives the CCTP
3692
+ * mint and calls the `DepositForHandler` to complete the fast deposit.
3693
+ * Deployed at the same address across all mainnet EVM destination chains.
3694
+ */ const GENERIC_EXECUTOR_EVM_MAINNET = '0xFa7be2f04F3Ad4ca969260729c6d45B5625984A7';
3695
+ /**
3696
+ * The `GenericExecutor` proxy contract address for EVM testnet networks.
3697
+ *
3698
+ * Identical across all testnet EVM destination chains.
3699
+ */ const GENERIC_EXECUTOR_EVM_TESTNET = '0xEdC81040756AcCfF070c21D37b265b9D0b5Ba45e';
3663
3700
 
3664
3701
  /**
3665
3702
  * Arc Testnet chain definition
@@ -3718,9 +3755,8 @@ var EarnChain;
3718
3755
  v1: {
3719
3756
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3720
3757
  minter: GATEWAY_MINTER_EVM_TESTNET,
3721
- // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3722
- // deposit into the GatewayWallet above.
3723
- depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3758
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
3759
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
3724
3760
  }
3725
3761
  },
3726
3762
  forwarderSupported: {
@@ -3915,7 +3951,9 @@ var EarnChain;
3915
3951
  contracts: {
3916
3952
  v1: {
3917
3953
  wallet: GATEWAY_WALLET_EVM_MAINNET,
3918
- minter: GATEWAY_MINTER_EVM_MAINNET
3954
+ minter: GATEWAY_MINTER_EVM_MAINNET,
3955
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_MAINNET,
3956
+ genericExecutor: GENERIC_EXECUTOR_EVM_MAINNET
3919
3957
  }
3920
3958
  },
3921
3959
  forwarderSupported: {
@@ -3979,7 +4017,9 @@ var EarnChain;
3979
4017
  contracts: {
3980
4018
  v1: {
3981
4019
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3982
- minter: GATEWAY_MINTER_EVM_TESTNET
4020
+ minter: GATEWAY_MINTER_EVM_TESTNET,
4021
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
4022
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
3983
4023
  }
3984
4024
  },
3985
4025
  forwarderSupported: {
@@ -4292,6 +4332,7 @@ var EarnChain;
4292
4332
  v2: {
4293
4333
  type: 'split',
4294
4334
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4335
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4295
4336
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4296
4337
  confirmations: 1,
4297
4338
  fastConfirmations: 1
@@ -4337,6 +4378,7 @@ var EarnChain;
4337
4378
  v2: {
4338
4379
  type: 'split',
4339
4380
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4381
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4340
4382
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4341
4383
  confirmations: 1,
4342
4384
  fastConfirmations: 1
@@ -4382,6 +4424,7 @@ var EarnChain;
4382
4424
  v2: {
4383
4425
  type: 'split',
4384
4426
  tokenMessenger: '0x98706A006bc632Df31CAdFCBD43F38887ce2ca5c',
4427
+ tokenMessengerWithFees: '0x3Ac96675F9a3E6922713e041645D82f3561d3686',
4385
4428
  messageTransmitter: '0x5b61381Fc9e58E70EfC13a4A97516997019198ee',
4386
4429
  confirmations: 65,
4387
4430
  fastConfirmations: 1
@@ -4427,6 +4470,7 @@ var EarnChain;
4427
4470
  v2: {
4428
4471
  type: 'split',
4429
4472
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4473
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4430
4474
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4431
4475
  confirmations: 65,
4432
4476
  fastConfirmations: 1
@@ -4775,6 +4819,7 @@ var EarnChain;
4775
4819
  v2: {
4776
4820
  type: 'split',
4777
4821
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
4822
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
4778
4823
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
4779
4824
  confirmations: 1,
4780
4825
  fastConfirmations: 1
@@ -4822,6 +4867,7 @@ var EarnChain;
4822
4867
  v2: {
4823
4868
  type: 'split',
4824
4869
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
4870
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
4825
4871
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
4826
4872
  confirmations: 1,
4827
4873
  fastConfirmations: 1
@@ -5103,6 +5149,7 @@ var EarnChain;
5103
5149
  v2: {
5104
5150
  type: 'split',
5105
5151
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5152
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5106
5153
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5107
5154
  confirmations: 1,
5108
5155
  fastConfirmations: 1
@@ -5148,6 +5195,7 @@ var EarnChain;
5148
5195
  v2: {
5149
5196
  type: 'split',
5150
5197
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5198
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5151
5199
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5152
5200
  confirmations: 64,
5153
5201
  fastConfirmations: 1
@@ -5193,6 +5241,7 @@ var EarnChain;
5193
5241
  v2: {
5194
5242
  type: 'split',
5195
5243
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5244
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5196
5245
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5197
5246
  confirmations: 64,
5198
5247
  fastConfirmations: 1
@@ -5494,6 +5543,7 @@ var EarnChain;
5494
5543
  v2: {
5495
5544
  type: 'split',
5496
5545
  tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5546
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_MAINNET,
5497
5547
  messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5498
5548
  confirmations: 1,
5499
5549
  fastConfirmations: 1
@@ -5540,6 +5590,7 @@ var EarnChain;
5540
5590
  v2: {
5541
5591
  type: 'split',
5542
5592
  tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5593
+ tokenMessengerWithFees: TOKEN_MESSENGER_WITH_FEES_EVM_TESTNET,
5543
5594
  messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5544
5595
  confirmations: 1,
5545
5596
  fastConfirmations: 1
@@ -5847,7 +5898,9 @@ var EarnChain;
5847
5898
  contracts: {
5848
5899
  v1: {
5849
5900
  wallet: GATEWAY_WALLET_EVM_MAINNET,
5850
- minter: GATEWAY_MINTER_EVM_MAINNET
5901
+ minter: GATEWAY_MINTER_EVM_MAINNET,
5902
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_MAINNET,
5903
+ genericExecutor: GENERIC_EXECUTOR_EVM_MAINNET
5851
5904
  }
5852
5905
  },
5853
5906
  forwarderSupported: {
@@ -5912,7 +5965,9 @@ var EarnChain;
5912
5965
  contracts: {
5913
5966
  v1: {
5914
5967
  wallet: GATEWAY_WALLET_EVM_TESTNET,
5915
- minter: GATEWAY_MINTER_EVM_TESTNET
5968
+ minter: GATEWAY_MINTER_EVM_TESTNET,
5969
+ depositForHandler: DEPOSIT_FOR_HANDLER_EVM_TESTNET,
5970
+ genericExecutor: GENERIC_EXECUTOR_EVM_TESTNET
5916
5971
  }
5917
5972
  },
5918
5973
  forwarderSupported: {
@@ -6998,18 +7053,48 @@ var Chains = /*#__PURE__*/Object.freeze({
6998
7053
  return chain.cctp?.contracts.v2 !== undefined;
6999
7054
  }
7000
7055
 
7056
+ /**
7057
+ * Chains the Fee Service accepts as a SOURCE for source-paid ("receive-exact")
7058
+ * CCTP v2 fees. An explicit allowlist is required because the
7059
+ * `TokenMessengerWithFees` wrapper address is now shared with the fast-deposit
7060
+ * forwarder path, so wrapper presence no longer implies source-fee support.
7061
+ * Keep in sync with backend coverage.
7062
+ */ const SOURCE_FEE_SUPPORTED_ALLOWLIST = new Set([
7063
+ // Mainnet
7064
+ Blockchain.Ethereum,
7065
+ Blockchain.Base,
7066
+ Blockchain.Arbitrum,
7067
+ Blockchain.Unichain,
7068
+ Blockchain.Optimism,
7069
+ Blockchain.Codex,
7070
+ Blockchain.Ink,
7071
+ Blockchain.Plume,
7072
+ Blockchain.Linea,
7073
+ Blockchain.World_Chain,
7074
+ // Testnet counterparts
7075
+ Blockchain.Ethereum_Sepolia,
7076
+ Blockchain.Base_Sepolia,
7077
+ Blockchain.Arbitrum_Sepolia,
7078
+ Blockchain.Unichain_Sepolia,
7079
+ Blockchain.Optimism_Sepolia,
7080
+ Blockchain.Codex_Testnet,
7081
+ Blockchain.Ink_Testnet,
7082
+ Blockchain.Plume_Testnet,
7083
+ Blockchain.Linea_Sepolia,
7084
+ Blockchain.World_Chain_Sepolia
7085
+ ]);
7001
7086
  /**
7002
7087
  * Check whether a chain supports source-paid ("receive-exact") CCTP v2 fees.
7003
7088
  *
7004
- * A chain supports source-paid fees when its CCTP v2 configuration carries a
7005
- * deployed `TokenMessengerWithFees` wrapper address. Bridge Kit routes
7006
- * `feePayment: 'source'` transfers through this wrapper via
7007
- * `depositForBurnWithHookAndFees`, so a chain without the wrapper cannot be a
7008
- * source for receive-exact bridging.
7089
+ * A chain qualifies when it supports CCTP v2, carries a `TokenMessengerWithFees`
7090
+ * wrapper, and is in {@link SOURCE_FEE_SUPPORTED_ALLOWLIST}.
7009
7091
  *
7010
- * @param chain - The chain definition to check.
7011
- * @returns `true` when the chain has a `tokenMessengerWithFees` wrapper
7012
- * configured, `false` otherwise.
7092
+ * @param chain - The chain definition to check. A nullish or non-object value
7093
+ * returns `false` rather than throwing, since consumers may call from plain
7094
+ * JavaScript.
7095
+ * @returns `true` when the chain supports CCTP v2, carries a
7096
+ * `TokenMessengerWithFees` wrapper, and is on the source-fee allowlist;
7097
+ * `false` otherwise.
7013
7098
  *
7014
7099
  * @example
7015
7100
  * ```typescript
@@ -7019,11 +7104,18 @@ var Chains = /*#__PURE__*/Object.freeze({
7019
7104
  * hasSourceFeeSupport(Chains.Solana) // false
7020
7105
  * ```
7021
7106
  */ function hasSourceFeeSupport(chain) {
7107
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for nullish/non-object input from plain JS
7108
+ if (chain === null || typeof chain !== 'object') {
7109
+ return false;
7110
+ }
7022
7111
  if (!isCCTPV2Supported(chain)) {
7023
7112
  return false;
7024
7113
  }
7025
7114
  const wrapper = chain.cctp.contracts.v2.tokenMessengerWithFees;
7026
- return typeof wrapper === 'string' && wrapper.length > 0;
7115
+ if (typeof wrapper !== 'string' || wrapper.length === 0) {
7116
+ return false;
7117
+ }
7118
+ return SOURCE_FEE_SUPPORTED_ALLOWLIST.has(chain.chain);
7027
7119
  }
7028
7120
 
7029
7121
  /**
@@ -7076,6 +7168,73 @@ var Chains = /*#__PURE__*/Object.freeze({
7076
7168
  return typeof contractAddress === 'string' && contractAddress.trim().length > 0;
7077
7169
  }
7078
7170
 
7171
+ /**
7172
+ * Check whether a given chain supports Gateway protocol version 1.
7173
+ *
7174
+ * This type guard function examines a chain definition to determine if it has Gateway v1
7175
+ * contract configurations. It checks that the chain has a gateway object with a
7176
+ * `contracts.v1` entry present.
7177
+ *
7178
+ * @param chain - The chain definition to check for Gateway v1 support
7179
+ * @returns `true` if `chain.gateway?.contracts?.v1` is defined, `false` otherwise
7180
+ *
7181
+ * @example
7182
+ * ```typescript
7183
+ * import { isGatewayV1Supported, Base } from '@core/chains'
7184
+ *
7185
+ * if (isGatewayV1Supported(Base)) {
7186
+ * // TypeScript knows Base.gateway is defined here
7187
+ * console.log('Gateway domain:', Base.gateway.domain)
7188
+ * console.log('Wallet address:', Base.gateway.contracts.v1.wallet)
7189
+ * console.log('Minter address:', Base.gateway.contracts.v1.minter)
7190
+ * }
7191
+ * ```
7192
+ *
7193
+ * @example
7194
+ * ```typescript
7195
+ * // Usage in conditional flow
7196
+ * function getGatewayWalletAddress(chain: ChainDefinition): string | null {
7197
+ * if (isGatewayV1Supported(chain)) {
7198
+ * return chain.gateway.contracts.v1.wallet
7199
+ * }
7200
+ * return null
7201
+ * }
7202
+ * ```
7203
+ */ function isGatewayV1Supported(chain) {
7204
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS consumers may pass a gateway object without contracts
7205
+ return chain.gateway?.contracts?.v1 !== undefined;
7206
+ }
7207
+
7208
+ /**
7209
+ * Temporary allowlist of chains permitted to initiate Gateway fast deposits.
7210
+ * Only chains keyed here are eligible; all others are rejected. Using the
7211
+ * {@link Blockchain} enum keeps entries type-safe and catches typos at compile
7212
+ * time. Remove this allowlist once roll-out is complete.
7213
+ */ new Set([
7214
+ // Mainnet
7215
+ Blockchain.Ethereum,
7216
+ Blockchain.Base,
7217
+ Blockchain.Arbitrum,
7218
+ Blockchain.Unichain,
7219
+ Blockchain.Optimism,
7220
+ Blockchain.Codex,
7221
+ Blockchain.Ink,
7222
+ Blockchain.Plume,
7223
+ Blockchain.Linea,
7224
+ Blockchain.World_Chain,
7225
+ // Testnet counterparts
7226
+ Blockchain.Ethereum_Sepolia,
7227
+ Blockchain.Base_Sepolia,
7228
+ Blockchain.Arbitrum_Sepolia,
7229
+ Blockchain.Unichain_Sepolia,
7230
+ Blockchain.Optimism_Sepolia,
7231
+ Blockchain.Codex_Testnet,
7232
+ Blockchain.Ink_Testnet,
7233
+ Blockchain.Plume_Testnet,
7234
+ Blockchain.Linea_Sepolia,
7235
+ Blockchain.World_Chain_Sepolia
7236
+ ]);
7237
+
7079
7238
  /**
7080
7239
  * Zod schema for validating Gateway v1 contract addresses.
7081
7240
  *
@@ -7097,7 +7256,10 @@ var Chains = /*#__PURE__*/Object.freeze({
7097
7256
  }).min(1, 'Gateway minter address cannot be empty.'),
7098
7257
  depositForHandler: z.string({
7099
7258
  invalid_type_error: 'Gateway depositForHandler address must be a string.'
7100
- }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
7259
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional(),
7260
+ genericExecutor: z.string({
7261
+ invalid_type_error: 'Gateway genericExecutor address must be a string.'
7262
+ }).min(1, 'Gateway genericExecutor address cannot be empty.').optional()
7101
7263
  }).strict() // Reject any additional properties not defined in the schema
7102
7264
  ;
7103
7265
  /**
@@ -9968,6 +10130,9 @@ const swapTokenEnumSchema = z.enum([
9968
10130
  * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9969
10131
  * This prefix is right-padded to 24 bytes in the final hookData.
9970
10132
  */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
10133
+ /**
10134
+ * Maximum value of the 32-bit `version` field in a `cctp-forward` frame.
10135
+ */ const MAX_UINT32 = 0xffffffff;
9971
10136
  /**
9972
10137
  * CCTP forwarding version number.
9973
10138
  *
@@ -10044,6 +10209,76 @@ function buildForwardingHookData() {
10044
10209
  cachedHookDataHex = '0x' + Array.from(buffer).map((b)=>b.toString(16).padStart(2, '0')).join('');
10045
10210
  return cachedHookDataHex;
10046
10211
  }
10212
+ /**
10213
+ * Build a `cctp-forward` hookData frame with a versioned header and an appended
10214
+ * opaque payload.
10215
+ *
10216
+ * Produces the 32-byte `cctp-forward` header (24-byte ASCII magic + `uint32`
10217
+ * version + `uint32` `dataLength = 0`) followed by `payload` appended verbatim.
10218
+ * Unlike {@link buildForwardingHookData} — which emits only the fixed,
10219
+ * version-0 empty frame — this lets the caller set the frame `version` and
10220
+ * carry an inner payload such as a GenericExecutor blob.
10221
+ *
10222
+ * @remarks
10223
+ * The forwarder reads only the 32-byte header to decide that a hook is
10224
+ * forwardable, then strips it before the inner payload is consumed downstream
10225
+ * (e.g. the GenericExecutor `abi.decode`s the appended blob, never the frame).
10226
+ * `dataLength` stays `0` because the appended bytes are opaque to the forwarder
10227
+ * — it is not the payload's length.
10228
+ *
10229
+ * @param version - The `uint32` frame version (e.g. `1` for the GenericExecutor
10230
+ * FORWARD path). Must be an integer in `[0, 0xFFFFFFFF]`.
10231
+ * @param payload - A 0x-prefixed hex string appended after the header (e.g. the
10232
+ * bare GenericExecutor blob from `buildDepositForGenericExecutorPayload`).
10233
+ * @returns A 0x-prefixed hex string: the 32-byte frame followed by `payload`.
10234
+ * @throws {KitError} If `version` is out of `uint32` range or `payload` is not
10235
+ * a 0x-prefixed hex string (INPUT_VALIDATION_FAILED).
10236
+ *
10237
+ * @example
10238
+ * ```typescript
10239
+ * import {
10240
+ * buildDepositForGenericExecutorPayload,
10241
+ * buildForwardingHookDataWithPayload,
10242
+ * padAddressToBytes32,
10243
+ * } from '@core/utils'
10244
+ *
10245
+ * const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
10246
+ * dappId: 'gateway_deposit',
10247
+ * domainId: 26,
10248
+ * handler: '0xHandlerAddressOnDestinationChain',
10249
+ * params: [USDC_ARC, user, 0],
10250
+ * recoveryAddress: padAddressToBytes32(user),
10251
+ * })
10252
+ *
10253
+ * // Wrap for the prepaid Quote-API FORWARD path (frame version 1).
10254
+ * const hookData = buildForwardingHookDataWithPayload(1, geBlob)
10255
+ * ```
10256
+ */ function buildForwardingHookDataWithPayload(version, payload) {
10257
+ if (!Number.isInteger(version) || version < 0 || version > MAX_UINT32) {
10258
+ throw createValidationFailedError$1('version', version, 'Expected an integer in the uint32 range [0, 4294967295]');
10259
+ }
10260
+ if (!isHexString(payload)) {
10261
+ throw createValidationFailedError$1('payload', payload, 'Expected a 0x-prefixed hex string');
10262
+ }
10263
+ // `isHexString` accepts odd-length hex (e.g. '0xabc'); catch it here so it
10264
+ // surfaces as a KitError rather than ethers' raw "hex data is odd-length"
10265
+ // from `concat` below.
10266
+ if (payload.length % 2 !== 0) {
10267
+ throw createValidationFailedError$1('payload', payload, 'Expected an even-length (whole-byte) hex string');
10268
+ }
10269
+ // 32-byte header: 24-byte magic + uint32 version + uint32 dataLength (0).
10270
+ const frame = new Uint8Array(32);
10271
+ frame.set(new TextEncoder().encode(CCTP_FORWARD_MAGIC_PREFIX), 0);
10272
+ const view = new DataView(frame.buffer);
10273
+ view.setUint32(24, version, false) // big-endian
10274
+ ;
10275
+ view.setUint32(28, CCTP_FORWARD_PAYLOAD_LENGTH, false) // big-endian, 0
10276
+ ;
10277
+ return hexlify(concat([
10278
+ frame,
10279
+ payload
10280
+ ]));
10281
+ }
10047
10282
  /**
10048
10283
  * Build a `cctp-forward` hookData frame that instructs Circle's Orbit relayer to
10049
10284
  * create the recipient's Associated Token Account (ATA) before minting on Solana.
@@ -10121,6 +10356,43 @@ function buildForwardingHookData() {
10121
10356
  ]));
10122
10357
  }
10123
10358
 
10359
+ /**
10360
+ * `version` field of the GenericExecutor hookData, in both the
10361
+ * `circle-generic-executor` header (`uint32`) and the ABI tuple (`uint8`). The
10362
+ * executor reverts if it is not `1`.
10363
+ *
10364
+ * @see https://circlepay.atlassian.net/wiki/spaces/~712020cd79585b52ea4353b4720c277fbfcca6/pages/3049291839
10365
+ */ const GENERIC_EXECUTOR_HOOK_DATA_VERSION = 1;
10366
+ /**
10367
+ * ASCII magic that prefixes a GenericExecutor hookData blob.
10368
+ *
10369
+ * The executor auto-detects its payload by this string. It is left-aligned and
10370
+ * zero-padded to 24 bytes in the header, mirroring the `cctp-forward` frame
10371
+ * layout (magic + `uint32` version + `uint32` dataLength).
10372
+ */ const GENERIC_EXECUTOR_MAGIC_PREFIX = 'circle-generic-executor';
10373
+ /**
10374
+ * Prepend the 32-byte `circle-generic-executor` header to the ABI tuple.
10375
+ *
10376
+ * Header layout (mirrors the `cctp-forward` frame): 24-byte zero-padded ASCII
10377
+ * magic + `uint32` version + `uint32` dataLength. Unlike the `cctp-forward`
10378
+ * frame (which the forwarder strips and so carries `dataLength = 0`), this
10379
+ * header's dataLength is the byte length of the ABI tuple that follows, since
10380
+ * the executor consumes both.
10381
+ */ function prependGenericExecutorHeader(abiTuple) {
10382
+ const header = new Uint8Array(32);
10383
+ header.set(new TextEncoder().encode(GENERIC_EXECUTOR_MAGIC_PREFIX), 0);
10384
+ // Byte length of the ABI tuple that the header announces.
10385
+ const tupleByteLength = (abiTuple.length - 2) / 2;
10386
+ const view = new DataView(header.buffer);
10387
+ view.setUint32(24, GENERIC_EXECUTOR_HOOK_DATA_VERSION, false) // big-endian
10388
+ ;
10389
+ view.setUint32(28, tupleByteLength, false) // big-endian
10390
+ ;
10391
+ return hexlify(concat([
10392
+ header,
10393
+ abiTuple
10394
+ ]));
10395
+ }
10124
10396
  /**
10125
10397
  * Left-pad a 20-byte EVM address to a 32-byte (`bytes32`) hex string.
10126
10398
  *
@@ -10146,6 +10418,309 @@ function buildForwardingHookData() {
10146
10418
  // matches ABI-decoded output.
10147
10419
  return hexZeroPad(getAddress(address), 32).toLowerCase();
10148
10420
  }
10421
+ /**
10422
+ * Encode the bare GenericExecutor + DepositForHandler payload for a CCTP v2
10423
+ * fast-transfer deposit into a dApp.
10424
+ *
10425
+ * Builds the layers inner→outer:
10426
+ * 1. dApp calldata — the dApp function selector + ABI params, with each amount
10427
+ * slot left as the caller-supplied placeholder.
10428
+ * 2. handler calldata — `(depositContract, approvalTarget, depositCalldata, amountIndices)`
10429
+ * for `DepositForHandler`.
10430
+ * 3. ABI tuple — `(uint8 version, bytes32 recoveryAddress, address handler, bytes handlerCalldata)`.
10431
+ * No handler selector travels on the wire; the executor applies a fixed one.
10432
+ * 4. header — the 32-byte `circle-generic-executor` magic frame prepended to the
10433
+ * tuple, by which the executor auto-detects the payload.
10434
+ *
10435
+ * The returned `hookData` is the bare GenericExecutor blob (header ‖ tuple) — the
10436
+ * exact bytes the executor consumes. It carries no `cctp-forward` envelope. For
10437
+ * the prepaid Quote-API FORWARD path, wrap it with
10438
+ * {@link buildForwardingHookDataWithPayload}; the forwarder strips that envelope
10439
+ * before the executor reads the blob.
10440
+ *
10441
+ * @param options - See {@link BuildDepositForGenericExecutorPayloadParams}.
10442
+ * @returns The encoded {@link DepositForGenericExecutorPayload} layers.
10443
+ * @throws {KitError} If `options` is not an object, `dappId` is unknown,
10444
+ * `config.deployments` is not an array, neither `domainId` nor
10445
+ * `destinationChain` resolves a domain (or the two disagree), no deployment
10446
+ * exists for the resolved domain, the deposit contract cannot be resolved (a
10447
+ * built-in deployment supplied without a `destinationChain`), no `handler` is
10448
+ * supplied and it cannot be resolved from `destinationChain`,
10449
+ * `recoveryAddress`/`handler`/contract addresses are malformed,
10450
+ * `config.function` is not a valid Solidity function signature, `params`
10451
+ * length does not match the dApp signature, `params` values fail ABI encoding
10452
+ * (type mismatch), `config.dynamicAmountIndices` is not an array, or an amount
10453
+ * index is out of range (all INPUT_VALIDATION_FAILED).
10454
+ *
10455
+ * @example Encode a 1-click cross-chain Circle Gateway deposit
10456
+ * ```typescript
10457
+ * import { buildDepositForGenericExecutorPayload, padAddressToBytes32 } from '@core/utils'
10458
+ * import { ArcTestnet } from '@core/chains'
10459
+ *
10460
+ * const user = '0x75275Aff2D01699D922f045b69ed291311209738'
10461
+ * const usdcArc = '0x3600000000000000000000000000000000000000'
10462
+ * const { hookData } = buildDepositForGenericExecutorPayload({
10463
+ * dappId: 'gateway_deposit',
10464
+ * destinationChain: ArcTestnet, // resolves the GatewayWallet + DepositForHandler
10465
+ * // depositFor(address token, address depositor, uint256 value), amount idx [2]
10466
+ * params: [usdcArc, user, 0],
10467
+ * recoveryAddress: padAddressToBytes32(user),
10468
+ * })
10469
+ *
10470
+ * // Pass hookData straight into the CCTP v2 fast transfer.
10471
+ * console.log(hookData)
10472
+ * ```
10473
+ */ function buildDepositForGenericExecutorPayload(options) {
10474
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard for plain-JS callers
10475
+ if (options === null || typeof options !== 'object') {
10476
+ throw createValidationFailedError$1('options', options, 'Expected an options object');
10477
+ }
10478
+ const { dappId, params: dappParams } = options;
10479
+ const registry = options.config ?? DAPP_CONFIG;
10480
+ // Look up the dApp before resolving the deposit contract or handler, so an
10481
+ // unknown dApp reports the actionable "Unknown dApp" error rather than an
10482
+ // unresolved-handler error.
10483
+ const config = registry[dappId];
10484
+ if (config === undefined) {
10485
+ throw createValidationFailedError$1('dappId', dappId, `Unknown dApp. Known dApps: ${Object.keys(registry).join(', ')}`);
10486
+ }
10487
+ if (!Array.isArray(config.deployments)) {
10488
+ throw createValidationFailedError$1('deployments', config.deployments, `Expected an array of deployments for dApp '${dappId}'`);
10489
+ }
10490
+ // Array.isArray narrows to `any[]`; re-assert the concrete type.
10491
+ const deployments = config.deployments;
10492
+ // Destination CCTP domain: taken from `destinationChain` when supplied (the
10493
+ // chain is the (network, domain) key), else the explicit `domainId`.
10494
+ const domainId = resolveDomainId(options);
10495
+ const deployment = deployments.find((d)=>d.domainId === domainId);
10496
+ if (deployment === undefined) {
10497
+ throw createValidationFailedError$1('domainId', domainId, `No '${dappId}' deployment for domain ${String(domainId)}`);
10498
+ }
10499
+ if (!Array.isArray(dappParams)) {
10500
+ throw createValidationFailedError$1('params', dappParams, 'Expected an array of ABI-ordered parameters');
10501
+ }
10502
+ if (!isHexString(options.recoveryAddress, 32)) {
10503
+ throw createValidationFailedError$1('recoveryAddress', options.recoveryAddress, 'Expected a 0x-prefixed 32-byte (bytes32) hex string');
10504
+ }
10505
+ // Deposit contract: the deployment's own address, or — for the built-in
10506
+ // gateway_deposit deployment, which carries none — the destination chain's
10507
+ // GatewayWallet, so the address lives only in @core/chains.
10508
+ const resolvedDepositContract = resolveDepositContract(deployment, options, dappId);
10509
+ // Handler: an explicit `handler` always wins; otherwise resolve the
10510
+ // DepositForHandler from `destinationChain`. The chain is the (network, domain)
10511
+ // key, so a shared CCTP domain (Arc is 26 on both testnet and mainnet) can
10512
+ // never resolve the wrong network's handler and strand funds.
10513
+ const handler = resolveDepositForHandler(options, domainId);
10514
+ if (!isAddress(handler)) {
10515
+ throw createValidationFailedError$1('handler', handler, 'Expected a valid EVM address');
10516
+ }
10517
+ const depositContract = assertAddress(resolvedDepositContract, 'depositContract');
10518
+ const approvalTarget = assertAddress(deployment.approvalTarget ?? resolvedDepositContract, 'approvalTarget');
10519
+ // 1. dApp calldata: selector + ABI-encoded params (amount slots stay as placeholders).
10520
+ let dappInterface;
10521
+ try {
10522
+ dappInterface = new Interface([
10523
+ `function ${config.function}`
10524
+ ]);
10525
+ } catch {
10526
+ throw createValidationFailedError$1('function', config.function, 'Expected a valid Solidity function signature');
10527
+ }
10528
+ const rawFragment = dappInterface.fragments[0];
10529
+ /* v8 ignore start -- defensive: guards against unexpected library behavior */ if (rawFragment === undefined || rawFragment.type !== 'function') {
10530
+ throw createValidationFailedError$1('function', config.function, 'Expected a valid Solidity function signature');
10531
+ }
10532
+ /* v8 ignore stop */ const fragment = rawFragment;
10533
+ if (dappParams.length !== fragment.inputs.length) {
10534
+ throw createValidationFailedError$1('params', dappParams, `'${config.function}' expects ${String(fragment.inputs.length)} params, got ${String(dappParams.length)}`);
10535
+ }
10536
+ let depositCalldata;
10537
+ try {
10538
+ depositCalldata = dappInterface.encodeFunctionData(fragment, dappParams);
10539
+ } catch {
10540
+ throw createValidationFailedError$1('params', dappParams, 'ABI encoding failed — check that each param matches the expected Solidity type');
10541
+ }
10542
+ if (!Array.isArray(config.dynamicAmountIndices)) {
10543
+ throw createValidationFailedError$1('dynamicAmountIndices', config.dynamicAmountIndices, `Expected an array of amount indices for dApp '${dappId}'`);
10544
+ }
10545
+ // Array.isArray narrows to `any[]`; re-assert the concrete type.
10546
+ const dynamicAmountIndices = config.dynamicAmountIndices;
10547
+ // Amount byte offsets in depositCalldata: 4 (selector) + paramIndex * 32.
10548
+ const amountIndices = dynamicAmountIndices.map((paramIndex)=>{
10549
+ if (!Number.isInteger(paramIndex) || paramIndex < 0 || paramIndex >= fragment.inputs.length) {
10550
+ throw createValidationFailedError$1('dynamicAmountIndices', paramIndex, `Index out of range for '${config.function}' (${String(fragment.inputs.length)} params)`);
10551
+ }
10552
+ return BigInt(4 + paramIndex * 32);
10553
+ });
10554
+ // 2. Handler layer.
10555
+ const handlerCalldata = defaultAbiCoder.encode([
10556
+ 'address',
10557
+ 'address',
10558
+ 'bytes',
10559
+ 'uint256[]'
10560
+ ], [
10561
+ depositContract,
10562
+ approvalTarget,
10563
+ depositCalldata,
10564
+ amountIndices
10565
+ ]);
10566
+ // 3. Executor ABI tuple. No handler selector travels on the wire — the
10567
+ // executor applies a fixed selector internally.
10568
+ const executorTuple = defaultAbiCoder.encode([
10569
+ 'uint8',
10570
+ 'bytes32',
10571
+ 'address',
10572
+ 'bytes'
10573
+ ], [
10574
+ GENERIC_EXECUTOR_HOOK_DATA_VERSION,
10575
+ options.recoveryAddress,
10576
+ getAddress(handler),
10577
+ handlerCalldata
10578
+ ]);
10579
+ // 4. Prepend the circle-generic-executor magic header; this is the final
10580
+ // bare GE blob the executor consumes.
10581
+ const hookData = prependGenericExecutorHeader(executorTuple);
10582
+ return {
10583
+ hookData,
10584
+ handlerCalldata,
10585
+ depositCalldata,
10586
+ amountIndices,
10587
+ depositContract,
10588
+ approvalTarget
10589
+ };
10590
+ }
10591
+ /**
10592
+ * Config key of the built-in Circle Gateway deposit dApp. Its deposit contract
10593
+ * is the destination chain's GatewayWallet, resolved from `destinationChain`
10594
+ * (not a hardcoded address), so this is the only dApp whose deployment may omit
10595
+ * `depositContract`.
10596
+ */ const GATEWAY_DEPOSIT_DAPP_ID = 'gateway_deposit';
10597
+ /**
10598
+ * Built-in dApp registry. Adding a new `depositFor`-style dApp is a config entry
10599
+ * here (or via {@link BuildDepositForGenericExecutorPayloadParams.config}).
10600
+ *
10601
+ * @remarks
10602
+ * Only dApps with confirmed deployment addresses and active callers are included.
10603
+ * The built-in `gateway_deposit` entry omits `depositContract` — it is resolved
10604
+ * from the destination chain's GatewayWallet (`@core/chains`) rather than
10605
+ * duplicated here. Pass a custom registry via `config` for unlisted dApps.
10606
+ */ const DAPP_CONFIG = {
10607
+ // Circle Gateway: depositFor(address token, address depositor, uint256 value)
10608
+ [GATEWAY_DEPOSIT_DAPP_ID]: {
10609
+ function: 'depositFor(address,address,uint256)',
10610
+ dynamicAmountIndices: [
10611
+ 2
10612
+ ],
10613
+ deployments: [
10614
+ // Arc Testnet (CCTP domain 26). The deposit contract is the chain's
10615
+ // GatewayWallet, resolved from `destinationChain` (@core/chains) rather
10616
+ // than duplicated here.
10617
+ {
10618
+ domainId: 26
10619
+ }
10620
+ ]
10621
+ }
10622
+ };
10623
+ /**
10624
+ * Resolve the destination CCTP domain for an encode request.
10625
+ *
10626
+ * Prefers {@link BuildDepositForGenericExecutorPayloadParams.destinationChain}
10627
+ * (`chain.cctp.domain`) — the chain is the (network, domain) key. Falls back to
10628
+ * an explicit `domainId`. When both are supplied they must agree.
10629
+ *
10630
+ * @param options - The encode request.
10631
+ * @returns The destination CCTP domain.
10632
+ * @throws {KitError} If no domain is available, or `domainId` disagrees with
10633
+ * `destinationChain` (INPUT_VALIDATION_FAILED).
10634
+ * @internal
10635
+ */ function resolveDomainId(options) {
10636
+ const chain = options.destinationChain;
10637
+ if (chain !== undefined) {
10638
+ const chainDomain = chain.cctp?.domain;
10639
+ if (chainDomain !== undefined) {
10640
+ if (options.domainId !== undefined && options.domainId !== chainDomain) {
10641
+ throw createValidationFailedError$1('domainId', options.domainId, `does not match destinationChain '${chain.name}' CCTP domain ` + String(chainDomain));
10642
+ }
10643
+ return chainDomain;
10644
+ }
10645
+ }
10646
+ if (options.domainId !== undefined) {
10647
+ return options.domainId;
10648
+ }
10649
+ throw createValidationFailedError$1('domainId', options.domainId, "Provide 'domainId', or a 'destinationChain' with a CCTP domain");
10650
+ }
10651
+ /**
10652
+ * Resolve the deposit contract the handler calls.
10653
+ *
10654
+ * Uses the deployment's own `depositContract` when present. Only the built-in
10655
+ * {@link GATEWAY_DEPOSIT_DAPP_ID} may omit it: its deposit contract is the
10656
+ * destination chain's Gateway v1 wallet, resolved from
10657
+ * {@link BuildDepositForGenericExecutorPayloadParams.destinationChain} so the
10658
+ * address is owned once in `@core/chains`. Any other dApp that omits
10659
+ * `depositContract` is a config error and fails here rather than silently
10660
+ * targeting the GatewayWallet.
10661
+ *
10662
+ * @param deployment - The resolved dApp deployment.
10663
+ * @param options - The encode request.
10664
+ * @param dappId - The dApp key, checked against {@link GATEWAY_DEPOSIT_DAPP_ID}.
10665
+ * @returns The deposit contract address (unvalidated; the caller checks it).
10666
+ * @throws {KitError} If a non-`gateway_deposit` deployment omits
10667
+ * `depositContract`, or if `gateway_deposit` has no Gateway v1
10668
+ * `destinationChain` to resolve one (INPUT_VALIDATION_FAILED).
10669
+ * @internal
10670
+ */ function resolveDepositContract(deployment, options, dappId) {
10671
+ if (deployment.depositContract !== undefined) {
10672
+ return deployment.depositContract;
10673
+ }
10674
+ // Only gateway_deposit may omit its address (it targets the chain's
10675
+ // GatewayWallet). Any other addressless deployment is a config mistake and
10676
+ // must fail rather than silently resolve to the GatewayWallet.
10677
+ if (dappId !== GATEWAY_DEPOSIT_DAPP_ID) {
10678
+ throw createValidationFailedError$1('depositContract', dappId, `dApp '${dappId}' must declare a 'depositContract'; only the built-in ` + `'${GATEWAY_DEPOSIT_DAPP_ID}' resolves its address from the ` + "destination chain's GatewayWallet");
10679
+ }
10680
+ const chain = options.destinationChain;
10681
+ if (chain === undefined || !isGatewayV1Supported(chain)) {
10682
+ throw createValidationFailedError$1('depositContract', dappId, `'${GATEWAY_DEPOSIT_DAPP_ID}' needs a 'destinationChain' with Gateway v1 ` + 'support to resolve its GatewayWallet, or an explicit deployment address');
10683
+ }
10684
+ return chain.gateway.contracts.v1.wallet;
10685
+ }
10686
+ /**
10687
+ * Resolve the `DepositForHandler` address for an encode request.
10688
+ *
10689
+ * An explicit `options.handler` always wins. Otherwise the handler is resolved
10690
+ * from {@link BuildDepositForGenericExecutorPayloadParams.destinationChain}
10691
+ * (`chain.gateway.contracts.v1.depositForHandler`). The chain is the (network,
10692
+ * domain) key, so a CCTP domain shared across a chain's testnet and mainnet
10693
+ * cannot resolve the wrong network's handler; a missing chain or an unregistered
10694
+ * handler throws rather than guessing.
10695
+ *
10696
+ * @param options - The encode request.
10697
+ * @param domainId - The resolved destination domain, reported in the error.
10698
+ * @returns The resolved handler address (unvalidated; the caller checks it).
10699
+ * @throws {KitError} If `handler` is omitted and cannot be resolved
10700
+ * (INPUT_VALIDATION_FAILED).
10701
+ * @internal
10702
+ */ function resolveDepositForHandler(options, domainId) {
10703
+ if (options.handler !== undefined) {
10704
+ return options.handler;
10705
+ }
10706
+ const chain = options.destinationChain;
10707
+ if (chain === undefined) {
10708
+ throw createValidationFailedError$1('handler', domainId, "No 'handler' supplied; pass 'handler' explicitly, or a " + "'destinationChain' whose Gateway config registers a DepositForHandler");
10709
+ }
10710
+ const registered = isGatewayV1Supported(chain) ? chain.gateway.contracts.v1.depositForHandler : undefined;
10711
+ if (registered === undefined) {
10712
+ throw createValidationFailedError$1('handler', domainId, `No DepositForHandler registered for domain ${String(domainId)} on ` + `chain '${chain.name}'; pass 'handler' explicitly`);
10713
+ }
10714
+ return registered;
10715
+ }
10716
+ /**
10717
+ * Validate and checksum an EVM address, throwing a consistent validation error.
10718
+ */ function assertAddress(address, field) {
10719
+ if (!isAddress(address)) {
10720
+ throw createValidationFailedError$1(field, address, 'Expected a valid EVM address');
10721
+ }
10722
+ return getAddress(address);
10723
+ }
10149
10724
 
10150
10725
  /**
10151
10726
  * Configuration for {@link retryAsync}.
@@ -10235,7 +10810,7 @@ function resolveOptions(options) {
10235
10810
  * allowlisted {@link ClientLogPayload} fields (and the allowlisted
10236
10811
  * sub-fields of `errorDetails` / `clientContext`) are copied across.
10237
10812
  * A regressing upstream mapper — or a plain-JS caller that bypasses the
10238
- * type — therefore cannot exfiltrate stray properties (secrets, PII,
10813
+ * type — therefore cannot exfiltrate stray properties (secrets,
10239
10814
  * raw error stacks) through the analytics channel. Optional fields are
10240
10815
  * only included when present so the serialised shape matches the
10241
10816
  * server's strict schema.
@@ -10258,6 +10833,9 @@ function resolveOptions(options) {
10258
10833
  if (payload.destinationChain !== undefined) safe['destinationChain'] = payload.destinationChain;
10259
10834
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
10260
10835
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
10836
+ if (payload.amountIn !== undefined) safe['amountIn'] = payload.amountIn;
10837
+ if (payload.durationMs !== undefined) safe['durationMs'] = payload.durationMs;
10838
+ if (payload.sourceAddress !== undefined) safe['sourceAddress'] = payload.sourceAddress;
10261
10839
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
10262
10840
  if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
10263
10841
  if (payload.errorDetails !== undefined) {
@@ -10454,14 +11032,28 @@ function resolveOptions(options) {
10454
11032
  }
10455
11033
 
10456
11034
  /**
10457
- * Soft signal for the case where building or emitting a telemetry payload
10458
- * threw — for example, a buggy `TelemetryContextResolver`, a regression in
11035
+ * Emit a stable console warning when building or emitting a telemetry payload
11036
+ * throws — for example, a buggy `TelemetryContextResolver`, a regression in
10459
11037
  * `extractErrorDetails`, or a synchronous failure inside `emitAnalyticsLog`
10460
- * before it could swallow the error itself. Logged with a stable prefix so
10461
- * consumers can grep for it. We deliberately do not re-throw: the caller's
10462
- * original operation error must always win.
11038
+ * before it could swallow the error itself. Uses a stable prefix so the
11039
+ * drop is discoverable via grep. Never re-throws: the caller's original
11040
+ * operation error must always win.
10463
11041
  *
10464
11042
  * @internal
11043
+ *
11044
+ * @param eventType - The telemetry event type that was being emitted.
11045
+ * @param cause - The error or value that caused the drop.
11046
+ *
11047
+ * @example
11048
+ * ```typescript
11049
+ * import { warnTelemetryDrop } from '@core/utils'
11050
+ *
11051
+ * try {
11052
+ * void emitAnalyticsLog(payload)
11053
+ * } catch (err) {
11054
+ * warnTelemetryDrop('my_event', err)
11055
+ * }
11056
+ * ```
10465
11057
  */ function warnTelemetryDrop(eventType, cause) {
10466
11058
  try {
10467
11059
  // Pass `cause` as the second console.warn argument rather than
@@ -10501,6 +11093,9 @@ function resolveOptions(options) {
10501
11093
  ...context?.tokenOut != null && {
10502
11094
  tokenOut: context.tokenOut
10503
11095
  },
11096
+ ...context?.amountIn != null && {
11097
+ amountIn: context.amountIn
11098
+ },
10504
11099
  ...context?.txHash != null && {
10505
11100
  txHash: context.txHash
10506
11101
  },
@@ -10602,7 +11197,7 @@ function resolveOptions(options) {
10602
11197
  const stepEntry = stepEventMap.find(([name])=>name === failedStep?.name);
10603
11198
  // `failedStep.errorMessage` is intentionally **not** copied into the payload.
10604
11199
  // Provider messages are unbounded and frequently contain operator data
10605
- // (addresses, signatures, partial intent payloads, raw RPC responses). The
11200
+ // (signatures, partial intent payloads, raw RPC responses). The
10606
11201
  // step name plus the surrounding context fields already identify *which*
10607
11202
  // phase failed; the *why* is left to the corresponding step-level logs that
10608
11203
  // the provider emits separately. The thrown-error path (`extractErrorDetails`
@@ -10619,7 +11214,7 @@ function resolveOptions(options) {
10619
11214
  }
10620
11215
 
10621
11216
  var name$2 = "@circle-fin/bridge-kit";
10622
- var version$3 = "1.14.0";
11217
+ var version$3 = "1.14.1";
10623
11218
  var pkg$3 = {
10624
11219
  name: name$2,
10625
11220
  version: version$3};
@@ -12313,7 +12908,7 @@ var TransferSpeed;
12313
12908
  * })
12314
12909
  * ```
12315
12910
  */ function createLogger(options, stream) {
12316
- const { redact, ...pinoOptions } = {};
12911
+ const { redact, ...pinoOptions } = options ?? {};
12317
12912
  // Build redaction config
12318
12913
  const redactConfig = buildRedactConfig(redact);
12319
12914
  // Build final pino options, only include redact if defined
@@ -13667,6 +14262,8 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
13667
14262
  ;
13668
14263
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
13669
14264
  ;
14265
+ /** Gas units consumed by `depositForBurnWithHookAndFees` on an EVM chain (prepaid-FORWARD path). */ const DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM = 626_584n // avg of the last 10 txns
14266
+ ;
13670
14267
  // Gas FLOORS, not ceilings — kept separate from the fee-estimate averages
13671
14268
  // above. `executePreparedChainRequest` submits
13672
14269
  // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
@@ -15948,7 +16545,7 @@ const mockAttestationMessage = {
15948
16545
  return step;
15949
16546
  }
15950
16547
 
15951
- var version$2 = "1.12.0";
16548
+ var version$2 = "1.13.0";
15952
16549
  var pkg$2 = {
15953
16550
  version: version$2};
15954
16551
 
@@ -16654,6 +17251,9 @@ var pkg$2 = {
16654
17251
  }
16655
17252
  }
16656
17253
 
17254
+ const logger = createLogger({
17255
+ name: 'provider-cctp-v2'
17256
+ });
16657
17257
  function isPlainObject(value) {
16658
17258
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
16659
17259
  return false;
@@ -17016,6 +17616,116 @@ function assertCCTPV2Config(config) {
17016
17616
  return estimateResult;
17017
17617
  }
17018
17618
  /**
17619
+ * Estimate source-chain gas for a prepaid-FORWARD deposit burn via
17620
+ * `TokenMessengerWithFees.depositForBurnWithHookAndFees`.
17621
+ *
17622
+ * Builds a size-correct GenericExecutor hookData placeholder — using the
17623
+ * pre-validated `contracts.executor` and `contracts.depositForHandler` — so
17624
+ * the EVM calldata length matches production. Attempts a live
17625
+ * `eth_estimateGas` via the adapter's `cctp.v2.depositForBurnWithFees`
17626
+ * action and falls back to the static
17627
+ * {@link DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM} constant when the live
17628
+ * RPC call fails.
17629
+ *
17630
+ * Gateway eligibility is the caller's responsibility: validate the
17631
+ * destination chain with `resolveGatewayExecutorContracts` (UBK) before
17632
+ * calling this method.
17633
+ *
17634
+ * @param params - Estimation parameters including adapter, chains, amount,
17635
+ * the signed fee quote returned by the Quote API, and the pre-validated
17636
+ * Gateway executor contracts resolved by the caller.
17637
+ * @returns Promise resolving to the estimated (or fallback) gas cost.
17638
+ * @throws KitError `SERVICE_INTERNAL_ERROR` (FATAL) if `dstChain`'s
17639
+ * `usdcAddress` is `null`.
17640
+ *
17641
+ * @example
17642
+ * ```typescript
17643
+ * const contracts = resolveGatewayExecutorContracts(srcChain, dstChain)
17644
+ * const gasEstimate = await CCTPV2BridgingProvider.estimateDepositBurn({
17645
+ * adapter: evmAdapter,
17646
+ * srcChain: Ethereum,
17647
+ * dstChain: ArcTestnet,
17648
+ * amountMinorUnits: 100_000_000n,
17649
+ * refundAddress: '0xUserWallet',
17650
+ * signedQuote: quote.signedQuote,
17651
+ * feeToken: quote.feeToken,
17652
+ * feeTotalAmount: BigInt(quote.feeTotalAmount),
17653
+ * resolvedContext,
17654
+ * contracts,
17655
+ * })
17656
+ * ```
17657
+ */ static async estimateDepositBurn(params) {
17658
+ const { adapter, srcChain, dstChain, amountMinorUnits, refundAddress, signedQuote, feeToken, feeTotalAmount, resolvedContext, contracts } = params;
17659
+ const { executor, depositForHandler } = contracts;
17660
+ if (dstChain.usdcAddress === null) {
17661
+ throw new KitError({
17662
+ ...ServiceError.INTERNAL_ERROR,
17663
+ recoverability: 'FATAL',
17664
+ message: `Destination chain ${dstChain.name} has no USDC address configured`
17665
+ });
17666
+ }
17667
+ // Build a size-correct hookData for eth_estimateGas. The EVM uses calldata
17668
+ // length to compute gas, so the byte layout must match production even
17669
+ // though field values are not final.
17670
+ const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
17671
+ dappId: 'gateway_deposit',
17672
+ destinationChain: dstChain,
17673
+ handler: depositForHandler,
17674
+ params: [
17675
+ dstChain.usdcAddress,
17676
+ refundAddress,
17677
+ 0n
17678
+ ],
17679
+ recoveryAddress: padAddressToBytes32(refundAddress),
17680
+ // Override deployments only — spread the canonical function signature
17681
+ // and amount indices from DAPP_CONFIG so they stay in sync.
17682
+ config: {
17683
+ gateway_deposit: {
17684
+ ...DAPP_CONFIG.gateway_deposit,
17685
+ deployments: [
17686
+ {
17687
+ domainId: dstChain.cctp.domain
17688
+ }
17689
+ ]
17690
+ }
17691
+ }
17692
+ });
17693
+ const hookData = buildForwardingHookDataWithPayload(GENERIC_EXECUTOR_HOOK_DATA_VERSION, geBlob);
17694
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee
17695
+ // item, so the hookData must carry a `cctp-forward` frame — matches the
17696
+ // assertion in `prepareDepositForBurn`.
17697
+ assertForwardHookData(hookData);
17698
+ try {
17699
+ const prepared = await adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
17700
+ fromChain: srcChain,
17701
+ toChain: dstChain,
17702
+ amount: amountMinorUnits,
17703
+ mintRecipient: executor,
17704
+ destinationCaller: executor,
17705
+ claim: {
17706
+ signedQuote,
17707
+ refundAddress
17708
+ },
17709
+ feeToken,
17710
+ feeTotalAmount,
17711
+ hookData
17712
+ }, resolvedContext);
17713
+ return await prepared.estimate(undefined);
17714
+ } catch (err) {
17715
+ logger.debug('estimateDepositBurn: live eth_estimateGas failed, using static fallback', {
17716
+ err,
17717
+ chain: srcChain.name
17718
+ });
17719
+ try {
17720
+ return await adapter.calculateTransactionFee(DEPOSIT_FOR_BURN_WITH_FEES_GAS_ESTIMATE_EVM, undefined, srcChain);
17721
+ } catch (feeErr) {
17722
+ throw createRpcEndpointError(srcChain.name, {
17723
+ rawError: feeErr
17724
+ });
17725
+ }
17726
+ }
17727
+ }
17728
+ /**
17019
17729
  * Extracts OperationContext from bridge parameters for a given wallet context.
17020
17730
  *
17021
17731
  * This method extracts the chain and address information from the wallet context
@@ -17642,6 +18352,99 @@ function assertCCTPV2Config(config) {
17642
18352
  return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
17643
18353
  }
17644
18354
  /**
18355
+ * Prepare the source-chain `depositForBurnWithHookAndFees` call for the
18356
+ * GenericExecutor FORWARD path.
18357
+ *
18358
+ * Exposed as a public static method so the UBK fast-deposit flow can invoke
18359
+ * it directly without holding a provider instance. The byte layout mirrors
18360
+ * {@link CCTPV2BridgingProvider.estimateDepositBurn} so gas estimates and the
18361
+ * executed call agree.
18362
+ *
18363
+ * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18364
+ * @param params - Burn parameters including adapter, chains, deposit action,
18365
+ * amount, signer address, signed fee quote, and pre-resolved adapter context.
18366
+ * @returns The prepared `depositForBurnWithHookAndFees` burn transaction.
18367
+ * @throws {KitError} `UNSUPPORTED_ROUTE` when `dstChain` lacks a
18368
+ * GenericExecutor or DepositForHandler, or the `deposit.dappId` is unknown.
18369
+ *
18370
+ * @example
18371
+ * ```typescript
18372
+ * import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
18373
+ * import { Ethereum, ArcTestnet } from '@core/chains'
18374
+ *
18375
+ * const prepared = await CCTPV2BridgingProvider.prepareDepositForBurn({
18376
+ * adapter,
18377
+ * srcChain: Ethereum,
18378
+ * dstChain: ArcTestnet,
18379
+ * deposit: { dappId: 'gateway_deposit', params: [usdcAddress, recipient, 0n] },
18380
+ * amountMinorUnits: 100_000_000n,
18381
+ * refundAddress: '0xSender...',
18382
+ * signedQuote: quote.signedQuote,
18383
+ * feeToken: quote.feeToken,
18384
+ * feeTotalAmount: BigInt(quote.feeTotalAmount),
18385
+ * resolvedContext,
18386
+ * })
18387
+ * const txHash = await prepared.execute()
18388
+ * ```
18389
+ */ static async prepareDepositForBurn(params) {
18390
+ const { adapter, srcChain, dstChain, deposit, amountMinorUnits, refundAddress, signedQuote, feeToken, feeTotalAmount, resolvedContext } = params;
18391
+ // Resolve executor and depositForHandler from the destination chain's
18392
+ // gateway config. Throw an unsupported-route error if either is absent.
18393
+ const executor = dstChain.gateway?.contracts?.v1?.genericExecutor;
18394
+ const depositForHandler = dstChain.gateway?.contracts?.v1?.depositForHandler;
18395
+ if (!executor || !depositForHandler) {
18396
+ throw createUnsupportedRouteError(srcChain.name, dstChain.name);
18397
+ }
18398
+ // Resolve the canonical dApp config (function signature + amount indices)
18399
+ // for THIS deposit's `dappId`. Reject an unknown `dappId` rather than
18400
+ // encoding the wrong ABI selector, which would burn on the source but
18401
+ // revert in the executor call on the destination.
18402
+ const dappConfig = DAPP_CONFIG[deposit.dappId];
18403
+ if (dappConfig === undefined) {
18404
+ throw createUnsupportedRouteError(srcChain.name, dstChain.name);
18405
+ }
18406
+ // Build the bare GenericExecutor payload, then wrap it in the `cctp-forward`
18407
+ // frame required by the prepaid-FORWARD wrapper.
18408
+ const { hookData: geBlob } = buildDepositForGenericExecutorPayload({
18409
+ dappId: deposit.dappId,
18410
+ destinationChain: dstChain,
18411
+ handler: depositForHandler,
18412
+ params: deposit.params,
18413
+ recoveryAddress: padAddressToBytes32(refundAddress),
18414
+ config: {
18415
+ [deposit.dappId]: {
18416
+ ...dappConfig,
18417
+ deployments: [
18418
+ {
18419
+ domainId: dstChain.cctp.domain
18420
+ }
18421
+ ]
18422
+ }
18423
+ }
18424
+ });
18425
+ const hookData = buildForwardingHookDataWithPayload(GENERIC_EXECUTOR_HOOK_DATA_VERSION, geBlob);
18426
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee
18427
+ // item, so the hookData must carry a `cctp-forward` frame; otherwise the
18428
+ // wrapper reverts `ForwardFeeWithoutHook`.
18429
+ assertForwardHookData(hookData);
18430
+ // Source-chain burn: `mintRecipient` AND `destinationCaller` are both the
18431
+ // executor; fees are prepaid against the signed quote.
18432
+ return adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18433
+ fromChain: srcChain,
18434
+ toChain: dstChain,
18435
+ amount: amountMinorUnits,
18436
+ mintRecipient: executor,
18437
+ destinationCaller: executor,
18438
+ hookData,
18439
+ claim: {
18440
+ signedQuote,
18441
+ refundAddress
18442
+ },
18443
+ feeToken,
18444
+ feeTotalAmount
18445
+ }, resolvedContext);
18446
+ }
18447
+ /**
17645
18448
  * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
17646
18449
  *
17647
18450
  * Build the source-chain `depositForBurnWithHookAndFees` call. Fees are
@@ -17906,9 +18709,13 @@ function assertCCTPV2Config(config) {
17906
18709
 
17907
18710
  /**
17908
18711
  * Base URL for Circle's Quote API (hosted in Iris) on mainnet/production.
18712
+ *
18713
+ * @internal
17909
18714
  */ const IRIS_API_BASE_URL = 'https://iris-api.circle.com';
17910
18715
  /**
17911
18716
  * Base URL for Circle's Quote API (hosted in Iris) on testnet/sandbox.
18717
+ *
18718
+ * @internal
17912
18719
  */ const IRIS_API_SANDBOX_BASE_URL = 'https://iris-api-sandbox.circle.com';
17913
18720
  /**
17914
18721
  * Native fee-token sentinel (the zero address).
@@ -17916,19 +18723,25 @@ function assertCCTPV2Config(config) {
17916
18723
  * When `feeToken` is the zero address the quote prices fees in the source
17917
18724
  * chain's native gas token (paid as `msg.value` on-chain). Pass a USDC token
17918
18725
  * address instead to denominate fees in USDC.
18726
+ *
18727
+ * @internal
17919
18728
  */ const NATIVE_FEE_TOKEN = '0x0000000000000000000000000000000000000000';
17920
18729
  /**
17921
18730
  * API path prefix for the CCTP v2 USDC burn quote endpoint.
17922
18731
  *
17923
18732
  * The full path is `${QUOTE_BURN_USDC_PATH}/{sourceDomain}/{destinationDomain}`;
17924
18733
  * `usdc` is a fixed literal, not a token parameter.
18734
+ *
18735
+ * @internal
17925
18736
  */ const QUOTE_BURN_USDC_PATH = '/v2/quote/burn/usdc';
17926
18737
  /**
17927
18738
  * API path prefix for the CCTP v2 USDC quote validate endpoint.
17928
18739
  *
17929
18740
  * The full path is `${QUOTE_VALIDATE_USDC_PATH}/{sourceDomain}`; accepts a
17930
- * `POST { abiSignature, args }` body and returns whether the signed quote is
17931
- * currently claimable together with its authoritative expiry status.
18741
+ * `POST { abiSignature, args }` body and returns `claimable`, `failedChecks`,
18742
+ * and the decoded `expiry`, `feeToken`, and `feeTotalAmount`.
18743
+ *
18744
+ * @internal
17932
18745
  */ const QUOTE_VALIDATE_USDC_PATH = '/v2/quote/validate/usdc';
17933
18746
  /**
17934
18747
  * Default polling configuration for Quote API calls.
@@ -17945,13 +18758,19 @@ function assertCCTPV2Config(config) {
17945
18758
  * `Content-Type: application/json` and adds `User-Agent` in Node. Browser
17946
18759
  * requests omit a user-agent header to avoid a CORS preflight, so duplicating
17947
18760
  * either header here would be dead configuration.
18761
+ *
18762
+ * @internal
17948
18763
  */ const FEE_QUOTE_DEFAULT_CONFIG = {
17949
18764
  timeout: 15_000,
17950
18765
  maxRetries: 1,
17951
18766
  retryDelay: 200
17952
18767
  };
17953
18768
 
17954
- /** 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
18769
+ /**
18770
+ * Decimal string in token minor units, constrained to be strictly positive.
18771
+ *
18772
+ * @internal
18773
+ */ 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
17955
18774
  // regex check above fails ("dirty"), so guard BigInt() against throwing on a
17956
18775
  // non-numeric value before comparing.
17957
18776
  .refine((value)=>/^\d+$/.test(value) && BigInt(value) > 0n, 'must be greater than zero');
@@ -17986,11 +18805,19 @@ const forwardRequestSchema = z.object({
17986
18805
  const preFinalityRequestSchema = z.object({
17987
18806
  type: z.literal('PRE_FINALITY')
17988
18807
  }).strict();
17989
- /** A single quote request item (`FORWARD` or `PRE_FINALITY`). */ const feeQuoteRequestSchema = z.discriminatedUnion('type', [
18808
+ /**
18809
+ * A single quote request item (`FORWARD` or `PRE_FINALITY`).
18810
+ *
18811
+ * @internal
18812
+ */ const feeQuoteRequestSchema = z.discriminatedUnion('type', [
17990
18813
  forwardRequestSchema,
17991
18814
  preFinalityRequestSchema
17992
18815
  ]);
17993
- /** 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');
18816
+ /**
18817
+ * A non-empty list of quote request items with unique types.
18818
+ *
18819
+ * @internal
18820
+ */ 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');
17994
18821
  /**
17995
18822
  * A structured `Partial<ApiPollingConfig>` polling override.
17996
18823
  *
@@ -18018,6 +18845,8 @@ const preFinalityRequestSchema = z.object({
18018
18845
  * it `undefined`, which is falsy and silently selects the production base URL.
18019
18846
  * (`buildFeeQuoteUrl` independently re-validates the domains for standalone
18020
18847
  * callers.)
18848
+ *
18849
+ * @internal
18021
18850
  */ const fetchFeeQuoteInputSchema = z.object({
18022
18851
  sourceDomain: z.number().int().nonnegative(),
18023
18852
  destinationDomain: z.number().int().nonnegative(),
@@ -18053,7 +18882,11 @@ const metadataSchema = z.object({
18053
18882
  blockEstimatedAt: z.number().int().nonnegative().optional()
18054
18883
  }).passthrough()
18055
18884
  ]);
18056
- /** Schema for a signed fee quote returned by the Quote API. */ const signedFeeQuoteSchema = z.object({
18885
+ /**
18886
+ * Schema for a signed fee quote returned by the Quote API.
18887
+ *
18888
+ * @internal
18889
+ */ const signedFeeQuoteSchema = z.object({
18057
18890
  // The runtime YAML spec maps signedQuote to a looser `hex` (which allows
18058
18891
  // an empty `0x`); we keep the stricter non-empty form. Do not relax
18059
18892
  // without a reason.
@@ -18082,10 +18915,16 @@ const metadataSchema = z.object({
18082
18915
  * console.log(payload.feeTotalAmount)
18083
18916
  * }
18084
18917
  * ```
18918
+ *
18919
+ * @internal
18085
18920
  */ function isSignedFeeQuote(value) {
18086
18921
  return signedFeeQuoteSchema.safeParse(value).success;
18087
18922
  }
18088
- /** Validate input to {@link validateQuote}. */ const validateQuoteInputSchema = z.object({
18923
+ /**
18924
+ * Validates input to {@link validateQuote}.
18925
+ *
18926
+ * @internal
18927
+ */ const validateQuoteInputSchema = z.object({
18089
18928
  sourceDomain: z.number().int().nonnegative(),
18090
18929
  abiSignature: z.string().min(1),
18091
18930
  args: z.array(z.union([
@@ -18120,11 +18959,13 @@ const validateQuoteItemSchema = z.object({
18120
18959
  computedArgsHash: bytes32Schema.optional()
18121
18960
  }).passthrough();
18122
18961
  /**
18123
- * Schema for a response from the Quote API validation endpoint.
18962
+ * Schema for a validate-quote result returned by the Iris `/validate/usdc/:sourceDomain` endpoint.
18124
18963
  *
18125
18964
  * The endpoint takes the source domain as a URL path parameter and does not
18126
18965
  * return it in the response body, so `sourceDomain` is intentionally not part
18127
18966
  * of this schema.
18967
+ *
18968
+ * @internal
18128
18969
  */ const validateQuoteResultSchema = z.object({
18129
18970
  signedQuote: nonEmptyHexSchema,
18130
18971
  expiry: quoteExpiryStatusSchema,
@@ -18138,20 +18979,22 @@ const validateQuoteItemSchema = z.object({
18138
18979
  items: z.array(validateQuoteItemSchema)
18139
18980
  }).passthrough();
18140
18981
  /**
18141
- * Validate a Quote API validation response.
18982
+ * Validate that an unknown value is a validate-quote result.
18142
18983
  *
18143
- * @param value - The unknown response value.
18144
- * @returns `true` when the value has the expected validation response shape.
18984
+ * @param value - The unknown value to validate.
18985
+ * @returns `true` when the value matches the validate-quote response shape.
18145
18986
  *
18146
18987
  * @example
18147
18988
  * ```typescript
18148
18989
  * import { isValidateQuoteResult } from '@circle-fin/provider-fee-v1'
18149
18990
  *
18150
- * declare const response: unknown
18151
- * if (isValidateQuoteResult(response)) {
18152
- * console.log(response.claimable)
18991
+ * declare const payload: unknown
18992
+ * if (isValidateQuoteResult(payload)) {
18993
+ * console.log(payload.claimable, payload.failedChecks)
18153
18994
  * }
18154
18995
  * ```
18996
+ *
18997
+ * @internal
18155
18998
  */ function isValidateQuoteResult(value) {
18156
18999
  return validateQuoteResultSchema.safeParse(value).success;
18157
19000
  }
@@ -18200,6 +19043,8 @@ const validateQuoteItemSchema = z.object({
18200
19043
  * })
18201
19044
  * // => 'https://iris-api.circle.com/v2/quote/burn/usdc/3/26'
18202
19045
  * ```
19046
+ *
19047
+ * @internal
18203
19048
  */ function buildFeeQuoteUrl(params) {
18204
19049
  const { sourceDomain, destinationDomain, isTestnet, baseUrl } = params;
18205
19050
  assertDomain(sourceDomain, 'sourceDomain');
@@ -18326,6 +19171,8 @@ const OPERATION$1 = 'getFeeQuote';
18326
19171
  * })
18327
19172
  * console.log(quote.feeTotalAmount, quote.expiry)
18328
19173
  * ```
19174
+ *
19175
+ * @internal
18329
19176
  */ async function fetchFeeQuote(params) {
18330
19177
  const { sourceDomain, destinationDomain, amount, requests, feeToken, isTestnet, baseUrl, config } = params;
18331
19178
  const parsed = fetchFeeQuoteInputSchema.safeParse({
@@ -18400,7 +19247,13 @@ const OPERATION$1 = 'getFeeQuote';
18400
19247
  const SERVICE = 'Quote API';
18401
19248
  const OPERATION = 'validateQuote';
18402
19249
  /**
18403
- * Validate a signed quote against a complete source-chain contract call.
19250
+ * Validate a signed fee quote against a full on-chain call via Circle's Iris
19251
+ * `/v2/quote/validate/usdc/:sourceDomain` endpoint.
19252
+ *
19253
+ * POSTs the ABI function signature and the encoded call arguments to Iris,
19254
+ * which verifies the signature, checks expiry, confirms the argsHash committed
19255
+ * in the quote matches the submitted args, and returns `claimable` plus the
19256
+ * decoded `expiry`, `feeToken`, and `feeTotalAmount`.
18404
19257
  *
18405
19258
  * @param params - The source domain, ABI signature, call arguments, and environment.
18406
19259
  * @returns The claimability, binding checks, and authoritative expiry status.
@@ -18427,7 +19280,12 @@ const OPERATION = 'validateQuote';
18427
19280
  * isTestnet: false,
18428
19281
  * })
18429
19282
  * console.log(result.claimable, result.expiry.secondsRemaining)
19283
+ * if (!result.claimable) {
19284
+ * console.error('Quote rejected:', result.failedChecks)
19285
+ * }
18430
19286
  * ```
19287
+ *
19288
+ * @internal
18431
19289
  */ async function validateQuote(params) {
18432
19290
  const parsed = validateQuoteInputSchema.safeParse(params);
18433
19291
  if (!parsed.success) {
@@ -18490,9 +19348,14 @@ function isQuoteNearEstimatedExpiry(quote) {
18490
19348
  }
18491
19349
  function assertSourceFeeRoute(params) {
18492
19350
  const { source, destination, config } = params;
19351
+ // Keep these checks explicit so a successful assertion guarantees the CCTP
19352
+ // v2 narrowing; hasSourceFeeSupport returns a plain boolean.
18493
19353
  if (source.chain.type !== 'evm' || destination.chain.type !== 'evm' || !isCCTPV2Supported(source.chain) || !isCCTPV2Supported(destination.chain)) {
18494
19354
  throw createUnsupportedRouteError(source.chain.name, destination.chain.name);
18495
19355
  }
19356
+ if (!hasSourceFeeSupport(source.chain)) {
19357
+ throw createValidationFailedError$1('config.feePayment', 'source', `Source-paid ("receive-exact") fees are not supported from ${source.chain.name}. ` + 'Use the default destination-paid fees for this source chain');
19358
+ }
18496
19359
  const useForwarder = destination.useForwarder;
18497
19360
  if (useForwarder !== true) {
18498
19361
  throw createValidationFailedError$1('to.useForwarder', useForwarder, "feePayment: 'source' requires useForwarder: true");
@@ -19580,7 +20443,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
19580
20443
  };
19581
20444
 
19582
20445
  var name$1 = "@circle-fin/swap-kit";
19583
- var version$1 = "1.6.0";
20446
+ var version$1 = "1.6.1";
19584
20447
  var pkg$1 = {
19585
20448
  name: name$1,
19586
20449
  version: version$1};
@@ -21602,7 +22465,7 @@ new Set(Object.values(Blockchain));
21602
22465
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
21603
22466
 
21604
22467
  var name = "@circle-fin/earn-kit";
21605
- var version = "1.6.0";
22468
+ var version = "1.6.1";
21606
22469
  var pkg = {
21607
22470
  name: name,
21608
22471
  version: version};