@circle-fin/app-kit 1.9.0 → 1.10.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
@@ -20,9 +20,9 @@ import { z } from 'zod';
20
20
  import pino from 'pino';
21
21
  import { parseUnits as parseUnits$1, formatUnits as formatUnits$1 } from '@ethersproject/units';
22
22
  import { hexlify, hexZeroPad } from '@ethersproject/bytes';
23
+ import '@ethersproject/abi';
23
24
  import { getAddress } from '@ethersproject/address';
24
25
  import bs58 from 'bs58';
25
- import '@ethersproject/abi';
26
26
  import { PublicKey } from '@solana/web3.js';
27
27
  import 'bn.js';
28
28
  import '@coral-xyz/anchor';
@@ -61,6 +61,7 @@ import { keccak256 } from '@ethersproject/keccak256';
61
61
  ...params,
62
62
  actions: {
63
63
  bridge: {},
64
+ earn: {},
64
65
  ...params.actions
65
66
  }
66
67
  };
@@ -3544,14 +3545,14 @@ class KitError extends Error {
3544
3545
  }
3545
3546
 
3546
3547
  /**
3547
- * Standardized error definitions for Earn/Zenith operations.
3548
+ * Standardized error definitions for Earn operations.
3548
3549
  *
3549
3550
  * These error codes provide fine-grained categorization of failures
3550
- * from the Zenith earn service, enabling SDK consumers to distinguish
3551
+ * from the Earn service, enabling SDK consumers to distinguish
3551
3552
  * between input errors (fix your request) and service errors (retry later).
3552
3553
  *
3553
3554
  * Error code ranges:
3554
- * - 1100-1105: INPUT errors — invalid inputs, unsupported configurations
3555
+ * - 1100-1106: INPUT errors — invalid, unsupported, or stale request state
3555
3556
  * - 8100-8105: SERVICE errors — retryable backend/provider failures
3556
3557
  *
3557
3558
  * @example
@@ -3604,6 +3605,14 @@ class KitError extends Error {
3604
3605
  name: 'EARN_UNSUPPORTED_BRIDGE_ROUTE',
3605
3606
  type: 'INPUT'
3606
3607
  },
3608
+ /**
3609
+ * The bridge quote expired. This is an INPUT error because the prepared
3610
+ * request is stale and must be replaced instead of retried.
3611
+ */ BRIDGE_QUOTE_EXPIRED: {
3612
+ code: 1106,
3613
+ name: 'EARN_BRIDGE_QUOTE_EXPIRED',
3614
+ type: 'INPUT'
3615
+ },
3607
3616
  /** The proxy signing call failed — retryable. */ SIGNING_FAILED: {
3608
3617
  code: 8100,
3609
3618
  name: 'EARN_SIGNING_FAILED',
@@ -3663,6 +3672,9 @@ function getOptionalString(value) {
3663
3672
  * internal-error, vault-refresh-busy, off-chain-paused, position-PnL-pending,
3664
3673
  * bridge failures/status lookup failures
3665
3674
  *
3675
+ * Quote expiry is INPUT/FATAL because callers must start a fresh bridge prepare
3676
+ * flow rather than retry the stale prepared bundle.
3677
+ *
3666
3678
  * Unrecognized codes fall through to `parseApiError` for HTTP-status-based
3667
3679
  * handling.
3668
3680
  *
@@ -3896,6 +3908,13 @@ function getOptionalString(value) {
3896
3908
  errorDef: EarnError.PROVIDER_ERROR,
3897
3909
  recoverability: 'FATAL'
3898
3910
  }
3911
+ ],
3912
+ [
3913
+ 380506,
3914
+ {
3915
+ errorDef: EarnError.BRIDGE_QUOTE_EXPIRED,
3916
+ recoverability: 'FATAL'
3917
+ }
3899
3918
  ]
3900
3919
  ]);
3901
3920
  /**
@@ -4640,7 +4659,10 @@ var EarnChain;
4640
4659
  contracts: {
4641
4660
  v1: {
4642
4661
  wallet: GATEWAY_WALLET_EVM_TESTNET,
4643
- minter: GATEWAY_MINTER_EVM_TESTNET
4662
+ minter: GATEWAY_MINTER_EVM_TESTNET,
4663
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
4664
+ // deposit into the GatewayWallet above.
4665
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
4644
4666
  }
4645
4667
  },
4646
4668
  forwarderSupported: {
@@ -7795,7 +7817,10 @@ var Chains = /*#__PURE__*/Object.freeze({
7795
7817
  minter: z.string({
7796
7818
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
7797
7819
  invalid_type_error: 'Gateway minter address must be a string.'
7798
- }).min(1, 'Gateway minter address cannot be empty.')
7820
+ }).min(1, 'Gateway minter address cannot be empty.'),
7821
+ depositForHandler: z.string({
7822
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
7823
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
7799
7824
  }).strict() // Reject any additional properties not defined in the schema
7800
7825
  ;
7801
7826
  /**
@@ -8335,21 +8360,31 @@ const swapTokenEnumSchema = z.enum([
8335
8360
  * returning the appropriate address based on the requested contract type.
8336
8361
  *
8337
8362
  * @param chain - The chain definition to resolve the contract address for
8338
- * @param contractType - The type of contract address to resolve ('tokenMessenger' or 'messageTransmitter')
8363
+ * @param contractType - The type of contract address to resolve ('tokenMessenger', 'messageTransmitter', or 'tokenMessengerWithFees')
8339
8364
  * @returns The contract address for the specified contract type
8340
8365
  * @throws Error when chain does not support CCTP v2 or has unsupported contract configuration
8366
+ * @throws Error when 'tokenMessengerWithFees' is requested but not configured on the chain
8341
8367
  */ const resolveCCTPV2ContractAddress = (chain, contractType)=>{
8342
8368
  // Handle custom bridge contract for tokenMessenger (burn transaction)
8343
- if (hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
8369
+ if (contractType === 'tokenMessenger' && hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
8344
8370
  return chain.kitContracts.bridge;
8345
8371
  }
8346
8372
  // At this point we know CCTP v2 is supported, so contracts exist
8347
8373
  const cctpConfig = chain.cctp;
8348
8374
  const contracts = cctpConfig.contracts.v2;
8375
+ // The `TokenMessengerWithFees` wrapper (prepaid FORWARD path) is an optional
8376
+ // deployment carried alongside both split and merged configurations.
8377
+ if (contractType === 'tokenMessengerWithFees') {
8378
+ const wrapper = contracts.tokenMessengerWithFees;
8379
+ if (wrapper === undefined || wrapper === '') {
8380
+ throw new Error(`TokenMessengerWithFees is not configured on chain ${chain.name}. The prepaid FORWARD path is unavailable on this chain.`);
8381
+ }
8382
+ return wrapper;
8383
+ }
8349
8384
  // Handle different contract types with explicit type checking
8350
8385
  switch(contracts.type){
8351
8386
  case 'split':
8352
- return contracts.tokenMessenger ;
8387
+ return contractType === 'tokenMessenger' ? contracts.tokenMessenger : contracts.messageTransmitter;
8353
8388
  case 'merged':
8354
8389
  return contracts.contract;
8355
8390
  default:
@@ -11623,7 +11658,7 @@ function resolveOptions(options) {
11623
11658
  }
11624
11659
 
11625
11660
  var name$4 = "@circle-fin/bridge-kit";
11626
- var version$5 = "1.12.0";
11661
+ var version$5 = "1.12.1";
11627
11662
  var pkg$5 = {
11628
11663
  name: name$4,
11629
11664
  version: version$5};
@@ -14166,6 +14201,144 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14166
14201
  return false;
14167
14202
  };
14168
14203
 
14204
+ /**
14205
+ * The zero address, denoting a native-currency fee in a signed quote.
14206
+ */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
14207
+ /**
14208
+ * Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
14209
+ *
14210
+ * Determines the native `msg.value` and the ERC-20 approvals required, honouring
14211
+ * the quote's `feeToken`:
14212
+ * - Native fee (`feeToken` is the zero address): attach exactly `feeTotalAmount`
14213
+ * as `msg.value`; approve only the burn amount.
14214
+ * - ERC-20 fee equal to the burn token (both USDC — the `isBurnTokenFee` case):
14215
+ * approve a single combined `amount + feeTotalAmount` and skip the redundant
14216
+ * second approval.
14217
+ * - ERC-20 fee different from the burn token: approve the burn amount and the fee
14218
+ * amount separately.
14219
+ *
14220
+ * This encodes only balance/allowance intent; it does not fetch balances. The
14221
+ * caller is responsible for a balance preflight against the fresh quote.
14222
+ *
14223
+ * @param params - The fee token, burn token, burn amount, and total fee amount.
14224
+ * @returns The resolved fee payment plan.
14225
+ * @throws KitError if `amount` or `feeTotalAmount` is negative.
14226
+ *
14227
+ * @example
14228
+ * ```typescript
14229
+ * // Native fee
14230
+ * resolveFeePayment({
14231
+ * feeToken: '0x0000000000000000000000000000000000000000',
14232
+ * burnToken: '0xUSDC...',
14233
+ * amount: 1_000_000n,
14234
+ * feeTotalAmount: 3_500_000n,
14235
+ * })
14236
+ * // → { isNativeFee: true, isBurnTokenFee: false, nativeValue: 3_500_000n,
14237
+ * // approvals: [{ token: '0xUSDC...', amount: 1_000_000n }] }
14238
+ * ```
14239
+ */ const resolveFeePayment = (params)=>{
14240
+ const { feeToken, burnToken, amount, feeTotalAmount } = params;
14241
+ if (typeof amount !== 'bigint' || amount < 0n) {
14242
+ throw createValidationFailedError$1('amount', amount, 'Must be a non-negative bigint');
14243
+ }
14244
+ if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
14245
+ throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
14246
+ }
14247
+ const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
14248
+ const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
14249
+ if (isNativeFee) {
14250
+ return {
14251
+ isNativeFee: true,
14252
+ isBurnTokenFee: false,
14253
+ nativeValue: feeTotalAmount,
14254
+ approvals: [
14255
+ {
14256
+ token: burnToken,
14257
+ amount
14258
+ }
14259
+ ]
14260
+ };
14261
+ }
14262
+ if (isBurnTokenFee) {
14263
+ // Fee and burn draw on the same token — a single combined approval covers
14264
+ // both; the redundant second approval is skipped.
14265
+ return {
14266
+ isNativeFee: false,
14267
+ isBurnTokenFee: true,
14268
+ nativeValue: 0n,
14269
+ approvals: [
14270
+ {
14271
+ token: burnToken,
14272
+ amount: amount + feeTotalAmount
14273
+ }
14274
+ ]
14275
+ };
14276
+ }
14277
+ return {
14278
+ isNativeFee: false,
14279
+ isBurnTokenFee: false,
14280
+ nativeValue: 0n,
14281
+ approvals: [
14282
+ {
14283
+ token: burnToken,
14284
+ amount
14285
+ },
14286
+ {
14287
+ token: feeToken,
14288
+ amount: feeTotalAmount
14289
+ }
14290
+ ]
14291
+ };
14292
+ };
14293
+
14294
+ /**
14295
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
14296
+ * hookData must start with.
14297
+ */ const CCTP_FORWARD_MAGIC_HEX = Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
14298
+ /**
14299
+ * Determine whether a hookData blob begins with the `cctp-forward` envelope.
14300
+ *
14301
+ * The prepaid FORWARD path requires the GenericExecutor blob to be wrapped in a
14302
+ * `cctp-forward` frame; without it the wrapper reverts `ForwardFeeWithoutHook`.
14303
+ *
14304
+ * @param hookData - The 0x-prefixed hookData hex string.
14305
+ * @returns True when the hookData starts with the `cctp-forward` magic.
14306
+ *
14307
+ * @example
14308
+ * ```typescript
14309
+ * hasForwardHook('0x636374702d666f7277617264...') // true
14310
+ * hasForwardHook('0xdeadbeef') // false
14311
+ * ```
14312
+ */ const hasForwardHook = (hookData)=>{
14313
+ if (typeof hookData !== 'string') {
14314
+ return false;
14315
+ }
14316
+ const normalized = (hookData.startsWith('0x') ? hookData.slice(2) : hookData).toLowerCase();
14317
+ return normalized.startsWith(CCTP_FORWARD_MAGIC_HEX);
14318
+ };
14319
+ /**
14320
+ * Assert that a hookData blob is forward-friendly for the prepaid FORWARD path.
14321
+ *
14322
+ * The prepaid FORWARD path always requests a FORWARD fee item, so the wrapper
14323
+ * requires the hookData to start with a `cctp-forward` frame. Validating this
14324
+ * before the burn surfaces the guaranteed `ForwardFeeWithoutHook` revert as a
14325
+ * typed input error instead of an on-chain failure.
14326
+ *
14327
+ * @param hookData - The 0x-prefixed hookData hex string.
14328
+ * @throws KitError (`INPUT_VALIDATION_FAILED`) if the hookData is missing or lacks
14329
+ * the `cctp-forward` frame.
14330
+ *
14331
+ * @example
14332
+ * ```typescript
14333
+ * assertForwardHookData(geForwardHookData) // ok
14334
+ * assertForwardHookData('0xdeadbeef') // throws — would revert ForwardFeeWithoutHook
14335
+ * ```
14336
+ */ const assertForwardHookData = (hookData)=>{
14337
+ if (!hasForwardHook(hookData)) {
14338
+ throw createValidationFailedError$1('hookData', hookData, 'Prepaid FORWARD burns require a cctp-forward-wrapped hookData; without it the TokenMessengerWithFees wrapper reverts ForwardFeeWithoutHook');
14339
+ }
14340
+ };
14341
+
14169
14342
  /**
14170
14343
  * Type guard to validate the forwardFee object structure.
14171
14344
  *
@@ -15097,6 +15270,109 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
15097
15270
  }
15098
15271
  }
15099
15272
 
15273
+ /**
15274
+ * Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
15275
+ *
15276
+ * Validates the full public-boundary input before any field destructuring,
15277
+ * `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
15278
+ * inputs always produce typed `KitError` validation failures.
15279
+ *
15280
+ * Checks performed (in order):
15281
+ * - `params` must be a non-null plain object
15282
+ * - `source` — valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
15283
+ * - `destinationChain` — present and supports CCTP v2
15284
+ * - source and destination chains must both be testnet or both mainnet
15285
+ * - source and destination chains must differ
15286
+ * - `executor` — non-empty string
15287
+ * - `amount` — bigint or non-empty string coercible to bigint
15288
+ * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
15289
+ * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
15290
+ * - `claim.signedQuote` — valid `0x`-prefixed hex string
15291
+ * - `claim.refundAddress` — valid EVM address
15292
+ * - `hookData` — valid `0x`-prefixed hex string when present
15293
+ *
15294
+ * @param params - The value to validate.
15295
+ * @throws {KitError} If any field is missing or invalid.
15296
+ *
15297
+ * @example
15298
+ * ```typescript
15299
+ * assertBurnWithFeesParams(params)
15300
+ * // params is now typed as BurnWithFeesParams and safe to use
15301
+ * const { source, destinationChain, amount } = params
15302
+ * ```
15303
+ */ function assertBurnWithFeesParams(params) {
15304
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
15305
+ throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
15306
+ }
15307
+ const p = params;
15308
+ // Source wallet context
15309
+ assertCCTPv2WalletContext(p['source']);
15310
+ const source = p['source'];
15311
+ // destinationChain
15312
+ const destinationChain = p['destinationChain'];
15313
+ if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
15314
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
15315
+ }
15316
+ if (!isCCTPV2Supported(destinationChain)) {
15317
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
15318
+ }
15319
+ const dest = destinationChain;
15320
+ // Testnet / mainnet mismatch
15321
+ if (source.chain.isTestnet !== dest.isTestnet) {
15322
+ throw createNetworkMismatchError(source.chain, dest);
15323
+ }
15324
+ // Same-chain guard
15325
+ if (source.chain.name === dest.name) {
15326
+ throw createUnsupportedRouteError(source.chain.name, dest.name);
15327
+ }
15328
+ // executor
15329
+ const executor = p['executor'];
15330
+ if (typeof executor !== 'string' || executor === '') {
15331
+ throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
15332
+ }
15333
+ // amount
15334
+ const rawAmount = p['amount'];
15335
+ if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
15336
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
15337
+ }
15338
+ try {
15339
+ BigInt(rawAmount);
15340
+ } catch {
15341
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
15342
+ }
15343
+ // feeTotalAmount
15344
+ const rawFee = p['feeTotalAmount'];
15345
+ if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
15346
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
15347
+ }
15348
+ try {
15349
+ BigInt(rawFee);
15350
+ } catch {
15351
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
15352
+ }
15353
+ // feeToken
15354
+ if (!evmAddressSchema.safeParse(p['feeToken']).success) {
15355
+ throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
15356
+ }
15357
+ // claim
15358
+ const rawClaim = p['claim'];
15359
+ if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
15360
+ throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
15361
+ }
15362
+ const claim = rawClaim;
15363
+ if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
15364
+ throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
15365
+ }
15366
+ if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
15367
+ throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
15368
+ }
15369
+ // hookData (optional)
15370
+ const hookData = p['hookData'];
15371
+ if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
15372
+ throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
15373
+ }
15374
+ }
15375
+
15100
15376
  /**
15101
15377
  * CCTP bridge step names that can occur in the bridging flow.
15102
15378
  *
@@ -16305,10 +16581,15 @@ const mockAttestationMessage = {
16305
16581
  const burnCallData = burnRequest.getCallData();
16306
16582
  // batchExecute may throw before submission (wallet declined) but never
16307
16583
  // after — post-submission errors are returned as empty receipts.
16584
+ // The sender is threaded for adapters whose execution is routed through a
16585
+ // signing strategy (which has no wallet account to read it from); the
16586
+ // wallet-client path ignores it.
16308
16587
  const batchResult = await adapter.batchExecute([
16309
16588
  approveCallData,
16310
16589
  burnCallData
16311
- ], chain);
16590
+ ], chain, {
16591
+ fromAddress: params.source.address
16592
+ });
16312
16593
  const approveReceipt = batchResult.receipts[0];
16313
16594
  const burnReceipt = batchResult.receipts[1];
16314
16595
  const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
@@ -16451,7 +16732,7 @@ const mockAttestationMessage = {
16451
16732
  return step;
16452
16733
  }
16453
16734
 
16454
- var version$4 = "1.9.0";
16735
+ var version$4 = "1.10.0";
16455
16736
  var pkg$4 = {
16456
16737
  version: version$4};
16457
16738
 
@@ -17044,7 +17325,7 @@ var pkg$4 = {
17044
17325
  * }
17045
17326
  * )
17046
17327
  * ```
17047
- */ async function retry(result, context, provider, invocationMeta) {
17328
+ */ async function retry$1(result, context, provider, invocationMeta) {
17048
17329
  const analysis = analyzeSteps(result);
17049
17330
  // Resolve invocation context for retry operation
17050
17331
  const resolvedInvocation = resolveRetryInvocation(invocationMeta);
@@ -17145,7 +17426,7 @@ var pkg$4 = {
17145
17426
  // Continue with remaining steps.
17146
17427
  // Recursive call handles subsequent pending states (e.g., next step may also
17147
17428
  // be pending), allowing the retry logic to loop through all actionable steps.
17148
- return await retry(result, context, provider);
17429
+ return await retry$1(result, context, provider);
17149
17430
  } catch (error) {
17150
17431
  // Re-throw FATAL validation errors - these indicate invalid input
17151
17432
  if (isFatalError(error)) {
@@ -17359,7 +17640,7 @@ function assertCCTPV2Config(config) {
17359
17640
  * const retryResult = await provider.retry(failedResult, retryContext)
17360
17641
  * ```
17361
17642
  */ async retry(result, context, invocationMeta) {
17362
- return retry(result, context, this, invocationMeta);
17643
+ return retry$1(result, context, this, invocationMeta);
17363
17644
  }
17364
17645
  /**
17365
17646
  * Estimate the cost and fees for a CCTP v2 cross-chain bridge operation.
@@ -17606,7 +17887,7 @@ function assertCCTPV2Config(config) {
17606
17887
  throw new Error(`Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
17607
17888
  }
17608
17889
  // Resolve spender address with proper error handling
17609
- const spenderAddress = resolveCCTPV2ContractAddress(chain);
17890
+ const spenderAddress = resolveCCTPV2ContractAddress(chain, 'tokenMessenger');
17610
17891
  // Prepare action parameters
17611
17892
  const actionParams = {
17612
17893
  amount: BigInt(amount),
@@ -18086,6 +18367,106 @@ function assertCCTPV2Config(config) {
18086
18367
  return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
18087
18368
  }
18088
18369
  /**
18370
+ * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
18371
+ *
18372
+ * Builds the source-chain `depositForBurnWithHookAndFees` call for the
18373
+ * GenericExecutor FORWARD path: fees are collected up front on the source chain
18374
+ * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
18375
+ * the GenericExecutor, and the GE `hookData` is passed through unchanged.
18376
+ *
18377
+ * This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
18378
+ * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
18379
+ * `claim` are produced elsewhere and passed in here:
18380
+ * - `hookData`: `buildForwardingHookDataWithPayload(version,
18381
+ * buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
18382
+ * - `claim.signedQuote` / `feeToken` / `feeTotalAmount`: from `fetchFeeQuote`
18383
+ * (`@circle-fin/provider-fee-v1`), whose FORWARD item must be bound to the
18384
+ * SAME `hookData` and executor `destinationCaller` used here.
18385
+ *
18386
+ * The returned approvals and burn are NOT executed — the caller executes the
18387
+ * approvals first (in order) and then the burn. The fee payment channel matches
18388
+ * the quote's `feeToken`:
18389
+ * - Native fee: exactly `feeTotalAmount` is attached as the burn's `msg.value`;
18390
+ * only the burn amount is approved.
18391
+ * - USDC fee (same token as the burn): a single combined `amount + feeTotalAmount`
18392
+ * approval covers both; the redundant second approval is skipped.
18393
+ *
18394
+ * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18395
+ * @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
18396
+ * @returns The prepared approvals, the prepared burn, and the resolved fee plan.
18397
+ * @throws {KitError} If the wallet context is invalid, `destinationChain` does not
18398
+ * support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
18399
+ * a bigint or a numeric string coercible to bigint, the hookData lacks a
18400
+ * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
18401
+ * context cannot be resolved.
18402
+ *
18403
+ * @example
18404
+ * ```typescript
18405
+ * const { approvals, burn } = await provider.burnWithFees({
18406
+ * source,
18407
+ * destinationChain: Arc,
18408
+ * amount: 1_000_000n,
18409
+ * executor: genericExecutorAddress,
18410
+ * hookData: geForwardHookData,
18411
+ * claim: { signedQuote: '0x01...', refundAddress: userAddress },
18412
+ * feeToken: '0x0000000000000000000000000000000000000000', // native
18413
+ * feeTotalAmount: 3_500_000n,
18414
+ * })
18415
+ * for (const approval of approvals) await approval.execute()
18416
+ * const txHash = await burn.execute()
18417
+ * ```
18418
+ */ async burnWithFees(params) {
18419
+ assertBurnWithFeesParams(params);
18420
+ const { source, destinationChain, executor, hookData, claim, feeToken } = params;
18421
+ const amount = BigInt(params.amount);
18422
+ const feeTotalAmount = BigInt(params.feeTotalAmount);
18423
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
18424
+ // so the hookData must carry a cctp-forward frame; otherwise the wrapper
18425
+ // reverts ForwardFeeWithoutHook. Surface it as a typed input error up front.
18426
+ assertForwardHookData(hookData);
18427
+ const burnToken = source.chain.usdcAddress;
18428
+ const feePayment = resolveFeePayment({
18429
+ feeToken,
18430
+ burnToken,
18431
+ amount,
18432
+ feeTotalAmount
18433
+ });
18434
+ // Resolve operation context from the source wallet context.
18435
+ const operationContext = this.extractOperationContext(source);
18436
+ let resolvedContext;
18437
+ try {
18438
+ resolvedContext = await resolveOperationContext(source.adapter, operationContext);
18439
+ } catch (error) {
18440
+ throw createValidationFailedError$1('source.adapter', undefined, `Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
18441
+ }
18442
+ const context = resolvedContext;
18443
+ const wrapperAddress = resolveCCTPV2ContractAddress(source.chain, 'tokenMessengerWithFees');
18444
+ // Build the ERC-20 approvals to the wrapper (burn token, plus a distinct fee
18445
+ // token only when the fee is not paid in the burn token).
18446
+ const approvals = await Promise.all(feePayment.approvals.map(async (approval)=>source.adapter.prepareAction('token.approve', {
18447
+ tokenAddress: approval.token,
18448
+ delegate: wrapperAddress,
18449
+ amount: approval.amount
18450
+ }, context)));
18451
+ // Build the burn: mintRecipient AND destinationCaller are both the executor.
18452
+ const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18453
+ fromChain: source.chain,
18454
+ toChain: destinationChain,
18455
+ amount,
18456
+ mintRecipient: executor,
18457
+ destinationCaller: executor,
18458
+ hookData,
18459
+ claim,
18460
+ feeToken,
18461
+ feeTotalAmount
18462
+ }, context);
18463
+ return {
18464
+ approvals,
18465
+ burn,
18466
+ feePayment
18467
+ };
18468
+ }
18469
+ /**
18089
18470
  * Waits for a transaction to be mined and confirmed on the blockchain.
18090
18471
  *
18091
18472
  * This method should block until the transaction is confirmed on the blockchain.
@@ -18965,7 +19346,7 @@ registerKit(`${pkg$5.name}/${pkg$5.version}`);
18965
19346
  };
18966
19347
 
18967
19348
  var name$3 = "@circle-fin/swap-kit";
18968
- var version$3 = "1.3.2";
19349
+ var version$3 = "1.4.0";
18969
19350
  var pkg$3 = {
18970
19351
  name: name$3,
18971
19352
  version: version$3};
@@ -19030,7 +19411,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
19030
19411
  }).min(1, 'kitKey must be a non-empty string').optional(),
19031
19412
  provider: z.string({
19032
19413
  invalid_type_error: 'provider must be a string'
19033
- }).min(1, 'provider must be a non-empty string').optional()
19414
+ }).min(1, 'provider must be a non-empty string').optional(),
19415
+ batchTransactions: z.boolean({
19416
+ invalid_type_error: 'batchTransactions must be a boolean'
19417
+ }).optional()
19034
19418
  });
19035
19419
  /**
19036
19420
  * Zod schema for adapter context.
@@ -19561,7 +19945,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19561
19945
  /**
19562
19946
  * Circle Stablecoin Service API Key.
19563
19947
  * Must be a valid API key format.
19564
- */ apiKey: apiKeySchema
19948
+ */ apiKey: apiKeySchema.optional()
19565
19949
  }).superRefine(requireCrossChainQuoteToAddress);
19566
19950
  /**
19567
19951
  * Zod schema for validating CreateSwapRequest parameters.
@@ -19619,7 +20003,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19619
20003
  /**
19620
20004
  * Circle Stablecoin Service API Key.
19621
20005
  * Must be a valid API key format.
19622
- */ apiKey: apiKeySchema
20006
+ */ apiKey: apiKeySchema.optional()
19623
20007
  });
19624
20008
  /**
19625
20009
  * Zod schema for validating GetSwapStatusResponse data.
@@ -19655,7 +20039,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19655
20039
  toChain: z.string({
19656
20040
  invalid_type_error: 'toChain must be a string'
19657
20041
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
19658
- apiKey: apiKeySchema
20042
+ apiKey: apiKeySchema.optional()
19659
20043
  });
19660
20044
  /**
19661
20045
  * Zod schema for validating CreateSwapResponse payloads.
@@ -19664,13 +20048,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19664
20048
  required_error: 'fee token is required',
19665
20049
  invalid_type_error: 'fee token must be a string'
19666
20050
  }).min(1, 'fee token must be a non-empty string'),
19667
- amount: feeAmountSchema
20051
+ amount: feeAmountSchema,
20052
+ decimals: z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
20053
+ symbol: z.string({
20054
+ invalid_type_error: 'fee token symbol must be a string'
20055
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
19668
20056
  });
19669
20057
  /**
19670
20058
  * Developer fee item schema with basis field.
19671
- */ const createSwapDeveloperFeeItemSchema = z.object({
19672
- token: z.string().min(1, 'fee token must be a non-empty string'),
19673
- amount: feeAmountSchema,
20059
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
19674
20060
  basis: z.enum([
19675
20061
  'inputAmount',
19676
20062
  'estimatedAmount'
@@ -19762,7 +20148,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19762
20148
  addresses: z.array(z.string({
19763
20149
  invalid_type_error: 'addresses entries must be strings'
19764
20150
  }).min(1, 'addresses entries must be non-empty strings')).min(1, 'addresses must contain at least one entry when provided').max(MAX_RATE_ADDRESSES_PER_REQUEST, `addresses supports at most ${String(MAX_RATE_ADDRESSES_PER_REQUEST)} values per request`).optional(),
19765
- apiKey: apiKeySchema
20151
+ apiKey: apiKeySchema.optional()
19766
20152
  });
19767
20153
  /**
19768
20154
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -19979,7 +20365,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19979
20365
  ...DEFAULT_CONFIG$1,
19980
20366
  headers: {
19981
20367
  ...DEFAULT_CONFIG$1.headers,
19982
- Authorization: `Bearer ${apiKey}`
20368
+ // Permissionless mode: no Authorization header when the kit key is absent.
20369
+ ...apiKey !== undefined && {
20370
+ Authorization: `Bearer ${apiKey}`
20371
+ }
19983
20372
  }
19984
20373
  };
19985
20374
  try {
@@ -20133,7 +20522,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20133
20522
  ...DEFAULT_CONFIG$1,
20134
20523
  headers: {
20135
20524
  ...DEFAULT_CONFIG$1.headers,
20136
- Authorization: `Bearer ${validatedParams.apiKey}`
20525
+ // Permissionless mode: no Authorization header when the kit key is absent.
20526
+ ...validatedParams.apiKey !== undefined && {
20527
+ Authorization: `Bearer ${validatedParams.apiKey}`
20528
+ }
20137
20529
  }
20138
20530
  };
20139
20531
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
@@ -20188,7 +20580,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20188
20580
  const validatedParams = {
20189
20581
  txHash: result.data.txHash,
20190
20582
  chain: result.data.chain,
20191
- apiKey: result.data.apiKey,
20583
+ ...result.data.apiKey !== undefined && {
20584
+ apiKey: result.data.apiKey
20585
+ },
20192
20586
  ...result.data.toChain !== undefined && {
20193
20587
  toChain: result.data.toChain
20194
20588
  }
@@ -20198,7 +20592,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20198
20592
  ...DEFAULT_CONFIG$1,
20199
20593
  headers: {
20200
20594
  ...DEFAULT_CONFIG$1.headers,
20201
- Authorization: `Bearer ${validatedParams.apiKey}`
20595
+ // Permissionless mode: no Authorization header when the kit key is absent.
20596
+ ...validatedParams.apiKey !== undefined && {
20597
+ Authorization: `Bearer ${validatedParams.apiKey}`
20598
+ }
20202
20599
  }
20203
20600
  };
20204
20601
  return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
@@ -20287,7 +20684,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20287
20684
  }
20288
20685
  const validatedParams = {
20289
20686
  chain: result.data.chain,
20290
- apiKey: result.data.apiKey,
20687
+ ...result.data.apiKey !== undefined && {
20688
+ apiKey: result.data.apiKey
20689
+ },
20291
20690
  ...result.data.addresses !== undefined && {
20292
20691
  addresses: result.data.addresses
20293
20692
  }
@@ -20297,7 +20696,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20297
20696
  ...DEFAULT_CONFIG$1,
20298
20697
  headers: {
20299
20698
  ...DEFAULT_CONFIG$1.headers,
20300
- Authorization: `Bearer ${validatedParams.apiKey}`
20699
+ // Permissionless mode: no Authorization header when the kit key is absent.
20700
+ ...validatedParams.apiKey !== undefined && {
20701
+ Authorization: `Bearer ${validatedParams.apiKey}`
20702
+ }
20301
20703
  }
20302
20704
  };
20303
20705
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
@@ -22342,6 +22744,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
22342
22744
  */ function hasSignTypedData(adapter) {
22343
22745
  return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
22344
22746
  }
22747
+ /**
22748
+ * Type guard to check if an adapter can actually produce an EIP-712
22749
+ * typed-data signature.
22750
+ *
22751
+ * @remarks
22752
+ * Strengthens {@link hasSignTypedData}: having a `signTypedData` method
22753
+ * does not guarantee it can succeed. Adapters whose signer is delegated
22754
+ * (e.g. through a signing strategy backed by a smart contract account)
22755
+ * expose the method but reject typed-data payloads at runtime. Such
22756
+ * adapters report their real capability through an optional
22757
+ * `supportsSignTypedData()` method, which this guard consults when
22758
+ * present. Adapters without the capability method are assumed able to
22759
+ * sign, preserving the previous duck-typing behavior.
22760
+ *
22761
+ * @param adapter - The adapter to check
22762
+ * @returns True if calling `signTypedData` can be expected to succeed
22763
+ *
22764
+ * @example
22765
+ * ```typescript
22766
+ * import { canSignTypedData } from '@core/adapter-evm'
22767
+ *
22768
+ * if (canSignTypedData(adapter)) {
22769
+ * const signature = await adapter.signTypedData(typedData, context)
22770
+ * } else {
22771
+ * // take an on-chain approval path instead of a permit signature
22772
+ * }
22773
+ * ```
22774
+ */ function canSignTypedData(adapter) {
22775
+ if (!hasSignTypedData(adapter)) {
22776
+ return false;
22777
+ }
22778
+ if (typeof adapter.supportsSignTypedData === 'function') {
22779
+ // The value is `boolean` per the interface, but a plain-JS adapter may
22780
+ // return anything; treat it as untrusted and coerce to a strict
22781
+ // boolean. Comparing an `unknown` (not a `boolean`) also keeps the
22782
+ // lint autofix from stripping this as a redundant `=== true`.
22783
+ const supported = adapter.supportsSignTypedData();
22784
+ return supported === true;
22785
+ }
22786
+ return true;
22787
+ }
22345
22788
 
22346
22789
  /**
22347
22790
  * Build EIP-2612 typed data for permit signing.
@@ -23874,10 +24317,13 @@ function writeBytes32(buffer, hex, offset) {
23874
24317
  * at usage time rather than construction time.
23875
24318
  *
23876
24319
  * Validates:
23877
- * - Kit key is present and matches required format (KIT_KEY:id:secret)
24320
+ * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
24321
+ * An absent or empty kit key is permitted (permissionless mode) — the swap
24322
+ * service now treats the key as optional.
23878
24323
  *
23879
- * @param kitKey - The inline kit key from the swap operation config
23880
- * @throws KitError with VALIDATION_FAILED if kit key is invalid or missing
24324
+ * @param kitKey - The inline kit key from the swap operation config (optional)
24325
+ * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
24326
+ * match the KIT_KEY:<keyId>:<keySecret> format
23881
24327
  *
23882
24328
  * @example
23883
24329
  * ```typescript
@@ -23889,9 +24335,11 @@ function writeBytes32(buffer, hex, offset) {
23889
24335
  * assertKitKey(kitKey)
23890
24336
  * ```
23891
24337
  */ function assertKitKey(kitKey) {
23892
- // Validate API key format using existing schema from service-client
24338
+ // Permissionless mode: the swap service treats the kit key as optional, so an
24339
+ // absent (or empty) key is valid. Only validate the format when a key is
24340
+ // actually provided.
23893
24341
  if (!kitKey) {
23894
- throw createValidationFailedError$1('kitKey', kitKey, 'Kit key is required. Expected format: KIT_KEY:<keyId>:<keySecret>. Provide it inline via config.kitKey parameter. Get your free Kit Key at: https://developers.circle.com/w3s/keys#kit-keys');
24342
+ return;
23895
24343
  }
23896
24344
  const apiKeyResult = apiKeySchema.safeParse(kitKey);
23897
24345
  if (!apiKeyResult.success) {
@@ -24188,8 +24636,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
24188
24636
  validateResolvedAddress(resolvedTokenInAddress, chain);
24189
24637
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
24190
24638
  validateResolvedAddress(to, destinationChain);
24191
- const kitKey = config?.kitKey ?? '';
24192
- // Validates the kit key
24639
+ const kitKey = config?.kitKey;
24640
+ // Validate the kit key format when one is provided (permissionless otherwise).
24193
24641
  assertKitKey(kitKey);
24194
24642
  // Validate custom fee configuration if present
24195
24643
  const customFee = config?.customFee;
@@ -24234,7 +24682,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
24234
24682
  }
24235
24683
  }
24236
24684
  },
24237
- apiKey: kitKey
24685
+ // Map kitKey → apiKey for the service client; omitted in permissionless mode.
24686
+ ...kitKey ? {
24687
+ apiKey: kitKey
24688
+ } : {}
24238
24689
  };
24239
24690
  }
24240
24691
 
@@ -24888,6 +25339,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
24888
25339
  }
24889
25340
  }
24890
25341
 
25342
+ /**
25343
+ * Determine whether an adapter can produce an EIP-2612 permit signature.
25344
+ *
25345
+ * @remarks
25346
+ * A gasless permit needs two adapter capabilities: fetching the token's
25347
+ * EIP-2612 nonce and producing an EIP-712 typed-data signature. The
25348
+ * typed-data check uses {@link canSignTypedData} rather than a bare
25349
+ * `hasSignTypedData` guard so that an adapter routed through a signing
25350
+ * strategy that cannot produce typed-data signatures — one whose manifest
25351
+ * omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
25352
+ * — is correctly excluded. Such an adapter falls back to an on-chain approval
25353
+ * (batched into a single submission when it supports atomic execution) instead
25354
+ * of attempting a permit its strategy would reject.
25355
+ *
25356
+ * @param adapter - The source adapter to inspect.
25357
+ * @returns `true` when the adapter can both fetch a nonce and sign typed data.
25358
+ *
25359
+ * @example
25360
+ * ```typescript
25361
+ * import { adapterSupportsPermit } from './utils'
25362
+ *
25363
+ * if (adapterSupportsPermit(adapter)) {
25364
+ * // gasless permit path — fold the approval into the swap transaction
25365
+ * } else {
25366
+ * // on-chain approval path (batched when supportsAtomicBatch is true)
25367
+ * }
25368
+ * ```
25369
+ */ function adapterSupportsPermit(adapter) {
25370
+ return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
25371
+ }
25372
+
24891
25373
  /**
24892
25374
  * Generate EIP-2612 permit signature for token approval.
24893
25375
  *
@@ -25023,8 +25505,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25023
25505
  }
25024
25506
  // Skip permit generation if the adapter lacks the required capabilities.
25025
25507
  // handleEvmTokenApproval will have already sent an on-chain approval in this case.
25026
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
25027
- if (!adapterSupportsPermit) {
25508
+ if (!adapterSupportsPermit(adapter)) {
25028
25509
  return [
25029
25510
  createFallbackTokenInput(tokenInAddress, inputAmount)
25030
25511
  ];
@@ -25583,6 +26064,65 @@ const TOKEN_REGISTRY$3 = createTokenRegistry();
25583
26064
  return `Insufficient ${displaySymbol} balance for swap operation.\n\n` + `Wallet: ${walletAddress}\n` + `Current balance: ${currentDisplay}\n` + `Required: ${requiredDisplay}\n` + `Shortfall: ${shortfallDisplay}\n\n` + `This swap requires ${requiredSummary} to complete the transaction.\n\n` + `Action: Add at least ${actionAmount} to your wallet to complete this swap.`;
25584
26065
  }
25585
26066
 
26067
+ /**
26068
+ * Determine which chain a fee token should be resolved and formatted against.
26069
+ *
26070
+ * @remarks
26071
+ * Fees returned by the service may be denominated in either the input token
26072
+ * (on the source chain) or the output token (on the destination chain). A
26073
+ * contract address only resolves on the chain it belongs to, so formatting a
26074
+ * destination-denominated fee against the source chain causes
26075
+ * {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
26076
+ * units (e.g. a cross-chain swap charging a fee in the destination output
26077
+ * token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
26078
+ * resolved against the source chain). This is the fallback for fee items that
26079
+ * are not self-described with their own `decimals`/`chain`.
26080
+ *
26081
+ * Prefer the source chain (covers same-chain swaps and input-denominated
26082
+ * fees), then fall back to the destination chain when the token only resolves
26083
+ * there. When neither chain recognises the token, default to the source chain
26084
+ * so existing on-chain decimal lookups via the source adapter still apply.
26085
+ *
26086
+ * Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
26087
+ * source-first preference keeps them on the source chain. That is correct for
26088
+ * registry stablecoins, and for `'NATIVE'` only when both chains share native
26089
+ * decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
26090
+ * `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
26091
+ * mis-scaled. This is latent — providers emit the address form, and
26092
+ * self-describing fee items carry their own `decimals` and never reach this
26093
+ * helper — so the gap only opens for a future `'NATIVE'` fee that arrives
26094
+ * without `decimals` on a cross-native-decimal route.
26095
+ *
26096
+ * @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
26097
+ * @param sourceChain - The chain the swap originates from.
26098
+ * @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
26099
+ * @returns The chain definition the fee token should be resolved against.
26100
+ *
26101
+ * @example
26102
+ * ```typescript
26103
+ * import { resolveFeeChain } from './resolveFeeChain'
26104
+ * import { Ethereum, Base } from '@core/chains'
26105
+ *
26106
+ * // Cross-chain swap fee charged in the destination (output) token
26107
+ * resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
26108
+ * // => Base (EURC resolves on Base, not Ethereum)
26109
+ *
26110
+ * // Symbol or source-token fees stay on the source chain
26111
+ * resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
26112
+ * ```
26113
+ */ function resolveFeeChain(token, sourceChain, destinationChain) {
26114
+ if (sourceChain.chain === destinationChain.chain) {
26115
+ return sourceChain;
26116
+ }
26117
+ if (resolveTokenSymbol(token, sourceChain) !== null) {
26118
+ return sourceChain;
26119
+ }
26120
+ if (resolveTokenSymbol(token, destinationChain) !== null) {
26121
+ return destinationChain;
26122
+ }
26123
+ return sourceChain;
26124
+ }
26125
+
25586
26126
  const TOKEN_REGISTRY$2 = createTokenRegistry();
25587
26127
  /**
25588
26128
  * Format a raw base-unit amount into a human-readable decimal string.
@@ -25673,6 +26213,186 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
25673
26213
  }
25674
26214
  }
25675
26215
 
26216
+ /**
26217
+ * Runtime guard for {@link BatchCapableSwapAdapter}.
26218
+ *
26219
+ * @param adapter - The adapter to inspect.
26220
+ * @returns `true` when the adapter exposes both batch methods.
26221
+ *
26222
+ * @example
26223
+ * ```typescript
26224
+ * if (isBatchCapableSwapAdapter(adapter)) {
26225
+ * // adapter.supportsAtomicBatch / adapter.batchExecute are available
26226
+ * }
26227
+ * ```
26228
+ */ function isBatchCapableSwapAdapter(adapter) {
26229
+ return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
26230
+ }
26231
+ /**
26232
+ * Decide whether the EVM swap should take the batched approve-and-swap path.
26233
+ *
26234
+ * @remarks
26235
+ * Batching only helps when an on-chain approval would otherwise be required, so
26236
+ * it is skipped for native tokens (no approval) and for the gasless permit path
26237
+ * (already a single transaction). USDT is skipped because its reset-to-zero
26238
+ * allowance flow cannot be expressed as a fixed approve+swap pair. When those
26239
+ * gates pass, the adapter's actual atomic-batch capability is queried; any
26240
+ * failure resolves to `false` so the swap falls back to the sequential path.
26241
+ *
26242
+ * @param args - The decision inputs.
26243
+ * @param args.adapter - The source adapter.
26244
+ * @param args.chain - The source chain definition.
26245
+ * @param args.tokenInAddress - The resolved input-token address.
26246
+ * @param args.allowanceStrategy - Optional allowance strategy override.
26247
+ * @param args.batchTransactions - Optional explicit opt-out (`false` disables).
26248
+ * @returns `true` when the batched approve-and-swap path should be used.
26249
+ *
26250
+ * @example
26251
+ * ```typescript
26252
+ * const useBatched = await shouldUseBatchedSwap({
26253
+ * adapter,
26254
+ * chain,
26255
+ * tokenInAddress: '0xA0b8...',
26256
+ * allowanceStrategy: config?.allowanceStrategy,
26257
+ * batchTransactions: config?.batchTransactions,
26258
+ * })
26259
+ * ```
26260
+ */ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
26261
+ // Explicit opt-out.
26262
+ if (batchTransactions === false) {
26263
+ return false;
26264
+ }
26265
+ // Batching is an EVM capability (EIP-5792 or a signing strategy).
26266
+ if (chain.type !== 'evm') {
26267
+ return false;
26268
+ }
26269
+ // Native tokens need no approval — the swap is already a single transaction.
26270
+ if (isNativeEvmAddress(tokenInAddress)) {
26271
+ return false;
26272
+ }
26273
+ // A gasless permit folds the approval into the swap transaction, so there is
26274
+ // nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
26275
+ const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
26276
+ if (canUsePermit) {
26277
+ return false;
26278
+ }
26279
+ // USDT's reset-to-zero allowance dance cannot be expressed as a fixed
26280
+ // approve+swap pair; leave it on the sequential path.
26281
+ const usdt = chain.usdtAddress?.toLowerCase();
26282
+ if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
26283
+ return false;
26284
+ }
26285
+ if (!isBatchCapableSwapAdapter(adapter)) {
26286
+ return false;
26287
+ }
26288
+ try {
26289
+ return await adapter.supportsAtomicBatch(chain);
26290
+ } catch {
26291
+ return false;
26292
+ }
26293
+ }
26294
+ /**
26295
+ * Execute the approval and swap as a single atomic batch.
26296
+ *
26297
+ * @remarks
26298
+ * Extracts the raw call data from both prepared requests, submits them as one
26299
+ * batch via `adapter.batchExecute`, and maps the swap receipt back to a
26300
+ * transaction hash. The `fromAddress` is threaded for adapters routed through a
26301
+ * signing strategy (which have no wallet account to read the sender from); the
26302
+ * wallet-client path ignores it.
26303
+ *
26304
+ * Following the batch contract, `batchExecute` never throws once the batch is
26305
+ * submitted — a missing or failed swap receipt is surfaced here as a thrown
26306
+ * {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
26307
+ * batch and double-swap.
26308
+ *
26309
+ * @param args - The execution inputs.
26310
+ * @param args.adapter - The batch-capable source adapter.
26311
+ * @param args.chain - The EVM chain to execute on.
26312
+ * @param args.approveRequest - The prepared ERC-20 approval request.
26313
+ * @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
26314
+ * @param args.fromAddress - The address authorizing the batch.
26315
+ * @returns The swap transaction hash and the executed approval + swap records.
26316
+ * @throws {@link KitError} when the prepared requests cannot yield call data.
26317
+ * @throws {@link KitError} when the batch does not confirm or the swap reverts.
26318
+ *
26319
+ * @example
26320
+ * ```typescript
26321
+ * const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
26322
+ * adapter,
26323
+ * chain,
26324
+ * approveRequest,
26325
+ * swapRequest,
26326
+ * fromAddress: '0x742d...',
26327
+ * })
26328
+ * ```
26329
+ */ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
26330
+ if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
26331
+ throw new KitError({
26332
+ ...InputError.UNSUPPORTED_ACTION,
26333
+ recoverability: 'FATAL',
26334
+ message: 'Batched swap requires EVM prepared requests with getCallData() support.'
26335
+ });
26336
+ }
26337
+ const approveCallData = approveRequest.getCallData();
26338
+ const swapCallData = swapRequest.getCallData();
26339
+ const batchResult = await adapter.batchExecute([
26340
+ approveCallData,
26341
+ swapCallData
26342
+ ], chain, {
26343
+ fromAddress
26344
+ });
26345
+ const swapReceipt = batchResult.receipts[1];
26346
+ // A missing swap receipt means the batch never confirmed (polling timed out
26347
+ // or the wallet returned fewer receipts than calls). Re-throw the underlying
26348
+ // error when present (already FATAL); otherwise surface a FATAL timeout so the
26349
+ // caller checks the batch status rather than resubmitting.
26350
+ if (swapReceipt === undefined || swapReceipt.txHash === '') {
26351
+ if (isKitError(batchResult.error)) {
26352
+ throw batchResult.error;
26353
+ }
26354
+ throw new KitError({
26355
+ ...NetworkError.TIMEOUT,
26356
+ recoverability: 'FATAL',
26357
+ message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
26358
+ // Preserve the underlying confirmation failure when it isn't a KitError —
26359
+ // the signing-strategy path returns a raw viem error (e.g. a dropped or
26360
+ // replaced tx) — so the root cause survives behind the generic timeout.
26361
+ cause: {
26362
+ trace: {
26363
+ batchId: batchResult.batchId,
26364
+ ...batchResult.error != null && {
26365
+ error: batchResult.error
26366
+ }
26367
+ }
26368
+ }
26369
+ });
26370
+ }
26371
+ if (swapReceipt.status !== 'success') {
26372
+ throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
26373
+ }
26374
+ const executedTransactions = [];
26375
+ const approveReceipt = batchResult.receipts[0];
26376
+ // An atomic batch is a single on-chain transaction, so the approve and swap
26377
+ // receipts share one hash. Only surface a distinct approval record when it is
26378
+ // genuinely a separate transaction; otherwise the lone swap record represents
26379
+ // the batch, avoiding a phantom duplicate tx in executedTransactions.
26380
+ if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
26381
+ executedTransactions.push({
26382
+ type: 'approval',
26383
+ txHash: approveReceipt.txHash
26384
+ });
26385
+ }
26386
+ executedTransactions.push({
26387
+ type: 'swap',
26388
+ txHash: swapReceipt.txHash
26389
+ });
26390
+ return {
26391
+ swapTxHash: swapReceipt.txHash,
26392
+ executedTransactions
26393
+ };
26394
+ }
26395
+
25676
26396
  /**
25677
26397
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
25678
26398
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -25799,7 +26519,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
25799
26519
  const statusResult = await getSwapStatus$2({
25800
26520
  txHash,
25801
26521
  chain: chain.chain,
25802
- apiKey
26522
+ ...apiKey !== undefined && {
26523
+ apiKey
26524
+ }
25803
26525
  });
25804
26526
  if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
25805
26527
  return {
@@ -26390,8 +27112,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26390
27112
  // Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
26391
27113
  const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
26392
27114
  const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
26393
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
26394
- const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
27115
+ const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
26395
27116
  const needsApproval = !isNativeToken && !canUsePermitFlow;
26396
27117
  if (!needsApproval) {
26397
27118
  return;
@@ -26670,34 +27391,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26670
27391
  const serviceResponse = await createSwap(serviceParams);
26671
27392
  // Track executed transactions
26672
27393
  const executedTransactions = [];
26673
- // Prepare swap action based on chain type
26674
- let preparedAction;
26675
- if (chain.type === 'solana') {
26676
- // Solana: No approval needed, directly prepare swap action
26677
- preparedAction = await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext);
26678
- } else {
26679
- // EVM chains: Handle token approval if needed
26680
- await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
26681
- // EVM chains: prepareEvmSwapAction handles EIP-2612 permit generation
26682
- // Adapter contract address is read from chain.kitContracts.adapter
26683
- // Use the already-resolved context from above
26684
- preparedAction = await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy);
26685
- }
27394
+ // Prepare swap action(s) based on chain type and batch capability.
27395
+ // Returns either a single prepared action (Solana / sequential EVM) or a
27396
+ // batched approve+swap plan (EVM atomic-batch path).
27397
+ const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
27398
+ adapter,
27399
+ chain,
27400
+ serviceResponse,
27401
+ resolvedContext,
27402
+ executionCtx,
27403
+ config,
27404
+ executedTransactions
27405
+ });
26686
27406
  // Execute swap transaction via adapter
26687
27407
  // For EVM chains, use gas limit from proxy service API
26688
27408
  let txHash;
26689
27409
  const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
26690
27410
  try {
26691
- txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
26692
- executedTransactions.push({
26693
- type: 'swap',
26694
- txHash
26695
- });
26696
- // Wait for transaction confirmation and verify success
26697
- const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
26698
- if (txReceipt.status === 'reverted') {
26699
- const explorerUrl = buildExplorerUrl(chain, txHash);
26700
- throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
27411
+ if (batchedSwapPlan) {
27412
+ // Approve + swap submitted as one atomic batch. batchExecute confirms
27413
+ // the swap internally, so no separate waitForTransaction is needed.
27414
+ const batched = await executeBatchedApproveAndSwap({
27415
+ adapter: adapter,
27416
+ chain: chain,
27417
+ approveRequest: batchedSwapPlan.approveRequest,
27418
+ swapRequest: batchedSwapPlan.swapRequest,
27419
+ fromAddress: executionCtx.fromAddress
27420
+ });
27421
+ txHash = batched.swapTxHash;
27422
+ executedTransactions.push(...batched.executedTransactions);
27423
+ } else if (preparedAction) {
27424
+ txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
27425
+ executedTransactions.push({
27426
+ type: 'swap',
27427
+ txHash
27428
+ });
27429
+ // Wait for transaction confirmation and verify success
27430
+ const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
27431
+ if (txReceipt.status === 'reverted') {
27432
+ const explorerUrl = buildExplorerUrl(chain, txHash);
27433
+ throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
27434
+ }
27435
+ } else {
27436
+ // Unreachable: the preparation step always yields either a batched plan
27437
+ // or a prepared action.
27438
+ throw new KitError({
27439
+ ...InputError.UNSUPPORTED_ACTION,
27440
+ recoverability: 'FATAL',
27441
+ message: 'No swap execution path was prepared.'
27442
+ });
26701
27443
  }
26702
27444
  } catch (err) {
26703
27445
  handleSwapExecutionError(err, txHash, chain);
@@ -26716,7 +27458,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26716
27458
  isCrossChainSwap,
26717
27459
  txHash,
26718
27460
  chain,
26719
- apiKey: serviceParams.apiKey
27461
+ ...serviceParams.apiKey !== undefined && {
27462
+ apiKey: serviceParams.apiKey
27463
+ }
26720
27464
  });
26721
27465
  // Build and return SwapResult
26722
27466
  return {
@@ -26741,6 +27485,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26741
27485
  };
26742
27486
  }
26743
27487
  /**
27488
+ * Prepare the swap execution request(s) for the source wallet's chain.
27489
+ *
27490
+ * Produces either a single {@link PreparedChainRequest} (Solana, or the
27491
+ * sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
27492
+ * atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
27493
+ * batching). The caller executes whichever field is populated. Any on-chain
27494
+ * approval sent on the sequential path is appended to `executedTransactions`.
27495
+ *
27496
+ * @typeParam TFromAdapterCapabilities - Source-adapter capability set.
27497
+ * @param args - Inputs derived from the validated swap request.
27498
+ * @param args.adapter - Source-chain wallet adapter.
27499
+ * @param args.chain - Source chain definition.
27500
+ * @param args.serviceResponse - Validated createSwap response.
27501
+ * @param args.resolvedContext - Resolved operation context.
27502
+ * @param args.executionCtx - Minimal on-chain execution context.
27503
+ * @param args.config - Optional swap configuration (allowance/batch flags).
27504
+ * @param args.executedTransactions - Array appended with any sent approval.
27505
+ * @returns The prepared action or the batched approve+swap plan.
27506
+ * @throws KitError when the EVM atomic-batch path is selected but the chain
27507
+ * has no configured adapter contract.
27508
+ */ async prepareSwapRequests(args) {
27509
+ const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
27510
+ if (chain.type === 'solana') {
27511
+ // Solana: No approval needed, directly prepare swap action
27512
+ return {
27513
+ preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
27514
+ };
27515
+ }
27516
+ const useBatch = await shouldUseBatchedSwap({
27517
+ adapter,
27518
+ chain,
27519
+ tokenInAddress: executionCtx.tokenInAddress,
27520
+ allowanceStrategy: config?.allowanceStrategy,
27521
+ batchTransactions: config?.batchTransactions
27522
+ });
27523
+ if (useBatch) {
27524
+ // EVM chains: fuse the ERC-20 approval and the swap into a single atomic
27525
+ // batch (one signing challenge for smart-contract wallets). Force the
27526
+ // swap onto the pre-approval (PermitType.NONE) path since the approval
27527
+ // rides in the same batch.
27528
+ const adapterContractAddress = chain.kitContracts?.adapter;
27529
+ if (!adapterContractAddress) {
27530
+ throw new KitError({
27531
+ ...InputError.VALIDATION_FAILED,
27532
+ recoverability: 'FATAL',
27533
+ message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
27534
+ cause: {
27535
+ trace: {
27536
+ chain: chain.name
27537
+ }
27538
+ }
27539
+ });
27540
+ }
27541
+ const [approveRequest, swapRequest] = await Promise.all([
27542
+ this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
27543
+ prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
27544
+ ]);
27545
+ return {
27546
+ batchedSwapPlan: {
27547
+ approveRequest,
27548
+ swapRequest
27549
+ }
27550
+ };
27551
+ }
27552
+ // EVM chains: Handle token approval if needed, then prepare the swap.
27553
+ // prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
27554
+ // contract address is read from chain.kitContracts.adapter.
27555
+ await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
27556
+ return {
27557
+ preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
27558
+ };
27559
+ }
27560
+ /**
26744
27561
  * Executes a swap transaction with the appropriate gas limit for the chain type.
26745
27562
  *
26746
27563
  * For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
@@ -26789,8 +27606,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26789
27606
  */ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
26790
27607
  if (!fees) return [];
26791
27608
  const [providerFees, swapFees, developerFees] = await Promise.all([
26792
- this.formatServiceFees(fees.provider, chain, 'provider', adapter),
26793
- this.formatServiceFees(fees.swap, chain, 'swap', adapter),
27609
+ this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
27610
+ this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
26794
27611
  recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
26795
27612
  ]);
26796
27613
  return [
@@ -26800,6 +27617,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26800
27617
  ];
26801
27618
  }
26802
27619
  /**
27620
+ * Resolve a single fee item to its display token and human-readable amount.
27621
+ *
27622
+ * @remarks
27623
+ * Prefer the self-describing metadata the service attaches to each fee:
27624
+ * `decimals` (and `symbol`) come straight from the provider quote, so they
27625
+ * are authoritative even for a token absent from the SDK registry on both
27626
+ * chains. That is the case {@link resolveFeeChain} cannot recover — a
27627
+ * destination-denominated fee token resolves on neither the source registry
27628
+ * nor the source-bound adapter, leaving the amount as raw base units. When
27629
+ * the service omits `decimals` (optional during rollout), fall back to
27630
+ * inferring the fee token's chain and resolving via the registry/adapter.
27631
+ *
27632
+ * Like {@link formatTokenValue}, this never throws: fee display is cosmetic
27633
+ * and must not fail an estimate/swap. A malformed self-describing `decimals`
27634
+ * (e.g. a non-numeric `amount` or invalid decimal count that makes
27635
+ * {@link formatUnits} throw) falls through to chain-based resolution rather
27636
+ * than propagating out of {@link buildFormattedFees}.
27637
+ *
27638
+ * @param fee - The fee item from the service response.
27639
+ * @param chain - The source chain definition.
27640
+ * @param destinationChain - The destination chain definition.
27641
+ * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
27642
+ * @returns Promise resolving to the formatted amount and display token.
27643
+ */ async formatFeeValue(fee, chain, destinationChain, adapter) {
27644
+ if (fee.decimals != null) {
27645
+ try {
27646
+ return {
27647
+ amount: formatUnits(fee.amount, fee.decimals),
27648
+ token: fee.symbol ?? fee.token
27649
+ };
27650
+ } catch {
27651
+ // Malformed service metadata — fall through to chain-based resolution,
27652
+ // which never throws (worst case: raw passthrough).
27653
+ }
27654
+ }
27655
+ const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
27656
+ return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
27657
+ }
27658
+ /**
26803
27659
  * Format service fee items into the SDK's ServiceSwapFee structure.
26804
27660
  *
26805
27661
  * @remarks
@@ -26810,14 +27666,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26810
27666
  * - Raw passthrough only when both registry and adapter fail
26811
27667
  *
26812
27668
  * @param feeItems - Array of fee items from the service response.
26813
- * @param chain - The chain definition for token resolution and formatting.
27669
+ * @param chain - The source chain definition for token resolution and formatting.
27670
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
26814
27671
  * @param type - The fee type to assign ('provider' or 'swap').
26815
27672
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
26816
27673
  * @returns Promise resolving to formatted ServiceSwapFee array.
26817
- */ async formatServiceFees(feeItems, chain, type, adapter) {
27674
+ */ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
26818
27675
  if (!feeItems) return [];
26819
27676
  return Promise.all(feeItems.map(async (fee)=>{
26820
- const formatted = await formatTokenValue(fee.amount, fee.token, chain, adapter);
27677
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
26821
27678
  return {
26822
27679
  token: formatted.token,
26823
27680
  amount: formatted.amount,
@@ -26829,16 +27686,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26829
27686
  * Format developer fee items into the SDK's ServiceSwapFee structure.
26830
27687
  *
26831
27688
  * @param feeItems - Array of developer fee items from the service response.
26832
- * @param chain - The chain definition for token resolution and formatting.
27689
+ * @param chain - The source chain definition for token resolution and formatting.
27690
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
26833
27691
  * @param recipientAddress - The developer's fee recipient address from config.
26834
27692
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
26835
27693
  * @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
26836
27694
  */ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
26837
27695
  if (!feeItems) return [];
26838
- const isCrossChainSwap = destinationChain.chain !== chain.chain;
26839
27696
  return Promise.all(feeItems.map(async (fee)=>{
26840
- const feeChain = !isCrossChainSwap && fee.basis === 'estimatedAmount' ? destinationChain : chain;
26841
- const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
27697
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
26842
27698
  return {
26843
27699
  token: formatted.token,
26844
27700
  amount: formatted.amount,
@@ -28878,7 +29734,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
28878
29734
  /**
28879
29735
  * Resolve fee recipient address from context or policy.
28880
29736
  * @internal
28881
- */ async function resolveFeeRecipient(context, params, existingRecipient) {
29737
+ */ async function resolveFeeRecipient$1(context, params, existingRecipient) {
28882
29738
  if (existingRecipient) {
28883
29739
  return existingRecipient;
28884
29740
  }
@@ -28906,7 +29762,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
28906
29762
  * @internal
28907
29763
  */ async function applyExistingFeeAmount(context, params, existingAmount, existingFeeRecipient) {
28908
29764
  await validateFeeAmount(context, params, existingAmount);
28909
- const feeRecipient = await resolveFeeRecipient(context, params, existingFeeRecipient);
29765
+ const feeRecipient = await resolveFeeRecipient$1(context, params, existingFeeRecipient);
28910
29766
  if (feeRecipient) {
28911
29767
  params.config = {
28912
29768
  ...params.config,
@@ -28940,7 +29796,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
28940
29796
  ...params,
28941
29797
  type: 'input'
28942
29798
  };
28943
- const feeRecipient = await resolveFeeRecipient(context, params, existingFeeRecipient);
29799
+ const feeRecipient = await resolveFeeRecipient$1(context, params, existingFeeRecipient);
28944
29800
  const amount = await context.customFeePolicy?.computeFee(feeContext);
28945
29801
  if (amount !== undefined) {
28946
29802
  await validateInputFee(context, params, BigInt(amount), amount);
@@ -29233,22 +30089,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
29233
30089
  try {
29234
30090
  // Step 1: Build quote params directly (no need for buildServiceParams)
29235
30091
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
30092
+ // The kit key is optional (permissionless mode); when absent the quote is
30093
+ // fetched without an Authorization header.
29236
30094
  const kitKey = params.config?.kitKey;
29237
- if (!kitKey) {
29238
- throw new KitError({
29239
- code: 1098,
29240
- name: 'INPUT_VALIDATION_FAILED',
29241
- type: 'INPUT',
29242
- recoverability: 'FATAL',
29243
- message: 'kitKey is required in config for callback-based fees',
29244
- cause: {
29245
- trace: {
29246
- operation: 'handleOutputFeeCallback',
29247
- params
29248
- }
29249
- }
29250
- });
29251
- }
29252
30095
  // Resolve token aliases to addresses for the quote API
29253
30096
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
29254
30097
  const chain = params.from.chain;
@@ -29273,7 +30116,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
29273
30116
  ...params.config?.slippageBps !== undefined && {
29274
30117
  slippageBps: params.config.slippageBps
29275
30118
  },
29276
- apiKey: kitKey
30119
+ ...kitKey ? {
30120
+ apiKey: kitKey
30121
+ } : {}
29277
30122
  };
29278
30123
  // Step 2: Get quote from service
29279
30124
  const quoteResponse = await getQuote(quoteParams);
@@ -29715,7 +30560,9 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
29715
30560
  ...isCrossChain && {
29716
30561
  toChain: chainOut
29717
30562
  },
29718
- apiKey: params.kitKey
30563
+ ...params.kitKey ? {
30564
+ apiKey: params.kitKey
30565
+ } : {}
29719
30566
  };
29720
30567
  let raw = await getSwapStatus$2(request);
29721
30568
  // When the service hasn't finished indexing a just-submitted swap it
@@ -29855,7 +30702,9 @@ const isResultShape = (params)=>'result' in params;
29855
30702
  ...chainOut !== undefined && {
29856
30703
  chainOut
29857
30704
  },
29858
- kitKey: params.kitKey
30705
+ ...params.kitKey ? {
30706
+ kitKey: params.kitKey
30707
+ } : {}
29859
30708
  };
29860
30709
  const deadline = Date.now() + timeoutMs;
29861
30710
  let pollIndex = 0;
@@ -30016,7 +30865,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
30016
30865
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
30017
30866
  return getTokenRates$2({
30018
30867
  chain,
30019
- apiKey: params.kitKey,
30868
+ ...params.kitKey ? {
30869
+ apiKey: params.kitKey
30870
+ } : {},
30020
30871
  ...resolvedAddresses !== undefined && {
30021
30872
  addresses: resolvedAddresses
30022
30873
  }
@@ -31013,7 +31864,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
31013
31864
  };
31014
31865
 
31015
31866
  var name$2 = "@circle-fin/earn-kit";
31016
- var version$2 = "1.2.2";
31867
+ var version$2 = "1.3.0";
31017
31868
  var pkg$2 = {
31018
31869
  name: name$2,
31019
31870
  version: version$2};
@@ -31480,7 +32331,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31480
32331
  *
31481
32332
  * @param params - Adapter, chain, token/delegate/wallet addresses, the required
31482
32333
  * allowance for the signed payload, and a revert message for on-chain failure.
31483
- * @returns The approval transaction hash when an approval was submitted, or
32334
+ * @returns The approval transaction result when an approval was submitted, or
31484
32335
  * `undefined` when the existing allowance already covers `requiredAllowance`
31485
32336
  * (or `requiredAllowance` is zero).
31486
32337
  * @throws {@link KitError} If the `token.allowance` response is malformed.
@@ -31488,7 +32339,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31488
32339
  *
31489
32340
  * @example
31490
32341
  * ```typescript
31491
- * const txHash = await approveAllowanceIfNeeded({
32342
+ * const approval = await approveAllowanceIfNeeded({
31492
32343
  * adapter,
31493
32344
  * chain,
31494
32345
  * tokenAddress: usdcAddress,
@@ -31550,7 +32401,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31550
32401
  maxAttempts: params.allowancePropagation?.maxAttempts ?? DEFAULT_PROPAGATION_ATTEMPTS,
31551
32402
  delayMs: params.allowancePropagation?.delayMs ?? DEFAULT_PROPAGATION_DELAY_MS
31552
32403
  });
31553
- return approvalTxHash;
32404
+ return {
32405
+ txHash: approvalTxHash,
32406
+ ...approvalReceipt.gasUsed !== undefined && {
32407
+ gasUsed: approvalReceipt.gasUsed
32408
+ },
32409
+ ...approvalReceipt.effectiveGasPrice !== undefined && {
32410
+ effectiveGasPrice: approvalReceipt.effectiveGasPrice
32411
+ }
32412
+ };
31554
32413
  }
31555
32414
 
31556
32415
  /** @internal */ function isSameAddress(actual, expected) {
@@ -31698,7 +32557,13 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31698
32557
  }
31699
32558
  return {
31700
32559
  txHash,
31701
- explorerUrl
32560
+ explorerUrl,
32561
+ ...receipt.gasUsed !== undefined && {
32562
+ gasUsed: receipt.gasUsed
32563
+ },
32564
+ ...receipt.effectiveGasPrice !== undefined && {
32565
+ effectiveGasPrice: receipt.effectiveGasPrice
32566
+ }
31702
32567
  };
31703
32568
  }
31704
32569
 
@@ -32010,112 +32875,6 @@ const EARN_OPERATIONS = new Set([
32010
32875
  return hasEarnServiceParamsShape(operation, candidate['params']);
32011
32876
  }
32012
32877
 
32013
- function buildGasFeeBase(name, chain) {
32014
- return {
32015
- name,
32016
- token: chain.nativeCurrency.symbol,
32017
- blockchain: chain.chain
32018
- };
32019
- }
32020
- function buildGasFeeSuccess(name, chain, fees) {
32021
- return {
32022
- ...buildGasFeeBase(name, chain),
32023
- fees
32024
- };
32025
- }
32026
- function buildGasFeeFailure(name, chain, error) {
32027
- return {
32028
- ...buildGasFeeBase(name, chain),
32029
- fees: null,
32030
- error: getErrorMessage(error)
32031
- };
32032
- }
32033
- async function estimatePreparedGasFee(name, chain, prepared) {
32034
- try {
32035
- const estimate = bufferEstimatedGas(await prepared.estimate());
32036
- if (estimate.gas <= 0n) {
32037
- throw createValidationFailedError$1('estimate.gas', estimate.gas.toString(), 'gas estimate must be greater than zero');
32038
- }
32039
- return buildGasFeeSuccess(name, chain, estimate);
32040
- } catch (error) {
32041
- return buildGasFeeFailure(name, chain, error);
32042
- }
32043
- }
32044
- async function estimateApprovalGasFeeIfNeeded(params) {
32045
- const { adapter, chain, address, tokenAddress, delegate, requiredAllowance } = params;
32046
- if (requiredAllowance <= 0n) {
32047
- return undefined;
32048
- }
32049
- try {
32050
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
32051
- tokenAddress,
32052
- delegate
32053
- }, {
32054
- chain,
32055
- address
32056
- });
32057
- const allowanceRaw = await allowancePrepared.execute();
32058
- const currentAllowance = parseAllowanceResponse(allowanceRaw);
32059
- if (currentAllowance >= requiredAllowance) {
32060
- return undefined;
32061
- }
32062
- // Reuse the execute path's approval builder so the estimate simulates the
32063
- // exact approval (action, amount, and WARM_SLOT_RESIDUAL) that
32064
- // approveAllowanceIfNeeded later submits.
32065
- const approvalPrepared = await prepareApprovalAction({
32066
- adapter,
32067
- chain,
32068
- address,
32069
- tokenAddress,
32070
- delegate,
32071
- currentAllowance,
32072
- requiredAllowance
32073
- });
32074
- return await estimatePreparedGasFee('Approve', chain, approvalPrepared);
32075
- } catch (error) {
32076
- return buildGasFeeFailure('Approve', chain, error);
32077
- }
32078
- }
32079
- /**
32080
- * Estimate gas fee entries for an earn quote without submitting transactions.
32081
- *
32082
- * Each entry is produced by simulating the prepared transaction against
32083
- * current chain state. When an approval is required (allowance below the
32084
- * signed payload's required amount), the subsequent action simulation runs
32085
- * without that approval in place and is expected to revert — the action entry
32086
- * then carries `fees: null` with the revert message while the approval entry
32087
- * still estimates normally. Quote consumers must treat that as "estimate
32088
- * pending approval", not a hard failure.
32089
- *
32090
- * @internal
32091
- */ async function estimateEarnQuoteGasFees(params) {
32092
- const { adapter, chain, address, actionName, actionKey, actionParams, approval } = params;
32093
- const gasFees = [];
32094
- if (approval !== undefined) {
32095
- const approvalEstimate = await estimateApprovalGasFeeIfNeeded({
32096
- adapter,
32097
- chain,
32098
- address,
32099
- tokenAddress: approval.token,
32100
- delegate: approval.delegate,
32101
- requiredAllowance: approval.requiredAllowance
32102
- });
32103
- if (approvalEstimate !== undefined) {
32104
- gasFees.push(approvalEstimate);
32105
- }
32106
- }
32107
- try {
32108
- const actionPrepared = await adapter.prepareAction(actionKey, actionParams, {
32109
- chain,
32110
- address
32111
- });
32112
- gasFees.push(await estimatePreparedGasFee(actionName, chain, actionPrepared));
32113
- } catch (error) {
32114
- gasFees.push(buildGasFeeFailure(actionName, chain, error));
32115
- }
32116
- return gasFees;
32117
- }
32118
-
32119
32878
  // ---------------------------------------------------------------------------
32120
32879
  // Shared primitives
32121
32880
  // ---------------------------------------------------------------------------
@@ -32193,7 +32952,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
32193
32952
  asset: z.string(),
32194
32953
  assetAddress: z.string(),
32195
32954
  lltv: z.number(),
32196
- supplyUsd: z.number()
32955
+ supplyUsd: z.number(),
32956
+ // Optional during the expand/contract window (a backend that predates the
32957
+ // field omits the key), mirroring the `.optional()` facets on the base
32958
+ // schema; `null` when the product exposes no per-market allocation (V2).
32959
+ allocationPct: z.number().nullable().optional()
32197
32960
  });
32198
32961
  /**
32199
32962
  * Zod schema for a Morpho vault warning in the API response.
@@ -32207,7 +32970,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
32207
32970
  ])
32208
32971
  });
32209
32972
  /**
32210
- * Zod schema for a single vault info object in the API response.
32973
+ * Zod schema for the manager (curator) facet in the API response.
32974
+ *
32975
+ * @internal
32976
+ */ const managerSchema = z.object({
32977
+ name: z.string(),
32978
+ address: z.string().optional(),
32979
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
32980
+ // are added here as the providers that emit them land, rather than shipped
32981
+ // speculatively.
32982
+ type: z.enum([
32983
+ 'curator'
32984
+ ])
32985
+ });
32986
+ /**
32987
+ * Zod schema for the APY profile facet in the API response.
32988
+ *
32989
+ * @internal
32990
+ */ const apyProfileSchema = z.object({
32991
+ current: z.number(),
32992
+ native: z.number().nullable(),
32993
+ d7: z.number().nullable(),
32994
+ d30: z.number().nullable(),
32995
+ d90: z.number().nullable(),
32996
+ rewardShare: z.number().nullable(),
32997
+ source: z.string().optional(),
32998
+ asOf: z.string().optional()
32999
+ });
33000
+ /**
33001
+ * Zod schema for the fee split facet in the API response.
33002
+ *
33003
+ * @internal
33004
+ */ const feeInfoSchema = z.object({
33005
+ performance: z.number().nullable(),
33006
+ management: z.number().nullable()
33007
+ });
33008
+ /**
33009
+ * Zod schema for the liquidity profile facet in the API response.
33010
+ *
33011
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
33012
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
33013
+ *
33014
+ * @internal
33015
+ */ const liquidityProfileSchema = z.object({
33016
+ totalDeposits: amountJsonSchema,
33017
+ available: amountJsonSchema,
33018
+ totalSupply: amountJsonSchema,
33019
+ status: z.enum([
33020
+ 'active',
33021
+ 'low_liquidity'
33022
+ ])
33023
+ });
33024
+ /**
33025
+ * Zod schema for the risk signals facet in the API response.
33026
+ *
33027
+ * @internal
33028
+ */ const riskSignalsSchema = z.object({
33029
+ circleSentinel: z.boolean(),
33030
+ warnings: z.array(vaultWarningSchema).optional(),
33031
+ earnKitWarnings: z.array(z.string()).optional()
33032
+ });
33033
+ /**
33034
+ * Zod schema for the universal earn-opportunity base in the API response.
33035
+ *
33036
+ * Retains every existing deprecated flat field (kept validated through the
33037
+ * expand/contract window so default-strip does not drop them) and adds the
33038
+ * new nested facets. The nested facets are `.optional()` during the
33039
+ * transition so the SDK still validates against a not-yet-fully-deployed
33040
+ * backend; they become required after Expand ships.
32211
33041
  *
32212
33042
  * @internal
32213
33043
  */ const vaultInfoResponseSchema = z.object({
@@ -32232,6 +33062,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
32232
33062
  warnings: z.array(vaultWarningSchema).optional(),
32233
33063
  earnKitWarnings: z.array(z.string()).optional()
32234
33064
  });
33065
+ /**
33066
+ * Shared base schema: existing flat fields (kept) plus the new nested
33067
+ * facets and neutral identity. Facets are `.optional()` during the
33068
+ * transition; flip to required once the backend is confirmed emitting.
33069
+ *
33070
+ * @internal
33071
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
33072
+ address: z.string().optional(),
33073
+ asOf: z.string().optional(),
33074
+ manager: managerSchema.nullable().optional(),
33075
+ apyProfile: apyProfileSchema.optional(),
33076
+ fee: feeInfoSchema.optional(),
33077
+ liquidityProfile: liquidityProfileSchema.optional(),
33078
+ riskSignals: riskSignalsSchema.optional()
33079
+ });
33080
+ /**
33081
+ * Zod schema for the `vault` opportunity variant.
33082
+ *
33083
+ * @internal
33084
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
33085
+ productType: z.literal('vault'),
33086
+ collateral: z.array(collateralSchema)
33087
+ });
33088
+ /**
33089
+ * Discriminated union over `productType`. Add union members here as new
33090
+ * product types (e.g. `lending_market`, `rwa_token`) land.
33091
+ *
33092
+ * @internal
33093
+ */ const earnOpportunityVariants = [
33094
+ vaultOpportunitySchema
33095
+ ];
33096
+ /** @internal */ const earnOpportunitySchema = z.discriminatedUnion('productType', earnOpportunityVariants);
33097
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
33098
+ /**
33099
+ * Tolerant list parser for earn opportunities.
33100
+ *
33101
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
33102
+ * `z.array` fails the whole array if any element fails. Two migration-window
33103
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
33104
+ *
33105
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
33106
+ * only opportunity type then, so default a missing discriminant to `'vault'`
33107
+ * rather than dropping every vault the backend returns.
33108
+ * - A future backend adds a *second* `productType` this SDK version does not
33109
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
33110
+ * of rejecting the whole list.
33111
+ *
33112
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
33113
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
33114
+ * primitives, or an object whose `productType` is malformed — is passed through
33115
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
33116
+ * validation failure. It is deliberately not silently dropped (which would hide
33117
+ * malformed backend data) and never throws here (an unguarded property read on
33118
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
33119
+ * `ZodError`).
33120
+ *
33121
+ * @internal
33122
+ */ const earnOpportunityListSchema = z.preprocess((raw)=>{
33123
+ if (!Array.isArray(raw)) {
33124
+ return raw;
33125
+ }
33126
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
33127
+ // map/filter chain stays type-safe and no `any` leaks into the return.
33128
+ const entries = raw;
33129
+ return entries.map((entry)=>{
33130
+ // Only touch plain objects; non-objects fall through to fail validation.
33131
+ if (typeof entry !== 'object' || entry === null) {
33132
+ return entry;
33133
+ }
33134
+ const record = entry;
33135
+ // Older backend predating productType: default to the only type then.
33136
+ return record.productType === undefined ? {
33137
+ ...record,
33138
+ productType: 'vault'
33139
+ } : record;
33140
+ }).filter((entry)=>{
33141
+ // Drop ONLY a present-but-unknown string discriminant (a future
33142
+ // productType this SDK version doesn't know). Everything else —
33143
+ // non-objects, a non-string productType — flows through to
33144
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
33145
+ if (typeof entry !== 'object' || entry === null) {
33146
+ return true;
33147
+ }
33148
+ const productType = entry.productType;
33149
+ if (typeof productType !== 'string') {
33150
+ return true;
33151
+ }
33152
+ return knownProductTypes.has(productType);
33153
+ });
33154
+ }, z.array(earnOpportunitySchema));
32235
33155
  // ---------------------------------------------------------------------------
32236
33156
  // Position response schema
32237
33157
  // ---------------------------------------------------------------------------
@@ -32361,6 +33281,7 @@ const positionPnlSchema = z.discriminatedUnion('status', [
32361
33281
  *
32362
33282
  * @internal
32363
33283
  */ const depositPayloadSchema = z.object({
33284
+ execId: bridgeDepositExecIdSchema,
32364
33285
  executionParams: depositExecutionParamsSchema,
32365
33286
  signature: hexSignatureSchema
32366
33287
  });
@@ -32452,6 +33373,21 @@ const bridgeDepositPrepareReviewSchema = z.object({
32452
33373
  amount: amountJsonSchema,
32453
33374
  vaultAddress: hexAddressSchema
32454
33375
  }).passthrough();
33376
+ /** @internal */ const bridgeQuoteExpirySchema = z.discriminatedUnion('mode', [
33377
+ z.object({
33378
+ mode: z.literal('TIMESTAMP'),
33379
+ expiresAt: z.string().datetime({
33380
+ offset: true
33381
+ })
33382
+ }),
33383
+ z.object({
33384
+ mode: z.literal('BLOCK_NUMBER'),
33385
+ expiresAtBlock: z.number().int(),
33386
+ blockEstimatedAt: z.string().datetime({
33387
+ offset: true
33388
+ }).optional()
33389
+ })
33390
+ ]).optional().catch(undefined);
32455
33391
  /**
32456
33392
  * Zod schema for the bridge deposit prepare payload.
32457
33393
  *
@@ -32463,6 +33399,10 @@ const bridgeDepositPrepareReviewSchema = z.object({
32463
33399
  execId: bridgeDepositExecIdSchema,
32464
33400
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
32465
33401
  expiresAt: z.string().datetime(),
33402
+ quoteIssuedAt: z.string().datetime({
33403
+ offset: true
33404
+ }).optional().catch(undefined),
33405
+ quoteExpiry: bridgeQuoteExpirySchema,
32466
33406
  review: bridgeDepositPrepareReviewSchema
32467
33407
  });
32468
33408
  /**
@@ -32528,6 +33468,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
32528
33468
  *
32529
33469
  * @internal
32530
33470
  */ const withdrawPayloadSchema = z.object({
33471
+ execId: bridgeDepositExecIdSchema,
32531
33472
  executionParams: withdrawExecutionParamsSchema,
32532
33473
  signature: hexSignatureSchema
32533
33474
  });
@@ -32541,6 +33482,27 @@ const bridgeDepositPrepareReviewSchema = z.object({
32541
33482
  data: withdrawPayloadSchema
32542
33483
  });
32543
33484
  // ---------------------------------------------------------------------------
33485
+ // Transaction report response schema
33486
+ // ---------------------------------------------------------------------------
33487
+ /**
33488
+ * Zod schema for the transaction report payload inside the API `data` envelope.
33489
+ *
33490
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
33491
+ * schema accepts any object shape and does not require specific fields.
33492
+ *
33493
+ * @internal
33494
+ */ const transactionReportPayloadSchema = z.object({}).passthrough();
33495
+ /**
33496
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
33497
+ *
33498
+ * The Earn Service API wraps the transaction report payload in a `data`
33499
+ * envelope.
33500
+ *
33501
+ * @internal
33502
+ */ const transactionReportResponseSchema = z.object({
33503
+ data: transactionReportPayloadSchema
33504
+ });
33505
+ // ---------------------------------------------------------------------------
32544
33506
  // Claim rewards response schema
32545
33507
  // ---------------------------------------------------------------------------
32546
33508
  /**
@@ -32601,6 +33563,30 @@ const bridgeDepositPrepareReviewSchema = z.object({
32601
33563
  token: z.string(),
32602
33564
  amount: amountJsonSchema
32603
33565
  });
33566
+ /**
33567
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
33568
+ *
33569
+ * The Earn Service backend estimates gas server-side and returns one entry per
33570
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
33571
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
33572
+ * integer string in the chain's native base units. When the backend cannot
33573
+ * estimate an action it returns `fees: null` with an `error` message instead.
33574
+ *
33575
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
33576
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
33577
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
33578
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
33579
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
33580
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
33581
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
33582
+ * `fee`) must never fail Zod validation and reject the entire quote.
33583
+ *
33584
+ * @internal
33585
+ */ const quoteGasFeeSchema = z.object({
33586
+ name: z.string().optional(),
33587
+ fees: z.unknown(),
33588
+ error: z.string().optional()
33589
+ }).passthrough();
32604
33590
  /**
32605
33591
  * Zod schema for the inner deposit quote payload.
32606
33592
  *
@@ -32616,7 +33602,8 @@ const bridgeDepositPrepareReviewSchema = z.object({
32616
33602
  expectedShares: amountJsonSchema,
32617
33603
  sharePrice: z.string(),
32618
33604
  currentApy: z.number(),
32619
- fees: z.array(feeSchema).optional()
33605
+ fees: z.array(feeSchema).optional(),
33606
+ gasFees: z.array(quoteGasFeeSchema).optional()
32620
33607
  });
32621
33608
  /**
32622
33609
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -32643,6 +33630,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
32643
33630
  sharePrice: z.string(),
32644
33631
  maxWithdrawable: amountJsonSchema,
32645
33632
  fees: z.array(feeSchema),
33633
+ gasFees: z.array(quoteGasFeeSchema).optional(),
32646
33634
  warnings: z.array(z.string()).optional()
32647
33635
  });
32648
33636
  /**
@@ -32700,7 +33688,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
32700
33688
  *
32701
33689
  * @internal
32702
33690
  */ const getVaultsPayloadSchema = z.object({
32703
- vaults: z.array(vaultInfoResponseSchema),
33691
+ vaults: earnOpportunityListSchema,
32704
33692
  errors: z.array(vaultErrorSchema)
32705
33693
  });
32706
33694
  /**
@@ -32730,7 +33718,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
32730
33718
  *
32731
33719
  * @internal
32732
33720
  */ const exploreVaultsPayloadSchema = z.object({
32733
- vaults: z.array(vaultInfoResponseSchema),
33721
+ vaults: earnOpportunityListSchema,
32734
33722
  pagination: explorePaginationSchema
32735
33723
  });
32736
33724
  /**
@@ -32823,6 +33811,16 @@ const bridgeDepositPrepareReviewSchema = z.object({
32823
33811
  */ function isWithdrawResponse(value) {
32824
33812
  return withdrawResponseSchema.safeParse(value).success;
32825
33813
  }
33814
+ /**
33815
+ * Type guard for the transaction report API response.
33816
+ *
33817
+ * @param value - Unknown response value to validate
33818
+ * @returns True when the value matches the transaction report response shape
33819
+ *
33820
+ * @internal
33821
+ */ function isTransactionReportResponse(value) {
33822
+ return transactionReportResponseSchema.safeParse(value).success;
33823
+ }
32826
33824
  /**
32827
33825
  * Type guard for the claim rewards API response.
32828
33826
  *
@@ -32865,7 +33863,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
32865
33863
  }
32866
33864
 
32867
33865
  var name$1 = "@circle-fin/provider-earn-service";
32868
- var version$1 = "1.2.2";
33866
+ var version$1 = "1.3.0";
32869
33867
  var pkg$1 = {
32870
33868
  name: name$1,
32871
33869
  version: version$1};
@@ -32985,7 +33983,7 @@ var pkg$1 = {
32985
33983
  }
32986
33984
 
32987
33985
  /**
32988
- * Convert an API vault info object into the SDK {@link VaultInfo} shape.
33986
+ * Convert an API vault info object into the SDK {@link EarnOpportunity} shape.
32989
33987
  *
32990
33988
  * Map the API chain code back to the SDK chain identifier and hydrate the
32991
33989
  * amount payloads into {@link Amount} instances.
@@ -32996,16 +33994,29 @@ var pkg$1 = {
32996
33994
  *
32997
33995
  * @internal
32998
33996
  */ function toVaultInfo(data) {
32999
- const { totalDeposits, liquidity, ...vault } = data;
33997
+ const { totalDeposits, liquidity, liquidityProfile, ...vault } = data;
33000
33998
  const chain = toSdkChain(vault.chain);
33001
33999
  if (chain === undefined) {
33002
34000
  throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
33003
34001
  }
34002
+ // The nested facets are `.optional()` in the schema (a backend that predates
34003
+ // them omits them) and are typed optional on `EarnOpportunity` to match.
34004
+ // Convert the nested liquidity amounts when present and pass the remaining
34005
+ // facets straight through; each absent facet stays absent rather than being
34006
+ // asserted present by a cast.
33004
34007
  return {
33005
34008
  ...vault,
33006
34009
  chain,
33007
34010
  totalDeposits: Amount.fromJSON(totalDeposits),
33008
- liquidity: Amount.fromJSON(liquidity)
34011
+ liquidity: Amount.fromJSON(liquidity),
34012
+ ...liquidityProfile !== undefined && {
34013
+ liquidityProfile: {
34014
+ ...liquidityProfile,
34015
+ totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
34016
+ available: Amount.fromJSON(liquidityProfile.available),
34017
+ totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
34018
+ }
34019
+ }
33009
34020
  };
33010
34021
  }
33011
34022
 
@@ -33040,8 +34051,11 @@ function toVaultError(error) {
33040
34051
  }
33041
34052
  try {
33042
34053
  const response = await pollApiGet(url.toString(), isGetVaultsResponse, pollingConfig);
34054
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
34055
+ // does not run the schema's preprocess. Parse explicitly so unknown
34056
+ // `productType` values are dropped before `toVaultInfo`.
33043
34057
  return {
33044
- vaults: response.data.vaults.map(toVaultInfo),
34058
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
33045
34059
  errors: response.data.errors.map(toVaultError)
33046
34060
  };
33047
34061
  } catch (error) {
@@ -33090,8 +34104,11 @@ function toVaultError(error) {
33090
34104
  }
33091
34105
  try {
33092
34106
  const response = await pollApiGet(url.toString(), isExploreVaultsResponse, pollingConfig);
34107
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
34108
+ // does not run the schema's preprocess. Parse explicitly so unknown
34109
+ // `productType` values are dropped before `toVaultInfo`.
33093
34110
  return {
33094
- vaults: response.data.vaults.map(toVaultInfo),
34111
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
33095
34112
  pagination: response.data.pagination
33096
34113
  };
33097
34114
  } catch (error) {
@@ -33255,6 +34272,12 @@ function toPositionInfo(data) {
33255
34272
  execId: response.data.execId,
33256
34273
  preparedBundle,
33257
34274
  expiresAt: response.data.expiresAt,
34275
+ ...response.data.quoteIssuedAt !== undefined && {
34276
+ quoteIssuedAt: response.data.quoteIssuedAt
34277
+ },
34278
+ ...response.data.quoteExpiry !== undefined && {
34279
+ quoteExpiry: response.data.quoteExpiry
34280
+ },
33258
34281
  review: response.data.review
33259
34282
  };
33260
34283
  } catch (error) {
@@ -33596,7 +34619,110 @@ function toClaimedAmount(reward) {
33596
34619
  }
33597
34620
  }
33598
34621
 
33599
- function toDepositQuoteInfo(data) {
34622
+ /**
34623
+ * Map the Earn Service's server-side quote gas estimates into the SDK
34624
+ * {@link EarnGasFeeEstimate} shape.
34625
+ *
34626
+ * The Earn Service estimates gas for each action (`Approve`, `Deposit`,
34627
+ * `Withdraw`) and returns `{ name, fees: { gas, gasPrice, fee } }` with raw
34628
+ * integer strings.
34629
+ * The SDK type additionally carries `token` (the chain's native currency
34630
+ * symbol) and `blockchain`, which are filled in here from the chain
34631
+ * definition.
34632
+ *
34633
+ * Gas reporting is best-effort: a malformed entry (e.g. a non-integer string
34634
+ * that fails `BigInt` parsing) degrades to a `{ fees: null, error }` estimate
34635
+ * rather than throwing, so one bad entry never fails the whole quote.
34636
+ *
34637
+ * @param gasFees - Backend gas-fee entries from the quote response, if any.
34638
+ * @param chain - Chain definition, used for the native token symbol and
34639
+ * blockchain identifier.
34640
+ * @returns One {@link EarnGasFeeEstimate} per backend entry (empty when the
34641
+ * backend returned none).
34642
+ *
34643
+ * @example
34644
+ * ```typescript
34645
+ * toQuoteGasFees(
34646
+ * [{ name: 'Deposit', fees: { gas: '364142', gasPrice: '21000000000', fee: '7646982000000000' } }],
34647
+ * arcTestnet,
34648
+ * )
34649
+ * // [{ name: 'Deposit', token: 'USDC', blockchain: 'Arc_Testnet',
34650
+ * // fees: { gas: 364142n, gasPrice: 21000000000n, fee: '7646982000000000' } }]
34651
+ * ```
34652
+ *
34653
+ * @internal
34654
+ */ function toQuoteGasFees(gasFees, chain) {
34655
+ if (gasFees === undefined) {
34656
+ return [];
34657
+ }
34658
+ return gasFees.map((entry)=>{
34659
+ const base = {
34660
+ // `name` is optional on the wire; label an unnamed entry rather than
34661
+ // emitting `name: undefined`.
34662
+ name: entry.name ?? 'Unknown',
34663
+ token: chain.nativeCurrency.symbol,
34664
+ blockchain: chain.chain
34665
+ };
34666
+ // The Earn Service itself reports a failed estimate as `fees: null` with
34667
+ // an error; propagate that soft failure verbatim.
34668
+ if (entry.fees === null || entry.fees === undefined) {
34669
+ return {
34670
+ ...base,
34671
+ fees: null,
34672
+ error: entry.error ?? 'gas estimate unavailable'
34673
+ };
34674
+ }
34675
+ // `fees` is `unknown` at the schema layer, so ALL validation happens here:
34676
+ // that it is an object at all, and that `gas`, `gasPrice`, and `fee` are
34677
+ // each parseable integer strings (including `fee`, which the SDK contract
34678
+ // requires be a numeric base-unit string). Any failure — a wrong type
34679
+ // (`fees: 123`), a missing field, or a non-numeric value — degrades the
34680
+ // whole entry to a `fees: null` soft failure rather than surfacing a
34681
+ // malformed "successful" estimate or rejecting the quote.
34682
+ try {
34683
+ if (typeof entry.fees !== 'object') {
34684
+ throw new TypeError(`gas fees must be an object (got ${typeof entry.fees})`);
34685
+ }
34686
+ const { gas, gasPrice, fee } = entry.fees;
34687
+ return {
34688
+ ...base,
34689
+ fees: {
34690
+ gas: toBigInt('gas', gas),
34691
+ gasPrice: toBigInt('gasPrice', gasPrice),
34692
+ fee: toBigInt('fee', fee).toString()
34693
+ }
34694
+ };
34695
+ } catch (error) {
34696
+ return {
34697
+ ...base,
34698
+ fees: null,
34699
+ error: getErrorMessage(error)
34700
+ };
34701
+ }
34702
+ });
34703
+ }
34704
+ /**
34705
+ * Parse an unknown value into a `bigint`, rejecting anything that is not a
34706
+ * non-empty integer string. `BigInt` alone is too permissive for this path —
34707
+ * it accepts numbers, booleans, and empty strings — so guard the type first.
34708
+ *
34709
+ * @param field - Field name, used in the thrown error message.
34710
+ * @param value - Raw value from the backend gas entry.
34711
+ * @returns The parsed `bigint`.
34712
+ * @throws {TypeError} When `value` is not a non-empty integer string.
34713
+ */ function toBigInt(field, value) {
34714
+ if (typeof value !== 'string' || value.trim() === '') {
34715
+ throw new TypeError(`gas fee field "${field}" must be an integer string`);
34716
+ }
34717
+ try {
34718
+ // BigInt throws on non-integer strings (e.g. "1.5", "not-a-number").
34719
+ return BigInt(value);
34720
+ } catch {
34721
+ throw new Error(`gas fee field "${field}" is not a valid integer string: ${value}`);
34722
+ }
34723
+ }
34724
+
34725
+ function toDepositQuoteInfo(data, chain) {
33600
34726
  const fees = (data.fees ?? []).map(({ token: feeTokenSymbol, ...fee })=>{
33601
34727
  // Earn Service returns fee.token as a display symbol, for example "USDC".
33602
34728
  return {
@@ -33625,7 +34751,10 @@ function toDepositQuoteInfo(data) {
33625
34751
  sharePrice: data.sharePrice,
33626
34752
  currentApy: data.currentApy,
33627
34753
  fees,
33628
- gasFees: []
34754
+ // The Earn Service estimates gas server-side; the chain fills token/blockchain.
34755
+ // Cross-chain quotes resolve no local chain definition, so gasFees stays
34756
+ // empty there (unchanged behavior).
34757
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain)
33629
34758
  };
33630
34759
  }
33631
34760
  /**
@@ -33660,7 +34789,7 @@ function toDepositQuoteInfo(data) {
33660
34789
  };
33661
34790
  try {
33662
34791
  const response = await pollApiPost(url.toString(), requestBody, isDepositQuoteResponse, pollingConfig);
33663
- return toDepositQuoteInfo(response.data);
34792
+ return toDepositQuoteInfo(response.data, params.chainDefinition);
33664
34793
  } catch (error) {
33665
34794
  throw parseEarnApiError(error, {
33666
34795
  operation: 'getDepositQuote'
@@ -33668,7 +34797,7 @@ function toDepositQuoteInfo(data) {
33668
34797
  }
33669
34798
  }
33670
34799
 
33671
- function toWithdrawalQuoteInfo(data) {
34800
+ function toWithdrawalQuoteInfo(data, chain) {
33672
34801
  return {
33673
34802
  vaultAddress: data.vaultAddress,
33674
34803
  vaultName: data.vaultName,
@@ -33696,7 +34825,7 @@ function toWithdrawalQuoteInfo(data) {
33696
34825
  status
33697
34826
  }
33698
34827
  })),
33699
- gasFees: [],
34828
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain),
33700
34829
  // Wire format uses `warnings`, but the SDK surface uses
33701
34830
  // `earnKitWarnings` to match the precedent set by `VaultInfo` —
33702
34831
  // `warnings` is reserved for the structured `VaultWarning` shape.
@@ -33728,7 +34857,7 @@ function toWithdrawalQuoteInfo(data) {
33728
34857
  };
33729
34858
  try {
33730
34859
  const response = await pollApiPost(url.toString(), requestBody, isWithdrawalQuoteResponse, pollingConfig);
33731
- return toWithdrawalQuoteInfo(response.data);
34860
+ return toWithdrawalQuoteInfo(response.data, params.chainDefinition);
33732
34861
  } catch (error) {
33733
34862
  throw parseEarnApiError(error, {
33734
34863
  operation: 'getWithdrawalQuote'
@@ -33764,6 +34893,8 @@ function toWithdrawalQuoteInfo(data) {
33764
34893
  amount: Amount.fromJSON(r.amount),
33765
34894
  address: r.token
33766
34895
  })),
34896
+ // The claimRewards/quote response does not carry a gas estimate (unlike
34897
+ // deposit/withdrawal quotes), so there is nothing to surface here.
33767
34898
  gasFees: []
33768
34899
  };
33769
34900
  } catch (error) {
@@ -33773,6 +34904,83 @@ function toWithdrawalQuoteInfo(data) {
33773
34904
  }
33774
34905
  }
33775
34906
 
34907
+ /**
34908
+ * Build the native gas triple the backend expects for `gasUsed`.
34909
+ *
34910
+ * Returns `undefined` unless both receipt components are present, so the
34911
+ * caller can omit the field entirely — the Earn Service treats a missing
34912
+ * triple as "skip the gas cache write, still return 200".
34913
+ *
34914
+ * @param gasUsed - Receipt gas units used.
34915
+ * @param effectiveGasPrice - Receipt effective gas price.
34916
+ * @returns The `{ gas, gasPrice, fee }` triple, or `undefined` when either
34917
+ * component is missing.
34918
+ *
34919
+ * @example
34920
+ * ```typescript
34921
+ * buildReportedGasUsed(362454n, 29466364605n)
34922
+ * // { gas: '362454', gasPrice: '29466364605', fee: '10680201716540670' }
34923
+ * ```
34924
+ *
34925
+ * @internal
34926
+ */ function buildReportedGasUsed(gasUsed, effectiveGasPrice) {
34927
+ if (gasUsed === undefined || effectiveGasPrice === undefined) {
34928
+ return undefined;
34929
+ }
34930
+ return {
34931
+ gas: gasUsed.toString(),
34932
+ gasPrice: effectiveGasPrice.toString(),
34933
+ fee: (gasUsed * effectiveGasPrice).toString()
34934
+ };
34935
+ }
34936
+ /**
34937
+ * Report the outcome of an SDK-submitted same-chain Earn transaction.
34938
+ *
34939
+ * @param params - Transaction report parameters.
34940
+ * @throws {@link KitError} When the API call fails.
34941
+ *
34942
+ * @internal
34943
+ */ async function reportEarnTransaction(params) {
34944
+ const { pollingConfig, baseUrl } = buildConfig(params.config);
34945
+ const url = new URL(`${EARN_KIT_API_PREFIX}/transactions/report`, baseUrl);
34946
+ // The report endpoint is not idempotent: success reports refresh the gas
34947
+ // cache and failure reports increment counts. If the first request succeeds
34948
+ // server-side but the client times out or sees a transient 5xx, retrying
34949
+ // would duplicate the report (double-writing an outcome or inflating failure
34950
+ // counts). Reporting is best-effort (see the fire-and-forget caller), so
34951
+ // make exactly one attempt and never retry — a single dropped report is
34952
+ // preferable to a duplicated one. `maxRetries` here is the total attempt
34953
+ // count in pollApiWithValidation (loop runs `attempt <= maxRetries`), so 1
34954
+ // means one request with no retry; 0 would skip the request entirely.
34955
+ const reportConfig = {
34956
+ ...pollingConfig,
34957
+ maxRetries: 1
34958
+ };
34959
+ const gasUsed = buildReportedGasUsed(params.gasUsed, params.effectiveGasPrice);
34960
+ const requestBody = {
34961
+ execId: params.execId,
34962
+ chain: params.chain,
34963
+ status: params.status,
34964
+ action: params.action,
34965
+ ...params.txHash !== undefined && {
34966
+ txHash: params.txHash
34967
+ },
34968
+ ...gasUsed !== undefined && {
34969
+ gasUsed
34970
+ },
34971
+ ...params.errorCode !== undefined && {
34972
+ errorCode: params.errorCode
34973
+ }
34974
+ };
34975
+ try {
34976
+ await pollApiPost(url.toString(), requestBody, isTransactionReportResponse, reportConfig);
34977
+ } catch (error) {
34978
+ throw parseEarnApiError(error, {
34979
+ operation: 'transactionReport'
34980
+ });
34981
+ }
34982
+ }
34983
+
33776
34984
  /**
33777
34985
  * Sum the amounts across every token input to size the allowance approval.
33778
34986
  *
@@ -33883,6 +35091,59 @@ function toWithdrawalQuoteInfo(data) {
33883
35091
  // Intentionally built-ins-only: Earn bridge support is limited to SDK-known
33884
35092
  // token contracts plus the explicit ERC-3009 domain allowlist below.
33885
35093
  const TOKEN_REGISTRY = createTokenRegistry();
35094
+ function submitTransactionReport(reportContext, action, status, details) {
35095
+ void reportEarnTransaction({
35096
+ execId: reportContext.execId,
35097
+ chain: reportContext.chain,
35098
+ config: reportContext.config,
35099
+ action,
35100
+ status,
35101
+ ...details
35102
+ }).catch(()=>undefined);
35103
+ }
35104
+ function reportTransactionSuccess(reportContext, action, result) {
35105
+ if (result === undefined) {
35106
+ return;
35107
+ }
35108
+ submitTransactionReport(reportContext, action, 'success', {
35109
+ txHash: result.txHash,
35110
+ gasUsed: result.gasUsed,
35111
+ effectiveGasPrice: result.effectiveGasPrice
35112
+ });
35113
+ }
35114
+ function reportTransactionFailure(reportContext, action, error) {
35115
+ submitTransactionReport(reportContext, action, 'failure', {
35116
+ txHash: transactionReportTxHash(error),
35117
+ errorCode: transactionReportErrorCode(error)
35118
+ });
35119
+ }
35120
+ function transactionReportErrorCode(error) {
35121
+ if (isKitError(error)) {
35122
+ return error.name;
35123
+ }
35124
+ const message = getErrorMessage(error);
35125
+ if (/user (rejected|denied)|rejected by user/i.test(message)) {
35126
+ return 'USER_REJECTED';
35127
+ }
35128
+ if (/insufficient funds/i.test(message)) {
35129
+ return 'INSUFFICIENT_FUNDS';
35130
+ }
35131
+ if (/timeout|timed out/i.test(message)) {
35132
+ return 'TIMEOUT';
35133
+ }
35134
+ return 'UNKNOWN_ERROR';
35135
+ }
35136
+ function transactionReportTxHash(error) {
35137
+ if (!isKitError(error)) {
35138
+ return undefined;
35139
+ }
35140
+ const trace = error.cause?.trace;
35141
+ if (typeof trace !== 'object' || trace === null) {
35142
+ return undefined;
35143
+ }
35144
+ const txHash = trace['txHash'];
35145
+ return typeof txHash === 'string' && txHash !== '' ? txHash : undefined;
35146
+ }
33886
35147
  /**
33887
35148
  * Build the typed error raised when a cross-chain wait is cancelled via its
33888
35149
  * `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
@@ -34214,7 +35475,7 @@ function finishElapsedWait(lastStatus, lastError) {
34214
35475
  const adapterContractAddress = requireAdapterContract(chain);
34215
35476
  const { adapter } = params.from;
34216
35477
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34217
- const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
35478
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
34218
35479
  vaultAddress,
34219
35480
  amount: params.amount,
34220
35481
  address,
@@ -34222,32 +35483,55 @@ function finishElapsedWait(lastStatus, lastError) {
34222
35483
  config
34223
35484
  }), ()=>undefined);
34224
35485
  validateExecutionDeadline(executionParams);
35486
+ const transactionReportContext = {
35487
+ execId,
35488
+ chain: apiChain,
35489
+ config
35490
+ };
34225
35491
  const approvalToken = resolveEarnApprovalToken(executionParams);
34226
35492
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
34227
35493
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34228
35494
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
34229
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
35495
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35496
+ try {
35497
+ const approval = await approveAllowanceIfNeeded({
35498
+ adapter,
35499
+ chain,
35500
+ tokenAddress: approvalToken,
35501
+ delegate: adapterContractAddress,
35502
+ address,
35503
+ requiredAllowance,
35504
+ revertMessage: 'Earn deposit token approval reverted on-chain'
35505
+ });
35506
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
35507
+ return approval;
35508
+ } catch (error) {
35509
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
35510
+ throw error;
35511
+ }
35512
+ }, (approval)=>approval?.txHash);
35513
+ }
35514
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
35515
+ try {
35516
+ const result = await executeEarnAction({
34230
35517
  adapter,
34231
35518
  chain,
34232
- tokenAddress: approvalToken,
34233
- delegate: adapterContractAddress,
34234
35519
  address,
34235
- requiredAllowance,
34236
- revertMessage: 'Earn deposit token approval reverted on-chain'
34237
- }), (txHash)=>txHash);
34238
- }
34239
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>executeEarnAction({
34240
- adapter,
34241
- chain,
34242
- address,
34243
- actionKey: 'earn.deposit',
34244
- actionParams: {
34245
- executeParams: executionParams,
34246
- tokenInputs,
34247
- signature
34248
- },
34249
- revertMessage: 'Earn deposit reverted on-chain'
34250
- }), ({ txHash })=>txHash);
35520
+ actionKey: 'earn.deposit',
35521
+ actionParams: {
35522
+ executeParams: executionParams,
35523
+ tokenInputs,
35524
+ signature
35525
+ },
35526
+ revertMessage: 'Earn deposit reverted on-chain'
35527
+ });
35528
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
35529
+ return result;
35530
+ } catch (error) {
35531
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
35532
+ throw error;
35533
+ }
35534
+ }, ({ txHash })=>txHash);
34251
35535
  return {
34252
35536
  kind: 'same-chain',
34253
35537
  txHash,
@@ -34327,7 +35611,13 @@ function finishElapsedWait(lastStatus, lastError) {
34327
35611
  amount: params.amount,
34328
35612
  sourceChain: sourceChain.chain,
34329
35613
  destinationChain: destinationChain.chain,
34330
- expiresAt: prepared.expiresAt
35614
+ expiresAt: prepared.expiresAt,
35615
+ ...prepared.quoteIssuedAt !== undefined && {
35616
+ quoteIssuedAt: prepared.quoteIssuedAt
35617
+ },
35618
+ ...prepared.quoteExpiry !== undefined && {
35619
+ quoteExpiry: prepared.quoteExpiry
35620
+ }
34331
35621
  };
34332
35622
  }
34333
35623
  /** {@inheritdoc} */ async withdraw(params) {
@@ -34348,7 +35638,7 @@ function finishElapsedWait(lastStatus, lastError) {
34348
35638
  const adapterContractAddress = requireAdapterContract(chain);
34349
35639
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34350
35640
  const { adapter } = params.from;
34351
- const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
35641
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
34352
35642
  vaultAddress,
34353
35643
  amount: params.amount,
34354
35644
  address,
@@ -34356,32 +35646,55 @@ function finishElapsedWait(lastStatus, lastError) {
34356
35646
  config
34357
35647
  }), ()=>undefined);
34358
35648
  validateExecutionDeadline(executionParams);
35649
+ const transactionReportContext = {
35650
+ execId,
35651
+ chain: apiChain,
35652
+ config
35653
+ };
34359
35654
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
34360
35655
  const approvalToken = tokenInputs[0]?.token;
34361
35656
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34362
35657
  if (!options.skipApprove && approvalToken !== undefined) {
34363
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
35658
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35659
+ try {
35660
+ const approval = await approveAllowanceIfNeeded({
35661
+ adapter,
35662
+ chain,
35663
+ tokenAddress: approvalToken,
35664
+ delegate: adapterContractAddress,
35665
+ address,
35666
+ requiredAllowance,
35667
+ revertMessage: 'Vault share token approval reverted on-chain'
35668
+ });
35669
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
35670
+ return approval;
35671
+ } catch (error) {
35672
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
35673
+ throw error;
35674
+ }
35675
+ }, (approval)=>approval?.txHash);
35676
+ }
35677
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
35678
+ try {
35679
+ const result = await executeEarnAction({
34364
35680
  adapter,
34365
35681
  chain,
34366
- tokenAddress: approvalToken,
34367
- delegate: adapterContractAddress,
34368
35682
  address,
34369
- requiredAllowance,
34370
- revertMessage: 'Vault share token approval reverted on-chain'
34371
- }), (txHash)=>txHash);
34372
- }
34373
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>executeEarnAction({
34374
- adapter,
34375
- chain,
34376
- address,
34377
- actionKey: 'earn.withdraw',
34378
- actionParams: {
34379
- executeParams: executionParams,
34380
- tokenInputs,
34381
- signature
34382
- },
34383
- revertMessage: 'Earn withdraw reverted on-chain'
34384
- }), ({ txHash })=>txHash);
35683
+ actionKey: 'earn.withdraw',
35684
+ actionParams: {
35685
+ executeParams: executionParams,
35686
+ tokenInputs,
35687
+ signature
35688
+ },
35689
+ revertMessage: 'Earn withdraw reverted on-chain'
35690
+ });
35691
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
35692
+ return result;
35693
+ } catch (error) {
35694
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
35695
+ throw error;
35696
+ }
35697
+ }, ({ txHash })=>txHash);
34385
35698
  return {
34386
35699
  txHash,
34387
35700
  explorerUrl,
@@ -34515,141 +35828,6 @@ function finishElapsedWait(lastStatus, lastError) {
34515
35828
  }
34516
35829
  }
34517
35830
  }
34518
- gasEstimateFailure(name, chain, error) {
34519
- return {
34520
- name,
34521
- token: chain.nativeCurrency.symbol,
34522
- blockchain: chain.chain,
34523
- fees: null,
34524
- error: getErrorMessage(error)
34525
- };
34526
- }
34527
- async estimateDepositQuoteGasFees(params) {
34528
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
34529
- try {
34530
- const adapterContractAddress = requireAdapterContract(chain);
34531
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34532
- const { executionParams, signature } = await fetchDeposit({
34533
- vaultAddress: normalizedVaultAddress,
34534
- amount,
34535
- address,
34536
- chain: apiChain,
34537
- config
34538
- });
34539
- validateExecutionDeadline(executionParams);
34540
- const approvalToken = resolveEarnApprovalToken(executionParams);
34541
- const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
34542
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34543
- return await estimateEarnQuoteGasFees({
34544
- adapter,
34545
- chain,
34546
- address,
34547
- actionName: 'Deposit',
34548
- actionKey: 'earn.deposit',
34549
- actionParams: {
34550
- executeParams: executionParams,
34551
- tokenInputs,
34552
- signature
34553
- },
34554
- approval: approvalToken !== undefined && requiredAllowance > 0n ? {
34555
- token: approvalToken,
34556
- delegate: adapterContractAddress,
34557
- requiredAllowance
34558
- } : undefined
34559
- });
34560
- } catch (error) {
34561
- return [
34562
- this.gasEstimateFailure('Deposit', chain, error)
34563
- ];
34564
- }
34565
- }
34566
- async estimateWithdrawalQuoteGasFees(params) {
34567
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
34568
- try {
34569
- const adapterContractAddress = requireAdapterContract(chain);
34570
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34571
- const { executionParams, signature } = await fetchWithdraw({
34572
- vaultAddress: normalizedVaultAddress,
34573
- amount,
34574
- address,
34575
- chain: apiChain,
34576
- config
34577
- });
34578
- validateExecutionDeadline(executionParams);
34579
- const tokenInputs = buildEarnTokenInputs(executionParams, normalizedVaultAddress);
34580
- const approvalToken = tokenInputs[0]?.token;
34581
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34582
- return await estimateEarnQuoteGasFees({
34583
- adapter,
34584
- chain,
34585
- address,
34586
- actionName: 'Withdraw',
34587
- actionKey: 'earn.withdraw',
34588
- actionParams: {
34589
- executeParams: executionParams,
34590
- tokenInputs,
34591
- signature
34592
- },
34593
- approval: approvalToken !== undefined ? {
34594
- token: approvalToken,
34595
- delegate: adapterContractAddress,
34596
- requiredAllowance
34597
- } : undefined
34598
- });
34599
- } catch (error) {
34600
- return [
34601
- this.gasEstimateFailure('Withdraw', chain, error)
34602
- ];
34603
- }
34604
- }
34605
- async estimateClaimRewardsQuoteGasFees(params) {
34606
- const { adapter, chain, apiChain, address, vaultAddress, config } = params;
34607
- try {
34608
- requireAdapterContract(chain);
34609
- const { rewards, executionParams, signature } = await fetchClaimRewards({
34610
- address,
34611
- chain: apiChain,
34612
- vaultAddress,
34613
- config
34614
- });
34615
- if (rewards.length === 0) {
34616
- return [];
34617
- }
34618
- const missingExecutionParams = executionParams === undefined;
34619
- const missingSignature = signature === undefined;
34620
- if (missingExecutionParams || missingSignature) {
34621
- throw new KitError({
34622
- ...EarnError.INTERNAL_ERROR,
34623
- recoverability: 'RETRYABLE',
34624
- message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
34625
- cause: {
34626
- trace: {
34627
- rewardsCount: rewards.length,
34628
- missingExecutionParams,
34629
- missingSignature
34630
- }
34631
- }
34632
- });
34633
- }
34634
- validateExecutionDeadline(executionParams);
34635
- return await estimateEarnQuoteGasFees({
34636
- adapter,
34637
- chain,
34638
- address,
34639
- actionName: 'Claim Rewards',
34640
- actionKey: 'earn.claimRewards',
34641
- actionParams: {
34642
- executeParams: executionParams,
34643
- tokenInputs: [],
34644
- signature
34645
- }
34646
- });
34647
- } catch (error) {
34648
- return [
34649
- this.gasEstimateFailure('Claim Rewards', chain, error)
34650
- ];
34651
- }
34652
- }
34653
35831
  /** {@inheritdoc} */ async getDepositQuote(params) {
34654
35832
  const config = this.resolveConfig(params.config);
34655
35833
  if (hasQuoteDestinationChain(params)) {
@@ -34672,96 +35850,43 @@ function finishElapsedWait(lastStatus, lastError) {
34672
35850
  throw createValidationFailedError$1('chain', destinationChain.chain, 'chain is only supported for cross-chain Earn deposit quotes; omit chain/address when quoting on the source chain');
34673
35851
  }
34674
35852
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
34675
- // The quote fetch and the gas estimation share no data, so run them
34676
- // concurrently. The estimator never rejects (failures fold into
34677
- // `{ fees: null }` entries), so only a quote failure can throw here.
34678
- const [quote, gasFees] = await Promise.all([
34679
- fetchDepositQuote({
34680
- vaultAddress: params.vaultAddress,
34681
- amount: params.amount,
34682
- address,
34683
- chain,
34684
- config
34685
- }),
34686
- this.estimateDepositQuoteGasFees({
34687
- adapter: params.from.adapter,
34688
- chain: chainDefinition,
34689
- apiChain: chain,
34690
- address,
34691
- vaultAddress: params.vaultAddress,
34692
- amount: params.amount,
34693
- config
34694
- })
34695
- ]);
34696
- return {
34697
- ...quote,
34698
- gasFees
34699
- };
35853
+ // Gas is estimated server-side by the Earn Service and returned on the quote, so the
35854
+ // SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
35855
+ // the native token symbol / blockchain on each gas entry.
35856
+ return fetchDepositQuote({
35857
+ vaultAddress: params.vaultAddress,
35858
+ amount: params.amount,
35859
+ address,
35860
+ chain,
35861
+ config,
35862
+ chainDefinition
35863
+ });
34700
35864
  }
34701
35865
  /** {@inheritdoc} */ async getWithdrawalQuote(params) {
34702
35866
  const config = this.resolveConfig(params.config);
34703
35867
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
34704
- // The quote fetch and the gas estimation share no data, so run them
34705
- // concurrently. The estimator never rejects (failures fold into
34706
- // `{ fees: null }` entries), so only a quote failure can throw here.
34707
- const [quote, gasFees] = await Promise.all([
34708
- fetchWithdrawalQuote({
34709
- vaultAddress: params.vaultAddress,
34710
- amount: params.amount,
34711
- address,
34712
- chain,
34713
- config
34714
- }),
34715
- this.estimateWithdrawalQuoteGasFees({
34716
- adapter: params.from.adapter,
34717
- chain: chainDefinition,
34718
- apiChain: chain,
34719
- address,
34720
- vaultAddress: params.vaultAddress,
34721
- amount: params.amount,
34722
- config
34723
- })
34724
- ]);
34725
- return {
34726
- ...quote,
34727
- gasFees
34728
- };
35868
+ // Gas is estimated server-side by the Earn Service and returned on the quote.
35869
+ return fetchWithdrawalQuote({
35870
+ vaultAddress: params.vaultAddress,
35871
+ amount: params.amount,
35872
+ address,
35873
+ chain,
35874
+ config,
35875
+ chainDefinition
35876
+ });
34729
35877
  }
34730
35878
  /** {@inheritdoc} */ async getClaimRewardsQuote(params) {
34731
35879
  const config = this.resolveConfig(params.config);
34732
- const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
34733
- const quote = await fetchClaimRewardsQuote({
35880
+ const { address, chain } = await resolveAdapterContext(params.from);
35881
+ // The claimRewards/quote response carries no gas estimate (unlike
35882
+ // deposit/withdrawal quotes), and the SDK no longer estimates gas locally,
35883
+ // so gasFees is always empty for claim rewards.
35884
+ return fetchClaimRewardsQuote({
34734
35885
  vaultAddress: params.vaultAddress,
34735
35886
  address,
34736
35887
  chain,
34737
35888
  config
34738
35889
  });
34739
- // No claimable rewards means there is nothing to execute, so there is no
34740
- // gas to estimate. Short-circuit on the already-fetched quote rather than
34741
- // calling the (heavier) claim execution endpoint again — this also keeps
34742
- // `gasFees` empty as documented, instead of risking a `{ fees: null }`
34743
- // estimation-error entry when the adapter/RPC is unavailable. This
34744
- // short-circuit is why the claim path stays sequential instead of using
34745
- // the Promise.all pattern of the deposit/withdrawal quotes: estimating in
34746
- // parallel would hit the signing endpoint even when nothing is claimable.
34747
- if (quote.rewards.length === 0) {
34748
- return {
34749
- ...quote,
34750
- gasFees: []
34751
- };
34752
- }
34753
- const gasFees = await this.estimateClaimRewardsQuoteGasFees({
34754
- adapter: params.from.adapter,
34755
- chain: chainDefinition,
34756
- apiChain: chain,
34757
- address,
34758
- vaultAddress: params.vaultAddress,
34759
- config
34760
- });
34761
- return {
34762
- ...quote,
34763
- gasFees
34764
- };
34765
35890
  }
34766
35891
  }
34767
35892
  function hasDepositDestination(params) {
@@ -34999,11 +36124,27 @@ function formatPositionPnL(pnl) {
34999
36124
  * @param vault - Provider vault info with raw amount objects
35000
36125
  * @returns Vault info with total deposits and liquidity formatted as strings
35001
36126
  */ function formatVaultInfo(vault) {
35002
- const { totalDeposits, liquidity, ...rest } = vault;
36127
+ // The flat `totalDeposits`/`liquidity` are deprecated aliases that are
36128
+ // intentionally dual-read through the migration window so existing
36129
+ // consumers keep receiving them until Contract.
36130
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
36131
+ const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
36132
+ // `liquidityProfile` is `.optional()` in the response schema during the
36133
+ // expand/contract window (an old backend that predates the nested facets
36134
+ // omits it), so only format and re-attach it when present — matching the
36135
+ // provider-side `toVaultInfo` mapper.
35003
36136
  return {
35004
36137
  ...rest,
35005
36138
  totalDeposits: formatAmount$1(totalDeposits),
35006
- liquidity: formatAmount$1(liquidity)
36139
+ liquidity: formatAmount$1(liquidity),
36140
+ ...liquidityProfile !== undefined && {
36141
+ liquidityProfile: {
36142
+ ...liquidityProfile,
36143
+ totalDeposits: formatAmount$1(liquidityProfile.totalDeposits),
36144
+ available: formatAmount$1(liquidityProfile.available),
36145
+ totalSupply: formatAmount$1(liquidityProfile.totalSupply)
36146
+ }
36147
+ }
35007
36148
  };
35008
36149
  }
35009
36150
  /**
@@ -36915,25 +38056,6 @@ function formatRetryResult(operation, result) {
36915
38056
  // Auto-register this kit for user agent tracking
36916
38057
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
36917
38058
 
36918
- /**
36919
- * Create an EarnKit instance for AppKit earn operations.
36920
- *
36921
- * @remarks The context parameter is reserved for future EarnKit wiring.
36922
- * EarnKit does not currently support AppKit developer fee hooks, so the
36923
- * factory does not read fee callbacks from the context. Earn custom fees remain
36924
- * reserved until EarnKit fee support ships.
36925
- *
36926
- * When EarnKit supports developer fee hooks, this factory can wire AppKit context through.
36927
- *
36928
- * @param context - AppKit context reserved for future EarnKit wiring
36929
- * @returns A new EarnKit instance
36930
- *
36931
- * @example
36932
- * ```typescript
36933
- * const earnKit = createEarnKit(context)
36934
- * ```
36935
- */ const createEarnKit = ()=>new EarnKit();
36936
-
36937
38059
  /**
36938
38060
  * Register event handlers from a context actions map to a kit instance.
36939
38061
  *
@@ -36998,6 +38120,29 @@ registerKit(`${pkg$2.name}/${pkg$2.version}`);
36998
38120
  }
36999
38121
  };
37000
38122
 
38123
+ /**
38124
+ * Create an EarnKit instance for AppKit earn operations.
38125
+ *
38126
+ * Attaches any earn event handlers previously registered on the AppKit
38127
+ * context (via `kit.on('earn.*', …)` or `kit.on('*', …)`) so step events
38128
+ * fire during the returned kit's earn operations.
38129
+ *
38130
+ * @remarks Developer fee hooks from the AppKit context are not applied.
38131
+ * EarnKit does not yet support custom fee policies.
38132
+ *
38133
+ * @param context - AppKit context with earn event handlers and kit options
38134
+ * @returns An EarnKit instance ready for AppKit earn operations
38135
+ *
38136
+ * @example
38137
+ * ```typescript
38138
+ * const earnKit = createEarnKit(context)
38139
+ * ```
38140
+ */ const createEarnKit = (context)=>{
38141
+ const kit = new EarnKit();
38142
+ registerActionHandlers(kit, context.actions.earn, 'earn');
38143
+ return kit;
38144
+ };
38145
+
37001
38146
  /**
37002
38147
  * List of all supported token aliases for App Kit send operations.
37003
38148
  *
@@ -37788,7 +38933,8 @@ const tokens = createTokenRegistry();
37788
38933
  * polling loop yourself.
37789
38934
  *
37790
38935
  * @param context - AppKit context.
37791
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
38936
+ * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
38937
+ * `kitKey`.
37792
38938
  * @returns A snapshot of the swap's status at the time of the call.
37793
38939
  *
37794
38940
  * @example
@@ -37877,7 +39023,7 @@ const tokens = createTokenRegistry();
37877
39023
  * addresses before indexing into `result.rates[chain]`.
37878
39024
  *
37879
39025
  * @param context - AppKit context.
37880
- * @param params - `chain`, optional `tokens`, and `kitKey`.
39026
+ * @param params - `chain`, plus optional `tokens` and `kitKey`.
37881
39027
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
37882
39028
  *
37883
39029
  * @example
@@ -37972,7 +39118,7 @@ const tokens = createTokenRegistry();
37972
39118
  }
37973
39119
  // Return earn-specific chains
37974
39120
  if (operationType === 'earn') {
37975
- const earnKit = createEarnKit();
39121
+ const earnKit = createEarnKit(context);
37976
39122
  return earnKit.getSupportedChains();
37977
39123
  }
37978
39124
  // Return unified balance chains
@@ -37983,7 +39129,7 @@ const tokens = createTokenRegistry();
37983
39129
  // Reuse kit instances here if provider constructors become expensive or stateful.
37984
39130
  const bridgeKit = createBridgeKit(context);
37985
39131
  const swapKit = createSwapKit(context);
37986
- const earnKit = createEarnKit();
39132
+ const earnKit = createEarnKit(context);
37987
39133
  const bridgeChains = bridgeKit.getSupportedChains();
37988
39134
  const swapChains = swapKit.getSupportedChains();
37989
39135
  const earnChains = earnKit.getSupportedChains();
@@ -38005,7 +39151,7 @@ const tokens = createTokenRegistry();
38005
39151
  };
38006
39152
 
38007
39153
  async function deposit$2(context, params) {
38008
- return createEarnKit().deposit(params);
39154
+ return createEarnKit(context).deposit(params);
38009
39155
  }
38010
39156
  /**
38011
39157
  * Execute an earn withdrawal operation.
@@ -38029,7 +39175,7 @@ async function deposit$2(context, params) {
38029
39175
  * })
38030
39176
  * ```
38031
39177
  */ async function withdraw(context, params) {
38032
- return createEarnKit().withdraw(params);
39178
+ return createEarnKit(context).withdraw(params);
38033
39179
  }
38034
39180
  /**
38035
39181
  * Claim earn rewards.
@@ -38052,7 +39198,7 @@ async function deposit$2(context, params) {
38052
39198
  * })
38053
39199
  * ```
38054
39200
  */ async function claimRewards(context, params) {
38055
- return createEarnKit().claimRewards(params);
39201
+ return createEarnKit(context).claimRewards(params);
38056
39202
  }
38057
39203
  /**
38058
39204
  * Fetch vault information.
@@ -38074,7 +39220,7 @@ async function deposit$2(context, params) {
38074
39220
  * })
38075
39221
  * ```
38076
39222
  */ async function getVaults(context, params) {
38077
- return createEarnKit().getVaults(params);
39223
+ return createEarnKit(context).getVaults(params);
38078
39224
  }
38079
39225
  /**
38080
39226
  * Discover vaults available on a chain.
@@ -38098,7 +39244,7 @@ async function deposit$2(context, params) {
38098
39244
  * })
38099
39245
  * ```
38100
39246
  */ async function exploreVaults(context, params) {
38101
- return createEarnKit().exploreVaults(params);
39247
+ return createEarnKit(context).exploreVaults(params);
38102
39248
  }
38103
39249
  /**
38104
39250
  * Lazily iterate every vault available on a chain.
@@ -38122,7 +39268,7 @@ async function deposit$2(context, params) {
38122
39268
  * }
38123
39269
  * ```
38124
39270
  */ function exploreVaultsIterator(context, params) {
38125
- return createEarnKit().exploreVaultsIterator(params);
39271
+ return createEarnKit(context).exploreVaultsIterator(params);
38126
39272
  }
38127
39273
  /**
38128
39274
  * Fetch a wallet position in a vault.
@@ -38145,7 +39291,7 @@ async function deposit$2(context, params) {
38145
39291
  * })
38146
39292
  * ```
38147
39293
  */ async function getPosition(context, params) {
38148
- return createEarnKit().getPosition(params);
39294
+ return createEarnKit(context).getPosition(params);
38149
39295
  }
38150
39296
  /**
38151
39297
  * Fetch the current status of a cross-chain Earn deposit.
@@ -38167,7 +39313,7 @@ async function deposit$2(context, params) {
38167
39313
  * console.log(status.status)
38168
39314
  * ```
38169
39315
  */ async function getCrossChainDepositStatus(context, params) {
38170
- return createEarnKit().getCrossChainDepositStatus(params);
39316
+ return createEarnKit(context).getCrossChainDepositStatus(params);
38171
39317
  }
38172
39318
  /**
38173
39319
  * Poll a cross-chain Earn deposit until it reaches a terminal bridge state.
@@ -38190,7 +39336,7 @@ async function deposit$2(context, params) {
38190
39336
  * console.log(result.outcome)
38191
39337
  * ```
38192
39338
  */ async function waitForCrossChainDeposit(context, params) {
38193
- return createEarnKit().waitForCrossChainDeposit(params);
39339
+ return createEarnKit(context).waitForCrossChainDeposit(params);
38194
39340
  }
38195
39341
  /**
38196
39342
  * Fetch a deposit quote.
@@ -38214,7 +39360,7 @@ async function deposit$2(context, params) {
38214
39360
  * })
38215
39361
  * ```
38216
39362
  */ async function getDepositQuote(context, params) {
38217
- return createEarnKit().getDepositQuote(params);
39363
+ return createEarnKit(context).getDepositQuote(params);
38218
39364
  }
38219
39365
  /**
38220
39366
  * Fetch a withdrawal quote.
@@ -38238,7 +39384,7 @@ async function deposit$2(context, params) {
38238
39384
  * })
38239
39385
  * ```
38240
39386
  */ async function getWithdrawalQuote(context, params) {
38241
- return createEarnKit().getWithdrawalQuote(params);
39387
+ return createEarnKit(context).getWithdrawalQuote(params);
38242
39388
  }
38243
39389
  /**
38244
39390
  * Fetch a claim rewards quote.
@@ -38261,11 +39407,47 @@ async function deposit$2(context, params) {
38261
39407
  * })
38262
39408
  * ```
38263
39409
  */ async function getClaimRewardsQuote(context, params) {
38264
- return createEarnKit().getClaimRewardsQuote(params);
39410
+ return createEarnKit(context).getClaimRewardsQuote(params);
39411
+ }
39412
+ /**
39413
+ * Resume a multi-phase earn operation that previously failed.
39414
+ *
39415
+ * Pass the {@link KitError} caught from `deposit`, `withdraw`, or
39416
+ * `claimRewards`. Completed phases can be skipped when the error carries
39417
+ * earn retry context. Call `isRetryableError(error)` first.
39418
+ *
39419
+ * @remarks
39420
+ * Retry re-fetches execution params and may re-submit the execute
39421
+ * transaction. Treat this as best-effort recovery if a prior execute
39422
+ * broadcast may still be in flight.
39423
+ *
39424
+ * @param context - AppKit context
39425
+ * @param error - The error caught from a previous multi-phase earn operation
39426
+ * @returns Promise resolving to the result of the resumed operation
39427
+ * @throws If the error is not retryable or lacks earn retry context
39428
+ *
39429
+ * @example
39430
+ * ```typescript
39431
+ * import { isRetryableError } from '@circle-fin/app-kit'
39432
+ * import { createContext } from '@circle-fin/app-kit/context'
39433
+ * import { retry } from '@circle-fin/app-kit/earn'
39434
+ *
39435
+ * const context = createContext()
39436
+ *
39437
+ * try {
39438
+ * await deposit(context, params)
39439
+ * } catch (error) {
39440
+ * if (isRetryableError(error)) {
39441
+ * const result = await retry(context, error)
39442
+ * }
39443
+ * }
39444
+ * ```
39445
+ */ async function retry(context, error) {
39446
+ return createEarnKit(context).retry(error);
38265
39447
  }
38266
39448
 
38267
39449
  var name = "@circle-fin/unified-balance-kit";
38268
- var version = "1.2.2";
39450
+ var version = "1.3.0";
38269
39451
  var pkg = {
38270
39452
  name: name,
38271
39453
  version: version};
@@ -40733,29 +41915,33 @@ const CIRCLE_BPS_DIVISOR = 10_000n;
40733
41915
  const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40734
41916
  /**
40735
41917
  * Return the estimated Gateway gas fee for a chain in USDC atomic units.
40736
- * Falls back to a conservative 0.1 USDC for unlisted chains.
41918
+ * Prefers an entry in `overrides` (the real per-chain fee derived from a
41919
+ * prior estimate), then the static {@link GAS_FEE_BY_CHAIN} constant, and
41920
+ * finally a conservative 0.1 USDC fallback for unlisted chains.
40737
41921
  *
40738
41922
  * @param chain - The source blockchain.
41923
+ * @param overrides - Optional real per-chain gas fees keyed by chain.
40739
41924
  * @returns Gas fee in USDC atomic units.
40740
- */ function getGasFee(chain) {
40741
- return GAS_FEE_BY_CHAIN.get(chain) ?? DEFAULT_GAS_FEE;
41925
+ */ function getGasFee(chain, overrides) {
41926
+ return overrides?.get(chain) ?? GAS_FEE_BY_CHAIN.get(chain) ?? DEFAULT_GAS_FEE;
40742
41927
  }
40743
41928
  /**
40744
41929
  * Return the estimated forwarder fee for the destination chain
40745
41930
  * (service fee + destination gas fee).
40746
41931
  *
40747
41932
  * @param destinationChain - The mint destination chain.
41933
+ * @param overrides - Optional real per-chain gas fees keyed by chain.
40748
41934
  * @returns Forwarder fee in USDC atomic units.
40749
- */ function getForwarderFee(destinationChain) {
40750
- const destGas = getGasFee(destinationChain);
41935
+ */ function getForwarderFee(destinationChain, overrides) {
41936
+ const destGas = getGasFee(destinationChain, overrides);
40751
41937
  return FORWARDER_SERVICE_FEE + destGas;
40752
41938
  }
40753
41939
  /**
40754
41940
  * Estimate the fixed fees (gas + forwarder) and compute the maximum
40755
41941
  * amount that can be drawn from this chain for a single intent,
40756
41942
  * accounting for the 0.5 bps transfer fee if cross-chain.
40757
- */ function computeMaxDrawable(slot, forwarderFeeRemaining) {
40758
- const gasFee = getGasFee(slot.chain);
41943
+ */ function computeMaxDrawable(slot, forwarderFeeRemaining, overrides) {
41944
+ const gasFee = getGasFee(slot.chain, overrides);
40759
41945
  let fixedFees = gasFee;
40760
41946
  let forwarderFeeUsed = 0n;
40761
41947
  if (forwarderFeeRemaining > 0n) {
@@ -40787,14 +41973,14 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40787
41973
  * buffer), and returns the allocations for this pass.
40788
41974
  *
40789
41975
  * Mutates `slot.remaining` so the next pass sees reduced balances.
40790
- */ function greedyAllocate(slots, amount, destinationChain, useForwarder) {
41976
+ */ function greedyAllocate(slots, amount, destinationChain, useForwarder, overrides) {
40791
41977
  const result = [];
40792
41978
  let remaining = amount;
40793
- let forwarderFeeRemaining = useForwarder ? getForwarderFee(destinationChain) : 0n;
41979
+ let forwarderFeeRemaining = useForwarder ? getForwarderFee(destinationChain, overrides) : 0n;
40794
41980
  if (remaining <= 0n) return result;
40795
41981
  for (const slot of slots){
40796
41982
  if (remaining <= 0n) break;
40797
- const { drawable, gasFee, forwarderFeeUsed } = computeMaxDrawable(slot, forwarderFeeRemaining);
41983
+ const { drawable, gasFee, forwarderFeeUsed } = computeMaxDrawable(slot, forwarderFeeRemaining, overrides);
40798
41984
  if (drawable <= 0n) continue;
40799
41985
  // Greedy: take as much as we can from this chain
40800
41986
  const take = remaining < drawable ? remaining : drawable // NOSONAR: This is a false positive — Math.min() only accepts number, not bigint, so the ternary is the correct pattern here.
@@ -40893,7 +42079,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40893
42079
  // After this pass, slot.remaining reflects consumed capacity.
40894
42080
  // -----------------------------------------------------------------------
40895
42081
  const transferAmount = parseUnits(ctx.amountIn, USDC_DECIMALS);
40896
- const allocations = greedyAllocate(slots, transferAmount, ctx.destinationChain, ctx.useForwarder);
42082
+ const allocations = greedyAllocate(slots, transferAmount, ctx.destinationChain, ctx.useForwarder, ctx.gasFeeOverrides);
40897
42083
  assertFullyAllocated(allocations, transferAmount, ctx.amountIn);
40898
42084
  // -----------------------------------------------------------------------
40899
42085
  // 4. Phase 2 — Allocate developer fee
@@ -40901,7 +42087,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40901
42087
  // Same-chain first here too — if the destination chain still has
40902
42088
  // capacity, use it (same-chain fee intent = cheapest gas).
40903
42089
  // -----------------------------------------------------------------------
40904
- const developerFeeAllocations = greedyAllocate(slots, devFeeAmount, ctx.destinationChain, false);
42090
+ const developerFeeAllocations = greedyAllocate(slots, devFeeAmount, ctx.destinationChain, false, ctx.gasFeeOverrides);
40905
42091
  if (devFeeAmount > 0n) {
40906
42092
  assertFullyAllocated(developerFeeAllocations, devFeeAmount, formatUnits(devFeeAmount.toString(), USDC_DECIMALS));
40907
42093
  }
@@ -40910,7 +42096,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40910
42096
  // Again same ordering, same shared reduced balances.
40911
42097
  // Same-chain first for the same reason.
40912
42098
  // -----------------------------------------------------------------------
40913
- const circleFeeAllocations = greedyAllocate(slots, circleFeeAmount, ctx.destinationChain, false);
42099
+ const circleFeeAllocations = greedyAllocate(slots, circleFeeAmount, ctx.destinationChain, false, ctx.gasFeeOverrides);
40914
42100
  if (circleFeeAmount > 0n) {
40915
42101
  assertFullyAllocated(circleFeeAllocations, circleFeeAmount, formatUnits(circleFeeAmount.toString(), USDC_DECIMALS));
40916
42102
  }
@@ -41009,10 +42195,12 @@ const BPS_DIVISOR = 100_000n;
41009
42195
  *
41010
42196
  * Unlike `findChainNameByDomain` (which returns the display `name`),
41011
42197
  * this returns `chain.chain` — the enum identifier expected by
41012
- * {@link FeeAllocation}.
42198
+ * {@link FeeAllocation}. Returns `undefined` when no allocation covers the
42199
+ * domain so callers skip the intent rather than bucketing it under a
42200
+ * fabricated sentinel.
41013
42201
  */ function findBlockchainByDomain(domain, allocations) {
41014
42202
  const alloc = allocations.find((a)=>a.chain.gateway.domain === domain);
41015
- return alloc?.chain.chain ?? 'Unknown';
42203
+ return alloc?.chain.chain;
41016
42204
  }
41017
42205
  /**
41018
42206
  * Normalize any address/salt format to lowercase bytes32 hex.
@@ -41136,6 +42324,33 @@ const BPS_DIVISOR = 100_000n;
41136
42324
  };
41137
42325
  });
41138
42326
  }
42327
+ /**
42328
+ * Read an intent's transfer value as a BigInt, tolerating the string form
42329
+ * that can appear on estimate-response specs.
42330
+ */ function intentValue(intent) {
42331
+ const { value } = intent.spec;
42332
+ return typeof value === 'bigint' ? value : safeBigInt(String(value), 'spec.value');
42333
+ }
42334
+ /**
42335
+ * Split a single intent's `maxFee` into its transfer-fee and gas-fee
42336
+ * components.
42337
+ *
42338
+ * `transferFee = value * GATEWAY_TRANSFER_FEE_SCALED_BPS / BPS_DIVISOR`
42339
+ * `gasFee = maxFee - transferFee`
42340
+ *
42341
+ * Same-chain transfers (withdrawals) do not incur a transfer fee, so the
42342
+ * whole `maxFee` is gas. See {@link aggregateFeesByIntent} for the caveats
42343
+ * on re-deriving the split locally.
42344
+ */ function splitIntentFee(intent) {
42345
+ const { maxFee, spec } = intent;
42346
+ const isSameChain = spec.sourceDomain === spec.destinationDomain;
42347
+ const transferFee = isSameChain ? 0n : intentValue(intent) * GATEWAY_TRANSFER_FEE_SCALED_BPS / BPS_DIVISOR;
42348
+ const gasFee = maxFee > transferFee ? maxFee - transferFee : 0n;
42349
+ return {
42350
+ transferFee,
42351
+ gasFee
42352
+ };
42353
+ }
41139
42354
  /**
41140
42355
  * Decompose each intent's `maxFee` into a transfer fee and a gas fee,
41141
42356
  * then aggregate both by source chain.
@@ -41160,7 +42375,6 @@ const BPS_DIVISOR = 100_000n;
41160
42375
  * names from source domains.
41161
42376
  * @returns Per-chain and total transfer/gas fee breakdowns.
41162
42377
  */ function aggregateFeesByIntent(estimatedIntents, allocations) {
41163
- const transferFeeBps = GATEWAY_TRANSFER_FEE_SCALED_BPS;
41164
42378
  const transferFeeByChain = new Map();
41165
42379
  const gasFeeByChain = new Map();
41166
42380
  let totalTransferFee = 0n;
@@ -41168,18 +42382,18 @@ const BPS_DIVISOR = 100_000n;
41168
42382
  for (const intent of estimatedIntents){
41169
42383
  const { maxFee, spec } = intent;
41170
42384
  if (maxFee === 0n) continue;
41171
- const chainName = findBlockchainByDomain(spec.sourceDomain, allocations);
41172
- const value = typeof spec.value === 'bigint' ? spec.value : safeBigInt(String(spec.value), 'spec.value');
41173
- const isSameChain = spec.sourceDomain === spec.destinationDomain;
41174
- const transferFee = isSameChain ? 0n : value * transferFeeBps / BPS_DIVISOR;
41175
- const gasFee = maxFee > transferFee ? maxFee - transferFee : 0n;
42385
+ const { transferFee, gasFee } = splitIntentFee(intent);
42386
+ totalTransferFee += transferFee;
42387
+ totalGasFee += gasFee;
42388
+ // Totals stay complete even if a domain can't be resolved; only the
42389
+ // per-chain breakdown skips it rather than inventing a placeholder chain.
42390
+ const chain = findBlockchainByDomain(spec.sourceDomain, allocations);
42391
+ if (chain === undefined) continue;
41176
42392
  if (transferFee > 0n) {
41177
- totalTransferFee += transferFee;
41178
- transferFeeByChain.set(chainName, (transferFeeByChain.get(chainName) ?? 0n) + transferFee);
42393
+ transferFeeByChain.set(chain, (transferFeeByChain.get(chain) ?? 0n) + transferFee);
41179
42394
  }
41180
42395
  if (gasFee > 0n) {
41181
- totalGasFee += gasFee;
41182
- gasFeeByChain.set(chainName, (gasFeeByChain.get(chainName) ?? 0n) + gasFee);
42396
+ gasFeeByChain.set(chain, (gasFeeByChain.get(chain) ?? 0n) + gasFee);
41183
42397
  }
41184
42398
  }
41185
42399
  return {
@@ -41243,6 +42457,91 @@ const BPS_DIVISOR = 100_000n;
41243
42457
  }
41244
42458
  return fees;
41245
42459
  }
42460
+ /**
42461
+ * Derive the real per-chain Gateway gas fee from estimated intents, keyed by
42462
+ * source {@link Blockchain}.
42463
+ *
42464
+ * The value is the maximum single-intent gas fee observed on each chain
42465
+ * (`maxFee − transferFee`) — the amount `computeAutoAllocation` must reserve
42466
+ * per burn intent on that chain. Gas is (near) amount-independent, so every
42467
+ * intent on a chain pays roughly the same; taking the max is a conservative
42468
+ * choice for the multi-intent-per-chain case.
42469
+ *
42470
+ * Intended for `AutoAllocationContext.gasFeeOverrides` so the corrective
42471
+ * re-allocation pass reserves the API's real fee instead of the static
42472
+ * {@link GAS_FEE_BY_CHAIN} constant.
42473
+ *
42474
+ * @param estimatedIntents - Intents with `maxFee` from {@link parseEstimateResponse}.
42475
+ * @param allocations - Normalised allocations used to resolve chain from source domain.
42476
+ * @returns Per-chain real gas fees in USDC atomic units.
42477
+ *
42478
+ * @example
42479
+ * ```typescript
42480
+ * import type { BurnIntent } from '../createIntent/types'
42481
+ * import type { NormalizedAllocation } from '../allocations'
42482
+ *
42483
+ * declare const estimatedIntents: BurnIntent[]
42484
+ * declare const allocations: NormalizedAllocation[]
42485
+ *
42486
+ * // Real per-chain gas, ready to pass as AutoAllocationContext.gasFeeOverrides
42487
+ * // to re-run computeAutoAllocation with the corrected reserve.
42488
+ * const overrides = deriveGasFeeOverrides(estimatedIntents, allocations)
42489
+ * ```
42490
+ */ function deriveGasFeeOverrides(estimatedIntents, allocations) {
42491
+ const overrides = new Map();
42492
+ for (const intent of estimatedIntents){
42493
+ if (intent.maxFee === 0n) continue;
42494
+ const chain = findBlockchainByDomain(intent.spec.sourceDomain, allocations);
42495
+ if (chain === undefined) continue;
42496
+ const { gasFee } = splitIntentFee(intent);
42497
+ const prev = overrides.get(chain) ?? 0n;
42498
+ if (gasFee > prev) overrides.set(chain, gasFee);
42499
+ }
42500
+ return overrides;
42501
+ }
42502
+ /**
42503
+ * Sum the total balance each source chain must cover, keyed by source
42504
+ * {@link Blockchain}.
42505
+ *
42506
+ * Approximates the Gateway API's balance validation, which rejects a transfer
42507
+ * (`BALANCE_INSUFFICIENT_TOKEN`) when a depositor's confirmed balance on a
42508
+ * source chain is below `sum(intent.value + intent.maxFee)` for that
42509
+ * depositor's intents. This aggregates by chain across all sources, so it is
42510
+ * exact for the common single-depositor-per-chain wallet. When several
42511
+ * depositors hold USDC on the same chain, the chain-level sum can mask a
42512
+ * per-depositor shortfall (or a surplus on one depositor can hide it); the
42513
+ * API's own per-depositor `9001` remains the backstop for that case. Scope the
42514
+ * comparison per (depositor, chain) if that multi-depositor case must be caught
42515
+ * pre-submit.
42516
+ *
42517
+ * @param estimatedIntents - Intents with `maxFee` from {@link parseEstimateResponse}.
42518
+ * @param allocations - Normalised allocations used to resolve chain from source domain.
42519
+ * @returns Per-chain required amount (transfer value + fees) in USDC atomic units.
42520
+ *
42521
+ * @example
42522
+ * ```typescript
42523
+ * import { Blockchain } from '@core/chains'
42524
+ * import type { BurnIntent } from '../createIntent/types'
42525
+ * import type { NormalizedAllocation } from '../allocations'
42526
+ *
42527
+ * declare const estimatedIntents: BurnIntent[]
42528
+ * declare const allocations: NormalizedAllocation[]
42529
+ * declare const confirmedBalanceAtomic: bigint
42530
+ *
42531
+ * const required = sumRequiredPerChain(estimatedIntents, allocations)
42532
+ * const overDrawn =
42533
+ * (required.get(Blockchain.Ethereum) ?? 0n) > confirmedBalanceAtomic
42534
+ * ```
42535
+ */ function sumRequiredPerChain(estimatedIntents, allocations) {
42536
+ const required = new Map();
42537
+ for (const intent of estimatedIntents){
42538
+ const chain = findBlockchainByDomain(intent.spec.sourceDomain, allocations);
42539
+ if (chain === undefined) continue;
42540
+ const amount = intentValue(intent) + intent.maxFee;
42541
+ required.set(chain, (required.get(chain) ?? 0n) + amount);
42542
+ }
42543
+ return required;
42544
+ }
41246
42545
 
41247
42546
  /**
41248
42547
  * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
@@ -41497,69 +42796,127 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41497
42796
  * non-forwarder transfer response is missing attestation or signature.
41498
42797
  * @throws KitError Propagated from adapter signing if the user rejects
41499
42798
  * or the signer is unavailable.
41500
- */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
41501
- if (params.amountIn) {
41502
- const rawSources = Array.isArray(params.from) ? params.from : [
41503
- params.from
41504
- ];
41505
- const sourcesArray = rawSources.filter((s)=>s != null);
41506
- const networkType = destChain.isTestnet ? 'testnet' : 'mainnet';
41507
- const balanceResults = await Promise.all(sourcesArray.map(async (source)=>{
41508
- // When sourceAccount is set (delegate flow), scope the balance
41509
- // query to the Gateway depositor not the signer. Using the
41510
- // address-only path bypasses adapter address resolution, which
41511
- // would otherwise return the signer's balance (developer-
41512
- // controlled) or reject an explicit address (user-controlled).
41513
- let querySource;
41514
- if (source.sourceAccount) {
41515
- querySource = {
41516
- address: source.sourceAccount
41517
- };
41518
- } else {
41519
- querySource = {
41520
- adapter: source.adapter
41521
- };
41522
- if ('address' in source && source.address) {
41523
- querySource['address'] = source.address;
41524
- }
41525
- }
41526
- return getBalances$1({
41527
- token: params.token,
41528
- sources: querySource,
41529
- networkType
41530
- });
41531
- }));
41532
- const chainBalances = [];
41533
- for(let i = 0; i < balanceResults.length; i++){
41534
- const breakdowns = balanceResults[i]?.breakdown[0]?.breakdown ?? [];
41535
- for (const b of breakdowns){
41536
- chainBalances.push({
41537
- chain: b.chain,
41538
- confirmedBalance: b.confirmedBalance,
41539
- sourceIndex: i
41540
- });
42799
+ */ /**
42800
+ * Fetch confirmed per-chain USDC balances for every auto-allocation source.
42801
+ *
42802
+ * Used only on the `amountIn` (auto-allocation) path. Returns one
42803
+ * {@link ChainBalance} per (source, chain) pair so the greedy allocator — and
42804
+ * the corrective re-allocation pass — can reason about draw limits without a
42805
+ * second balance round-trip.
42806
+ *
42807
+ * @param params - Spend parameters (source(s) and token).
42808
+ * @param destChain - Resolved destination chain (used for network type).
42809
+ * @returns Confirmed balances tagged with their originating source index.
42810
+ */ async function fetchChainBalances(params, destChain) {
42811
+ const rawSources = Array.isArray(params.from) ? params.from : [
42812
+ params.from
42813
+ ];
42814
+ const sourcesArray = rawSources.filter((s)=>s != null);
42815
+ const networkType = destChain.isTestnet ? 'testnet' : 'mainnet';
42816
+ const balanceResults = await Promise.all(sourcesArray.map(async (source)=>{
42817
+ // When sourceAccount is set (delegate flow), scope the balance
42818
+ // query to the Gateway depositor — not the signer. Using the
42819
+ // address-only path bypasses adapter address resolution, which
42820
+ // would otherwise return the signer's balance (developer-
42821
+ // controlled) or reject an explicit address (user-controlled).
42822
+ let querySource;
42823
+ if (source.sourceAccount) {
42824
+ querySource = {
42825
+ address: source.sourceAccount
42826
+ };
42827
+ } else {
42828
+ querySource = {
42829
+ adapter: source.adapter
42830
+ };
42831
+ if ('address' in source && source.address) {
42832
+ querySource['address'] = source.address;
41541
42833
  }
41542
42834
  }
41543
- const customFeeConfig = params.config?.customFee;
41544
- const autoAllocResult = computeAutoAllocation({
41545
- amountIn: params.amountIn,
41546
- destinationChain: destChain.chain,
41547
- chainBalances,
41548
- useForwarder,
41549
- ...customFeeConfig ? {
41550
- customFee: customFeeConfig
41551
- } : {}
42835
+ return getBalances$1({
42836
+ token: params.token,
42837
+ sources: querySource,
42838
+ networkType
41552
42839
  });
41553
- const normalizedAutoAllocations = await normalizeAutoAllocations(autoAllocResult, sourcesArray);
41554
- const intents = buildAutoAllocatedBurnIntents(normalizedAutoAllocations, destChain, recipientAddress, params.token, params.config?.customFee);
41555
- const allocations = [
41556
- ...normalizedAutoAllocations.user,
41557
- ...normalizedAutoAllocations.devFee,
41558
- ...normalizedAutoAllocations.circleFee
41559
- ];
42840
+ }));
42841
+ const chainBalances = [];
42842
+ for(let i = 0; i < balanceResults.length; i++){
42843
+ const breakdowns = balanceResults[i]?.breakdown[0]?.breakdown ?? [];
42844
+ for (const b of breakdowns){
42845
+ chainBalances.push({
42846
+ chain: b.chain,
42847
+ confirmedBalance: b.confirmedBalance,
42848
+ sourceIndex: i
42849
+ });
42850
+ }
42851
+ }
42852
+ return chainBalances;
42853
+ }
42854
+ /**
42855
+ * Build auto-allocated normalised allocations and burn intents from
42856
+ * pre-fetched balances.
42857
+ *
42858
+ * Performs no balance API call, so it can be re-invoked with
42859
+ * `gasFeeOverrides` (the real per-chain gas from a prior estimate) to correct
42860
+ * an over-draw without re-querying balances.
42861
+ *
42862
+ * @param params - Spend parameters (source(s), token, optional custom fee).
42863
+ * @param destChain - Resolved destination chain with Gateway v1 config.
42864
+ * @param recipientAddress - Resolved recipient address on the destination chain.
42865
+ * @param useForwarder - Whether the Forwarding Service path is active.
42866
+ * @param amountIn - Human-readable USDC amount to allocate.
42867
+ * @param chainBalances - Confirmed balances from {@link fetchChainBalances}.
42868
+ * @param gasFeeOverrides - Optional real per-chain gas fees to reserve.
42869
+ * @returns Normalised allocations and burn intents for the estimate/transfer API.
42870
+ */ async function buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, amountIn, chainBalances, gasFeeOverrides) {
42871
+ const rawSources = Array.isArray(params.from) ? params.from : [
42872
+ params.from
42873
+ ];
42874
+ const sourcesArray = rawSources.filter((s)=>s != null);
42875
+ const customFeeConfig = params.config?.customFee;
42876
+ const autoAllocResult = computeAutoAllocation({
42877
+ amountIn,
42878
+ destinationChain: destChain.chain,
42879
+ chainBalances,
42880
+ useForwarder,
42881
+ ...customFeeConfig ? {
42882
+ customFee: customFeeConfig
42883
+ } : {},
42884
+ ...gasFeeOverrides ? {
42885
+ gasFeeOverrides
42886
+ } : {}
42887
+ });
42888
+ const normalizedAutoAllocations = await normalizeAutoAllocations(autoAllocResult, sourcesArray);
42889
+ const intents = buildAutoAllocatedBurnIntents(normalizedAutoAllocations, destChain, recipientAddress, params.token, params.config?.customFee);
42890
+ const allocations = [
42891
+ ...normalizedAutoAllocations.user,
42892
+ ...normalizedAutoAllocations.devFee,
42893
+ ...normalizedAutoAllocations.circleFee
42894
+ ];
42895
+ return {
42896
+ allocations,
42897
+ intents
42898
+ };
42899
+ }
42900
+ /**
42901
+ * Resolve allocations and burn intents for the spend.
42902
+ *
42903
+ * Auto-allocation (`amountIn`) fetches balances once and returns them so the
42904
+ * caller can detect and correct over-draw without re-querying. Explicit
42905
+ * allocations return no balances (they are user-authoritative).
42906
+ *
42907
+ * @param params - Spend parameters.
42908
+ * @param destChain - Resolved destination chain with Gateway v1 config.
42909
+ * @param recipientAddress - Resolved recipient address.
42910
+ * @param useForwarder - Whether the Forwarding Service path is active.
42911
+ * @returns Allocations, intents, and (auto-allocation only) confirmed balances.
42912
+ */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
42913
+ if (params.amountIn) {
42914
+ const chainBalances = await fetchChainBalances(params, destChain);
42915
+ const { allocations, intents } = await buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, params.amountIn, chainBalances);
41560
42916
  return {
41561
42917
  allocations,
41562
- intents
42918
+ intents,
42919
+ chainBalances
41563
42920
  };
41564
42921
  }
41565
42922
  const allocations = await normalizeAllocations(params);
@@ -41601,6 +42958,148 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41601
42958
  forwardingFee: undefined
41602
42959
  };
41603
42960
  }
42961
+ /** Sum confirmed balances (atomic USDC) per source chain. */ function computeAvailablePerChain(chainBalances) {
42962
+ const available = new Map();
42963
+ for (const b of chainBalances){
42964
+ const atomic = parseUnits(b.confirmedBalance, USDC_DECIMALS$1);
42965
+ available.set(b.chain, (available.get(b.chain) ?? 0n) + atomic);
42966
+ }
42967
+ return available;
42968
+ }
42969
+ /**
42970
+ * Detect source chains whose required draw (value + maxFee across their
42971
+ * intents) exceeds the confirmed balance — the condition the Gateway API
42972
+ * rejects with `BALANCE_INSUFFICIENT_TOKEN` at `/v1/transfer`.
42973
+ *
42974
+ * Both sides are summed per chain (see {@link sumRequiredPerChain} and
42975
+ * {@link computeAvailablePerChain}), so detection is exact for the common
42976
+ * single-depositor-per-chain wallet. When multiple depositors hold USDC on the
42977
+ * same chain, a chain-level surplus can mask a per-depositor shortfall; the
42978
+ * API's own per-depositor `9001` remains the backstop in that case.
42979
+ */ function findOverdrawnChains(estimatedIntents, allocations, chainBalances) {
42980
+ const required = sumRequiredPerChain(estimatedIntents, allocations);
42981
+ const available = computeAvailablePerChain(chainBalances);
42982
+ const overdrawn = [];
42983
+ for (const [chain, req] of required){
42984
+ const avail = available.get(chain) ?? 0n;
42985
+ if (req > avail) {
42986
+ overdrawn.push({
42987
+ chain,
42988
+ required: req,
42989
+ available: avail
42990
+ });
42991
+ }
42992
+ }
42993
+ return overdrawn;
42994
+ }
42995
+ /**
42996
+ * Build a descriptive KitError for an auto-allocation gas shortfall that
42997
+ * survives the corrective re-allocation, naming the per-chain gap so the
42998
+ * caller sees the real cause instead of the opaque API 9001 rejection.
42999
+ */ function createAutoAllocationGasError(overdrawn, cause) {
43000
+ const detail = overdrawn.map((o)=>`${String(o.chain)} needs ${formatUnits(o.required.toString(), USDC_DECIMALS$1)} USDC ` + `(transfer + gas) but only ${formatUnits(o.available.toString(), USDC_DECIMALS$1)} USDC is available`).join('; ');
43001
+ return new KitError({
43002
+ ...BalanceError.INSUFFICIENT_GAS,
43003
+ recoverability: 'FATAL',
43004
+ message: `Insufficient USDC to cover the transfer amount plus Gateway gas fees: ${detail}. ` + `Reduce the amount or add USDC on the affected chain(s).`,
43005
+ ...cause === undefined ? {} : {
43006
+ cause: {
43007
+ trace: {
43008
+ cause
43009
+ }
43010
+ }
43011
+ }
43012
+ });
43013
+ }
43014
+ /**
43015
+ * Validate allocations against the network/forwarder rules, call the estimate
43016
+ * API, and return the estimated intents with any forwarding fee.
43017
+ *
43018
+ * @param allocations - Normalised allocations for the estimate.
43019
+ * @param intents - Burn intents to estimate.
43020
+ * @param destChain - Resolved destination chain with Gateway v1 config.
43021
+ * @param useForwarder - Whether the Forwarding Service path is active.
43022
+ * @returns Estimated intents (with real maxFee) and optional forwarding fee.
43023
+ */ async function validateAndEstimate(allocations, intents, destChain, useForwarder) {
43024
+ assertNetworkCompatibility(allocations, destChain);
43025
+ if (useForwarder) {
43026
+ assertForwarderRouteSupport(destChain, allocations);
43027
+ }
43028
+ const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
43029
+ const estimateBody = buildEstimateRequestBody(intents);
43030
+ const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
43031
+ const estimatedIntents = parseEstimateResponse(entries, intents);
43032
+ return {
43033
+ estimatedIntents,
43034
+ forwardingFee
43035
+ };
43036
+ }
43037
+ /**
43038
+ * Fold newly-observed per-chain gas into the accumulated overrides, keeping the
43039
+ * higher fee per chain so a chain a later pass reveals is never under-reserved.
43040
+ */ function mergeGasFeeOverrides(base, next) {
43041
+ const merged = new Map(base);
43042
+ for (const [chain, fee] of next){
43043
+ const prev = merged.get(chain);
43044
+ if (prev === undefined || fee > prev) {
43045
+ merged.set(chain, fee);
43046
+ }
43047
+ }
43048
+ return merged;
43049
+ }
43050
+ /**
43051
+ * Maximum corrective re-allocation passes before failing fast. One pass fixes
43052
+ * the common case; a second/third covers a chain that a spill only introduces
43053
+ * after gas is reserved. Bounds the worst case at this many extra estimate
43054
+ * round-trips (only ever reached when the balance genuinely falls short).
43055
+ */ const MAX_CORRECTION_PASSES = 3;
43056
+ /**
43057
+ * Correct an auto-allocation over-draw: reserve the estimate's real per-chain
43058
+ * gas, re-allocate from the same balances, and re-estimate — repeating up to
43059
+ * {@link MAX_CORRECTION_PASSES} times, accumulating the real gas each pass
43060
+ * reveals.
43061
+ *
43062
+ * One pass fixes the common case, where the over-drawn chain was already in the
43063
+ * first estimate. A further pass covers a chain that a spill only introduces
43064
+ * once gas is reserved on the destination: that chain isn't in the first
43065
+ * estimate, so its real gas is unknown until it appears, and its first
43066
+ * re-allocation falls back to the static reserve. Each pass folds the newly
43067
+ * revealed gas into the overrides (see {@link mergeGasFeeOverrides}) so the
43068
+ * next pass reserves it too. Per-chain gas is ~amount-independent, so once
43069
+ * every drawn chain's real gas is known the allocation converges.
43070
+ *
43071
+ * When the shortfall is genuine — the re-allocation can't cover the amount, or
43072
+ * the passes are exhausted while still over-drawn — throws a gas-specific
43073
+ * {@link KitError} instead of submitting a doomed transfer.
43074
+ */ async function correctOverdraw(opts) {
43075
+ let overrides = deriveGasFeeOverrides(opts.estimatedIntents, opts.allocations);
43076
+ let overdrawn = opts.overdrawn;
43077
+ for(let pass = 0; pass < MAX_CORRECTION_PASSES; pass++){
43078
+ let corrected;
43079
+ try {
43080
+ corrected = await buildAutoAllocatedFromBalances(opts.params, opts.destChain, opts.recipientAddress, opts.useForwarder, opts.amountIn, opts.chainBalances, overrides);
43081
+ } catch (err) {
43082
+ // Re-allocating with the real gas reserved can't cover the amount →
43083
+ // surface a gas-specific error instead of the opaque API rejection.
43084
+ if (err instanceof KitError && err.code === BalanceError.INSUFFICIENT_TOKEN.code) {
43085
+ throw createAutoAllocationGasError(overdrawn, err);
43086
+ }
43087
+ throw err;
43088
+ }
43089
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder);
43090
+ const stillOverdrawn = findOverdrawnChains(estimatedIntents, corrected.allocations, opts.chainBalances);
43091
+ if (stillOverdrawn.length === 0) {
43092
+ return {
43093
+ allocations: corrected.allocations,
43094
+ estimatedIntents,
43095
+ forwardingFee
43096
+ };
43097
+ }
43098
+ overdrawn = stillOverdrawn;
43099
+ overrides = mergeGasFeeOverrides(overrides, deriveGasFeeOverrides(estimatedIntents, corrected.allocations));
43100
+ }
43101
+ throw createAutoAllocationGasError(overdrawn);
43102
+ }
41604
43103
  /**
41605
43104
  * Validate allocations, call the estimate API, and return estimated intents.
41606
43105
  *
@@ -41608,6 +43107,14 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41608
43107
  * path. Handles forwarder route validation, network compatibility, and the
41609
43108
  * estimate API call.
41610
43109
  *
43110
+ * For auto-allocation (`amountIn`), the greedy allocator reserves a static
43111
+ * per-chain gas fee that can undershoot the API's real fee, draining a source
43112
+ * (typically the destination chain) below `value + maxFee` and triggering a
43113
+ * `BALANCE_INSUFFICIENT_TOKEN` rejection. When the first estimate reveals such
43114
+ * an over-draw, a bounded corrective re-allocation reserves the real gas and
43115
+ * re-estimates until it converges or fails fast (see {@link correctOverdraw}).
43116
+ * Explicit allocations are user-authoritative and never re-allocated.
43117
+ *
41611
43118
  * @param params - Spend parameters.
41612
43119
  * @param destChain - Resolved destination chain with Gateway v1 config.
41613
43120
  * @param recipientAddress - Resolved recipient address.
@@ -41617,15 +43124,24 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41617
43124
  if (useForwarder) {
41618
43125
  assertForwarderRouteSupport(destChain);
41619
43126
  }
41620
- const { allocations, intents } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
41621
- assertNetworkCompatibility(allocations, destChain);
41622
- if (useForwarder) {
41623
- assertForwarderRouteSupport(destChain, allocations);
43127
+ const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
43128
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder);
43129
+ if (params.amountIn && chainBalances) {
43130
+ const overdrawn = findOverdrawnChains(estimatedIntents, allocations, chainBalances);
43131
+ if (overdrawn.length > 0) {
43132
+ return correctOverdraw({
43133
+ params,
43134
+ destChain,
43135
+ recipientAddress,
43136
+ useForwarder,
43137
+ amountIn: params.amountIn,
43138
+ chainBalances,
43139
+ estimatedIntents,
43140
+ allocations,
43141
+ overdrawn
43142
+ });
43143
+ }
41624
43144
  }
41625
- const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
41626
- const estimateBody = buildEstimateRequestBody(intents);
41627
- const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
41628
- const estimatedIntents = parseEstimateResponse(entries, intents);
41629
43145
  return {
41630
43146
  allocations,
41631
43147
  estimatedIntents,
@@ -42533,8 +44049,10 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
42533
44049
  *
42534
44050
  * - `computeFee` — required function that receives resolved spend params
42535
44051
  * and returns a fee as a string (or `Promise<string>`).
42536
- * - `resolveFeeRecipientAddress` — required function that returns a
42537
- * recipient address as a string (or `Promise<string>`).
44052
+ * - `resolveFeeRecipientAddress` — optional function that returns a
44053
+ * recipient address as a string (or `Promise<string>`). Omit it when
44054
+ * using `setFeeRecipients()`'s declarative map instead — a policy
44055
+ * with neither throws at spend time.
42538
44056
  *
42539
44057
  * @example
42540
44058
  * ```ts
@@ -42547,7 +44065,7 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
42547
44065
  * ```
42548
44066
  */ const customFeePolicySchema = z.object({
42549
44067
  computeFee: z.function().returns(z.string().or(z.promise(z.string()))),
42550
- resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
44068
+ resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string()))).optional()
42551
44069
  }).strict();
42552
44070
  /**
42553
44071
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
@@ -42569,6 +44087,71 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
42569
44087
  validateWithStateTracking(config, customFeePolicySchema, 'UnifiedBalanceKit custom fee policy', assertCustomFeePolicySymbol);
42570
44088
  }
42571
44089
 
44090
+ const assertFeeRecipientsConfigSymbol = Symbol('assertFeeRecipientsConfig');
44091
+ /**
44092
+ * Schema for validating {@link FeeRecipientsConfig}.
44093
+ *
44094
+ * Requires at least one of `evm`/`solana`, non-empty string values for
44095
+ * whichever keys are present, and — mirroring the `depositAccount`
44096
+ * validation in `deposit/validate/assertions` — an address format that
44097
+ * matches the given chain type (EVM hex vs Solana base58).
44098
+ *
44099
+ * @example
44100
+ * ```ts
44101
+ * const config = {
44102
+ * evm: '0x1234567890123456789012345678901234567890',
44103
+ * solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
44104
+ * }
44105
+ * const result = feeRecipientsConfigSchema.safeParse(config)
44106
+ * // result.success === true
44107
+ * ```
44108
+ */ const feeRecipientsConfigSchema = z.object({
44109
+ evm: z.string().min(1, 'Fee recipient address is required.').optional(),
44110
+ solana: z.string().min(1, 'Fee recipient address is required.').optional()
44111
+ }).strict().refine((config)=>Object.keys(config).length > 0, {
44112
+ message: 'At least one fee recipient (evm or solana) is required.'
44113
+ }).superRefine((config, ctx)=>{
44114
+ for (const type of Object.keys(config)){
44115
+ const address = config[type];
44116
+ if (address == null) continue;
44117
+ // `{ name: type, type }` is a placeholder chain identifier — only
44118
+ // `.type` is checked by these two helpers today, `.name` is unused.
44119
+ // No real ChainDefinition exists here, since validation runs before
44120
+ // a destination chain is resolved.
44121
+ if (!isValidAddressForChain(address, {
44122
+ name: type,
44123
+ type
44124
+ })) {
44125
+ const { expectedAddressFormat } = extractChainInfo({
44126
+ name: type,
44127
+ type
44128
+ });
44129
+ ctx.addIssue({
44130
+ code: z.ZodIssueCode.custom,
44131
+ path: [
44132
+ type
44133
+ ],
44134
+ message: `Invalid ${type} address "${address}". Expected ${expectedAddressFormat}.`
44135
+ });
44136
+ }
44137
+ }
44138
+ });
44139
+ /**
44140
+ * Assert that the provided value conforms to {@link FeeRecipientsConfig}.
44141
+ *
44142
+ * Throws a validation error with annotated paths if the configuration is
44143
+ * malformed.
44144
+ *
44145
+ * @param config - The fee recipients map to validate.
44146
+ *
44147
+ * @example
44148
+ * ```ts
44149
+ * assertFeeRecipientsConfig({ evm: '0x1234567890123456789012345678901234567890' })
44150
+ * ```
44151
+ */ function assertFeeRecipientsConfig(config) {
44152
+ validateWithStateTracking(config, feeRecipientsConfigSchema, 'UnifiedBalanceKit fee recipients config', assertFeeRecipientsConfigSymbol);
44153
+ }
44154
+
42572
44155
  function sameChain(a, b) {
42573
44156
  return a.chain !== undefined && a.chain === b.chain;
42574
44157
  }
@@ -43549,6 +45132,105 @@ function assertSourceAccountAddresses(from) {
43549
45132
  config: params.config
43550
45133
  };
43551
45134
  }
45135
+ /**
45136
+ * Tracks, per {@link CustomFeePolicy} instance, which chain types have
45137
+ * already triggered the "falling back to resolveFeeRecipientAddress"
45138
+ * warning, so repeated `spend()`/`estimateSpend()` calls (e.g. live
45139
+ * quoting) warn once per (policy, chain type) pair rather than on every
45140
+ * call.
45141
+ */ const warnedFeeRecipientFallbacks = new WeakMap();
45142
+ /**
45143
+ * Invoke `resolveFeeRecipientAddress` and validate its return value has a
45144
+ * plausible address format for `destChain`, the same check
45145
+ * `setFeeRecipients()` already applies at config time. Unlike the map,
45146
+ * the callback's return value can't be validated ahead of time, so it's
45147
+ * checked here instead — a malformed value throws immediately rather
45148
+ * than silently becoming the fee recipient.
45149
+ *
45150
+ * @internal
45151
+ */ async function resolveFeeRecipientFromCallback(callback, destChain, params) {
45152
+ const address = await callback(destChain, params);
45153
+ if (!isValidAddressForChain(address, destChain)) {
45154
+ throw new KitError({
45155
+ ...InputError.VALIDATION_FAILED,
45156
+ recoverability: 'FATAL',
45157
+ message: `resolveFeeRecipientAddress returned an invalid address ` + `"${address}" for chain type "${destChain.type}" ` + `(resolved destination: ${destChain.name}).`
45158
+ });
45159
+ }
45160
+ return address;
45161
+ }
45162
+ /**
45163
+ * Resolve the single fee recipient address for a spend.
45164
+ *
45165
+ * Every fee burn intent in a spend mints to the same destination
45166
+ * chain regardless of which source chain(s) funded it, so exactly one
45167
+ * recipient address — valid on `destChain` — is ever needed.
45168
+ *
45169
+ * `feeRecipients` (set via `setFeeRecipients`) takes priority over the
45170
+ * policy's `resolveFeeRecipientAddress` callback for any chain type it
45171
+ * has an entry for, since it's a direct lookup and doesn't require
45172
+ * invoking developer code. For a chain type `feeRecipients` doesn't
45173
+ * cover, it falls back to `resolveFeeRecipientAddress` if one is
45174
+ * configured — a warning is logged once per (policy, chain type) pair
45175
+ * so the fallback isn't a silent surprise, without spamming repeated
45176
+ * `estimateSpend()` calls used for live quoting. Throws if neither
45177
+ * resolves `destChain`'s type, or if `resolveFeeRecipientAddress`
45178
+ * resolves it to a malformed address (see
45179
+ * {@link resolveFeeRecipientFromCallback}).
45180
+ *
45181
+ * @internal
45182
+ */ async function resolveFeeRecipient(destChain, policy, feeRecipients, params) {
45183
+ if (feeRecipients) {
45184
+ // `destChain.type` is `@core/chains`' broader `ChainType` union;
45185
+ // `FeeRecipientChainType` is the narrower subset this map supports
45186
+ // today. A type not present as a key simply has no configured
45187
+ // recipient, which is handled below.
45188
+ const type = destChain.type;
45189
+ const recipientAddress = feeRecipients[type];
45190
+ if (recipientAddress) {
45191
+ return recipientAddress;
45192
+ }
45193
+ if (policy.resolveFeeRecipientAddress) {
45194
+ const warnedTypes = warnedFeeRecipientFallbacks.get(policy);
45195
+ if (!warnedTypes?.has(type)) {
45196
+ warnedFeeRecipientFallbacks.set(policy, (warnedTypes ?? new Set()).add(type));
45197
+ console.warn(`setFeeRecipients() is configured but has no entry for chain ` + `type "${type}" — falling back to customFeePolicy.` + `resolveFeeRecipientAddress for this chain type. Add a ` + `"${type}" entry to setFeeRecipients() to avoid this fallback.`);
45198
+ }
45199
+ return resolveFeeRecipientFromCallback(policy.resolveFeeRecipientAddress, destChain, params);
45200
+ }
45201
+ throw new KitError({
45202
+ ...InputError.VALIDATION_FAILED,
45203
+ recoverability: 'FATAL',
45204
+ message: `No fee recipient configured for chain type "${type}" ` + `(resolved destination: ${destChain.name}). Call setFeeRecipients() ` + `with an entry for "${type}", or provide resolveFeeRecipientAddress ` + `on the custom fee policy.`
45205
+ });
45206
+ }
45207
+ if (!policy.resolveFeeRecipientAddress) {
45208
+ throw new KitError({
45209
+ ...InputError.VALIDATION_FAILED,
45210
+ recoverability: 'FATAL',
45211
+ message: 'No fee recipient configured — call setFeeRecipients() or provide ' + 'resolveFeeRecipientAddress on the custom fee policy.'
45212
+ });
45213
+ }
45214
+ return resolveFeeRecipientFromCallback(policy.resolveFeeRecipientAddress, destChain, params);
45215
+ }
45216
+ /**
45217
+ * Guard against a common misconfiguration: a developer sets the
45218
+ * declarative `feeRecipients` map expecting it alone to drive fee
45219
+ * collection, but no fee is ever charged without a `computeFee` from
45220
+ * `customFeePolicy` to determine the amount. Without this check that
45221
+ * misconfiguration fails silently — no fee is charged and no error is
45222
+ * raised.
45223
+ *
45224
+ * @internal
45225
+ */ function assertFeeRecipientsHasPolicy(feeRecipients) {
45226
+ if (feeRecipients) {
45227
+ throw new KitError({
45228
+ ...InputError.VALIDATION_FAILED,
45229
+ recoverability: 'FATAL',
45230
+ message: 'setFeeRecipients() is configured but no developer fee will be ' + 'charged: setCustomFeePolicy() must also be set to provide ' + 'computeFee, which determines the fee amount. Call ' + 'setCustomFeePolicy(), or remove setFeeRecipients() if no ' + 'developer fee is intended.'
45231
+ });
45232
+ }
45233
+ }
43552
45234
  /**
43553
45235
  * Apply a {@link CustomFeePolicy} to an adapter-only spend.
43554
45236
  *
@@ -43557,15 +45239,20 @@ function assertSourceAccountAddresses(from) {
43557
45239
  * `config.customFee` so the provider sees it.
43558
45240
  *
43559
45241
  * @internal
43560
- */ async function mergeCustomFeePolicyForAdapterOnly(params, policy) {
43561
- if (params.config?.customFee || !policy) {
45242
+ */ async function mergeCustomFeePolicyForAdapterOnly(params, policy, feeRecipients) {
45243
+ if (params.config?.customFee) {
45244
+ return params;
45245
+ }
45246
+ if (!policy) {
45247
+ assertFeeRecipientsHasPolicy(feeRecipients);
43562
45248
  return params;
43563
45249
  }
43564
45250
  const destChain = resolveChainIdentifier(params.to.chain);
43565
- const [feeValue, recipientAddress] = await Promise.all([
43566
- policy.computeFee(params),
43567
- policy.resolveFeeRecipientAddress(destChain, params)
43568
- ]);
45251
+ // Resolve the recipient before computing the fee: a KitError here
45252
+ // (missing/unresolvable recipient) shouldn't be preceded by an
45253
+ // otherwise-wasted computeFee call, which may be a network request.
45254
+ const recipientAddress = await resolveFeeRecipient(destChain, policy, feeRecipients, params);
45255
+ const feeValue = await policy.computeFee(params);
43569
45256
  return {
43570
45257
  ...params,
43571
45258
  config: {
@@ -43595,18 +45282,35 @@ function assertSourceAccountAddresses(from) {
43595
45282
  });
43596
45283
  }
43597
45284
  }
43598
- async function mergeCustomFeeConfig(resolved, policy) {
43599
- if (resolved.config?.customFee || !policy) {
45285
+ async function mergeCustomFeeConfig(resolved, policy, feeRecipients) {
45286
+ if (resolved.config?.customFee) {
43600
45287
  return resolved;
43601
45288
  }
43602
- const firstSourceChain = resolved.from[0]?.allocations[0]?.chain;
43603
- if (!firstSourceChain) {
45289
+ if (!policy) {
45290
+ assertFeeRecipientsHasPolicy(feeRecipients);
43604
45291
  return resolved;
43605
45292
  }
43606
- const [feeValue, recipientAddress] = await Promise.all([
43607
- policy.computeFee(resolved),
43608
- policy.resolveFeeRecipientAddress(firstSourceChain, resolved)
43609
- ]);
45293
+ // Skip fee resolution when there's no source chain to spend from at
45294
+ // all. This state can't arise from validated input today — the
45295
+ // caller re-checks and throws "No source chain found" right after
45296
+ // this returns — but skipping here isn't dead code: verified that
45297
+ // removing it lets a degenerate zero-allocation resolved value reach
45298
+ // computeFee/assertDeveloperFeeWithinBounds first, which throws a
45299
+ // misleading "Developer fee must be less than the total spend
45300
+ // amount" (0 >= 0 total allocation) instead of the correct "No
45301
+ // source chain found" error — or, for a real developer computeFee
45302
+ // that assumes a non-empty allocation, an uncaught raw exception
45303
+ // instead of any KitError at all. This guard exists to guarantee the
45304
+ // caller's clear error is what actually surfaces, not for
45305
+ // correctness.
45306
+ if (collectSourceChains(resolved).length === 0) {
45307
+ return resolved;
45308
+ }
45309
+ // Resolve the recipient before computing the fee: a KitError here
45310
+ // (missing/unresolvable recipient) shouldn't be preceded by an
45311
+ // otherwise-wasted computeFee call, which may be a network request.
45312
+ const recipientAddress = await resolveFeeRecipient(resolved.to.chain, policy, feeRecipients, resolved);
45313
+ const feeValue = await policy.computeFee(resolved);
43610
45314
  return {
43611
45315
  ...resolved,
43612
45316
  config: {
@@ -43662,14 +45366,14 @@ async function mergeCustomFeeConfig(resolved, policy) {
43662
45366
  }
43663
45367
  const destChain = resolveChainIdentifier(params.to.chain);
43664
45368
  if (!hasExplicitAllocations(params.from)) {
43665
- const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy);
45369
+ const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy, context.feeRecipients);
43666
45370
  assertDeveloperFeeWithinAmount(merged);
43667
45371
  const provider = findProviderForChain(context, normalizeToken(merged.token), destChain);
43668
45372
  return callSpend(provider, toProviderAdapterOnlyParams(merged));
43669
45373
  }
43670
45374
  const resolved = await resolveSpendParams(params);
43671
45375
  assertSpendNetworkCompatibility(resolved);
43672
- const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy);
45376
+ const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy, context.feeRecipients);
43673
45377
  assertDeveloperFeeWithinBounds(withFee);
43674
45378
  const sourceChains = collectSourceChains(withFee);
43675
45379
  if (sourceChains.length === 0) {
@@ -43709,14 +45413,14 @@ async function mergeCustomFeeConfig(resolved, policy) {
43709
45413
  assertSpendParams(params);
43710
45414
  const destChain = resolveChainIdentifier(params.to.chain);
43711
45415
  if (!hasExplicitAllocations(params.from)) {
43712
- const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy);
45416
+ const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy, context.feeRecipients);
43713
45417
  assertDeveloperFeeWithinAmount(merged);
43714
45418
  const provider = findProviderForChain(context, normalizeToken(merged.token), destChain);
43715
45419
  return provider.estimateSpend(toProviderAdapterOnlyParams(merged));
43716
45420
  }
43717
45421
  const resolved = await resolveSpendParams(params);
43718
45422
  assertSpendNetworkCompatibility(resolved);
43719
- const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy);
45423
+ const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy, context.feeRecipients);
43720
45424
  assertDeveloperFeeWithinBounds(withFee);
43721
45425
  const sourceChains = collectSourceChains(withFee);
43722
45426
  if (sourceChains.length === 0) {
@@ -44691,6 +46395,46 @@ const removeFundParamsSchema = z.object({
44691
46395
  */ removeCustomFeePolicy() {
44692
46396
  delete this.context.customFeePolicy;
44693
46397
  }
46398
+ /**
46399
+ * Set a declarative fee recipient map, keyed by chain type. Once set,
46400
+ * `spend()`/`estimateSpend()` resolve the fee recipient by looking up
46401
+ * the spend's destination chain type in this map — taking priority
46402
+ * over `customFeePolicy`'s `resolveFeeRecipientAddress` callback.
46403
+ *
46404
+ * @remarks
46405
+ * This only controls which address a fee is sent to — it does not by
46406
+ * itself cause any fee to be charged. You still need
46407
+ * {@link UnifiedBalanceKit.setCustomFeePolicy}'s `computeFee` to
46408
+ * determine the fee amount; calling `setFeeRecipients` without ever
46409
+ * calling `setCustomFeePolicy` throws at spend time (there is no
46410
+ * `computeFee` to determine an amount).
46411
+ *
46412
+ * @param config - Fee recipient addresses keyed by chain type (e.g.
46413
+ * `{ evm: '0x...', solana: 'Sol...' }`). Provide entries for every
46414
+ * chain type you expect to spend to; spending to a chain type with
46415
+ * no matching entry throws before any fee collection is attempted.
46416
+ *
46417
+ * @example
46418
+ * ```typescript
46419
+ * kit.setFeeRecipients({
46420
+ * evm: '0x1234567890123456789012345678901234567890',
46421
+ * solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
46422
+ * })
46423
+ * ```
46424
+ */ setFeeRecipients(config) {
46425
+ assertFeeRecipientsConfig(config);
46426
+ this.context.feeRecipients = config;
46427
+ }
46428
+ /**
46429
+ * Remove the declarative fee recipient map for the kit.
46430
+ *
46431
+ * @example
46432
+ * ```typescript
46433
+ * kit.removeFeeRecipients()
46434
+ * ```
46435
+ */ removeFeeRecipients() {
46436
+ delete this.context.feeRecipients;
46437
+ }
44694
46438
  }
44695
46439
 
44696
46440
  // Auto-register this kit for user agent tracking
@@ -45017,6 +46761,45 @@ registerKit(`${pkg.name}/${pkg.version}`);
45017
46761
  */ removeCustomFeePolicy() {
45018
46762
  this.kit.removeCustomFeePolicy();
45019
46763
  }
46764
+ /**
46765
+ * Set a declarative fee recipient map, keyed by chain type.
46766
+ *
46767
+ * Once set, `spend()`/`estimateSpend()` resolve the fee recipient by
46768
+ * looking up the spend's destination chain type in this map — taking
46769
+ * priority over `customFeePolicy`'s `resolveFeeRecipientAddress`
46770
+ * callback.
46771
+ *
46772
+ * @remarks
46773
+ * This only controls which address a fee is sent to — it does not by
46774
+ * itself cause any fee to be charged. You still need
46775
+ * `setCustomFeePolicy`'s `computeFee` to determine the fee amount;
46776
+ * calling `setFeeRecipients` without ever calling `setCustomFeePolicy`
46777
+ * throws at spend time (there is no `computeFee` to determine an
46778
+ * amount).
46779
+ *
46780
+ * @param config - Fee recipient addresses keyed by chain type (e.g.
46781
+ * `{ evm: '0x...', solana: 'Sol...' }`).
46782
+ *
46783
+ * @example
46784
+ * ```typescript
46785
+ * kit.unifiedBalance.setFeeRecipients({
46786
+ * evm: '0x1234567890123456789012345678901234567890',
46787
+ * solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
46788
+ * })
46789
+ * ```
46790
+ */ setFeeRecipients(config) {
46791
+ this.kit.setFeeRecipients(config);
46792
+ }
46793
+ /**
46794
+ * Remove the declarative fee recipient map.
46795
+ *
46796
+ * @example
46797
+ * ```typescript
46798
+ * kit.unifiedBalance.removeFeeRecipients()
46799
+ * ```
46800
+ */ removeFeeRecipients() {
46801
+ this.kit.removeFeeRecipients();
46802
+ }
45020
46803
  }
45021
46804
 
45022
46805
  /**
@@ -45168,7 +46951,8 @@ registerKit(`${pkg.name}/${pkg.version}`);
45168
46951
  waitForCrossChainDeposit: async (params)=>waitForCrossChainDeposit(this.context, params),
45169
46952
  getDepositQuote: async (params)=>getDepositQuote(this.context, params),
45170
46953
  getWithdrawalQuote: async (params)=>getWithdrawalQuote(this.context, params),
45171
- getClaimRewardsQuote: async (params)=>getClaimRewardsQuote(this.context, params)
46954
+ getClaimRewardsQuote: async (params)=>getClaimRewardsQuote(this.context, params),
46955
+ retry: async (error)=>retry(this.context, error)
45172
46956
  };
45173
46957
  }
45174
46958
  /**
@@ -45427,7 +47211,8 @@ registerKit(`${pkg.name}/${pkg.version}`);
45427
47211
  * or `'NOT_FOUND'`). Use {@link AppKit.waitForSwap} if you'd rather
45428
47212
  * not write the polling loop yourself.
45429
47213
  *
45430
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
47214
+ * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
47215
+ * `kitKey`.
45431
47216
  * @returns A snapshot of the swap's status at the time of the call.
45432
47217
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
45433
47218
  *
@@ -45541,7 +47326,7 @@ registerKit(`${pkg.name}/${pkg.version}`);
45541
47326
  * translates to the chain's native sentinel address — `0xEee…` for EVM,
45542
47327
  * `1111…` for Solana — before querying the service.
45543
47328
  *
45544
- * @param params - `chain`, optional `tokens`, and `kitKey`.
47329
+ * @param params - `chain`, plus optional `tokens` and `kitKey`.
45545
47330
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
45546
47331
  * @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
45547
47332
  * entries, or any entry is neither a registered symbol nor a
@@ -45605,6 +47390,10 @@ registerKit(`${pkg.name}/${pkg.version}`);
45605
47390
  this.context.actions.bridge[action] ??= [];
45606
47391
  this.context.actions.bridge[action].push(typedHandler);
45607
47392
  }
47393
+ if (action === '*' || action.startsWith('earn.')) {
47394
+ this.context.actions.earn[action] ??= [];
47395
+ this.context.actions.earn[action].push(typedHandler);
47396
+ }
45608
47397
  }
45609
47398
  off(actionOrWildCard, handler) {
45610
47399
  const action = actionOrWildCard;
@@ -45614,16 +47403,32 @@ registerKit(`${pkg.name}/${pkg.version}`);
45614
47403
  this.unifiedBalance.off(ubAction, typedHandler);
45615
47404
  }
45616
47405
  if (action === '*' || action.startsWith('bridge.')) {
45617
- const handlers = this.context.actions.bridge[action];
45618
- if (handlers) {
45619
- const index = handlers.indexOf(typedHandler);
45620
- if (index !== -1) {
45621
- handlers.splice(index, 1);
45622
- if (handlers.length === 0) {
45623
- Reflect.deleteProperty(this.context.actions.bridge, action);
45624
- }
45625
- }
45626
- }
47406
+ this.removeStoredActionHandler(this.context.actions.bridge, action, typedHandler);
47407
+ }
47408
+ if (action === '*' || action.startsWith('earn.')) {
47409
+ this.removeStoredActionHandler(this.context.actions.earn, action, typedHandler);
47410
+ }
47411
+ }
47412
+ /**
47413
+ * Remove one handler from a deferred AppKit action bucket.
47414
+ *
47415
+ * Deletes the action key when its handler list becomes empty.
47416
+ *
47417
+ * @param handlers - Action bucket to update (`bridge` or `earn`)
47418
+ * @param action - Stored action key, including namespace or `*`
47419
+ * @param handler - Handler reference previously passed to {@link on}
47420
+ */ removeStoredActionHandler(handlers, action, handler) {
47421
+ const actionHandlers = handlers[action];
47422
+ if (!actionHandlers) {
47423
+ return;
47424
+ }
47425
+ const index = actionHandlers.indexOf(handler);
47426
+ if (index === -1) {
47427
+ return;
47428
+ }
47429
+ actionHandlers.splice(index, 1);
47430
+ if (actionHandlers.length === 0) {
47431
+ Reflect.deleteProperty(handlers, action);
45627
47432
  }
45628
47433
  }
45629
47434
  }