@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.cjs CHANGED
@@ -22,9 +22,9 @@ var zod = require('zod');
22
22
  var pino = require('pino');
23
23
  var units = require('@ethersproject/units');
24
24
  var bytes = require('@ethersproject/bytes');
25
+ require('@ethersproject/abi');
25
26
  var address = require('@ethersproject/address');
26
27
  var bs58 = require('bs58');
27
- require('@ethersproject/abi');
28
28
  var web3_js = require('@solana/web3.js');
29
29
  require('bn.js');
30
30
  require('@coral-xyz/anchor');
@@ -68,6 +68,7 @@ var bs58__default = /*#__PURE__*/_interopDefault(bs58);
68
68
  ...params,
69
69
  actions: {
70
70
  bridge: {},
71
+ earn: {},
71
72
  ...params.actions
72
73
  }
73
74
  };
@@ -3551,14 +3552,14 @@ class KitError extends Error {
3551
3552
  }
3552
3553
 
3553
3554
  /**
3554
- * Standardized error definitions for Earn/Zenith operations.
3555
+ * Standardized error definitions for Earn operations.
3555
3556
  *
3556
3557
  * These error codes provide fine-grained categorization of failures
3557
- * from the Zenith earn service, enabling SDK consumers to distinguish
3558
+ * from the Earn service, enabling SDK consumers to distinguish
3558
3559
  * between input errors (fix your request) and service errors (retry later).
3559
3560
  *
3560
3561
  * Error code ranges:
3561
- * - 1100-1105: INPUT errors — invalid inputs, unsupported configurations
3562
+ * - 1100-1106: INPUT errors — invalid, unsupported, or stale request state
3562
3563
  * - 8100-8105: SERVICE errors — retryable backend/provider failures
3563
3564
  *
3564
3565
  * @example
@@ -3611,6 +3612,14 @@ class KitError extends Error {
3611
3612
  name: 'EARN_UNSUPPORTED_BRIDGE_ROUTE',
3612
3613
  type: 'INPUT'
3613
3614
  },
3615
+ /**
3616
+ * The bridge quote expired. This is an INPUT error because the prepared
3617
+ * request is stale and must be replaced instead of retried.
3618
+ */ BRIDGE_QUOTE_EXPIRED: {
3619
+ code: 1106,
3620
+ name: 'EARN_BRIDGE_QUOTE_EXPIRED',
3621
+ type: 'INPUT'
3622
+ },
3614
3623
  /** The proxy signing call failed — retryable. */ SIGNING_FAILED: {
3615
3624
  code: 8100,
3616
3625
  name: 'EARN_SIGNING_FAILED',
@@ -3670,6 +3679,9 @@ function getOptionalString(value) {
3670
3679
  * internal-error, vault-refresh-busy, off-chain-paused, position-PnL-pending,
3671
3680
  * bridge failures/status lookup failures
3672
3681
  *
3682
+ * Quote expiry is INPUT/FATAL because callers must start a fresh bridge prepare
3683
+ * flow rather than retry the stale prepared bundle.
3684
+ *
3673
3685
  * Unrecognized codes fall through to `parseApiError` for HTTP-status-based
3674
3686
  * handling.
3675
3687
  *
@@ -3903,6 +3915,13 @@ function getOptionalString(value) {
3903
3915
  errorDef: EarnError.PROVIDER_ERROR,
3904
3916
  recoverability: 'FATAL'
3905
3917
  }
3918
+ ],
3919
+ [
3920
+ 380506,
3921
+ {
3922
+ errorDef: EarnError.BRIDGE_QUOTE_EXPIRED,
3923
+ recoverability: 'FATAL'
3924
+ }
3906
3925
  ]
3907
3926
  ]);
3908
3927
  /**
@@ -4647,7 +4666,10 @@ exports.EarnChain = void 0;
4647
4666
  contracts: {
4648
4667
  v1: {
4649
4668
  wallet: GATEWAY_WALLET_EVM_TESTNET,
4650
- minter: GATEWAY_MINTER_EVM_TESTNET
4669
+ minter: GATEWAY_MINTER_EVM_TESTNET,
4670
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
4671
+ // deposit into the GatewayWallet above.
4672
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
4651
4673
  }
4652
4674
  },
4653
4675
  forwarderSupported: {
@@ -7802,7 +7824,10 @@ var Chains = {
7802
7824
  minter: zod.z.string({
7803
7825
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
7804
7826
  invalid_type_error: 'Gateway minter address must be a string.'
7805
- }).min(1, 'Gateway minter address cannot be empty.')
7827
+ }).min(1, 'Gateway minter address cannot be empty.'),
7828
+ depositForHandler: zod.z.string({
7829
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
7830
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
7806
7831
  }).strict() // Reject any additional properties not defined in the schema
7807
7832
  ;
7808
7833
  /**
@@ -8342,21 +8367,31 @@ const swapTokenEnumSchema = zod.z.enum([
8342
8367
  * returning the appropriate address based on the requested contract type.
8343
8368
  *
8344
8369
  * @param chain - The chain definition to resolve the contract address for
8345
- * @param contractType - The type of contract address to resolve ('tokenMessenger' or 'messageTransmitter')
8370
+ * @param contractType - The type of contract address to resolve ('tokenMessenger', 'messageTransmitter', or 'tokenMessengerWithFees')
8346
8371
  * @returns The contract address for the specified contract type
8347
8372
  * @throws Error when chain does not support CCTP v2 or has unsupported contract configuration
8373
+ * @throws Error when 'tokenMessengerWithFees' is requested but not configured on the chain
8348
8374
  */ const resolveCCTPV2ContractAddress = (chain, contractType)=>{
8349
8375
  // Handle custom bridge contract for tokenMessenger (burn transaction)
8350
- if (hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
8376
+ if (contractType === 'tokenMessenger' && hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
8351
8377
  return chain.kitContracts.bridge;
8352
8378
  }
8353
8379
  // At this point we know CCTP v2 is supported, so contracts exist
8354
8380
  const cctpConfig = chain.cctp;
8355
8381
  const contracts = cctpConfig.contracts.v2;
8382
+ // The `TokenMessengerWithFees` wrapper (prepaid FORWARD path) is an optional
8383
+ // deployment carried alongside both split and merged configurations.
8384
+ if (contractType === 'tokenMessengerWithFees') {
8385
+ const wrapper = contracts.tokenMessengerWithFees;
8386
+ if (wrapper === undefined || wrapper === '') {
8387
+ throw new Error(`TokenMessengerWithFees is not configured on chain ${chain.name}. The prepaid FORWARD path is unavailable on this chain.`);
8388
+ }
8389
+ return wrapper;
8390
+ }
8356
8391
  // Handle different contract types with explicit type checking
8357
8392
  switch(contracts.type){
8358
8393
  case 'split':
8359
- return contracts.tokenMessenger ;
8394
+ return contractType === 'tokenMessenger' ? contracts.tokenMessenger : contracts.messageTransmitter;
8360
8395
  case 'merged':
8361
8396
  return contracts.contract;
8362
8397
  default:
@@ -11630,7 +11665,7 @@ function resolveOptions(options) {
11630
11665
  }
11631
11666
 
11632
11667
  var name$4 = "@circle-fin/bridge-kit";
11633
- var version$5 = "1.12.0";
11668
+ var version$5 = "1.12.1";
11634
11669
  var pkg$5 = {
11635
11670
  name: name$4,
11636
11671
  version: version$5};
@@ -14173,6 +14208,144 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
14173
14208
  return false;
14174
14209
  };
14175
14210
 
14211
+ /**
14212
+ * The zero address, denoting a native-currency fee in a signed quote.
14213
+ */ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
14214
+ /**
14215
+ * Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
14216
+ *
14217
+ * Determines the native `msg.value` and the ERC-20 approvals required, honouring
14218
+ * the quote's `feeToken`:
14219
+ * - Native fee (`feeToken` is the zero address): attach exactly `feeTotalAmount`
14220
+ * as `msg.value`; approve only the burn amount.
14221
+ * - ERC-20 fee equal to the burn token (both USDC — the `isBurnTokenFee` case):
14222
+ * approve a single combined `amount + feeTotalAmount` and skip the redundant
14223
+ * second approval.
14224
+ * - ERC-20 fee different from the burn token: approve the burn amount and the fee
14225
+ * amount separately.
14226
+ *
14227
+ * This encodes only balance/allowance intent; it does not fetch balances. The
14228
+ * caller is responsible for a balance preflight against the fresh quote.
14229
+ *
14230
+ * @param params - The fee token, burn token, burn amount, and total fee amount.
14231
+ * @returns The resolved fee payment plan.
14232
+ * @throws KitError if `amount` or `feeTotalAmount` is negative.
14233
+ *
14234
+ * @example
14235
+ * ```typescript
14236
+ * // Native fee
14237
+ * resolveFeePayment({
14238
+ * feeToken: '0x0000000000000000000000000000000000000000',
14239
+ * burnToken: '0xUSDC...',
14240
+ * amount: 1_000_000n,
14241
+ * feeTotalAmount: 3_500_000n,
14242
+ * })
14243
+ * // → { isNativeFee: true, isBurnTokenFee: false, nativeValue: 3_500_000n,
14244
+ * // approvals: [{ token: '0xUSDC...', amount: 1_000_000n }] }
14245
+ * ```
14246
+ */ const resolveFeePayment = (params)=>{
14247
+ const { feeToken, burnToken, amount, feeTotalAmount } = params;
14248
+ if (typeof amount !== 'bigint' || amount < 0n) {
14249
+ throw createValidationFailedError$1('amount', amount, 'Must be a non-negative bigint');
14250
+ }
14251
+ if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
14252
+ throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
14253
+ }
14254
+ const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
14255
+ const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
14256
+ if (isNativeFee) {
14257
+ return {
14258
+ isNativeFee: true,
14259
+ isBurnTokenFee: false,
14260
+ nativeValue: feeTotalAmount,
14261
+ approvals: [
14262
+ {
14263
+ token: burnToken,
14264
+ amount
14265
+ }
14266
+ ]
14267
+ };
14268
+ }
14269
+ if (isBurnTokenFee) {
14270
+ // Fee and burn draw on the same token — a single combined approval covers
14271
+ // both; the redundant second approval is skipped.
14272
+ return {
14273
+ isNativeFee: false,
14274
+ isBurnTokenFee: true,
14275
+ nativeValue: 0n,
14276
+ approvals: [
14277
+ {
14278
+ token: burnToken,
14279
+ amount: amount + feeTotalAmount
14280
+ }
14281
+ ]
14282
+ };
14283
+ }
14284
+ return {
14285
+ isNativeFee: false,
14286
+ isBurnTokenFee: false,
14287
+ nativeValue: 0n,
14288
+ approvals: [
14289
+ {
14290
+ token: burnToken,
14291
+ amount
14292
+ },
14293
+ {
14294
+ token: feeToken,
14295
+ amount: feeTotalAmount
14296
+ }
14297
+ ]
14298
+ };
14299
+ };
14300
+
14301
+ /**
14302
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
14303
+ * hookData must start with.
14304
+ */ const CCTP_FORWARD_MAGIC_HEX = Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
14305
+ /**
14306
+ * Determine whether a hookData blob begins with the `cctp-forward` envelope.
14307
+ *
14308
+ * The prepaid FORWARD path requires the GenericExecutor blob to be wrapped in a
14309
+ * `cctp-forward` frame; without it the wrapper reverts `ForwardFeeWithoutHook`.
14310
+ *
14311
+ * @param hookData - The 0x-prefixed hookData hex string.
14312
+ * @returns True when the hookData starts with the `cctp-forward` magic.
14313
+ *
14314
+ * @example
14315
+ * ```typescript
14316
+ * hasForwardHook('0x636374702d666f7277617264...') // true
14317
+ * hasForwardHook('0xdeadbeef') // false
14318
+ * ```
14319
+ */ const hasForwardHook = (hookData)=>{
14320
+ if (typeof hookData !== 'string') {
14321
+ return false;
14322
+ }
14323
+ const normalized = (hookData.startsWith('0x') ? hookData.slice(2) : hookData).toLowerCase();
14324
+ return normalized.startsWith(CCTP_FORWARD_MAGIC_HEX);
14325
+ };
14326
+ /**
14327
+ * Assert that a hookData blob is forward-friendly for the prepaid FORWARD path.
14328
+ *
14329
+ * The prepaid FORWARD path always requests a FORWARD fee item, so the wrapper
14330
+ * requires the hookData to start with a `cctp-forward` frame. Validating this
14331
+ * before the burn surfaces the guaranteed `ForwardFeeWithoutHook` revert as a
14332
+ * typed input error instead of an on-chain failure.
14333
+ *
14334
+ * @param hookData - The 0x-prefixed hookData hex string.
14335
+ * @throws KitError (`INPUT_VALIDATION_FAILED`) if the hookData is missing or lacks
14336
+ * the `cctp-forward` frame.
14337
+ *
14338
+ * @example
14339
+ * ```typescript
14340
+ * assertForwardHookData(geForwardHookData) // ok
14341
+ * assertForwardHookData('0xdeadbeef') // throws — would revert ForwardFeeWithoutHook
14342
+ * ```
14343
+ */ const assertForwardHookData = (hookData)=>{
14344
+ if (!hasForwardHook(hookData)) {
14345
+ throw createValidationFailedError$1('hookData', hookData, 'Prepaid FORWARD burns require a cctp-forward-wrapped hookData; without it the TokenMessengerWithFees wrapper reverts ForwardFeeWithoutHook');
14346
+ }
14347
+ };
14348
+
14176
14349
  /**
14177
14350
  * Type guard to validate the forwardFee object structure.
14178
14351
  *
@@ -15104,6 +15277,109 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
15104
15277
  }
15105
15278
  }
15106
15279
 
15280
+ /**
15281
+ * Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
15282
+ *
15283
+ * Validates the full public-boundary input before any field destructuring,
15284
+ * `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
15285
+ * inputs always produce typed `KitError` validation failures.
15286
+ *
15287
+ * Checks performed (in order):
15288
+ * - `params` must be a non-null plain object
15289
+ * - `source` — valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
15290
+ * - `destinationChain` — present and supports CCTP v2
15291
+ * - source and destination chains must both be testnet or both mainnet
15292
+ * - source and destination chains must differ
15293
+ * - `executor` — non-empty string
15294
+ * - `amount` — bigint or non-empty string coercible to bigint
15295
+ * - `feeTotalAmount` — bigint or non-empty string coercible to bigint
15296
+ * - `feeToken` — valid EVM address (`0x` + 40 hex chars)
15297
+ * - `claim.signedQuote` — valid `0x`-prefixed hex string
15298
+ * - `claim.refundAddress` — valid EVM address
15299
+ * - `hookData` — valid `0x`-prefixed hex string when present
15300
+ *
15301
+ * @param params - The value to validate.
15302
+ * @throws {KitError} If any field is missing or invalid.
15303
+ *
15304
+ * @example
15305
+ * ```typescript
15306
+ * assertBurnWithFeesParams(params)
15307
+ * // params is now typed as BurnWithFeesParams and safe to use
15308
+ * const { source, destinationChain, amount } = params
15309
+ * ```
15310
+ */ function assertBurnWithFeesParams(params) {
15311
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
15312
+ throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
15313
+ }
15314
+ const p = params;
15315
+ // Source wallet context
15316
+ assertCCTPv2WalletContext(p['source']);
15317
+ const source = p['source'];
15318
+ // destinationChain
15319
+ const destinationChain = p['destinationChain'];
15320
+ if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
15321
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
15322
+ }
15323
+ if (!isCCTPV2Supported(destinationChain)) {
15324
+ throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
15325
+ }
15326
+ const dest = destinationChain;
15327
+ // Testnet / mainnet mismatch
15328
+ if (source.chain.isTestnet !== dest.isTestnet) {
15329
+ throw createNetworkMismatchError(source.chain, dest);
15330
+ }
15331
+ // Same-chain guard
15332
+ if (source.chain.name === dest.name) {
15333
+ throw createUnsupportedRouteError(source.chain.name, dest.name);
15334
+ }
15335
+ // executor
15336
+ const executor = p['executor'];
15337
+ if (typeof executor !== 'string' || executor === '') {
15338
+ throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
15339
+ }
15340
+ // amount
15341
+ const rawAmount = p['amount'];
15342
+ if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
15343
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
15344
+ }
15345
+ try {
15346
+ BigInt(rawAmount);
15347
+ } catch {
15348
+ throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
15349
+ }
15350
+ // feeTotalAmount
15351
+ const rawFee = p['feeTotalAmount'];
15352
+ if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
15353
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
15354
+ }
15355
+ try {
15356
+ BigInt(rawFee);
15357
+ } catch {
15358
+ throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
15359
+ }
15360
+ // feeToken
15361
+ if (!evmAddressSchema.safeParse(p['feeToken']).success) {
15362
+ throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
15363
+ }
15364
+ // claim
15365
+ const rawClaim = p['claim'];
15366
+ if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
15367
+ throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
15368
+ }
15369
+ const claim = rawClaim;
15370
+ if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
15371
+ throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
15372
+ }
15373
+ if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
15374
+ throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
15375
+ }
15376
+ // hookData (optional)
15377
+ const hookData = p['hookData'];
15378
+ if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
15379
+ throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
15380
+ }
15381
+ }
15382
+
15107
15383
  /**
15108
15384
  * CCTP bridge step names that can occur in the bridging flow.
15109
15385
  *
@@ -16312,10 +16588,15 @@ const mockAttestationMessage = {
16312
16588
  const burnCallData = burnRequest.getCallData();
16313
16589
  // batchExecute may throw before submission (wallet declined) but never
16314
16590
  // after — post-submission errors are returned as empty receipts.
16591
+ // The sender is threaded for adapters whose execution is routed through a
16592
+ // signing strategy (which has no wallet account to read it from); the
16593
+ // wallet-client path ignores it.
16315
16594
  const batchResult = await adapter.batchExecute([
16316
16595
  approveCallData,
16317
16596
  burnCallData
16318
- ], chain);
16597
+ ], chain, {
16598
+ fromAddress: params.source.address
16599
+ });
16319
16600
  const approveReceipt = batchResult.receipts[0];
16320
16601
  const burnReceipt = batchResult.receipts[1];
16321
16602
  const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
@@ -16458,7 +16739,7 @@ const mockAttestationMessage = {
16458
16739
  return step;
16459
16740
  }
16460
16741
 
16461
- var version$4 = "1.9.0";
16742
+ var version$4 = "1.10.0";
16462
16743
  var pkg$4 = {
16463
16744
  version: version$4};
16464
16745
 
@@ -17051,7 +17332,7 @@ var pkg$4 = {
17051
17332
  * }
17052
17333
  * )
17053
17334
  * ```
17054
- */ async function retry(result, context, provider, invocationMeta) {
17335
+ */ async function retry$1(result, context, provider, invocationMeta) {
17055
17336
  const analysis = analyzeSteps(result);
17056
17337
  // Resolve invocation context for retry operation
17057
17338
  const resolvedInvocation = resolveRetryInvocation(invocationMeta);
@@ -17152,7 +17433,7 @@ var pkg$4 = {
17152
17433
  // Continue with remaining steps.
17153
17434
  // Recursive call handles subsequent pending states (e.g., next step may also
17154
17435
  // be pending), allowing the retry logic to loop through all actionable steps.
17155
- return await retry(result, context, provider);
17436
+ return await retry$1(result, context, provider);
17156
17437
  } catch (error) {
17157
17438
  // Re-throw FATAL validation errors - these indicate invalid input
17158
17439
  if (isFatalError(error)) {
@@ -17366,7 +17647,7 @@ function assertCCTPV2Config(config) {
17366
17647
  * const retryResult = await provider.retry(failedResult, retryContext)
17367
17648
  * ```
17368
17649
  */ async retry(result, context, invocationMeta) {
17369
- return retry(result, context, this, invocationMeta);
17650
+ return retry$1(result, context, this, invocationMeta);
17370
17651
  }
17371
17652
  /**
17372
17653
  * Estimate the cost and fees for a CCTP v2 cross-chain bridge operation.
@@ -17613,7 +17894,7 @@ function assertCCTPV2Config(config) {
17613
17894
  throw new Error(`Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
17614
17895
  }
17615
17896
  // Resolve spender address with proper error handling
17616
- const spenderAddress = resolveCCTPV2ContractAddress(chain);
17897
+ const spenderAddress = resolveCCTPV2ContractAddress(chain, 'tokenMessenger');
17617
17898
  // Prepare action parameters
17618
17899
  const actionParams = {
17619
17900
  amount: BigInt(amount),
@@ -18093,6 +18374,106 @@ function assertCCTPV2Config(config) {
18093
18374
  return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
18094
18375
  }
18095
18376
  /**
18377
+ * Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
18378
+ *
18379
+ * Builds the source-chain `depositForBurnWithHookAndFees` call for the
18380
+ * GenericExecutor FORWARD path: fees are collected up front on the source chain
18381
+ * against a signed quote, `mintRecipient` and `destinationCaller` are both set to
18382
+ * the GenericExecutor, and the GE `hookData` is passed through unchanged.
18383
+ *
18384
+ * This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
18385
+ * and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
18386
+ * `claim` are produced elsewhere and passed in here:
18387
+ * - `hookData`: `buildForwardingHookDataWithPayload(version,
18388
+ * buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
18389
+ * - `claim.signedQuote` / `feeToken` / `feeTotalAmount`: from `fetchFeeQuote`
18390
+ * (`@circle-fin/provider-fee-v1`), whose FORWARD item must be bound to the
18391
+ * SAME `hookData` and executor `destinationCaller` used here.
18392
+ *
18393
+ * The returned approvals and burn are NOT executed — the caller executes the
18394
+ * approvals first (in order) and then the burn. The fee payment channel matches
18395
+ * the quote's `feeToken`:
18396
+ * - Native fee: exactly `feeTotalAmount` is attached as the burn's `msg.value`;
18397
+ * only the burn amount is approved.
18398
+ * - USDC fee (same token as the burn): a single combined `amount + feeTotalAmount`
18399
+ * approval covers both; the redundant second approval is skipped.
18400
+ *
18401
+ * @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
18402
+ * @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
18403
+ * @returns The prepared approvals, the prepared burn, and the resolved fee plan.
18404
+ * @throws {KitError} If the wallet context is invalid, `destinationChain` does not
18405
+ * support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
18406
+ * a bigint or a numeric string coercible to bigint, the hookData lacks a
18407
+ * `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
18408
+ * context cannot be resolved.
18409
+ *
18410
+ * @example
18411
+ * ```typescript
18412
+ * const { approvals, burn } = await provider.burnWithFees({
18413
+ * source,
18414
+ * destinationChain: Arc,
18415
+ * amount: 1_000_000n,
18416
+ * executor: genericExecutorAddress,
18417
+ * hookData: geForwardHookData,
18418
+ * claim: { signedQuote: '0x01...', refundAddress: userAddress },
18419
+ * feeToken: '0x0000000000000000000000000000000000000000', // native
18420
+ * feeTotalAmount: 3_500_000n,
18421
+ * })
18422
+ * for (const approval of approvals) await approval.execute()
18423
+ * const txHash = await burn.execute()
18424
+ * ```
18425
+ */ async burnWithFees(params) {
18426
+ assertBurnWithFeesParams(params);
18427
+ const { source, destinationChain, executor, hookData, claim, feeToken } = params;
18428
+ const amount = BigInt(params.amount);
18429
+ const feeTotalAmount = BigInt(params.feeTotalAmount);
18430
+ // Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
18431
+ // so the hookData must carry a cctp-forward frame; otherwise the wrapper
18432
+ // reverts ForwardFeeWithoutHook. Surface it as a typed input error up front.
18433
+ assertForwardHookData(hookData);
18434
+ const burnToken = source.chain.usdcAddress;
18435
+ const feePayment = resolveFeePayment({
18436
+ feeToken,
18437
+ burnToken,
18438
+ amount,
18439
+ feeTotalAmount
18440
+ });
18441
+ // Resolve operation context from the source wallet context.
18442
+ const operationContext = this.extractOperationContext(source);
18443
+ let resolvedContext;
18444
+ try {
18445
+ resolvedContext = await resolveOperationContext(source.adapter, operationContext);
18446
+ } catch (error) {
18447
+ throw createValidationFailedError$1('source.adapter', undefined, `Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
18448
+ }
18449
+ const context = resolvedContext;
18450
+ const wrapperAddress = resolveCCTPV2ContractAddress(source.chain, 'tokenMessengerWithFees');
18451
+ // Build the ERC-20 approvals to the wrapper (burn token, plus a distinct fee
18452
+ // token only when the fee is not paid in the burn token).
18453
+ const approvals = await Promise.all(feePayment.approvals.map(async (approval)=>source.adapter.prepareAction('token.approve', {
18454
+ tokenAddress: approval.token,
18455
+ delegate: wrapperAddress,
18456
+ amount: approval.amount
18457
+ }, context)));
18458
+ // Build the burn: mintRecipient AND destinationCaller are both the executor.
18459
+ const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
18460
+ fromChain: source.chain,
18461
+ toChain: destinationChain,
18462
+ amount,
18463
+ mintRecipient: executor,
18464
+ destinationCaller: executor,
18465
+ hookData,
18466
+ claim,
18467
+ feeToken,
18468
+ feeTotalAmount
18469
+ }, context);
18470
+ return {
18471
+ approvals,
18472
+ burn,
18473
+ feePayment
18474
+ };
18475
+ }
18476
+ /**
18096
18477
  * Waits for a transaction to be mined and confirmed on the blockchain.
18097
18478
  *
18098
18479
  * This method should block until the transaction is confirmed on the blockchain.
@@ -18972,7 +19353,7 @@ registerKit(`${pkg$5.name}/${pkg$5.version}`);
18972
19353
  };
18973
19354
 
18974
19355
  var name$3 = "@circle-fin/swap-kit";
18975
- var version$3 = "1.3.2";
19356
+ var version$3 = "1.4.0";
18976
19357
  var pkg$3 = {
18977
19358
  name: name$3,
18978
19359
  version: version$3};
@@ -19037,7 +19418,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
19037
19418
  }).min(1, 'kitKey must be a non-empty string').optional(),
19038
19419
  provider: zod.z.string({
19039
19420
  invalid_type_error: 'provider must be a string'
19040
- }).min(1, 'provider must be a non-empty string').optional()
19421
+ }).min(1, 'provider must be a non-empty string').optional(),
19422
+ batchTransactions: zod.z.boolean({
19423
+ invalid_type_error: 'batchTransactions must be a boolean'
19424
+ }).optional()
19041
19425
  });
19042
19426
  /**
19043
19427
  * Zod schema for adapter context.
@@ -19568,7 +19952,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19568
19952
  /**
19569
19953
  * Circle Stablecoin Service API Key.
19570
19954
  * Must be a valid API key format.
19571
- */ apiKey: apiKeySchema
19955
+ */ apiKey: apiKeySchema.optional()
19572
19956
  }).superRefine(requireCrossChainQuoteToAddress);
19573
19957
  /**
19574
19958
  * Zod schema for validating CreateSwapRequest parameters.
@@ -19626,7 +20010,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19626
20010
  /**
19627
20011
  * Circle Stablecoin Service API Key.
19628
20012
  * Must be a valid API key format.
19629
- */ apiKey: apiKeySchema
20013
+ */ apiKey: apiKeySchema.optional()
19630
20014
  });
19631
20015
  /**
19632
20016
  * Zod schema for validating GetSwapStatusResponse data.
@@ -19662,7 +20046,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19662
20046
  toChain: zod.z.string({
19663
20047
  invalid_type_error: 'toChain must be a string'
19664
20048
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
19665
- apiKey: apiKeySchema
20049
+ apiKey: apiKeySchema.optional()
19666
20050
  });
19667
20051
  /**
19668
20052
  * Zod schema for validating CreateSwapResponse payloads.
@@ -19671,13 +20055,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19671
20055
  required_error: 'fee token is required',
19672
20056
  invalid_type_error: 'fee token must be a string'
19673
20057
  }).min(1, 'fee token must be a non-empty string'),
19674
- amount: feeAmountSchema
20058
+ amount: feeAmountSchema,
20059
+ decimals: zod.z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
20060
+ symbol: zod.z.string({
20061
+ invalid_type_error: 'fee token symbol must be a string'
20062
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
19675
20063
  });
19676
20064
  /**
19677
20065
  * Developer fee item schema with basis field.
19678
- */ const createSwapDeveloperFeeItemSchema = zod.z.object({
19679
- token: zod.z.string().min(1, 'fee token must be a non-empty string'),
19680
- amount: feeAmountSchema,
20066
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
19681
20067
  basis: zod.z.enum([
19682
20068
  'inputAmount',
19683
20069
  'estimatedAmount'
@@ -19769,7 +20155,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19769
20155
  addresses: zod.z.array(zod.z.string({
19770
20156
  invalid_type_error: 'addresses entries must be strings'
19771
20157
  }).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(),
19772
- apiKey: apiKeySchema
20158
+ apiKey: apiKeySchema.optional()
19773
20159
  });
19774
20160
  /**
19775
20161
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -19986,7 +20372,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
19986
20372
  ...DEFAULT_CONFIG$1,
19987
20373
  headers: {
19988
20374
  ...DEFAULT_CONFIG$1.headers,
19989
- Authorization: `Bearer ${apiKey}`
20375
+ // Permissionless mode: no Authorization header when the kit key is absent.
20376
+ ...apiKey !== undefined && {
20377
+ Authorization: `Bearer ${apiKey}`
20378
+ }
19990
20379
  }
19991
20380
  };
19992
20381
  try {
@@ -20140,7 +20529,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20140
20529
  ...DEFAULT_CONFIG$1,
20141
20530
  headers: {
20142
20531
  ...DEFAULT_CONFIG$1.headers,
20143
- Authorization: `Bearer ${validatedParams.apiKey}`
20532
+ // Permissionless mode: no Authorization header when the kit key is absent.
20533
+ ...validatedParams.apiKey !== undefined && {
20534
+ Authorization: `Bearer ${validatedParams.apiKey}`
20535
+ }
20144
20536
  }
20145
20537
  };
20146
20538
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
@@ -20195,7 +20587,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20195
20587
  const validatedParams = {
20196
20588
  txHash: result.data.txHash,
20197
20589
  chain: result.data.chain,
20198
- apiKey: result.data.apiKey,
20590
+ ...result.data.apiKey !== undefined && {
20591
+ apiKey: result.data.apiKey
20592
+ },
20199
20593
  ...result.data.toChain !== undefined && {
20200
20594
  toChain: result.data.toChain
20201
20595
  }
@@ -20205,7 +20599,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20205
20599
  ...DEFAULT_CONFIG$1,
20206
20600
  headers: {
20207
20601
  ...DEFAULT_CONFIG$1.headers,
20208
- Authorization: `Bearer ${validatedParams.apiKey}`
20602
+ // Permissionless mode: no Authorization header when the kit key is absent.
20603
+ ...validatedParams.apiKey !== undefined && {
20604
+ Authorization: `Bearer ${validatedParams.apiKey}`
20605
+ }
20209
20606
  }
20210
20607
  };
20211
20608
  return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
@@ -20294,7 +20691,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20294
20691
  }
20295
20692
  const validatedParams = {
20296
20693
  chain: result.data.chain,
20297
- apiKey: result.data.apiKey,
20694
+ ...result.data.apiKey !== undefined && {
20695
+ apiKey: result.data.apiKey
20696
+ },
20298
20697
  ...result.data.addresses !== undefined && {
20299
20698
  addresses: result.data.addresses
20300
20699
  }
@@ -20304,7 +20703,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20304
20703
  ...DEFAULT_CONFIG$1,
20305
20704
  headers: {
20306
20705
  ...DEFAULT_CONFIG$1.headers,
20307
- Authorization: `Bearer ${validatedParams.apiKey}`
20706
+ // Permissionless mode: no Authorization header when the kit key is absent.
20707
+ ...validatedParams.apiKey !== undefined && {
20708
+ Authorization: `Bearer ${validatedParams.apiKey}`
20709
+ }
20308
20710
  }
20309
20711
  };
20310
20712
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
@@ -22349,6 +22751,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
22349
22751
  */ function hasSignTypedData(adapter) {
22350
22752
  return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
22351
22753
  }
22754
+ /**
22755
+ * Type guard to check if an adapter can actually produce an EIP-712
22756
+ * typed-data signature.
22757
+ *
22758
+ * @remarks
22759
+ * Strengthens {@link hasSignTypedData}: having a `signTypedData` method
22760
+ * does not guarantee it can succeed. Adapters whose signer is delegated
22761
+ * (e.g. through a signing strategy backed by a smart contract account)
22762
+ * expose the method but reject typed-data payloads at runtime. Such
22763
+ * adapters report their real capability through an optional
22764
+ * `supportsSignTypedData()` method, which this guard consults when
22765
+ * present. Adapters without the capability method are assumed able to
22766
+ * sign, preserving the previous duck-typing behavior.
22767
+ *
22768
+ * @param adapter - The adapter to check
22769
+ * @returns True if calling `signTypedData` can be expected to succeed
22770
+ *
22771
+ * @example
22772
+ * ```typescript
22773
+ * import { canSignTypedData } from '@core/adapter-evm'
22774
+ *
22775
+ * if (canSignTypedData(adapter)) {
22776
+ * const signature = await adapter.signTypedData(typedData, context)
22777
+ * } else {
22778
+ * // take an on-chain approval path instead of a permit signature
22779
+ * }
22780
+ * ```
22781
+ */ function canSignTypedData(adapter) {
22782
+ if (!hasSignTypedData(adapter)) {
22783
+ return false;
22784
+ }
22785
+ if (typeof adapter.supportsSignTypedData === 'function') {
22786
+ // The value is `boolean` per the interface, but a plain-JS adapter may
22787
+ // return anything; treat it as untrusted and coerce to a strict
22788
+ // boolean. Comparing an `unknown` (not a `boolean`) also keeps the
22789
+ // lint autofix from stripping this as a redundant `=== true`.
22790
+ const supported = adapter.supportsSignTypedData();
22791
+ return supported === true;
22792
+ }
22793
+ return true;
22794
+ }
22352
22795
 
22353
22796
  /**
22354
22797
  * Build EIP-2612 typed data for permit signing.
@@ -23881,10 +24324,13 @@ function writeBytes32(buffer, hex, offset) {
23881
24324
  * at usage time rather than construction time.
23882
24325
  *
23883
24326
  * Validates:
23884
- * - Kit key is present and matches required format (KIT_KEY:id:secret)
24327
+ * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
24328
+ * An absent or empty kit key is permitted (permissionless mode) — the swap
24329
+ * service now treats the key as optional.
23885
24330
  *
23886
- * @param kitKey - The inline kit key from the swap operation config
23887
- * @throws KitError with VALIDATION_FAILED if kit key is invalid or missing
24331
+ * @param kitKey - The inline kit key from the swap operation config (optional)
24332
+ * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
24333
+ * match the KIT_KEY:<keyId>:<keySecret> format
23888
24334
  *
23889
24335
  * @example
23890
24336
  * ```typescript
@@ -23896,9 +24342,11 @@ function writeBytes32(buffer, hex, offset) {
23896
24342
  * assertKitKey(kitKey)
23897
24343
  * ```
23898
24344
  */ function assertKitKey(kitKey) {
23899
- // Validate API key format using existing schema from service-client
24345
+ // Permissionless mode: the swap service treats the kit key as optional, so an
24346
+ // absent (or empty) key is valid. Only validate the format when a key is
24347
+ // actually provided.
23900
24348
  if (!kitKey) {
23901
- 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');
24349
+ return;
23902
24350
  }
23903
24351
  const apiKeyResult = apiKeySchema.safeParse(kitKey);
23904
24352
  if (!apiKeyResult.success) {
@@ -24195,8 +24643,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
24195
24643
  validateResolvedAddress(resolvedTokenInAddress, chain);
24196
24644
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
24197
24645
  validateResolvedAddress(to, destinationChain);
24198
- const kitKey = config?.kitKey ?? '';
24199
- // Validates the kit key
24646
+ const kitKey = config?.kitKey;
24647
+ // Validate the kit key format when one is provided (permissionless otherwise).
24200
24648
  assertKitKey(kitKey);
24201
24649
  // Validate custom fee configuration if present
24202
24650
  const customFee = config?.customFee;
@@ -24241,7 +24689,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
24241
24689
  }
24242
24690
  }
24243
24691
  },
24244
- apiKey: kitKey
24692
+ // Map kitKey → apiKey for the service client; omitted in permissionless mode.
24693
+ ...kitKey ? {
24694
+ apiKey: kitKey
24695
+ } : {}
24245
24696
  };
24246
24697
  }
24247
24698
 
@@ -24895,6 +25346,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
24895
25346
  }
24896
25347
  }
24897
25348
 
25349
+ /**
25350
+ * Determine whether an adapter can produce an EIP-2612 permit signature.
25351
+ *
25352
+ * @remarks
25353
+ * A gasless permit needs two adapter capabilities: fetching the token's
25354
+ * EIP-2612 nonce and producing an EIP-712 typed-data signature. The
25355
+ * typed-data check uses {@link canSignTypedData} rather than a bare
25356
+ * `hasSignTypedData` guard so that an adapter routed through a signing
25357
+ * strategy that cannot produce typed-data signatures — one whose manifest
25358
+ * omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
25359
+ * — is correctly excluded. Such an adapter falls back to an on-chain approval
25360
+ * (batched into a single submission when it supports atomic execution) instead
25361
+ * of attempting a permit its strategy would reject.
25362
+ *
25363
+ * @param adapter - The source adapter to inspect.
25364
+ * @returns `true` when the adapter can both fetch a nonce and sign typed data.
25365
+ *
25366
+ * @example
25367
+ * ```typescript
25368
+ * import { adapterSupportsPermit } from './utils'
25369
+ *
25370
+ * if (adapterSupportsPermit(adapter)) {
25371
+ * // gasless permit path — fold the approval into the swap transaction
25372
+ * } else {
25373
+ * // on-chain approval path (batched when supportsAtomicBatch is true)
25374
+ * }
25375
+ * ```
25376
+ */ function adapterSupportsPermit(adapter) {
25377
+ return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
25378
+ }
25379
+
24898
25380
  /**
24899
25381
  * Generate EIP-2612 permit signature for token approval.
24900
25382
  *
@@ -25030,8 +25512,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
25030
25512
  }
25031
25513
  // Skip permit generation if the adapter lacks the required capabilities.
25032
25514
  // handleEvmTokenApproval will have already sent an on-chain approval in this case.
25033
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
25034
- if (!adapterSupportsPermit) {
25515
+ if (!adapterSupportsPermit(adapter)) {
25035
25516
  return [
25036
25517
  createFallbackTokenInput(tokenInAddress, inputAmount)
25037
25518
  ];
@@ -25590,6 +26071,65 @@ const TOKEN_REGISTRY$3 = createTokenRegistry();
25590
26071
  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.`;
25591
26072
  }
25592
26073
 
26074
+ /**
26075
+ * Determine which chain a fee token should be resolved and formatted against.
26076
+ *
26077
+ * @remarks
26078
+ * Fees returned by the service may be denominated in either the input token
26079
+ * (on the source chain) or the output token (on the destination chain). A
26080
+ * contract address only resolves on the chain it belongs to, so formatting a
26081
+ * destination-denominated fee against the source chain causes
26082
+ * {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
26083
+ * units (e.g. a cross-chain swap charging a fee in the destination output
26084
+ * token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
26085
+ * resolved against the source chain). This is the fallback for fee items that
26086
+ * are not self-described with their own `decimals`/`chain`.
26087
+ *
26088
+ * Prefer the source chain (covers same-chain swaps and input-denominated
26089
+ * fees), then fall back to the destination chain when the token only resolves
26090
+ * there. When neither chain recognises the token, default to the source chain
26091
+ * so existing on-chain decimal lookups via the source adapter still apply.
26092
+ *
26093
+ * Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
26094
+ * source-first preference keeps them on the source chain. That is correct for
26095
+ * registry stablecoins, and for `'NATIVE'` only when both chains share native
26096
+ * decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
26097
+ * `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
26098
+ * mis-scaled. This is latent — providers emit the address form, and
26099
+ * self-describing fee items carry their own `decimals` and never reach this
26100
+ * helper — so the gap only opens for a future `'NATIVE'` fee that arrives
26101
+ * without `decimals` on a cross-native-decimal route.
26102
+ *
26103
+ * @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
26104
+ * @param sourceChain - The chain the swap originates from.
26105
+ * @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
26106
+ * @returns The chain definition the fee token should be resolved against.
26107
+ *
26108
+ * @example
26109
+ * ```typescript
26110
+ * import { resolveFeeChain } from './resolveFeeChain'
26111
+ * import { Ethereum, Base } from '@core/chains'
26112
+ *
26113
+ * // Cross-chain swap fee charged in the destination (output) token
26114
+ * resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
26115
+ * // => Base (EURC resolves on Base, not Ethereum)
26116
+ *
26117
+ * // Symbol or source-token fees stay on the source chain
26118
+ * resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
26119
+ * ```
26120
+ */ function resolveFeeChain(token, sourceChain, destinationChain) {
26121
+ if (sourceChain.chain === destinationChain.chain) {
26122
+ return sourceChain;
26123
+ }
26124
+ if (resolveTokenSymbol(token, sourceChain) !== null) {
26125
+ return sourceChain;
26126
+ }
26127
+ if (resolveTokenSymbol(token, destinationChain) !== null) {
26128
+ return destinationChain;
26129
+ }
26130
+ return sourceChain;
26131
+ }
26132
+
25593
26133
  const TOKEN_REGISTRY$2 = createTokenRegistry();
25594
26134
  /**
25595
26135
  * Format a raw base-unit amount into a human-readable decimal string.
@@ -25680,6 +26220,186 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
25680
26220
  }
25681
26221
  }
25682
26222
 
26223
+ /**
26224
+ * Runtime guard for {@link BatchCapableSwapAdapter}.
26225
+ *
26226
+ * @param adapter - The adapter to inspect.
26227
+ * @returns `true` when the adapter exposes both batch methods.
26228
+ *
26229
+ * @example
26230
+ * ```typescript
26231
+ * if (isBatchCapableSwapAdapter(adapter)) {
26232
+ * // adapter.supportsAtomicBatch / adapter.batchExecute are available
26233
+ * }
26234
+ * ```
26235
+ */ function isBatchCapableSwapAdapter(adapter) {
26236
+ return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
26237
+ }
26238
+ /**
26239
+ * Decide whether the EVM swap should take the batched approve-and-swap path.
26240
+ *
26241
+ * @remarks
26242
+ * Batching only helps when an on-chain approval would otherwise be required, so
26243
+ * it is skipped for native tokens (no approval) and for the gasless permit path
26244
+ * (already a single transaction). USDT is skipped because its reset-to-zero
26245
+ * allowance flow cannot be expressed as a fixed approve+swap pair. When those
26246
+ * gates pass, the adapter's actual atomic-batch capability is queried; any
26247
+ * failure resolves to `false` so the swap falls back to the sequential path.
26248
+ *
26249
+ * @param args - The decision inputs.
26250
+ * @param args.adapter - The source adapter.
26251
+ * @param args.chain - The source chain definition.
26252
+ * @param args.tokenInAddress - The resolved input-token address.
26253
+ * @param args.allowanceStrategy - Optional allowance strategy override.
26254
+ * @param args.batchTransactions - Optional explicit opt-out (`false` disables).
26255
+ * @returns `true` when the batched approve-and-swap path should be used.
26256
+ *
26257
+ * @example
26258
+ * ```typescript
26259
+ * const useBatched = await shouldUseBatchedSwap({
26260
+ * adapter,
26261
+ * chain,
26262
+ * tokenInAddress: '0xA0b8...',
26263
+ * allowanceStrategy: config?.allowanceStrategy,
26264
+ * batchTransactions: config?.batchTransactions,
26265
+ * })
26266
+ * ```
26267
+ */ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
26268
+ // Explicit opt-out.
26269
+ if (batchTransactions === false) {
26270
+ return false;
26271
+ }
26272
+ // Batching is an EVM capability (EIP-5792 or a signing strategy).
26273
+ if (chain.type !== 'evm') {
26274
+ return false;
26275
+ }
26276
+ // Native tokens need no approval — the swap is already a single transaction.
26277
+ if (isNativeEvmAddress(tokenInAddress)) {
26278
+ return false;
26279
+ }
26280
+ // A gasless permit folds the approval into the swap transaction, so there is
26281
+ // nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
26282
+ const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
26283
+ if (canUsePermit) {
26284
+ return false;
26285
+ }
26286
+ // USDT's reset-to-zero allowance dance cannot be expressed as a fixed
26287
+ // approve+swap pair; leave it on the sequential path.
26288
+ const usdt = chain.usdtAddress?.toLowerCase();
26289
+ if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
26290
+ return false;
26291
+ }
26292
+ if (!isBatchCapableSwapAdapter(adapter)) {
26293
+ return false;
26294
+ }
26295
+ try {
26296
+ return await adapter.supportsAtomicBatch(chain);
26297
+ } catch {
26298
+ return false;
26299
+ }
26300
+ }
26301
+ /**
26302
+ * Execute the approval and swap as a single atomic batch.
26303
+ *
26304
+ * @remarks
26305
+ * Extracts the raw call data from both prepared requests, submits them as one
26306
+ * batch via `adapter.batchExecute`, and maps the swap receipt back to a
26307
+ * transaction hash. The `fromAddress` is threaded for adapters routed through a
26308
+ * signing strategy (which have no wallet account to read the sender from); the
26309
+ * wallet-client path ignores it.
26310
+ *
26311
+ * Following the batch contract, `batchExecute` never throws once the batch is
26312
+ * submitted — a missing or failed swap receipt is surfaced here as a thrown
26313
+ * {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
26314
+ * batch and double-swap.
26315
+ *
26316
+ * @param args - The execution inputs.
26317
+ * @param args.adapter - The batch-capable source adapter.
26318
+ * @param args.chain - The EVM chain to execute on.
26319
+ * @param args.approveRequest - The prepared ERC-20 approval request.
26320
+ * @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
26321
+ * @param args.fromAddress - The address authorizing the batch.
26322
+ * @returns The swap transaction hash and the executed approval + swap records.
26323
+ * @throws {@link KitError} when the prepared requests cannot yield call data.
26324
+ * @throws {@link KitError} when the batch does not confirm or the swap reverts.
26325
+ *
26326
+ * @example
26327
+ * ```typescript
26328
+ * const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
26329
+ * adapter,
26330
+ * chain,
26331
+ * approveRequest,
26332
+ * swapRequest,
26333
+ * fromAddress: '0x742d...',
26334
+ * })
26335
+ * ```
26336
+ */ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
26337
+ if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
26338
+ throw new KitError({
26339
+ ...InputError.UNSUPPORTED_ACTION,
26340
+ recoverability: 'FATAL',
26341
+ message: 'Batched swap requires EVM prepared requests with getCallData() support.'
26342
+ });
26343
+ }
26344
+ const approveCallData = approveRequest.getCallData();
26345
+ const swapCallData = swapRequest.getCallData();
26346
+ const batchResult = await adapter.batchExecute([
26347
+ approveCallData,
26348
+ swapCallData
26349
+ ], chain, {
26350
+ fromAddress
26351
+ });
26352
+ const swapReceipt = batchResult.receipts[1];
26353
+ // A missing swap receipt means the batch never confirmed (polling timed out
26354
+ // or the wallet returned fewer receipts than calls). Re-throw the underlying
26355
+ // error when present (already FATAL); otherwise surface a FATAL timeout so the
26356
+ // caller checks the batch status rather than resubmitting.
26357
+ if (swapReceipt === undefined || swapReceipt.txHash === '') {
26358
+ if (isKitError(batchResult.error)) {
26359
+ throw batchResult.error;
26360
+ }
26361
+ throw new KitError({
26362
+ ...NetworkError.TIMEOUT,
26363
+ recoverability: 'FATAL',
26364
+ message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
26365
+ // Preserve the underlying confirmation failure when it isn't a KitError —
26366
+ // the signing-strategy path returns a raw viem error (e.g. a dropped or
26367
+ // replaced tx) — so the root cause survives behind the generic timeout.
26368
+ cause: {
26369
+ trace: {
26370
+ batchId: batchResult.batchId,
26371
+ ...batchResult.error != null && {
26372
+ error: batchResult.error
26373
+ }
26374
+ }
26375
+ }
26376
+ });
26377
+ }
26378
+ if (swapReceipt.status !== 'success') {
26379
+ throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
26380
+ }
26381
+ const executedTransactions = [];
26382
+ const approveReceipt = batchResult.receipts[0];
26383
+ // An atomic batch is a single on-chain transaction, so the approve and swap
26384
+ // receipts share one hash. Only surface a distinct approval record when it is
26385
+ // genuinely a separate transaction; otherwise the lone swap record represents
26386
+ // the batch, avoiding a phantom duplicate tx in executedTransactions.
26387
+ if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
26388
+ executedTransactions.push({
26389
+ type: 'approval',
26390
+ txHash: approveReceipt.txHash
26391
+ });
26392
+ }
26393
+ executedTransactions.push({
26394
+ type: 'swap',
26395
+ txHash: swapReceipt.txHash
26396
+ });
26397
+ return {
26398
+ swapTxHash: swapReceipt.txHash,
26399
+ executedTransactions
26400
+ };
26401
+ }
26402
+
25683
26403
  /**
25684
26404
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
25685
26405
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -25806,7 +26526,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
25806
26526
  const statusResult = await getSwapStatus$2({
25807
26527
  txHash,
25808
26528
  chain: chain.chain,
25809
- apiKey
26529
+ ...apiKey !== undefined && {
26530
+ apiKey
26531
+ }
25810
26532
  });
25811
26533
  if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
25812
26534
  return {
@@ -26397,8 +27119,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26397
27119
  // Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
26398
27120
  const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
26399
27121
  const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
26400
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
26401
- const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
27122
+ const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
26402
27123
  const needsApproval = !isNativeToken && !canUsePermitFlow;
26403
27124
  if (!needsApproval) {
26404
27125
  return;
@@ -26677,34 +27398,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26677
27398
  const serviceResponse = await createSwap(serviceParams);
26678
27399
  // Track executed transactions
26679
27400
  const executedTransactions = [];
26680
- // Prepare swap action based on chain type
26681
- let preparedAction;
26682
- if (chain.type === 'solana') {
26683
- // Solana: No approval needed, directly prepare swap action
26684
- preparedAction = await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext);
26685
- } else {
26686
- // EVM chains: Handle token approval if needed
26687
- await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
26688
- // EVM chains: prepareEvmSwapAction handles EIP-2612 permit generation
26689
- // Adapter contract address is read from chain.kitContracts.adapter
26690
- // Use the already-resolved context from above
26691
- preparedAction = await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy);
26692
- }
27401
+ // Prepare swap action(s) based on chain type and batch capability.
27402
+ // Returns either a single prepared action (Solana / sequential EVM) or a
27403
+ // batched approve+swap plan (EVM atomic-batch path).
27404
+ const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
27405
+ adapter,
27406
+ chain,
27407
+ serviceResponse,
27408
+ resolvedContext,
27409
+ executionCtx,
27410
+ config,
27411
+ executedTransactions
27412
+ });
26693
27413
  // Execute swap transaction via adapter
26694
27414
  // For EVM chains, use gas limit from proxy service API
26695
27415
  let txHash;
26696
27416
  const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
26697
27417
  try {
26698
- txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
26699
- executedTransactions.push({
26700
- type: 'swap',
26701
- txHash
26702
- });
26703
- // Wait for transaction confirmation and verify success
26704
- const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
26705
- if (txReceipt.status === 'reverted') {
26706
- const explorerUrl = buildExplorerUrl(chain, txHash);
26707
- throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
27418
+ if (batchedSwapPlan) {
27419
+ // Approve + swap submitted as one atomic batch. batchExecute confirms
27420
+ // the swap internally, so no separate waitForTransaction is needed.
27421
+ const batched = await executeBatchedApproveAndSwap({
27422
+ adapter: adapter,
27423
+ chain: chain,
27424
+ approveRequest: batchedSwapPlan.approveRequest,
27425
+ swapRequest: batchedSwapPlan.swapRequest,
27426
+ fromAddress: executionCtx.fromAddress
27427
+ });
27428
+ txHash = batched.swapTxHash;
27429
+ executedTransactions.push(...batched.executedTransactions);
27430
+ } else if (preparedAction) {
27431
+ txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
27432
+ executedTransactions.push({
27433
+ type: 'swap',
27434
+ txHash
27435
+ });
27436
+ // Wait for transaction confirmation and verify success
27437
+ const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
27438
+ if (txReceipt.status === 'reverted') {
27439
+ const explorerUrl = buildExplorerUrl(chain, txHash);
27440
+ throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
27441
+ }
27442
+ } else {
27443
+ // Unreachable: the preparation step always yields either a batched plan
27444
+ // or a prepared action.
27445
+ throw new KitError({
27446
+ ...InputError.UNSUPPORTED_ACTION,
27447
+ recoverability: 'FATAL',
27448
+ message: 'No swap execution path was prepared.'
27449
+ });
26708
27450
  }
26709
27451
  } catch (err) {
26710
27452
  handleSwapExecutionError(err, txHash, chain);
@@ -26723,7 +27465,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26723
27465
  isCrossChainSwap,
26724
27466
  txHash,
26725
27467
  chain,
26726
- apiKey: serviceParams.apiKey
27468
+ ...serviceParams.apiKey !== undefined && {
27469
+ apiKey: serviceParams.apiKey
27470
+ }
26727
27471
  });
26728
27472
  // Build and return SwapResult
26729
27473
  return {
@@ -26748,6 +27492,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26748
27492
  };
26749
27493
  }
26750
27494
  /**
27495
+ * Prepare the swap execution request(s) for the source wallet's chain.
27496
+ *
27497
+ * Produces either a single {@link PreparedChainRequest} (Solana, or the
27498
+ * sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
27499
+ * atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
27500
+ * batching). The caller executes whichever field is populated. Any on-chain
27501
+ * approval sent on the sequential path is appended to `executedTransactions`.
27502
+ *
27503
+ * @typeParam TFromAdapterCapabilities - Source-adapter capability set.
27504
+ * @param args - Inputs derived from the validated swap request.
27505
+ * @param args.adapter - Source-chain wallet adapter.
27506
+ * @param args.chain - Source chain definition.
27507
+ * @param args.serviceResponse - Validated createSwap response.
27508
+ * @param args.resolvedContext - Resolved operation context.
27509
+ * @param args.executionCtx - Minimal on-chain execution context.
27510
+ * @param args.config - Optional swap configuration (allowance/batch flags).
27511
+ * @param args.executedTransactions - Array appended with any sent approval.
27512
+ * @returns The prepared action or the batched approve+swap plan.
27513
+ * @throws KitError when the EVM atomic-batch path is selected but the chain
27514
+ * has no configured adapter contract.
27515
+ */ async prepareSwapRequests(args) {
27516
+ const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
27517
+ if (chain.type === 'solana') {
27518
+ // Solana: No approval needed, directly prepare swap action
27519
+ return {
27520
+ preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
27521
+ };
27522
+ }
27523
+ const useBatch = await shouldUseBatchedSwap({
27524
+ adapter,
27525
+ chain,
27526
+ tokenInAddress: executionCtx.tokenInAddress,
27527
+ allowanceStrategy: config?.allowanceStrategy,
27528
+ batchTransactions: config?.batchTransactions
27529
+ });
27530
+ if (useBatch) {
27531
+ // EVM chains: fuse the ERC-20 approval and the swap into a single atomic
27532
+ // batch (one signing challenge for smart-contract wallets). Force the
27533
+ // swap onto the pre-approval (PermitType.NONE) path since the approval
27534
+ // rides in the same batch.
27535
+ const adapterContractAddress = chain.kitContracts?.adapter;
27536
+ if (!adapterContractAddress) {
27537
+ throw new KitError({
27538
+ ...InputError.VALIDATION_FAILED,
27539
+ recoverability: 'FATAL',
27540
+ message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
27541
+ cause: {
27542
+ trace: {
27543
+ chain: chain.name
27544
+ }
27545
+ }
27546
+ });
27547
+ }
27548
+ const [approveRequest, swapRequest] = await Promise.all([
27549
+ this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
27550
+ prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
27551
+ ]);
27552
+ return {
27553
+ batchedSwapPlan: {
27554
+ approveRequest,
27555
+ swapRequest
27556
+ }
27557
+ };
27558
+ }
27559
+ // EVM chains: Handle token approval if needed, then prepare the swap.
27560
+ // prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
27561
+ // contract address is read from chain.kitContracts.adapter.
27562
+ await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
27563
+ return {
27564
+ preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
27565
+ };
27566
+ }
27567
+ /**
26751
27568
  * Executes a swap transaction with the appropriate gas limit for the chain type.
26752
27569
  *
26753
27570
  * For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
@@ -26796,8 +27613,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26796
27613
  */ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
26797
27614
  if (!fees) return [];
26798
27615
  const [providerFees, swapFees, developerFees] = await Promise.all([
26799
- this.formatServiceFees(fees.provider, chain, 'provider', adapter),
26800
- this.formatServiceFees(fees.swap, chain, 'swap', adapter),
27616
+ this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
27617
+ this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
26801
27618
  recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
26802
27619
  ]);
26803
27620
  return [
@@ -26807,6 +27624,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26807
27624
  ];
26808
27625
  }
26809
27626
  /**
27627
+ * Resolve a single fee item to its display token and human-readable amount.
27628
+ *
27629
+ * @remarks
27630
+ * Prefer the self-describing metadata the service attaches to each fee:
27631
+ * `decimals` (and `symbol`) come straight from the provider quote, so they
27632
+ * are authoritative even for a token absent from the SDK registry on both
27633
+ * chains. That is the case {@link resolveFeeChain} cannot recover — a
27634
+ * destination-denominated fee token resolves on neither the source registry
27635
+ * nor the source-bound adapter, leaving the amount as raw base units. When
27636
+ * the service omits `decimals` (optional during rollout), fall back to
27637
+ * inferring the fee token's chain and resolving via the registry/adapter.
27638
+ *
27639
+ * Like {@link formatTokenValue}, this never throws: fee display is cosmetic
27640
+ * and must not fail an estimate/swap. A malformed self-describing `decimals`
27641
+ * (e.g. a non-numeric `amount` or invalid decimal count that makes
27642
+ * {@link formatUnits} throw) falls through to chain-based resolution rather
27643
+ * than propagating out of {@link buildFormattedFees}.
27644
+ *
27645
+ * @param fee - The fee item from the service response.
27646
+ * @param chain - The source chain definition.
27647
+ * @param destinationChain - The destination chain definition.
27648
+ * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
27649
+ * @returns Promise resolving to the formatted amount and display token.
27650
+ */ async formatFeeValue(fee, chain, destinationChain, adapter) {
27651
+ if (fee.decimals != null) {
27652
+ try {
27653
+ return {
27654
+ amount: formatUnits(fee.amount, fee.decimals),
27655
+ token: fee.symbol ?? fee.token
27656
+ };
27657
+ } catch {
27658
+ // Malformed service metadata — fall through to chain-based resolution,
27659
+ // which never throws (worst case: raw passthrough).
27660
+ }
27661
+ }
27662
+ const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
27663
+ return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
27664
+ }
27665
+ /**
26810
27666
  * Format service fee items into the SDK's ServiceSwapFee structure.
26811
27667
  *
26812
27668
  * @remarks
@@ -26817,14 +27673,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26817
27673
  * - Raw passthrough only when both registry and adapter fail
26818
27674
  *
26819
27675
  * @param feeItems - Array of fee items from the service response.
26820
- * @param chain - The chain definition for token resolution and formatting.
27676
+ * @param chain - The source chain definition for token resolution and formatting.
27677
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
26821
27678
  * @param type - The fee type to assign ('provider' or 'swap').
26822
27679
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
26823
27680
  * @returns Promise resolving to formatted ServiceSwapFee array.
26824
- */ async formatServiceFees(feeItems, chain, type, adapter) {
27681
+ */ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
26825
27682
  if (!feeItems) return [];
26826
27683
  return Promise.all(feeItems.map(async (fee)=>{
26827
- const formatted = await formatTokenValue(fee.amount, fee.token, chain, adapter);
27684
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
26828
27685
  return {
26829
27686
  token: formatted.token,
26830
27687
  amount: formatted.amount,
@@ -26836,16 +27693,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
26836
27693
  * Format developer fee items into the SDK's ServiceSwapFee structure.
26837
27694
  *
26838
27695
  * @param feeItems - Array of developer fee items from the service response.
26839
- * @param chain - The chain definition for token resolution and formatting.
27696
+ * @param chain - The source chain definition for token resolution and formatting.
27697
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
26840
27698
  * @param recipientAddress - The developer's fee recipient address from config.
26841
27699
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
26842
27700
  * @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
26843
27701
  */ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
26844
27702
  if (!feeItems) return [];
26845
- const isCrossChainSwap = destinationChain.chain !== chain.chain;
26846
27703
  return Promise.all(feeItems.map(async (fee)=>{
26847
- const feeChain = !isCrossChainSwap && fee.basis === 'estimatedAmount' ? destinationChain : chain;
26848
- const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
27704
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
26849
27705
  return {
26850
27706
  token: formatted.token,
26851
27707
  amount: formatted.amount,
@@ -28885,7 +29741,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
28885
29741
  /**
28886
29742
  * Resolve fee recipient address from context or policy.
28887
29743
  * @internal
28888
- */ async function resolveFeeRecipient(context, params, existingRecipient) {
29744
+ */ async function resolveFeeRecipient$1(context, params, existingRecipient) {
28889
29745
  if (existingRecipient) {
28890
29746
  return existingRecipient;
28891
29747
  }
@@ -28913,7 +29769,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
28913
29769
  * @internal
28914
29770
  */ async function applyExistingFeeAmount(context, params, existingAmount, existingFeeRecipient) {
28915
29771
  await validateFeeAmount(context, params, existingAmount);
28916
- const feeRecipient = await resolveFeeRecipient(context, params, existingFeeRecipient);
29772
+ const feeRecipient = await resolveFeeRecipient$1(context, params, existingFeeRecipient);
28917
29773
  if (feeRecipient) {
28918
29774
  params.config = {
28919
29775
  ...params.config,
@@ -28947,7 +29803,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
28947
29803
  ...params,
28948
29804
  type: 'input'
28949
29805
  };
28950
- const feeRecipient = await resolveFeeRecipient(context, params, existingFeeRecipient);
29806
+ const feeRecipient = await resolveFeeRecipient$1(context, params, existingFeeRecipient);
28951
29807
  const amount = await context.customFeePolicy?.computeFee(feeContext);
28952
29808
  if (amount !== undefined) {
28953
29809
  await validateInputFee(context, params, BigInt(amount), amount);
@@ -29240,22 +30096,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
29240
30096
  try {
29241
30097
  // Step 1: Build quote params directly (no need for buildServiceParams)
29242
30098
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
30099
+ // The kit key is optional (permissionless mode); when absent the quote is
30100
+ // fetched without an Authorization header.
29243
30101
  const kitKey = params.config?.kitKey;
29244
- if (!kitKey) {
29245
- throw new KitError({
29246
- code: 1098,
29247
- name: 'INPUT_VALIDATION_FAILED',
29248
- type: 'INPUT',
29249
- recoverability: 'FATAL',
29250
- message: 'kitKey is required in config for callback-based fees',
29251
- cause: {
29252
- trace: {
29253
- operation: 'handleOutputFeeCallback',
29254
- params
29255
- }
29256
- }
29257
- });
29258
- }
29259
30102
  // Resolve token aliases to addresses for the quote API
29260
30103
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
29261
30104
  const chain = params.from.chain;
@@ -29280,7 +30123,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
29280
30123
  ...params.config?.slippageBps !== undefined && {
29281
30124
  slippageBps: params.config.slippageBps
29282
30125
  },
29283
- apiKey: kitKey
30126
+ ...kitKey ? {
30127
+ apiKey: kitKey
30128
+ } : {}
29284
30129
  };
29285
30130
  // Step 2: Get quote from service
29286
30131
  const quoteResponse = await getQuote(quoteParams);
@@ -29722,7 +30567,9 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
29722
30567
  ...isCrossChain && {
29723
30568
  toChain: chainOut
29724
30569
  },
29725
- apiKey: params.kitKey
30570
+ ...params.kitKey ? {
30571
+ apiKey: params.kitKey
30572
+ } : {}
29726
30573
  };
29727
30574
  let raw = await getSwapStatus$2(request);
29728
30575
  // When the service hasn't finished indexing a just-submitted swap it
@@ -29862,7 +30709,9 @@ const isResultShape = (params)=>'result' in params;
29862
30709
  ...chainOut !== undefined && {
29863
30710
  chainOut
29864
30711
  },
29865
- kitKey: params.kitKey
30712
+ ...params.kitKey ? {
30713
+ kitKey: params.kitKey
30714
+ } : {}
29866
30715
  };
29867
30716
  const deadline = Date.now() + timeoutMs;
29868
30717
  let pollIndex = 0;
@@ -30023,7 +30872,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
30023
30872
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
30024
30873
  return getTokenRates$2({
30025
30874
  chain,
30026
- apiKey: params.kitKey,
30875
+ ...params.kitKey ? {
30876
+ apiKey: params.kitKey
30877
+ } : {},
30027
30878
  ...resolvedAddresses !== undefined && {
30028
30879
  addresses: resolvedAddresses
30029
30880
  }
@@ -31020,7 +31871,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
31020
31871
  };
31021
31872
 
31022
31873
  var name$2 = "@circle-fin/earn-kit";
31023
- var version$2 = "1.2.2";
31874
+ var version$2 = "1.3.0";
31024
31875
  var pkg$2 = {
31025
31876
  name: name$2,
31026
31877
  version: version$2};
@@ -31487,7 +32338,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31487
32338
  *
31488
32339
  * @param params - Adapter, chain, token/delegate/wallet addresses, the required
31489
32340
  * allowance for the signed payload, and a revert message for on-chain failure.
31490
- * @returns The approval transaction hash when an approval was submitted, or
32341
+ * @returns The approval transaction result when an approval was submitted, or
31491
32342
  * `undefined` when the existing allowance already covers `requiredAllowance`
31492
32343
  * (or `requiredAllowance` is zero).
31493
32344
  * @throws {@link KitError} If the `token.allowance` response is malformed.
@@ -31495,7 +32346,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31495
32346
  *
31496
32347
  * @example
31497
32348
  * ```typescript
31498
- * const txHash = await approveAllowanceIfNeeded({
32349
+ * const approval = await approveAllowanceIfNeeded({
31499
32350
  * adapter,
31500
32351
  * chain,
31501
32352
  * tokenAddress: usdcAddress,
@@ -31557,7 +32408,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31557
32408
  maxAttempts: params.allowancePropagation?.maxAttempts ?? DEFAULT_PROPAGATION_ATTEMPTS,
31558
32409
  delayMs: params.allowancePropagation?.delayMs ?? DEFAULT_PROPAGATION_DELAY_MS
31559
32410
  });
31560
- return approvalTxHash;
32411
+ return {
32412
+ txHash: approvalTxHash,
32413
+ ...approvalReceipt.gasUsed !== undefined && {
32414
+ gasUsed: approvalReceipt.gasUsed
32415
+ },
32416
+ ...approvalReceipt.effectiveGasPrice !== undefined && {
32417
+ effectiveGasPrice: approvalReceipt.effectiveGasPrice
32418
+ }
32419
+ };
31561
32420
  }
31562
32421
 
31563
32422
  /** @internal */ function isSameAddress(actual, expected) {
@@ -31705,7 +32564,13 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
31705
32564
  }
31706
32565
  return {
31707
32566
  txHash,
31708
- explorerUrl
32567
+ explorerUrl,
32568
+ ...receipt.gasUsed !== undefined && {
32569
+ gasUsed: receipt.gasUsed
32570
+ },
32571
+ ...receipt.effectiveGasPrice !== undefined && {
32572
+ effectiveGasPrice: receipt.effectiveGasPrice
32573
+ }
31709
32574
  };
31710
32575
  }
31711
32576
 
@@ -32017,112 +32882,6 @@ const EARN_OPERATIONS = new Set([
32017
32882
  return hasEarnServiceParamsShape(operation, candidate['params']);
32018
32883
  }
32019
32884
 
32020
- function buildGasFeeBase(name, chain) {
32021
- return {
32022
- name,
32023
- token: chain.nativeCurrency.symbol,
32024
- blockchain: chain.chain
32025
- };
32026
- }
32027
- function buildGasFeeSuccess(name, chain, fees) {
32028
- return {
32029
- ...buildGasFeeBase(name, chain),
32030
- fees
32031
- };
32032
- }
32033
- function buildGasFeeFailure(name, chain, error) {
32034
- return {
32035
- ...buildGasFeeBase(name, chain),
32036
- fees: null,
32037
- error: getErrorMessage(error)
32038
- };
32039
- }
32040
- async function estimatePreparedGasFee(name, chain, prepared) {
32041
- try {
32042
- const estimate = bufferEstimatedGas(await prepared.estimate());
32043
- if (estimate.gas <= 0n) {
32044
- throw createValidationFailedError$1('estimate.gas', estimate.gas.toString(), 'gas estimate must be greater than zero');
32045
- }
32046
- return buildGasFeeSuccess(name, chain, estimate);
32047
- } catch (error) {
32048
- return buildGasFeeFailure(name, chain, error);
32049
- }
32050
- }
32051
- async function estimateApprovalGasFeeIfNeeded(params) {
32052
- const { adapter, chain, address, tokenAddress, delegate, requiredAllowance } = params;
32053
- if (requiredAllowance <= 0n) {
32054
- return undefined;
32055
- }
32056
- try {
32057
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
32058
- tokenAddress,
32059
- delegate
32060
- }, {
32061
- chain,
32062
- address
32063
- });
32064
- const allowanceRaw = await allowancePrepared.execute();
32065
- const currentAllowance = parseAllowanceResponse(allowanceRaw);
32066
- if (currentAllowance >= requiredAllowance) {
32067
- return undefined;
32068
- }
32069
- // Reuse the execute path's approval builder so the estimate simulates the
32070
- // exact approval (action, amount, and WARM_SLOT_RESIDUAL) that
32071
- // approveAllowanceIfNeeded later submits.
32072
- const approvalPrepared = await prepareApprovalAction({
32073
- adapter,
32074
- chain,
32075
- address,
32076
- tokenAddress,
32077
- delegate,
32078
- currentAllowance,
32079
- requiredAllowance
32080
- });
32081
- return await estimatePreparedGasFee('Approve', chain, approvalPrepared);
32082
- } catch (error) {
32083
- return buildGasFeeFailure('Approve', chain, error);
32084
- }
32085
- }
32086
- /**
32087
- * Estimate gas fee entries for an earn quote without submitting transactions.
32088
- *
32089
- * Each entry is produced by simulating the prepared transaction against
32090
- * current chain state. When an approval is required (allowance below the
32091
- * signed payload's required amount), the subsequent action simulation runs
32092
- * without that approval in place and is expected to revert — the action entry
32093
- * then carries `fees: null` with the revert message while the approval entry
32094
- * still estimates normally. Quote consumers must treat that as "estimate
32095
- * pending approval", not a hard failure.
32096
- *
32097
- * @internal
32098
- */ async function estimateEarnQuoteGasFees(params) {
32099
- const { adapter, chain, address, actionName, actionKey, actionParams, approval } = params;
32100
- const gasFees = [];
32101
- if (approval !== undefined) {
32102
- const approvalEstimate = await estimateApprovalGasFeeIfNeeded({
32103
- adapter,
32104
- chain,
32105
- address,
32106
- tokenAddress: approval.token,
32107
- delegate: approval.delegate,
32108
- requiredAllowance: approval.requiredAllowance
32109
- });
32110
- if (approvalEstimate !== undefined) {
32111
- gasFees.push(approvalEstimate);
32112
- }
32113
- }
32114
- try {
32115
- const actionPrepared = await adapter.prepareAction(actionKey, actionParams, {
32116
- chain,
32117
- address
32118
- });
32119
- gasFees.push(await estimatePreparedGasFee(actionName, chain, actionPrepared));
32120
- } catch (error) {
32121
- gasFees.push(buildGasFeeFailure(actionName, chain, error));
32122
- }
32123
- return gasFees;
32124
- }
32125
-
32126
32885
  // ---------------------------------------------------------------------------
32127
32886
  // Shared primitives
32128
32887
  // ---------------------------------------------------------------------------
@@ -32200,7 +32959,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
32200
32959
  asset: zod.z.string(),
32201
32960
  assetAddress: zod.z.string(),
32202
32961
  lltv: zod.z.number(),
32203
- supplyUsd: zod.z.number()
32962
+ supplyUsd: zod.z.number(),
32963
+ // Optional during the expand/contract window (a backend that predates the
32964
+ // field omits the key), mirroring the `.optional()` facets on the base
32965
+ // schema; `null` when the product exposes no per-market allocation (V2).
32966
+ allocationPct: zod.z.number().nullable().optional()
32204
32967
  });
32205
32968
  /**
32206
32969
  * Zod schema for a Morpho vault warning in the API response.
@@ -32214,7 +32977,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
32214
32977
  ])
32215
32978
  });
32216
32979
  /**
32217
- * Zod schema for a single vault info object in the API response.
32980
+ * Zod schema for the manager (curator) facet in the API response.
32981
+ *
32982
+ * @internal
32983
+ */ const managerSchema = zod.z.object({
32984
+ name: zod.z.string(),
32985
+ address: zod.z.string().optional(),
32986
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
32987
+ // are added here as the providers that emit them land, rather than shipped
32988
+ // speculatively.
32989
+ type: zod.z.enum([
32990
+ 'curator'
32991
+ ])
32992
+ });
32993
+ /**
32994
+ * Zod schema for the APY profile facet in the API response.
32995
+ *
32996
+ * @internal
32997
+ */ const apyProfileSchema = zod.z.object({
32998
+ current: zod.z.number(),
32999
+ native: zod.z.number().nullable(),
33000
+ d7: zod.z.number().nullable(),
33001
+ d30: zod.z.number().nullable(),
33002
+ d90: zod.z.number().nullable(),
33003
+ rewardShare: zod.z.number().nullable(),
33004
+ source: zod.z.string().optional(),
33005
+ asOf: zod.z.string().optional()
33006
+ });
33007
+ /**
33008
+ * Zod schema for the fee split facet in the API response.
33009
+ *
33010
+ * @internal
33011
+ */ const feeInfoSchema = zod.z.object({
33012
+ performance: zod.z.number().nullable(),
33013
+ management: zod.z.number().nullable()
33014
+ });
33015
+ /**
33016
+ * Zod schema for the liquidity profile facet in the API response.
33017
+ *
33018
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
33019
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
33020
+ *
33021
+ * @internal
33022
+ */ const liquidityProfileSchema = zod.z.object({
33023
+ totalDeposits: amountJsonSchema,
33024
+ available: amountJsonSchema,
33025
+ totalSupply: amountJsonSchema,
33026
+ status: zod.z.enum([
33027
+ 'active',
33028
+ 'low_liquidity'
33029
+ ])
33030
+ });
33031
+ /**
33032
+ * Zod schema for the risk signals facet in the API response.
33033
+ *
33034
+ * @internal
33035
+ */ const riskSignalsSchema = zod.z.object({
33036
+ circleSentinel: zod.z.boolean(),
33037
+ warnings: zod.z.array(vaultWarningSchema).optional(),
33038
+ earnKitWarnings: zod.z.array(zod.z.string()).optional()
33039
+ });
33040
+ /**
33041
+ * Zod schema for the universal earn-opportunity base in the API response.
33042
+ *
33043
+ * Retains every existing deprecated flat field (kept validated through the
33044
+ * expand/contract window so default-strip does not drop them) and adds the
33045
+ * new nested facets. The nested facets are `.optional()` during the
33046
+ * transition so the SDK still validates against a not-yet-fully-deployed
33047
+ * backend; they become required after Expand ships.
32218
33048
  *
32219
33049
  * @internal
32220
33050
  */ const vaultInfoResponseSchema = zod.z.object({
@@ -32239,6 +33069,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
32239
33069
  warnings: zod.z.array(vaultWarningSchema).optional(),
32240
33070
  earnKitWarnings: zod.z.array(zod.z.string()).optional()
32241
33071
  });
33072
+ /**
33073
+ * Shared base schema: existing flat fields (kept) plus the new nested
33074
+ * facets and neutral identity. Facets are `.optional()` during the
33075
+ * transition; flip to required once the backend is confirmed emitting.
33076
+ *
33077
+ * @internal
33078
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
33079
+ address: zod.z.string().optional(),
33080
+ asOf: zod.z.string().optional(),
33081
+ manager: managerSchema.nullable().optional(),
33082
+ apyProfile: apyProfileSchema.optional(),
33083
+ fee: feeInfoSchema.optional(),
33084
+ liquidityProfile: liquidityProfileSchema.optional(),
33085
+ riskSignals: riskSignalsSchema.optional()
33086
+ });
33087
+ /**
33088
+ * Zod schema for the `vault` opportunity variant.
33089
+ *
33090
+ * @internal
33091
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
33092
+ productType: zod.z.literal('vault'),
33093
+ collateral: zod.z.array(collateralSchema)
33094
+ });
33095
+ /**
33096
+ * Discriminated union over `productType`. Add union members here as new
33097
+ * product types (e.g. `lending_market`, `rwa_token`) land.
33098
+ *
33099
+ * @internal
33100
+ */ const earnOpportunityVariants = [
33101
+ vaultOpportunitySchema
33102
+ ];
33103
+ /** @internal */ const earnOpportunitySchema = zod.z.discriminatedUnion('productType', earnOpportunityVariants);
33104
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
33105
+ /**
33106
+ * Tolerant list parser for earn opportunities.
33107
+ *
33108
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
33109
+ * `z.array` fails the whole array if any element fails. Two migration-window
33110
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
33111
+ *
33112
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
33113
+ * only opportunity type then, so default a missing discriminant to `'vault'`
33114
+ * rather than dropping every vault the backend returns.
33115
+ * - A future backend adds a *second* `productType` this SDK version does not
33116
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
33117
+ * of rejecting the whole list.
33118
+ *
33119
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
33120
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
33121
+ * primitives, or an object whose `productType` is malformed — is passed through
33122
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
33123
+ * validation failure. It is deliberately not silently dropped (which would hide
33124
+ * malformed backend data) and never throws here (an unguarded property read on
33125
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
33126
+ * `ZodError`).
33127
+ *
33128
+ * @internal
33129
+ */ const earnOpportunityListSchema = zod.z.preprocess((raw)=>{
33130
+ if (!Array.isArray(raw)) {
33131
+ return raw;
33132
+ }
33133
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
33134
+ // map/filter chain stays type-safe and no `any` leaks into the return.
33135
+ const entries = raw;
33136
+ return entries.map((entry)=>{
33137
+ // Only touch plain objects; non-objects fall through to fail validation.
33138
+ if (typeof entry !== 'object' || entry === null) {
33139
+ return entry;
33140
+ }
33141
+ const record = entry;
33142
+ // Older backend predating productType: default to the only type then.
33143
+ return record.productType === undefined ? {
33144
+ ...record,
33145
+ productType: 'vault'
33146
+ } : record;
33147
+ }).filter((entry)=>{
33148
+ // Drop ONLY a present-but-unknown string discriminant (a future
33149
+ // productType this SDK version doesn't know). Everything else —
33150
+ // non-objects, a non-string productType — flows through to
33151
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
33152
+ if (typeof entry !== 'object' || entry === null) {
33153
+ return true;
33154
+ }
33155
+ const productType = entry.productType;
33156
+ if (typeof productType !== 'string') {
33157
+ return true;
33158
+ }
33159
+ return knownProductTypes.has(productType);
33160
+ });
33161
+ }, zod.z.array(earnOpportunitySchema));
32242
33162
  // ---------------------------------------------------------------------------
32243
33163
  // Position response schema
32244
33164
  // ---------------------------------------------------------------------------
@@ -32368,6 +33288,7 @@ const positionPnlSchema = zod.z.discriminatedUnion('status', [
32368
33288
  *
32369
33289
  * @internal
32370
33290
  */ const depositPayloadSchema = zod.z.object({
33291
+ execId: bridgeDepositExecIdSchema,
32371
33292
  executionParams: depositExecutionParamsSchema,
32372
33293
  signature: hexSignatureSchema
32373
33294
  });
@@ -32459,6 +33380,21 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32459
33380
  amount: amountJsonSchema,
32460
33381
  vaultAddress: hexAddressSchema
32461
33382
  }).passthrough();
33383
+ /** @internal */ const bridgeQuoteExpirySchema = zod.z.discriminatedUnion('mode', [
33384
+ zod.z.object({
33385
+ mode: zod.z.literal('TIMESTAMP'),
33386
+ expiresAt: zod.z.string().datetime({
33387
+ offset: true
33388
+ })
33389
+ }),
33390
+ zod.z.object({
33391
+ mode: zod.z.literal('BLOCK_NUMBER'),
33392
+ expiresAtBlock: zod.z.number().int(),
33393
+ blockEstimatedAt: zod.z.string().datetime({
33394
+ offset: true
33395
+ }).optional()
33396
+ })
33397
+ ]).optional().catch(undefined);
32462
33398
  /**
32463
33399
  * Zod schema for the bridge deposit prepare payload.
32464
33400
  *
@@ -32470,6 +33406,10 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32470
33406
  execId: bridgeDepositExecIdSchema,
32471
33407
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
32472
33408
  expiresAt: zod.z.string().datetime(),
33409
+ quoteIssuedAt: zod.z.string().datetime({
33410
+ offset: true
33411
+ }).optional().catch(undefined),
33412
+ quoteExpiry: bridgeQuoteExpirySchema,
32473
33413
  review: bridgeDepositPrepareReviewSchema
32474
33414
  });
32475
33415
  /**
@@ -32535,6 +33475,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32535
33475
  *
32536
33476
  * @internal
32537
33477
  */ const withdrawPayloadSchema = zod.z.object({
33478
+ execId: bridgeDepositExecIdSchema,
32538
33479
  executionParams: withdrawExecutionParamsSchema,
32539
33480
  signature: hexSignatureSchema
32540
33481
  });
@@ -32548,6 +33489,27 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32548
33489
  data: withdrawPayloadSchema
32549
33490
  });
32550
33491
  // ---------------------------------------------------------------------------
33492
+ // Transaction report response schema
33493
+ // ---------------------------------------------------------------------------
33494
+ /**
33495
+ * Zod schema for the transaction report payload inside the API `data` envelope.
33496
+ *
33497
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
33498
+ * schema accepts any object shape and does not require specific fields.
33499
+ *
33500
+ * @internal
33501
+ */ const transactionReportPayloadSchema = zod.z.object({}).passthrough();
33502
+ /**
33503
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
33504
+ *
33505
+ * The Earn Service API wraps the transaction report payload in a `data`
33506
+ * envelope.
33507
+ *
33508
+ * @internal
33509
+ */ const transactionReportResponseSchema = zod.z.object({
33510
+ data: transactionReportPayloadSchema
33511
+ });
33512
+ // ---------------------------------------------------------------------------
32551
33513
  // Claim rewards response schema
32552
33514
  // ---------------------------------------------------------------------------
32553
33515
  /**
@@ -32608,6 +33570,30 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32608
33570
  token: zod.z.string(),
32609
33571
  amount: amountJsonSchema
32610
33572
  });
33573
+ /**
33574
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
33575
+ *
33576
+ * The Earn Service backend estimates gas server-side and returns one entry per
33577
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
33578
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
33579
+ * integer string in the chain's native base units. When the backend cannot
33580
+ * estimate an action it returns `fees: null` with an `error` message instead.
33581
+ *
33582
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
33583
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
33584
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
33585
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
33586
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
33587
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
33588
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
33589
+ * `fee`) must never fail Zod validation and reject the entire quote.
33590
+ *
33591
+ * @internal
33592
+ */ const quoteGasFeeSchema = zod.z.object({
33593
+ name: zod.z.string().optional(),
33594
+ fees: zod.z.unknown(),
33595
+ error: zod.z.string().optional()
33596
+ }).passthrough();
32611
33597
  /**
32612
33598
  * Zod schema for the inner deposit quote payload.
32613
33599
  *
@@ -32623,7 +33609,8 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32623
33609
  expectedShares: amountJsonSchema,
32624
33610
  sharePrice: zod.z.string(),
32625
33611
  currentApy: zod.z.number(),
32626
- fees: zod.z.array(feeSchema).optional()
33612
+ fees: zod.z.array(feeSchema).optional(),
33613
+ gasFees: zod.z.array(quoteGasFeeSchema).optional()
32627
33614
  });
32628
33615
  /**
32629
33616
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -32650,6 +33637,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32650
33637
  sharePrice: zod.z.string(),
32651
33638
  maxWithdrawable: amountJsonSchema,
32652
33639
  fees: zod.z.array(feeSchema),
33640
+ gasFees: zod.z.array(quoteGasFeeSchema).optional(),
32653
33641
  warnings: zod.z.array(zod.z.string()).optional()
32654
33642
  });
32655
33643
  /**
@@ -32707,7 +33695,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32707
33695
  *
32708
33696
  * @internal
32709
33697
  */ const getVaultsPayloadSchema = zod.z.object({
32710
- vaults: zod.z.array(vaultInfoResponseSchema),
33698
+ vaults: earnOpportunityListSchema,
32711
33699
  errors: zod.z.array(vaultErrorSchema)
32712
33700
  });
32713
33701
  /**
@@ -32737,7 +33725,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32737
33725
  *
32738
33726
  * @internal
32739
33727
  */ const exploreVaultsPayloadSchema = zod.z.object({
32740
- vaults: zod.z.array(vaultInfoResponseSchema),
33728
+ vaults: earnOpportunityListSchema,
32741
33729
  pagination: explorePaginationSchema
32742
33730
  });
32743
33731
  /**
@@ -32830,6 +33818,16 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32830
33818
  */ function isWithdrawResponse(value) {
32831
33819
  return withdrawResponseSchema.safeParse(value).success;
32832
33820
  }
33821
+ /**
33822
+ * Type guard for the transaction report API response.
33823
+ *
33824
+ * @param value - Unknown response value to validate
33825
+ * @returns True when the value matches the transaction report response shape
33826
+ *
33827
+ * @internal
33828
+ */ function isTransactionReportResponse(value) {
33829
+ return transactionReportResponseSchema.safeParse(value).success;
33830
+ }
32833
33831
  /**
32834
33832
  * Type guard for the claim rewards API response.
32835
33833
  *
@@ -32872,7 +33870,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
32872
33870
  }
32873
33871
 
32874
33872
  var name$1 = "@circle-fin/provider-earn-service";
32875
- var version$1 = "1.2.2";
33873
+ var version$1 = "1.3.0";
32876
33874
  var pkg$1 = {
32877
33875
  name: name$1,
32878
33876
  version: version$1};
@@ -32992,7 +33990,7 @@ var pkg$1 = {
32992
33990
  }
32993
33991
 
32994
33992
  /**
32995
- * Convert an API vault info object into the SDK {@link VaultInfo} shape.
33993
+ * Convert an API vault info object into the SDK {@link EarnOpportunity} shape.
32996
33994
  *
32997
33995
  * Map the API chain code back to the SDK chain identifier and hydrate the
32998
33996
  * amount payloads into {@link Amount} instances.
@@ -33003,16 +34001,29 @@ var pkg$1 = {
33003
34001
  *
33004
34002
  * @internal
33005
34003
  */ function toVaultInfo(data) {
33006
- const { totalDeposits, liquidity, ...vault } = data;
34004
+ const { totalDeposits, liquidity, liquidityProfile, ...vault } = data;
33007
34005
  const chain = toSdkChain(vault.chain);
33008
34006
  if (chain === undefined) {
33009
34007
  throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
33010
34008
  }
34009
+ // The nested facets are `.optional()` in the schema (a backend that predates
34010
+ // them omits them) and are typed optional on `EarnOpportunity` to match.
34011
+ // Convert the nested liquidity amounts when present and pass the remaining
34012
+ // facets straight through; each absent facet stays absent rather than being
34013
+ // asserted present by a cast.
33011
34014
  return {
33012
34015
  ...vault,
33013
34016
  chain,
33014
34017
  totalDeposits: Amount.fromJSON(totalDeposits),
33015
- liquidity: Amount.fromJSON(liquidity)
34018
+ liquidity: Amount.fromJSON(liquidity),
34019
+ ...liquidityProfile !== undefined && {
34020
+ liquidityProfile: {
34021
+ ...liquidityProfile,
34022
+ totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
34023
+ available: Amount.fromJSON(liquidityProfile.available),
34024
+ totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
34025
+ }
34026
+ }
33016
34027
  };
33017
34028
  }
33018
34029
 
@@ -33047,8 +34058,11 @@ function toVaultError(error) {
33047
34058
  }
33048
34059
  try {
33049
34060
  const response = await pollApiGet(url.toString(), isGetVaultsResponse, pollingConfig);
34061
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
34062
+ // does not run the schema's preprocess. Parse explicitly so unknown
34063
+ // `productType` values are dropped before `toVaultInfo`.
33050
34064
  return {
33051
- vaults: response.data.vaults.map(toVaultInfo),
34065
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
33052
34066
  errors: response.data.errors.map(toVaultError)
33053
34067
  };
33054
34068
  } catch (error) {
@@ -33097,8 +34111,11 @@ function toVaultError(error) {
33097
34111
  }
33098
34112
  try {
33099
34113
  const response = await pollApiGet(url.toString(), isExploreVaultsResponse, pollingConfig);
34114
+ // `pollApiGet` validates via a boolean guard and returns the raw JSON — it
34115
+ // does not run the schema's preprocess. Parse explicitly so unknown
34116
+ // `productType` values are dropped before `toVaultInfo`.
33100
34117
  return {
33101
- vaults: response.data.vaults.map(toVaultInfo),
34118
+ vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
33102
34119
  pagination: response.data.pagination
33103
34120
  };
33104
34121
  } catch (error) {
@@ -33262,6 +34279,12 @@ function toPositionInfo(data) {
33262
34279
  execId: response.data.execId,
33263
34280
  preparedBundle,
33264
34281
  expiresAt: response.data.expiresAt,
34282
+ ...response.data.quoteIssuedAt !== undefined && {
34283
+ quoteIssuedAt: response.data.quoteIssuedAt
34284
+ },
34285
+ ...response.data.quoteExpiry !== undefined && {
34286
+ quoteExpiry: response.data.quoteExpiry
34287
+ },
33265
34288
  review: response.data.review
33266
34289
  };
33267
34290
  } catch (error) {
@@ -33603,7 +34626,110 @@ function toClaimedAmount(reward) {
33603
34626
  }
33604
34627
  }
33605
34628
 
33606
- function toDepositQuoteInfo(data) {
34629
+ /**
34630
+ * Map the Earn Service's server-side quote gas estimates into the SDK
34631
+ * {@link EarnGasFeeEstimate} shape.
34632
+ *
34633
+ * The Earn Service estimates gas for each action (`Approve`, `Deposit`,
34634
+ * `Withdraw`) and returns `{ name, fees: { gas, gasPrice, fee } }` with raw
34635
+ * integer strings.
34636
+ * The SDK type additionally carries `token` (the chain's native currency
34637
+ * symbol) and `blockchain`, which are filled in here from the chain
34638
+ * definition.
34639
+ *
34640
+ * Gas reporting is best-effort: a malformed entry (e.g. a non-integer string
34641
+ * that fails `BigInt` parsing) degrades to a `{ fees: null, error }` estimate
34642
+ * rather than throwing, so one bad entry never fails the whole quote.
34643
+ *
34644
+ * @param gasFees - Backend gas-fee entries from the quote response, if any.
34645
+ * @param chain - Chain definition, used for the native token symbol and
34646
+ * blockchain identifier.
34647
+ * @returns One {@link EarnGasFeeEstimate} per backend entry (empty when the
34648
+ * backend returned none).
34649
+ *
34650
+ * @example
34651
+ * ```typescript
34652
+ * toQuoteGasFees(
34653
+ * [{ name: 'Deposit', fees: { gas: '364142', gasPrice: '21000000000', fee: '7646982000000000' } }],
34654
+ * arcTestnet,
34655
+ * )
34656
+ * // [{ name: 'Deposit', token: 'USDC', blockchain: 'Arc_Testnet',
34657
+ * // fees: { gas: 364142n, gasPrice: 21000000000n, fee: '7646982000000000' } }]
34658
+ * ```
34659
+ *
34660
+ * @internal
34661
+ */ function toQuoteGasFees(gasFees, chain) {
34662
+ if (gasFees === undefined) {
34663
+ return [];
34664
+ }
34665
+ return gasFees.map((entry)=>{
34666
+ const base = {
34667
+ // `name` is optional on the wire; label an unnamed entry rather than
34668
+ // emitting `name: undefined`.
34669
+ name: entry.name ?? 'Unknown',
34670
+ token: chain.nativeCurrency.symbol,
34671
+ blockchain: chain.chain
34672
+ };
34673
+ // The Earn Service itself reports a failed estimate as `fees: null` with
34674
+ // an error; propagate that soft failure verbatim.
34675
+ if (entry.fees === null || entry.fees === undefined) {
34676
+ return {
34677
+ ...base,
34678
+ fees: null,
34679
+ error: entry.error ?? 'gas estimate unavailable'
34680
+ };
34681
+ }
34682
+ // `fees` is `unknown` at the schema layer, so ALL validation happens here:
34683
+ // that it is an object at all, and that `gas`, `gasPrice`, and `fee` are
34684
+ // each parseable integer strings (including `fee`, which the SDK contract
34685
+ // requires be a numeric base-unit string). Any failure — a wrong type
34686
+ // (`fees: 123`), a missing field, or a non-numeric value — degrades the
34687
+ // whole entry to a `fees: null` soft failure rather than surfacing a
34688
+ // malformed "successful" estimate or rejecting the quote.
34689
+ try {
34690
+ if (typeof entry.fees !== 'object') {
34691
+ throw new TypeError(`gas fees must be an object (got ${typeof entry.fees})`);
34692
+ }
34693
+ const { gas, gasPrice, fee } = entry.fees;
34694
+ return {
34695
+ ...base,
34696
+ fees: {
34697
+ gas: toBigInt('gas', gas),
34698
+ gasPrice: toBigInt('gasPrice', gasPrice),
34699
+ fee: toBigInt('fee', fee).toString()
34700
+ }
34701
+ };
34702
+ } catch (error) {
34703
+ return {
34704
+ ...base,
34705
+ fees: null,
34706
+ error: getErrorMessage(error)
34707
+ };
34708
+ }
34709
+ });
34710
+ }
34711
+ /**
34712
+ * Parse an unknown value into a `bigint`, rejecting anything that is not a
34713
+ * non-empty integer string. `BigInt` alone is too permissive for this path —
34714
+ * it accepts numbers, booleans, and empty strings — so guard the type first.
34715
+ *
34716
+ * @param field - Field name, used in the thrown error message.
34717
+ * @param value - Raw value from the backend gas entry.
34718
+ * @returns The parsed `bigint`.
34719
+ * @throws {TypeError} When `value` is not a non-empty integer string.
34720
+ */ function toBigInt(field, value) {
34721
+ if (typeof value !== 'string' || value.trim() === '') {
34722
+ throw new TypeError(`gas fee field "${field}" must be an integer string`);
34723
+ }
34724
+ try {
34725
+ // BigInt throws on non-integer strings (e.g. "1.5", "not-a-number").
34726
+ return BigInt(value);
34727
+ } catch {
34728
+ throw new Error(`gas fee field "${field}" is not a valid integer string: ${value}`);
34729
+ }
34730
+ }
34731
+
34732
+ function toDepositQuoteInfo(data, chain) {
33607
34733
  const fees = (data.fees ?? []).map(({ token: feeTokenSymbol, ...fee })=>{
33608
34734
  // Earn Service returns fee.token as a display symbol, for example "USDC".
33609
34735
  return {
@@ -33632,7 +34758,10 @@ function toDepositQuoteInfo(data) {
33632
34758
  sharePrice: data.sharePrice,
33633
34759
  currentApy: data.currentApy,
33634
34760
  fees,
33635
- gasFees: []
34761
+ // The Earn Service estimates gas server-side; the chain fills token/blockchain.
34762
+ // Cross-chain quotes resolve no local chain definition, so gasFees stays
34763
+ // empty there (unchanged behavior).
34764
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain)
33636
34765
  };
33637
34766
  }
33638
34767
  /**
@@ -33667,7 +34796,7 @@ function toDepositQuoteInfo(data) {
33667
34796
  };
33668
34797
  try {
33669
34798
  const response = await pollApiPost(url.toString(), requestBody, isDepositQuoteResponse, pollingConfig);
33670
- return toDepositQuoteInfo(response.data);
34799
+ return toDepositQuoteInfo(response.data, params.chainDefinition);
33671
34800
  } catch (error) {
33672
34801
  throw parseEarnApiError(error, {
33673
34802
  operation: 'getDepositQuote'
@@ -33675,7 +34804,7 @@ function toDepositQuoteInfo(data) {
33675
34804
  }
33676
34805
  }
33677
34806
 
33678
- function toWithdrawalQuoteInfo(data) {
34807
+ function toWithdrawalQuoteInfo(data, chain) {
33679
34808
  return {
33680
34809
  vaultAddress: data.vaultAddress,
33681
34810
  vaultName: data.vaultName,
@@ -33703,7 +34832,7 @@ function toWithdrawalQuoteInfo(data) {
33703
34832
  status
33704
34833
  }
33705
34834
  })),
33706
- gasFees: [],
34835
+ gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain),
33707
34836
  // Wire format uses `warnings`, but the SDK surface uses
33708
34837
  // `earnKitWarnings` to match the precedent set by `VaultInfo` —
33709
34838
  // `warnings` is reserved for the structured `VaultWarning` shape.
@@ -33735,7 +34864,7 @@ function toWithdrawalQuoteInfo(data) {
33735
34864
  };
33736
34865
  try {
33737
34866
  const response = await pollApiPost(url.toString(), requestBody, isWithdrawalQuoteResponse, pollingConfig);
33738
- return toWithdrawalQuoteInfo(response.data);
34867
+ return toWithdrawalQuoteInfo(response.data, params.chainDefinition);
33739
34868
  } catch (error) {
33740
34869
  throw parseEarnApiError(error, {
33741
34870
  operation: 'getWithdrawalQuote'
@@ -33771,6 +34900,8 @@ function toWithdrawalQuoteInfo(data) {
33771
34900
  amount: Amount.fromJSON(r.amount),
33772
34901
  address: r.token
33773
34902
  })),
34903
+ // The claimRewards/quote response does not carry a gas estimate (unlike
34904
+ // deposit/withdrawal quotes), so there is nothing to surface here.
33774
34905
  gasFees: []
33775
34906
  };
33776
34907
  } catch (error) {
@@ -33780,6 +34911,83 @@ function toWithdrawalQuoteInfo(data) {
33780
34911
  }
33781
34912
  }
33782
34913
 
34914
+ /**
34915
+ * Build the native gas triple the backend expects for `gasUsed`.
34916
+ *
34917
+ * Returns `undefined` unless both receipt components are present, so the
34918
+ * caller can omit the field entirely — the Earn Service treats a missing
34919
+ * triple as "skip the gas cache write, still return 200".
34920
+ *
34921
+ * @param gasUsed - Receipt gas units used.
34922
+ * @param effectiveGasPrice - Receipt effective gas price.
34923
+ * @returns The `{ gas, gasPrice, fee }` triple, or `undefined` when either
34924
+ * component is missing.
34925
+ *
34926
+ * @example
34927
+ * ```typescript
34928
+ * buildReportedGasUsed(362454n, 29466364605n)
34929
+ * // { gas: '362454', gasPrice: '29466364605', fee: '10680201716540670' }
34930
+ * ```
34931
+ *
34932
+ * @internal
34933
+ */ function buildReportedGasUsed(gasUsed, effectiveGasPrice) {
34934
+ if (gasUsed === undefined || effectiveGasPrice === undefined) {
34935
+ return undefined;
34936
+ }
34937
+ return {
34938
+ gas: gasUsed.toString(),
34939
+ gasPrice: effectiveGasPrice.toString(),
34940
+ fee: (gasUsed * effectiveGasPrice).toString()
34941
+ };
34942
+ }
34943
+ /**
34944
+ * Report the outcome of an SDK-submitted same-chain Earn transaction.
34945
+ *
34946
+ * @param params - Transaction report parameters.
34947
+ * @throws {@link KitError} When the API call fails.
34948
+ *
34949
+ * @internal
34950
+ */ async function reportEarnTransaction(params) {
34951
+ const { pollingConfig, baseUrl } = buildConfig(params.config);
34952
+ const url = new URL(`${EARN_KIT_API_PREFIX}/transactions/report`, baseUrl);
34953
+ // The report endpoint is not idempotent: success reports refresh the gas
34954
+ // cache and failure reports increment counts. If the first request succeeds
34955
+ // server-side but the client times out or sees a transient 5xx, retrying
34956
+ // would duplicate the report (double-writing an outcome or inflating failure
34957
+ // counts). Reporting is best-effort (see the fire-and-forget caller), so
34958
+ // make exactly one attempt and never retry — a single dropped report is
34959
+ // preferable to a duplicated one. `maxRetries` here is the total attempt
34960
+ // count in pollApiWithValidation (loop runs `attempt <= maxRetries`), so 1
34961
+ // means one request with no retry; 0 would skip the request entirely.
34962
+ const reportConfig = {
34963
+ ...pollingConfig,
34964
+ maxRetries: 1
34965
+ };
34966
+ const gasUsed = buildReportedGasUsed(params.gasUsed, params.effectiveGasPrice);
34967
+ const requestBody = {
34968
+ execId: params.execId,
34969
+ chain: params.chain,
34970
+ status: params.status,
34971
+ action: params.action,
34972
+ ...params.txHash !== undefined && {
34973
+ txHash: params.txHash
34974
+ },
34975
+ ...gasUsed !== undefined && {
34976
+ gasUsed
34977
+ },
34978
+ ...params.errorCode !== undefined && {
34979
+ errorCode: params.errorCode
34980
+ }
34981
+ };
34982
+ try {
34983
+ await pollApiPost(url.toString(), requestBody, isTransactionReportResponse, reportConfig);
34984
+ } catch (error) {
34985
+ throw parseEarnApiError(error, {
34986
+ operation: 'transactionReport'
34987
+ });
34988
+ }
34989
+ }
34990
+
33783
34991
  /**
33784
34992
  * Sum the amounts across every token input to size the allowance approval.
33785
34993
  *
@@ -33890,6 +35098,59 @@ function toWithdrawalQuoteInfo(data) {
33890
35098
  // Intentionally built-ins-only: Earn bridge support is limited to SDK-known
33891
35099
  // token contracts plus the explicit ERC-3009 domain allowlist below.
33892
35100
  const TOKEN_REGISTRY = createTokenRegistry();
35101
+ function submitTransactionReport(reportContext, action, status, details) {
35102
+ void reportEarnTransaction({
35103
+ execId: reportContext.execId,
35104
+ chain: reportContext.chain,
35105
+ config: reportContext.config,
35106
+ action,
35107
+ status,
35108
+ ...details
35109
+ }).catch(()=>undefined);
35110
+ }
35111
+ function reportTransactionSuccess(reportContext, action, result) {
35112
+ if (result === undefined) {
35113
+ return;
35114
+ }
35115
+ submitTransactionReport(reportContext, action, 'success', {
35116
+ txHash: result.txHash,
35117
+ gasUsed: result.gasUsed,
35118
+ effectiveGasPrice: result.effectiveGasPrice
35119
+ });
35120
+ }
35121
+ function reportTransactionFailure(reportContext, action, error) {
35122
+ submitTransactionReport(reportContext, action, 'failure', {
35123
+ txHash: transactionReportTxHash(error),
35124
+ errorCode: transactionReportErrorCode(error)
35125
+ });
35126
+ }
35127
+ function transactionReportErrorCode(error) {
35128
+ if (isKitError(error)) {
35129
+ return error.name;
35130
+ }
35131
+ const message = getErrorMessage(error);
35132
+ if (/user (rejected|denied)|rejected by user/i.test(message)) {
35133
+ return 'USER_REJECTED';
35134
+ }
35135
+ if (/insufficient funds/i.test(message)) {
35136
+ return 'INSUFFICIENT_FUNDS';
35137
+ }
35138
+ if (/timeout|timed out/i.test(message)) {
35139
+ return 'TIMEOUT';
35140
+ }
35141
+ return 'UNKNOWN_ERROR';
35142
+ }
35143
+ function transactionReportTxHash(error) {
35144
+ if (!isKitError(error)) {
35145
+ return undefined;
35146
+ }
35147
+ const trace = error.cause?.trace;
35148
+ if (typeof trace !== 'object' || trace === null) {
35149
+ return undefined;
35150
+ }
35151
+ const txHash = trace['txHash'];
35152
+ return typeof txHash === 'string' && txHash !== '' ? txHash : undefined;
35153
+ }
33893
35154
  /**
33894
35155
  * Build the typed error raised when a cross-chain wait is cancelled via its
33895
35156
  * `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
@@ -34221,7 +35482,7 @@ function finishElapsedWait(lastStatus, lastError) {
34221
35482
  const adapterContractAddress = requireAdapterContract(chain);
34222
35483
  const { adapter } = params.from;
34223
35484
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34224
- const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
35485
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
34225
35486
  vaultAddress,
34226
35487
  amount: params.amount,
34227
35488
  address,
@@ -34229,32 +35490,55 @@ function finishElapsedWait(lastStatus, lastError) {
34229
35490
  config
34230
35491
  }), ()=>undefined);
34231
35492
  validateExecutionDeadline(executionParams);
35493
+ const transactionReportContext = {
35494
+ execId,
35495
+ chain: apiChain,
35496
+ config
35497
+ };
34232
35498
  const approvalToken = resolveEarnApprovalToken(executionParams);
34233
35499
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
34234
35500
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34235
35501
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
34236
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
35502
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35503
+ try {
35504
+ const approval = await approveAllowanceIfNeeded({
35505
+ adapter,
35506
+ chain,
35507
+ tokenAddress: approvalToken,
35508
+ delegate: adapterContractAddress,
35509
+ address,
35510
+ requiredAllowance,
35511
+ revertMessage: 'Earn deposit token approval reverted on-chain'
35512
+ });
35513
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
35514
+ return approval;
35515
+ } catch (error) {
35516
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
35517
+ throw error;
35518
+ }
35519
+ }, (approval)=>approval?.txHash);
35520
+ }
35521
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
35522
+ try {
35523
+ const result = await executeEarnAction({
34237
35524
  adapter,
34238
35525
  chain,
34239
- tokenAddress: approvalToken,
34240
- delegate: adapterContractAddress,
34241
35526
  address,
34242
- requiredAllowance,
34243
- revertMessage: 'Earn deposit token approval reverted on-chain'
34244
- }), (txHash)=>txHash);
34245
- }
34246
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>executeEarnAction({
34247
- adapter,
34248
- chain,
34249
- address,
34250
- actionKey: 'earn.deposit',
34251
- actionParams: {
34252
- executeParams: executionParams,
34253
- tokenInputs,
34254
- signature
34255
- },
34256
- revertMessage: 'Earn deposit reverted on-chain'
34257
- }), ({ txHash })=>txHash);
35527
+ actionKey: 'earn.deposit',
35528
+ actionParams: {
35529
+ executeParams: executionParams,
35530
+ tokenInputs,
35531
+ signature
35532
+ },
35533
+ revertMessage: 'Earn deposit reverted on-chain'
35534
+ });
35535
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
35536
+ return result;
35537
+ } catch (error) {
35538
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
35539
+ throw error;
35540
+ }
35541
+ }, ({ txHash })=>txHash);
34258
35542
  return {
34259
35543
  kind: 'same-chain',
34260
35544
  txHash,
@@ -34334,7 +35618,13 @@ function finishElapsedWait(lastStatus, lastError) {
34334
35618
  amount: params.amount,
34335
35619
  sourceChain: sourceChain.chain,
34336
35620
  destinationChain: destinationChain.chain,
34337
- expiresAt: prepared.expiresAt
35621
+ expiresAt: prepared.expiresAt,
35622
+ ...prepared.quoteIssuedAt !== undefined && {
35623
+ quoteIssuedAt: prepared.quoteIssuedAt
35624
+ },
35625
+ ...prepared.quoteExpiry !== undefined && {
35626
+ quoteExpiry: prepared.quoteExpiry
35627
+ }
34338
35628
  };
34339
35629
  }
34340
35630
  /** {@inheritdoc} */ async withdraw(params) {
@@ -34355,7 +35645,7 @@ function finishElapsedWait(lastStatus, lastError) {
34355
35645
  const adapterContractAddress = requireAdapterContract(chain);
34356
35646
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34357
35647
  const { adapter } = params.from;
34358
- const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
35648
+ const { execId, executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
34359
35649
  vaultAddress,
34360
35650
  amount: params.amount,
34361
35651
  address,
@@ -34363,32 +35653,55 @@ function finishElapsedWait(lastStatus, lastError) {
34363
35653
  config
34364
35654
  }), ()=>undefined);
34365
35655
  validateExecutionDeadline(executionParams);
35656
+ const transactionReportContext = {
35657
+ execId,
35658
+ chain: apiChain,
35659
+ config
35660
+ };
34366
35661
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
34367
35662
  const approvalToken = tokenInputs[0]?.token;
34368
35663
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34369
35664
  if (!options.skipApprove && approvalToken !== undefined) {
34370
- await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
35665
+ await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35666
+ try {
35667
+ const approval = await approveAllowanceIfNeeded({
35668
+ adapter,
35669
+ chain,
35670
+ tokenAddress: approvalToken,
35671
+ delegate: adapterContractAddress,
35672
+ address,
35673
+ requiredAllowance,
35674
+ revertMessage: 'Vault share token approval reverted on-chain'
35675
+ });
35676
+ reportTransactionSuccess(transactionReportContext, 'Approve', approval);
35677
+ return approval;
35678
+ } catch (error) {
35679
+ reportTransactionFailure(transactionReportContext, 'Approve', error);
35680
+ throw error;
35681
+ }
35682
+ }, (approval)=>approval?.txHash);
35683
+ }
35684
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
35685
+ try {
35686
+ const result = await executeEarnAction({
34371
35687
  adapter,
34372
35688
  chain,
34373
- tokenAddress: approvalToken,
34374
- delegate: adapterContractAddress,
34375
35689
  address,
34376
- requiredAllowance,
34377
- revertMessage: 'Vault share token approval reverted on-chain'
34378
- }), (txHash)=>txHash);
34379
- }
34380
- const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>executeEarnAction({
34381
- adapter,
34382
- chain,
34383
- address,
34384
- actionKey: 'earn.withdraw',
34385
- actionParams: {
34386
- executeParams: executionParams,
34387
- tokenInputs,
34388
- signature
34389
- },
34390
- revertMessage: 'Earn withdraw reverted on-chain'
34391
- }), ({ txHash })=>txHash);
35690
+ actionKey: 'earn.withdraw',
35691
+ actionParams: {
35692
+ executeParams: executionParams,
35693
+ tokenInputs,
35694
+ signature
35695
+ },
35696
+ revertMessage: 'Earn withdraw reverted on-chain'
35697
+ });
35698
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
35699
+ return result;
35700
+ } catch (error) {
35701
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
35702
+ throw error;
35703
+ }
35704
+ }, ({ txHash })=>txHash);
34392
35705
  return {
34393
35706
  txHash,
34394
35707
  explorerUrl,
@@ -34522,141 +35835,6 @@ function finishElapsedWait(lastStatus, lastError) {
34522
35835
  }
34523
35836
  }
34524
35837
  }
34525
- gasEstimateFailure(name, chain, error) {
34526
- return {
34527
- name,
34528
- token: chain.nativeCurrency.symbol,
34529
- blockchain: chain.chain,
34530
- fees: null,
34531
- error: getErrorMessage(error)
34532
- };
34533
- }
34534
- async estimateDepositQuoteGasFees(params) {
34535
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
34536
- try {
34537
- const adapterContractAddress = requireAdapterContract(chain);
34538
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34539
- const { executionParams, signature } = await fetchDeposit({
34540
- vaultAddress: normalizedVaultAddress,
34541
- amount,
34542
- address,
34543
- chain: apiChain,
34544
- config
34545
- });
34546
- validateExecutionDeadline(executionParams);
34547
- const approvalToken = resolveEarnApprovalToken(executionParams);
34548
- const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
34549
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34550
- return await estimateEarnQuoteGasFees({
34551
- adapter,
34552
- chain,
34553
- address,
34554
- actionName: 'Deposit',
34555
- actionKey: 'earn.deposit',
34556
- actionParams: {
34557
- executeParams: executionParams,
34558
- tokenInputs,
34559
- signature
34560
- },
34561
- approval: approvalToken !== undefined && requiredAllowance > 0n ? {
34562
- token: approvalToken,
34563
- delegate: adapterContractAddress,
34564
- requiredAllowance
34565
- } : undefined
34566
- });
34567
- } catch (error) {
34568
- return [
34569
- this.gasEstimateFailure('Deposit', chain, error)
34570
- ];
34571
- }
34572
- }
34573
- async estimateWithdrawalQuoteGasFees(params) {
34574
- const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
34575
- try {
34576
- const adapterContractAddress = requireAdapterContract(chain);
34577
- const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
34578
- const { executionParams, signature } = await fetchWithdraw({
34579
- vaultAddress: normalizedVaultAddress,
34580
- amount,
34581
- address,
34582
- chain: apiChain,
34583
- config
34584
- });
34585
- validateExecutionDeadline(executionParams);
34586
- const tokenInputs = buildEarnTokenInputs(executionParams, normalizedVaultAddress);
34587
- const approvalToken = tokenInputs[0]?.token;
34588
- const requiredAllowance = sumTokenInputAmounts(tokenInputs);
34589
- return await estimateEarnQuoteGasFees({
34590
- adapter,
34591
- chain,
34592
- address,
34593
- actionName: 'Withdraw',
34594
- actionKey: 'earn.withdraw',
34595
- actionParams: {
34596
- executeParams: executionParams,
34597
- tokenInputs,
34598
- signature
34599
- },
34600
- approval: approvalToken !== undefined ? {
34601
- token: approvalToken,
34602
- delegate: adapterContractAddress,
34603
- requiredAllowance
34604
- } : undefined
34605
- });
34606
- } catch (error) {
34607
- return [
34608
- this.gasEstimateFailure('Withdraw', chain, error)
34609
- ];
34610
- }
34611
- }
34612
- async estimateClaimRewardsQuoteGasFees(params) {
34613
- const { adapter, chain, apiChain, address, vaultAddress, config } = params;
34614
- try {
34615
- requireAdapterContract(chain);
34616
- const { rewards, executionParams, signature } = await fetchClaimRewards({
34617
- address,
34618
- chain: apiChain,
34619
- vaultAddress,
34620
- config
34621
- });
34622
- if (rewards.length === 0) {
34623
- return [];
34624
- }
34625
- const missingExecutionParams = executionParams === undefined;
34626
- const missingSignature = signature === undefined;
34627
- if (missingExecutionParams || missingSignature) {
34628
- throw new KitError({
34629
- ...EarnError.INTERNAL_ERROR,
34630
- recoverability: 'RETRYABLE',
34631
- message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
34632
- cause: {
34633
- trace: {
34634
- rewardsCount: rewards.length,
34635
- missingExecutionParams,
34636
- missingSignature
34637
- }
34638
- }
34639
- });
34640
- }
34641
- validateExecutionDeadline(executionParams);
34642
- return await estimateEarnQuoteGasFees({
34643
- adapter,
34644
- chain,
34645
- address,
34646
- actionName: 'Claim Rewards',
34647
- actionKey: 'earn.claimRewards',
34648
- actionParams: {
34649
- executeParams: executionParams,
34650
- tokenInputs: [],
34651
- signature
34652
- }
34653
- });
34654
- } catch (error) {
34655
- return [
34656
- this.gasEstimateFailure('Claim Rewards', chain, error)
34657
- ];
34658
- }
34659
- }
34660
35838
  /** {@inheritdoc} */ async getDepositQuote(params) {
34661
35839
  const config = this.resolveConfig(params.config);
34662
35840
  if (hasQuoteDestinationChain(params)) {
@@ -34679,96 +35857,43 @@ function finishElapsedWait(lastStatus, lastError) {
34679
35857
  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');
34680
35858
  }
34681
35859
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
34682
- // The quote fetch and the gas estimation share no data, so run them
34683
- // concurrently. The estimator never rejects (failures fold into
34684
- // `{ fees: null }` entries), so only a quote failure can throw here.
34685
- const [quote, gasFees] = await Promise.all([
34686
- fetchDepositQuote({
34687
- vaultAddress: params.vaultAddress,
34688
- amount: params.amount,
34689
- address,
34690
- chain,
34691
- config
34692
- }),
34693
- this.estimateDepositQuoteGasFees({
34694
- adapter: params.from.adapter,
34695
- chain: chainDefinition,
34696
- apiChain: chain,
34697
- address,
34698
- vaultAddress: params.vaultAddress,
34699
- amount: params.amount,
34700
- config
34701
- })
34702
- ]);
34703
- return {
34704
- ...quote,
34705
- gasFees
34706
- };
35860
+ // Gas is estimated server-side by the Earn Service and returned on the quote, so the
35861
+ // SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
35862
+ // the native token symbol / blockchain on each gas entry.
35863
+ return fetchDepositQuote({
35864
+ vaultAddress: params.vaultAddress,
35865
+ amount: params.amount,
35866
+ address,
35867
+ chain,
35868
+ config,
35869
+ chainDefinition
35870
+ });
34707
35871
  }
34708
35872
  /** {@inheritdoc} */ async getWithdrawalQuote(params) {
34709
35873
  const config = this.resolveConfig(params.config);
34710
35874
  const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
34711
- // The quote fetch and the gas estimation share no data, so run them
34712
- // concurrently. The estimator never rejects (failures fold into
34713
- // `{ fees: null }` entries), so only a quote failure can throw here.
34714
- const [quote, gasFees] = await Promise.all([
34715
- fetchWithdrawalQuote({
34716
- vaultAddress: params.vaultAddress,
34717
- amount: params.amount,
34718
- address,
34719
- chain,
34720
- config
34721
- }),
34722
- this.estimateWithdrawalQuoteGasFees({
34723
- adapter: params.from.adapter,
34724
- chain: chainDefinition,
34725
- apiChain: chain,
34726
- address,
34727
- vaultAddress: params.vaultAddress,
34728
- amount: params.amount,
34729
- config
34730
- })
34731
- ]);
34732
- return {
34733
- ...quote,
34734
- gasFees
34735
- };
35875
+ // Gas is estimated server-side by the Earn Service and returned on the quote.
35876
+ return fetchWithdrawalQuote({
35877
+ vaultAddress: params.vaultAddress,
35878
+ amount: params.amount,
35879
+ address,
35880
+ chain,
35881
+ config,
35882
+ chainDefinition
35883
+ });
34736
35884
  }
34737
35885
  /** {@inheritdoc} */ async getClaimRewardsQuote(params) {
34738
35886
  const config = this.resolveConfig(params.config);
34739
- const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
34740
- const quote = await fetchClaimRewardsQuote({
35887
+ const { address, chain } = await resolveAdapterContext(params.from);
35888
+ // The claimRewards/quote response carries no gas estimate (unlike
35889
+ // deposit/withdrawal quotes), and the SDK no longer estimates gas locally,
35890
+ // so gasFees is always empty for claim rewards.
35891
+ return fetchClaimRewardsQuote({
34741
35892
  vaultAddress: params.vaultAddress,
34742
35893
  address,
34743
35894
  chain,
34744
35895
  config
34745
35896
  });
34746
- // No claimable rewards means there is nothing to execute, so there is no
34747
- // gas to estimate. Short-circuit on the already-fetched quote rather than
34748
- // calling the (heavier) claim execution endpoint again — this also keeps
34749
- // `gasFees` empty as documented, instead of risking a `{ fees: null }`
34750
- // estimation-error entry when the adapter/RPC is unavailable. This
34751
- // short-circuit is why the claim path stays sequential instead of using
34752
- // the Promise.all pattern of the deposit/withdrawal quotes: estimating in
34753
- // parallel would hit the signing endpoint even when nothing is claimable.
34754
- if (quote.rewards.length === 0) {
34755
- return {
34756
- ...quote,
34757
- gasFees: []
34758
- };
34759
- }
34760
- const gasFees = await this.estimateClaimRewardsQuoteGasFees({
34761
- adapter: params.from.adapter,
34762
- chain: chainDefinition,
34763
- apiChain: chain,
34764
- address,
34765
- vaultAddress: params.vaultAddress,
34766
- config
34767
- });
34768
- return {
34769
- ...quote,
34770
- gasFees
34771
- };
34772
35897
  }
34773
35898
  }
34774
35899
  function hasDepositDestination(params) {
@@ -35006,11 +36131,27 @@ function formatPositionPnL(pnl) {
35006
36131
  * @param vault - Provider vault info with raw amount objects
35007
36132
  * @returns Vault info with total deposits and liquidity formatted as strings
35008
36133
  */ function formatVaultInfo(vault) {
35009
- const { totalDeposits, liquidity, ...rest } = vault;
36134
+ // The flat `totalDeposits`/`liquidity` are deprecated aliases that are
36135
+ // intentionally dual-read through the migration window so existing
36136
+ // consumers keep receiving them until Contract.
36137
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
36138
+ const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
36139
+ // `liquidityProfile` is `.optional()` in the response schema during the
36140
+ // expand/contract window (an old backend that predates the nested facets
36141
+ // omits it), so only format and re-attach it when present — matching the
36142
+ // provider-side `toVaultInfo` mapper.
35010
36143
  return {
35011
36144
  ...rest,
35012
36145
  totalDeposits: formatAmount$1(totalDeposits),
35013
- liquidity: formatAmount$1(liquidity)
36146
+ liquidity: formatAmount$1(liquidity),
36147
+ ...liquidityProfile !== undefined && {
36148
+ liquidityProfile: {
36149
+ ...liquidityProfile,
36150
+ totalDeposits: formatAmount$1(liquidityProfile.totalDeposits),
36151
+ available: formatAmount$1(liquidityProfile.available),
36152
+ totalSupply: formatAmount$1(liquidityProfile.totalSupply)
36153
+ }
36154
+ }
35014
36155
  };
35015
36156
  }
35016
36157
  /**
@@ -36922,25 +38063,6 @@ function formatRetryResult(operation, result) {
36922
38063
  // Auto-register this kit for user agent tracking
36923
38064
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
36924
38065
 
36925
- /**
36926
- * Create an EarnKit instance for AppKit earn operations.
36927
- *
36928
- * @remarks The context parameter is reserved for future EarnKit wiring.
36929
- * EarnKit does not currently support AppKit developer fee hooks, so the
36930
- * factory does not read fee callbacks from the context. Earn custom fees remain
36931
- * reserved until EarnKit fee support ships.
36932
- *
36933
- * When EarnKit supports developer fee hooks, this factory can wire AppKit context through.
36934
- *
36935
- * @param context - AppKit context reserved for future EarnKit wiring
36936
- * @returns A new EarnKit instance
36937
- *
36938
- * @example
36939
- * ```typescript
36940
- * const earnKit = createEarnKit(context)
36941
- * ```
36942
- */ const createEarnKit = ()=>new EarnKit();
36943
-
36944
38066
  /**
36945
38067
  * Register event handlers from a context actions map to a kit instance.
36946
38068
  *
@@ -37005,6 +38127,29 @@ registerKit(`${pkg$2.name}/${pkg$2.version}`);
37005
38127
  }
37006
38128
  };
37007
38129
 
38130
+ /**
38131
+ * Create an EarnKit instance for AppKit earn operations.
38132
+ *
38133
+ * Attaches any earn event handlers previously registered on the AppKit
38134
+ * context (via `kit.on('earn.*', …)` or `kit.on('*', …)`) so step events
38135
+ * fire during the returned kit's earn operations.
38136
+ *
38137
+ * @remarks Developer fee hooks from the AppKit context are not applied.
38138
+ * EarnKit does not yet support custom fee policies.
38139
+ *
38140
+ * @param context - AppKit context with earn event handlers and kit options
38141
+ * @returns An EarnKit instance ready for AppKit earn operations
38142
+ *
38143
+ * @example
38144
+ * ```typescript
38145
+ * const earnKit = createEarnKit(context)
38146
+ * ```
38147
+ */ const createEarnKit = (context)=>{
38148
+ const kit = new EarnKit();
38149
+ registerActionHandlers(kit, context.actions.earn, 'earn');
38150
+ return kit;
38151
+ };
38152
+
37008
38153
  /**
37009
38154
  * List of all supported token aliases for App Kit send operations.
37010
38155
  *
@@ -37795,7 +38940,8 @@ const tokens = createTokenRegistry();
37795
38940
  * polling loop yourself.
37796
38941
  *
37797
38942
  * @param context - AppKit context.
37798
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
38943
+ * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
38944
+ * `kitKey`.
37799
38945
  * @returns A snapshot of the swap's status at the time of the call.
37800
38946
  *
37801
38947
  * @example
@@ -37884,7 +39030,7 @@ const tokens = createTokenRegistry();
37884
39030
  * addresses before indexing into `result.rates[chain]`.
37885
39031
  *
37886
39032
  * @param context - AppKit context.
37887
- * @param params - `chain`, optional `tokens`, and `kitKey`.
39033
+ * @param params - `chain`, plus optional `tokens` and `kitKey`.
37888
39034
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
37889
39035
  *
37890
39036
  * @example
@@ -37979,7 +39125,7 @@ const tokens = createTokenRegistry();
37979
39125
  }
37980
39126
  // Return earn-specific chains
37981
39127
  if (operationType === 'earn') {
37982
- const earnKit = createEarnKit();
39128
+ const earnKit = createEarnKit(context);
37983
39129
  return earnKit.getSupportedChains();
37984
39130
  }
37985
39131
  // Return unified balance chains
@@ -37990,7 +39136,7 @@ const tokens = createTokenRegistry();
37990
39136
  // Reuse kit instances here if provider constructors become expensive or stateful.
37991
39137
  const bridgeKit = createBridgeKit(context);
37992
39138
  const swapKit = createSwapKit(context);
37993
- const earnKit = createEarnKit();
39139
+ const earnKit = createEarnKit(context);
37994
39140
  const bridgeChains = bridgeKit.getSupportedChains();
37995
39141
  const swapChains = swapKit.getSupportedChains();
37996
39142
  const earnChains = earnKit.getSupportedChains();
@@ -38012,7 +39158,7 @@ const tokens = createTokenRegistry();
38012
39158
  };
38013
39159
 
38014
39160
  async function deposit$2(context, params) {
38015
- return createEarnKit().deposit(params);
39161
+ return createEarnKit(context).deposit(params);
38016
39162
  }
38017
39163
  /**
38018
39164
  * Execute an earn withdrawal operation.
@@ -38036,7 +39182,7 @@ async function deposit$2(context, params) {
38036
39182
  * })
38037
39183
  * ```
38038
39184
  */ async function withdraw(context, params) {
38039
- return createEarnKit().withdraw(params);
39185
+ return createEarnKit(context).withdraw(params);
38040
39186
  }
38041
39187
  /**
38042
39188
  * Claim earn rewards.
@@ -38059,7 +39205,7 @@ async function deposit$2(context, params) {
38059
39205
  * })
38060
39206
  * ```
38061
39207
  */ async function claimRewards(context, params) {
38062
- return createEarnKit().claimRewards(params);
39208
+ return createEarnKit(context).claimRewards(params);
38063
39209
  }
38064
39210
  /**
38065
39211
  * Fetch vault information.
@@ -38081,7 +39227,7 @@ async function deposit$2(context, params) {
38081
39227
  * })
38082
39228
  * ```
38083
39229
  */ async function getVaults(context, params) {
38084
- return createEarnKit().getVaults(params);
39230
+ return createEarnKit(context).getVaults(params);
38085
39231
  }
38086
39232
  /**
38087
39233
  * Discover vaults available on a chain.
@@ -38105,7 +39251,7 @@ async function deposit$2(context, params) {
38105
39251
  * })
38106
39252
  * ```
38107
39253
  */ async function exploreVaults(context, params) {
38108
- return createEarnKit().exploreVaults(params);
39254
+ return createEarnKit(context).exploreVaults(params);
38109
39255
  }
38110
39256
  /**
38111
39257
  * Lazily iterate every vault available on a chain.
@@ -38129,7 +39275,7 @@ async function deposit$2(context, params) {
38129
39275
  * }
38130
39276
  * ```
38131
39277
  */ function exploreVaultsIterator(context, params) {
38132
- return createEarnKit().exploreVaultsIterator(params);
39278
+ return createEarnKit(context).exploreVaultsIterator(params);
38133
39279
  }
38134
39280
  /**
38135
39281
  * Fetch a wallet position in a vault.
@@ -38152,7 +39298,7 @@ async function deposit$2(context, params) {
38152
39298
  * })
38153
39299
  * ```
38154
39300
  */ async function getPosition(context, params) {
38155
- return createEarnKit().getPosition(params);
39301
+ return createEarnKit(context).getPosition(params);
38156
39302
  }
38157
39303
  /**
38158
39304
  * Fetch the current status of a cross-chain Earn deposit.
@@ -38174,7 +39320,7 @@ async function deposit$2(context, params) {
38174
39320
  * console.log(status.status)
38175
39321
  * ```
38176
39322
  */ async function getCrossChainDepositStatus(context, params) {
38177
- return createEarnKit().getCrossChainDepositStatus(params);
39323
+ return createEarnKit(context).getCrossChainDepositStatus(params);
38178
39324
  }
38179
39325
  /**
38180
39326
  * Poll a cross-chain Earn deposit until it reaches a terminal bridge state.
@@ -38197,7 +39343,7 @@ async function deposit$2(context, params) {
38197
39343
  * console.log(result.outcome)
38198
39344
  * ```
38199
39345
  */ async function waitForCrossChainDeposit(context, params) {
38200
- return createEarnKit().waitForCrossChainDeposit(params);
39346
+ return createEarnKit(context).waitForCrossChainDeposit(params);
38201
39347
  }
38202
39348
  /**
38203
39349
  * Fetch a deposit quote.
@@ -38221,7 +39367,7 @@ async function deposit$2(context, params) {
38221
39367
  * })
38222
39368
  * ```
38223
39369
  */ async function getDepositQuote(context, params) {
38224
- return createEarnKit().getDepositQuote(params);
39370
+ return createEarnKit(context).getDepositQuote(params);
38225
39371
  }
38226
39372
  /**
38227
39373
  * Fetch a withdrawal quote.
@@ -38245,7 +39391,7 @@ async function deposit$2(context, params) {
38245
39391
  * })
38246
39392
  * ```
38247
39393
  */ async function getWithdrawalQuote(context, params) {
38248
- return createEarnKit().getWithdrawalQuote(params);
39394
+ return createEarnKit(context).getWithdrawalQuote(params);
38249
39395
  }
38250
39396
  /**
38251
39397
  * Fetch a claim rewards quote.
@@ -38268,11 +39414,47 @@ async function deposit$2(context, params) {
38268
39414
  * })
38269
39415
  * ```
38270
39416
  */ async function getClaimRewardsQuote(context, params) {
38271
- return createEarnKit().getClaimRewardsQuote(params);
39417
+ return createEarnKit(context).getClaimRewardsQuote(params);
39418
+ }
39419
+ /**
39420
+ * Resume a multi-phase earn operation that previously failed.
39421
+ *
39422
+ * Pass the {@link KitError} caught from `deposit`, `withdraw`, or
39423
+ * `claimRewards`. Completed phases can be skipped when the error carries
39424
+ * earn retry context. Call `isRetryableError(error)` first.
39425
+ *
39426
+ * @remarks
39427
+ * Retry re-fetches execution params and may re-submit the execute
39428
+ * transaction. Treat this as best-effort recovery if a prior execute
39429
+ * broadcast may still be in flight.
39430
+ *
39431
+ * @param context - AppKit context
39432
+ * @param error - The error caught from a previous multi-phase earn operation
39433
+ * @returns Promise resolving to the result of the resumed operation
39434
+ * @throws If the error is not retryable or lacks earn retry context
39435
+ *
39436
+ * @example
39437
+ * ```typescript
39438
+ * import { isRetryableError } from '@circle-fin/app-kit'
39439
+ * import { createContext } from '@circle-fin/app-kit/context'
39440
+ * import { retry } from '@circle-fin/app-kit/earn'
39441
+ *
39442
+ * const context = createContext()
39443
+ *
39444
+ * try {
39445
+ * await deposit(context, params)
39446
+ * } catch (error) {
39447
+ * if (isRetryableError(error)) {
39448
+ * const result = await retry(context, error)
39449
+ * }
39450
+ * }
39451
+ * ```
39452
+ */ async function retry(context, error) {
39453
+ return createEarnKit(context).retry(error);
38272
39454
  }
38273
39455
 
38274
39456
  var name = "@circle-fin/unified-balance-kit";
38275
- var version = "1.2.2";
39457
+ var version = "1.3.0";
38276
39458
  var pkg = {
38277
39459
  name: name,
38278
39460
  version: version};
@@ -40740,29 +41922,33 @@ const CIRCLE_BPS_DIVISOR = 10_000n;
40740
41922
  const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40741
41923
  /**
40742
41924
  * Return the estimated Gateway gas fee for a chain in USDC atomic units.
40743
- * Falls back to a conservative 0.1 USDC for unlisted chains.
41925
+ * Prefers an entry in `overrides` (the real per-chain fee derived from a
41926
+ * prior estimate), then the static {@link GAS_FEE_BY_CHAIN} constant, and
41927
+ * finally a conservative 0.1 USDC fallback for unlisted chains.
40744
41928
  *
40745
41929
  * @param chain - The source blockchain.
41930
+ * @param overrides - Optional real per-chain gas fees keyed by chain.
40746
41931
  * @returns Gas fee in USDC atomic units.
40747
- */ function getGasFee(chain) {
40748
- return GAS_FEE_BY_CHAIN.get(chain) ?? DEFAULT_GAS_FEE;
41932
+ */ function getGasFee(chain, overrides) {
41933
+ return overrides?.get(chain) ?? GAS_FEE_BY_CHAIN.get(chain) ?? DEFAULT_GAS_FEE;
40749
41934
  }
40750
41935
  /**
40751
41936
  * Return the estimated forwarder fee for the destination chain
40752
41937
  * (service fee + destination gas fee).
40753
41938
  *
40754
41939
  * @param destinationChain - The mint destination chain.
41940
+ * @param overrides - Optional real per-chain gas fees keyed by chain.
40755
41941
  * @returns Forwarder fee in USDC atomic units.
40756
- */ function getForwarderFee(destinationChain) {
40757
- const destGas = getGasFee(destinationChain);
41942
+ */ function getForwarderFee(destinationChain, overrides) {
41943
+ const destGas = getGasFee(destinationChain, overrides);
40758
41944
  return FORWARDER_SERVICE_FEE + destGas;
40759
41945
  }
40760
41946
  /**
40761
41947
  * Estimate the fixed fees (gas + forwarder) and compute the maximum
40762
41948
  * amount that can be drawn from this chain for a single intent,
40763
41949
  * accounting for the 0.5 bps transfer fee if cross-chain.
40764
- */ function computeMaxDrawable(slot, forwarderFeeRemaining) {
40765
- const gasFee = getGasFee(slot.chain);
41950
+ */ function computeMaxDrawable(slot, forwarderFeeRemaining, overrides) {
41951
+ const gasFee = getGasFee(slot.chain, overrides);
40766
41952
  let fixedFees = gasFee;
40767
41953
  let forwarderFeeUsed = 0n;
40768
41954
  if (forwarderFeeRemaining > 0n) {
@@ -40794,14 +41980,14 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40794
41980
  * buffer), and returns the allocations for this pass.
40795
41981
  *
40796
41982
  * Mutates `slot.remaining` so the next pass sees reduced balances.
40797
- */ function greedyAllocate(slots, amount, destinationChain, useForwarder) {
41983
+ */ function greedyAllocate(slots, amount, destinationChain, useForwarder, overrides) {
40798
41984
  const result = [];
40799
41985
  let remaining = amount;
40800
- let forwarderFeeRemaining = useForwarder ? getForwarderFee(destinationChain) : 0n;
41986
+ let forwarderFeeRemaining = useForwarder ? getForwarderFee(destinationChain, overrides) : 0n;
40801
41987
  if (remaining <= 0n) return result;
40802
41988
  for (const slot of slots){
40803
41989
  if (remaining <= 0n) break;
40804
- const { drawable, gasFee, forwarderFeeUsed } = computeMaxDrawable(slot, forwarderFeeRemaining);
41990
+ const { drawable, gasFee, forwarderFeeUsed } = computeMaxDrawable(slot, forwarderFeeRemaining, overrides);
40805
41991
  if (drawable <= 0n) continue;
40806
41992
  // Greedy: take as much as we can from this chain
40807
41993
  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.
@@ -40900,7 +42086,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40900
42086
  // After this pass, slot.remaining reflects consumed capacity.
40901
42087
  // -----------------------------------------------------------------------
40902
42088
  const transferAmount = parseUnits(ctx.amountIn, USDC_DECIMALS);
40903
- const allocations = greedyAllocate(slots, transferAmount, ctx.destinationChain, ctx.useForwarder);
42089
+ const allocations = greedyAllocate(slots, transferAmount, ctx.destinationChain, ctx.useForwarder, ctx.gasFeeOverrides);
40904
42090
  assertFullyAllocated(allocations, transferAmount, ctx.amountIn);
40905
42091
  // -----------------------------------------------------------------------
40906
42092
  // 4. Phase 2 — Allocate developer fee
@@ -40908,7 +42094,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40908
42094
  // Same-chain first here too — if the destination chain still has
40909
42095
  // capacity, use it (same-chain fee intent = cheapest gas).
40910
42096
  // -----------------------------------------------------------------------
40911
- const developerFeeAllocations = greedyAllocate(slots, devFeeAmount, ctx.destinationChain, false);
42097
+ const developerFeeAllocations = greedyAllocate(slots, devFeeAmount, ctx.destinationChain, false, ctx.gasFeeOverrides);
40912
42098
  if (devFeeAmount > 0n) {
40913
42099
  assertFullyAllocated(developerFeeAllocations, devFeeAmount, formatUnits(devFeeAmount.toString(), USDC_DECIMALS));
40914
42100
  }
@@ -40917,7 +42103,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
40917
42103
  // Again same ordering, same shared reduced balances.
40918
42104
  // Same-chain first for the same reason.
40919
42105
  // -----------------------------------------------------------------------
40920
- const circleFeeAllocations = greedyAllocate(slots, circleFeeAmount, ctx.destinationChain, false);
42106
+ const circleFeeAllocations = greedyAllocate(slots, circleFeeAmount, ctx.destinationChain, false, ctx.gasFeeOverrides);
40921
42107
  if (circleFeeAmount > 0n) {
40922
42108
  assertFullyAllocated(circleFeeAllocations, circleFeeAmount, formatUnits(circleFeeAmount.toString(), USDC_DECIMALS));
40923
42109
  }
@@ -41016,10 +42202,12 @@ const BPS_DIVISOR = 100_000n;
41016
42202
  *
41017
42203
  * Unlike `findChainNameByDomain` (which returns the display `name`),
41018
42204
  * this returns `chain.chain` — the enum identifier expected by
41019
- * {@link FeeAllocation}.
42205
+ * {@link FeeAllocation}. Returns `undefined` when no allocation covers the
42206
+ * domain so callers skip the intent rather than bucketing it under a
42207
+ * fabricated sentinel.
41020
42208
  */ function findBlockchainByDomain(domain, allocations) {
41021
42209
  const alloc = allocations.find((a)=>a.chain.gateway.domain === domain);
41022
- return alloc?.chain.chain ?? 'Unknown';
42210
+ return alloc?.chain.chain;
41023
42211
  }
41024
42212
  /**
41025
42213
  * Normalize any address/salt format to lowercase bytes32 hex.
@@ -41143,6 +42331,33 @@ const BPS_DIVISOR = 100_000n;
41143
42331
  };
41144
42332
  });
41145
42333
  }
42334
+ /**
42335
+ * Read an intent's transfer value as a BigInt, tolerating the string form
42336
+ * that can appear on estimate-response specs.
42337
+ */ function intentValue(intent) {
42338
+ const { value } = intent.spec;
42339
+ return typeof value === 'bigint' ? value : safeBigInt(String(value), 'spec.value');
42340
+ }
42341
+ /**
42342
+ * Split a single intent's `maxFee` into its transfer-fee and gas-fee
42343
+ * components.
42344
+ *
42345
+ * `transferFee = value * GATEWAY_TRANSFER_FEE_SCALED_BPS / BPS_DIVISOR`
42346
+ * `gasFee = maxFee - transferFee`
42347
+ *
42348
+ * Same-chain transfers (withdrawals) do not incur a transfer fee, so the
42349
+ * whole `maxFee` is gas. See {@link aggregateFeesByIntent} for the caveats
42350
+ * on re-deriving the split locally.
42351
+ */ function splitIntentFee(intent) {
42352
+ const { maxFee, spec } = intent;
42353
+ const isSameChain = spec.sourceDomain === spec.destinationDomain;
42354
+ const transferFee = isSameChain ? 0n : intentValue(intent) * GATEWAY_TRANSFER_FEE_SCALED_BPS / BPS_DIVISOR;
42355
+ const gasFee = maxFee > transferFee ? maxFee - transferFee : 0n;
42356
+ return {
42357
+ transferFee,
42358
+ gasFee
42359
+ };
42360
+ }
41146
42361
  /**
41147
42362
  * Decompose each intent's `maxFee` into a transfer fee and a gas fee,
41148
42363
  * then aggregate both by source chain.
@@ -41167,7 +42382,6 @@ const BPS_DIVISOR = 100_000n;
41167
42382
  * names from source domains.
41168
42383
  * @returns Per-chain and total transfer/gas fee breakdowns.
41169
42384
  */ function aggregateFeesByIntent(estimatedIntents, allocations) {
41170
- const transferFeeBps = GATEWAY_TRANSFER_FEE_SCALED_BPS;
41171
42385
  const transferFeeByChain = new Map();
41172
42386
  const gasFeeByChain = new Map();
41173
42387
  let totalTransferFee = 0n;
@@ -41175,18 +42389,18 @@ const BPS_DIVISOR = 100_000n;
41175
42389
  for (const intent of estimatedIntents){
41176
42390
  const { maxFee, spec } = intent;
41177
42391
  if (maxFee === 0n) continue;
41178
- const chainName = findBlockchainByDomain(spec.sourceDomain, allocations);
41179
- const value = typeof spec.value === 'bigint' ? spec.value : safeBigInt(String(spec.value), 'spec.value');
41180
- const isSameChain = spec.sourceDomain === spec.destinationDomain;
41181
- const transferFee = isSameChain ? 0n : value * transferFeeBps / BPS_DIVISOR;
41182
- const gasFee = maxFee > transferFee ? maxFee - transferFee : 0n;
42392
+ const { transferFee, gasFee } = splitIntentFee(intent);
42393
+ totalTransferFee += transferFee;
42394
+ totalGasFee += gasFee;
42395
+ // Totals stay complete even if a domain can't be resolved; only the
42396
+ // per-chain breakdown skips it rather than inventing a placeholder chain.
42397
+ const chain = findBlockchainByDomain(spec.sourceDomain, allocations);
42398
+ if (chain === undefined) continue;
41183
42399
  if (transferFee > 0n) {
41184
- totalTransferFee += transferFee;
41185
- transferFeeByChain.set(chainName, (transferFeeByChain.get(chainName) ?? 0n) + transferFee);
42400
+ transferFeeByChain.set(chain, (transferFeeByChain.get(chain) ?? 0n) + transferFee);
41186
42401
  }
41187
42402
  if (gasFee > 0n) {
41188
- totalGasFee += gasFee;
41189
- gasFeeByChain.set(chainName, (gasFeeByChain.get(chainName) ?? 0n) + gasFee);
42403
+ gasFeeByChain.set(chain, (gasFeeByChain.get(chain) ?? 0n) + gasFee);
41190
42404
  }
41191
42405
  }
41192
42406
  return {
@@ -41250,6 +42464,91 @@ const BPS_DIVISOR = 100_000n;
41250
42464
  }
41251
42465
  return fees;
41252
42466
  }
42467
+ /**
42468
+ * Derive the real per-chain Gateway gas fee from estimated intents, keyed by
42469
+ * source {@link Blockchain}.
42470
+ *
42471
+ * The value is the maximum single-intent gas fee observed on each chain
42472
+ * (`maxFee − transferFee`) — the amount `computeAutoAllocation` must reserve
42473
+ * per burn intent on that chain. Gas is (near) amount-independent, so every
42474
+ * intent on a chain pays roughly the same; taking the max is a conservative
42475
+ * choice for the multi-intent-per-chain case.
42476
+ *
42477
+ * Intended for `AutoAllocationContext.gasFeeOverrides` so the corrective
42478
+ * re-allocation pass reserves the API's real fee instead of the static
42479
+ * {@link GAS_FEE_BY_CHAIN} constant.
42480
+ *
42481
+ * @param estimatedIntents - Intents with `maxFee` from {@link parseEstimateResponse}.
42482
+ * @param allocations - Normalised allocations used to resolve chain from source domain.
42483
+ * @returns Per-chain real gas fees in USDC atomic units.
42484
+ *
42485
+ * @example
42486
+ * ```typescript
42487
+ * import type { BurnIntent } from '../createIntent/types'
42488
+ * import type { NormalizedAllocation } from '../allocations'
42489
+ *
42490
+ * declare const estimatedIntents: BurnIntent[]
42491
+ * declare const allocations: NormalizedAllocation[]
42492
+ *
42493
+ * // Real per-chain gas, ready to pass as AutoAllocationContext.gasFeeOverrides
42494
+ * // to re-run computeAutoAllocation with the corrected reserve.
42495
+ * const overrides = deriveGasFeeOverrides(estimatedIntents, allocations)
42496
+ * ```
42497
+ */ function deriveGasFeeOverrides(estimatedIntents, allocations) {
42498
+ const overrides = new Map();
42499
+ for (const intent of estimatedIntents){
42500
+ if (intent.maxFee === 0n) continue;
42501
+ const chain = findBlockchainByDomain(intent.spec.sourceDomain, allocations);
42502
+ if (chain === undefined) continue;
42503
+ const { gasFee } = splitIntentFee(intent);
42504
+ const prev = overrides.get(chain) ?? 0n;
42505
+ if (gasFee > prev) overrides.set(chain, gasFee);
42506
+ }
42507
+ return overrides;
42508
+ }
42509
+ /**
42510
+ * Sum the total balance each source chain must cover, keyed by source
42511
+ * {@link Blockchain}.
42512
+ *
42513
+ * Approximates the Gateway API's balance validation, which rejects a transfer
42514
+ * (`BALANCE_INSUFFICIENT_TOKEN`) when a depositor's confirmed balance on a
42515
+ * source chain is below `sum(intent.value + intent.maxFee)` for that
42516
+ * depositor's intents. This aggregates by chain across all sources, so it is
42517
+ * exact for the common single-depositor-per-chain wallet. When several
42518
+ * depositors hold USDC on the same chain, the chain-level sum can mask a
42519
+ * per-depositor shortfall (or a surplus on one depositor can hide it); the
42520
+ * API's own per-depositor `9001` remains the backstop for that case. Scope the
42521
+ * comparison per (depositor, chain) if that multi-depositor case must be caught
42522
+ * pre-submit.
42523
+ *
42524
+ * @param estimatedIntents - Intents with `maxFee` from {@link parseEstimateResponse}.
42525
+ * @param allocations - Normalised allocations used to resolve chain from source domain.
42526
+ * @returns Per-chain required amount (transfer value + fees) in USDC atomic units.
42527
+ *
42528
+ * @example
42529
+ * ```typescript
42530
+ * import { Blockchain } from '@core/chains'
42531
+ * import type { BurnIntent } from '../createIntent/types'
42532
+ * import type { NormalizedAllocation } from '../allocations'
42533
+ *
42534
+ * declare const estimatedIntents: BurnIntent[]
42535
+ * declare const allocations: NormalizedAllocation[]
42536
+ * declare const confirmedBalanceAtomic: bigint
42537
+ *
42538
+ * const required = sumRequiredPerChain(estimatedIntents, allocations)
42539
+ * const overDrawn =
42540
+ * (required.get(Blockchain.Ethereum) ?? 0n) > confirmedBalanceAtomic
42541
+ * ```
42542
+ */ function sumRequiredPerChain(estimatedIntents, allocations) {
42543
+ const required = new Map();
42544
+ for (const intent of estimatedIntents){
42545
+ const chain = findBlockchainByDomain(intent.spec.sourceDomain, allocations);
42546
+ if (chain === undefined) continue;
42547
+ const amount = intentValue(intent) + intent.maxFee;
42548
+ required.set(chain, (required.get(chain) ?? 0n) + amount);
42549
+ }
42550
+ return required;
42551
+ }
41253
42552
 
41254
42553
  /**
41255
42554
  * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
@@ -41504,69 +42803,127 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41504
42803
  * non-forwarder transfer response is missing attestation or signature.
41505
42804
  * @throws KitError Propagated from adapter signing if the user rejects
41506
42805
  * or the signer is unavailable.
41507
- */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
41508
- if (params.amountIn) {
41509
- const rawSources = Array.isArray(params.from) ? params.from : [
41510
- params.from
41511
- ];
41512
- const sourcesArray = rawSources.filter((s)=>s != null);
41513
- const networkType = destChain.isTestnet ? 'testnet' : 'mainnet';
41514
- const balanceResults = await Promise.all(sourcesArray.map(async (source)=>{
41515
- // When sourceAccount is set (delegate flow), scope the balance
41516
- // query to the Gateway depositor not the signer. Using the
41517
- // address-only path bypasses adapter address resolution, which
41518
- // would otherwise return the signer's balance (developer-
41519
- // controlled) or reject an explicit address (user-controlled).
41520
- let querySource;
41521
- if (source.sourceAccount) {
41522
- querySource = {
41523
- address: source.sourceAccount
41524
- };
41525
- } else {
41526
- querySource = {
41527
- adapter: source.adapter
41528
- };
41529
- if ('address' in source && source.address) {
41530
- querySource['address'] = source.address;
41531
- }
41532
- }
41533
- return getBalances$1({
41534
- token: params.token,
41535
- sources: querySource,
41536
- networkType
41537
- });
41538
- }));
41539
- const chainBalances = [];
41540
- for(let i = 0; i < balanceResults.length; i++){
41541
- const breakdowns = balanceResults[i]?.breakdown[0]?.breakdown ?? [];
41542
- for (const b of breakdowns){
41543
- chainBalances.push({
41544
- chain: b.chain,
41545
- confirmedBalance: b.confirmedBalance,
41546
- sourceIndex: i
41547
- });
42806
+ */ /**
42807
+ * Fetch confirmed per-chain USDC balances for every auto-allocation source.
42808
+ *
42809
+ * Used only on the `amountIn` (auto-allocation) path. Returns one
42810
+ * {@link ChainBalance} per (source, chain) pair so the greedy allocator — and
42811
+ * the corrective re-allocation pass — can reason about draw limits without a
42812
+ * second balance round-trip.
42813
+ *
42814
+ * @param params - Spend parameters (source(s) and token).
42815
+ * @param destChain - Resolved destination chain (used for network type).
42816
+ * @returns Confirmed balances tagged with their originating source index.
42817
+ */ async function fetchChainBalances(params, destChain) {
42818
+ const rawSources = Array.isArray(params.from) ? params.from : [
42819
+ params.from
42820
+ ];
42821
+ const sourcesArray = rawSources.filter((s)=>s != null);
42822
+ const networkType = destChain.isTestnet ? 'testnet' : 'mainnet';
42823
+ const balanceResults = await Promise.all(sourcesArray.map(async (source)=>{
42824
+ // When sourceAccount is set (delegate flow), scope the balance
42825
+ // query to the Gateway depositor — not the signer. Using the
42826
+ // address-only path bypasses adapter address resolution, which
42827
+ // would otherwise return the signer's balance (developer-
42828
+ // controlled) or reject an explicit address (user-controlled).
42829
+ let querySource;
42830
+ if (source.sourceAccount) {
42831
+ querySource = {
42832
+ address: source.sourceAccount
42833
+ };
42834
+ } else {
42835
+ querySource = {
42836
+ adapter: source.adapter
42837
+ };
42838
+ if ('address' in source && source.address) {
42839
+ querySource['address'] = source.address;
41548
42840
  }
41549
42841
  }
41550
- const customFeeConfig = params.config?.customFee;
41551
- const autoAllocResult = computeAutoAllocation({
41552
- amountIn: params.amountIn,
41553
- destinationChain: destChain.chain,
41554
- chainBalances,
41555
- useForwarder,
41556
- ...customFeeConfig ? {
41557
- customFee: customFeeConfig
41558
- } : {}
42842
+ return getBalances$1({
42843
+ token: params.token,
42844
+ sources: querySource,
42845
+ networkType
41559
42846
  });
41560
- const normalizedAutoAllocations = await normalizeAutoAllocations(autoAllocResult, sourcesArray);
41561
- const intents = buildAutoAllocatedBurnIntents(normalizedAutoAllocations, destChain, recipientAddress, params.token, params.config?.customFee);
41562
- const allocations = [
41563
- ...normalizedAutoAllocations.user,
41564
- ...normalizedAutoAllocations.devFee,
41565
- ...normalizedAutoAllocations.circleFee
41566
- ];
42847
+ }));
42848
+ const chainBalances = [];
42849
+ for(let i = 0; i < balanceResults.length; i++){
42850
+ const breakdowns = balanceResults[i]?.breakdown[0]?.breakdown ?? [];
42851
+ for (const b of breakdowns){
42852
+ chainBalances.push({
42853
+ chain: b.chain,
42854
+ confirmedBalance: b.confirmedBalance,
42855
+ sourceIndex: i
42856
+ });
42857
+ }
42858
+ }
42859
+ return chainBalances;
42860
+ }
42861
+ /**
42862
+ * Build auto-allocated normalised allocations and burn intents from
42863
+ * pre-fetched balances.
42864
+ *
42865
+ * Performs no balance API call, so it can be re-invoked with
42866
+ * `gasFeeOverrides` (the real per-chain gas from a prior estimate) to correct
42867
+ * an over-draw without re-querying balances.
42868
+ *
42869
+ * @param params - Spend parameters (source(s), token, optional custom fee).
42870
+ * @param destChain - Resolved destination chain with Gateway v1 config.
42871
+ * @param recipientAddress - Resolved recipient address on the destination chain.
42872
+ * @param useForwarder - Whether the Forwarding Service path is active.
42873
+ * @param amountIn - Human-readable USDC amount to allocate.
42874
+ * @param chainBalances - Confirmed balances from {@link fetchChainBalances}.
42875
+ * @param gasFeeOverrides - Optional real per-chain gas fees to reserve.
42876
+ * @returns Normalised allocations and burn intents for the estimate/transfer API.
42877
+ */ async function buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, amountIn, chainBalances, gasFeeOverrides) {
42878
+ const rawSources = Array.isArray(params.from) ? params.from : [
42879
+ params.from
42880
+ ];
42881
+ const sourcesArray = rawSources.filter((s)=>s != null);
42882
+ const customFeeConfig = params.config?.customFee;
42883
+ const autoAllocResult = computeAutoAllocation({
42884
+ amountIn,
42885
+ destinationChain: destChain.chain,
42886
+ chainBalances,
42887
+ useForwarder,
42888
+ ...customFeeConfig ? {
42889
+ customFee: customFeeConfig
42890
+ } : {},
42891
+ ...gasFeeOverrides ? {
42892
+ gasFeeOverrides
42893
+ } : {}
42894
+ });
42895
+ const normalizedAutoAllocations = await normalizeAutoAllocations(autoAllocResult, sourcesArray);
42896
+ const intents = buildAutoAllocatedBurnIntents(normalizedAutoAllocations, destChain, recipientAddress, params.token, params.config?.customFee);
42897
+ const allocations = [
42898
+ ...normalizedAutoAllocations.user,
42899
+ ...normalizedAutoAllocations.devFee,
42900
+ ...normalizedAutoAllocations.circleFee
42901
+ ];
42902
+ return {
42903
+ allocations,
42904
+ intents
42905
+ };
42906
+ }
42907
+ /**
42908
+ * Resolve allocations and burn intents for the spend.
42909
+ *
42910
+ * Auto-allocation (`amountIn`) fetches balances once and returns them so the
42911
+ * caller can detect and correct over-draw without re-querying. Explicit
42912
+ * allocations return no balances (they are user-authoritative).
42913
+ *
42914
+ * @param params - Spend parameters.
42915
+ * @param destChain - Resolved destination chain with Gateway v1 config.
42916
+ * @param recipientAddress - Resolved recipient address.
42917
+ * @param useForwarder - Whether the Forwarding Service path is active.
42918
+ * @returns Allocations, intents, and (auto-allocation only) confirmed balances.
42919
+ */ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
42920
+ if (params.amountIn) {
42921
+ const chainBalances = await fetchChainBalances(params, destChain);
42922
+ const { allocations, intents } = await buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, params.amountIn, chainBalances);
41567
42923
  return {
41568
42924
  allocations,
41569
- intents
42925
+ intents,
42926
+ chainBalances
41570
42927
  };
41571
42928
  }
41572
42929
  const allocations = await normalizeAllocations(params);
@@ -41608,6 +42965,148 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41608
42965
  forwardingFee: undefined
41609
42966
  };
41610
42967
  }
42968
+ /** Sum confirmed balances (atomic USDC) per source chain. */ function computeAvailablePerChain(chainBalances) {
42969
+ const available = new Map();
42970
+ for (const b of chainBalances){
42971
+ const atomic = parseUnits(b.confirmedBalance, USDC_DECIMALS$1);
42972
+ available.set(b.chain, (available.get(b.chain) ?? 0n) + atomic);
42973
+ }
42974
+ return available;
42975
+ }
42976
+ /**
42977
+ * Detect source chains whose required draw (value + maxFee across their
42978
+ * intents) exceeds the confirmed balance — the condition the Gateway API
42979
+ * rejects with `BALANCE_INSUFFICIENT_TOKEN` at `/v1/transfer`.
42980
+ *
42981
+ * Both sides are summed per chain (see {@link sumRequiredPerChain} and
42982
+ * {@link computeAvailablePerChain}), so detection is exact for the common
42983
+ * single-depositor-per-chain wallet. When multiple depositors hold USDC on the
42984
+ * same chain, a chain-level surplus can mask a per-depositor shortfall; the
42985
+ * API's own per-depositor `9001` remains the backstop in that case.
42986
+ */ function findOverdrawnChains(estimatedIntents, allocations, chainBalances) {
42987
+ const required = sumRequiredPerChain(estimatedIntents, allocations);
42988
+ const available = computeAvailablePerChain(chainBalances);
42989
+ const overdrawn = [];
42990
+ for (const [chain, req] of required){
42991
+ const avail = available.get(chain) ?? 0n;
42992
+ if (req > avail) {
42993
+ overdrawn.push({
42994
+ chain,
42995
+ required: req,
42996
+ available: avail
42997
+ });
42998
+ }
42999
+ }
43000
+ return overdrawn;
43001
+ }
43002
+ /**
43003
+ * Build a descriptive KitError for an auto-allocation gas shortfall that
43004
+ * survives the corrective re-allocation, naming the per-chain gap so the
43005
+ * caller sees the real cause instead of the opaque API 9001 rejection.
43006
+ */ function createAutoAllocationGasError(overdrawn, cause) {
43007
+ 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('; ');
43008
+ return new KitError({
43009
+ ...BalanceError.INSUFFICIENT_GAS,
43010
+ recoverability: 'FATAL',
43011
+ message: `Insufficient USDC to cover the transfer amount plus Gateway gas fees: ${detail}. ` + `Reduce the amount or add USDC on the affected chain(s).`,
43012
+ ...cause === undefined ? {} : {
43013
+ cause: {
43014
+ trace: {
43015
+ cause
43016
+ }
43017
+ }
43018
+ }
43019
+ });
43020
+ }
43021
+ /**
43022
+ * Validate allocations against the network/forwarder rules, call the estimate
43023
+ * API, and return the estimated intents with any forwarding fee.
43024
+ *
43025
+ * @param allocations - Normalised allocations for the estimate.
43026
+ * @param intents - Burn intents to estimate.
43027
+ * @param destChain - Resolved destination chain with Gateway v1 config.
43028
+ * @param useForwarder - Whether the Forwarding Service path is active.
43029
+ * @returns Estimated intents (with real maxFee) and optional forwarding fee.
43030
+ */ async function validateAndEstimate(allocations, intents, destChain, useForwarder) {
43031
+ assertNetworkCompatibility(allocations, destChain);
43032
+ if (useForwarder) {
43033
+ assertForwarderRouteSupport(destChain, allocations);
43034
+ }
43035
+ const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
43036
+ const estimateBody = buildEstimateRequestBody(intents);
43037
+ const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
43038
+ const estimatedIntents = parseEstimateResponse(entries, intents);
43039
+ return {
43040
+ estimatedIntents,
43041
+ forwardingFee
43042
+ };
43043
+ }
43044
+ /**
43045
+ * Fold newly-observed per-chain gas into the accumulated overrides, keeping the
43046
+ * higher fee per chain so a chain a later pass reveals is never under-reserved.
43047
+ */ function mergeGasFeeOverrides(base, next) {
43048
+ const merged = new Map(base);
43049
+ for (const [chain, fee] of next){
43050
+ const prev = merged.get(chain);
43051
+ if (prev === undefined || fee > prev) {
43052
+ merged.set(chain, fee);
43053
+ }
43054
+ }
43055
+ return merged;
43056
+ }
43057
+ /**
43058
+ * Maximum corrective re-allocation passes before failing fast. One pass fixes
43059
+ * the common case; a second/third covers a chain that a spill only introduces
43060
+ * after gas is reserved. Bounds the worst case at this many extra estimate
43061
+ * round-trips (only ever reached when the balance genuinely falls short).
43062
+ */ const MAX_CORRECTION_PASSES = 3;
43063
+ /**
43064
+ * Correct an auto-allocation over-draw: reserve the estimate's real per-chain
43065
+ * gas, re-allocate from the same balances, and re-estimate — repeating up to
43066
+ * {@link MAX_CORRECTION_PASSES} times, accumulating the real gas each pass
43067
+ * reveals.
43068
+ *
43069
+ * One pass fixes the common case, where the over-drawn chain was already in the
43070
+ * first estimate. A further pass covers a chain that a spill only introduces
43071
+ * once gas is reserved on the destination: that chain isn't in the first
43072
+ * estimate, so its real gas is unknown until it appears, and its first
43073
+ * re-allocation falls back to the static reserve. Each pass folds the newly
43074
+ * revealed gas into the overrides (see {@link mergeGasFeeOverrides}) so the
43075
+ * next pass reserves it too. Per-chain gas is ~amount-independent, so once
43076
+ * every drawn chain's real gas is known the allocation converges.
43077
+ *
43078
+ * When the shortfall is genuine — the re-allocation can't cover the amount, or
43079
+ * the passes are exhausted while still over-drawn — throws a gas-specific
43080
+ * {@link KitError} instead of submitting a doomed transfer.
43081
+ */ async function correctOverdraw(opts) {
43082
+ let overrides = deriveGasFeeOverrides(opts.estimatedIntents, opts.allocations);
43083
+ let overdrawn = opts.overdrawn;
43084
+ for(let pass = 0; pass < MAX_CORRECTION_PASSES; pass++){
43085
+ let corrected;
43086
+ try {
43087
+ corrected = await buildAutoAllocatedFromBalances(opts.params, opts.destChain, opts.recipientAddress, opts.useForwarder, opts.amountIn, opts.chainBalances, overrides);
43088
+ } catch (err) {
43089
+ // Re-allocating with the real gas reserved can't cover the amount →
43090
+ // surface a gas-specific error instead of the opaque API rejection.
43091
+ if (err instanceof KitError && err.code === BalanceError.INSUFFICIENT_TOKEN.code) {
43092
+ throw createAutoAllocationGasError(overdrawn, err);
43093
+ }
43094
+ throw err;
43095
+ }
43096
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder);
43097
+ const stillOverdrawn = findOverdrawnChains(estimatedIntents, corrected.allocations, opts.chainBalances);
43098
+ if (stillOverdrawn.length === 0) {
43099
+ return {
43100
+ allocations: corrected.allocations,
43101
+ estimatedIntents,
43102
+ forwardingFee
43103
+ };
43104
+ }
43105
+ overdrawn = stillOverdrawn;
43106
+ overrides = mergeGasFeeOverrides(overrides, deriveGasFeeOverrides(estimatedIntents, corrected.allocations));
43107
+ }
43108
+ throw createAutoAllocationGasError(overdrawn);
43109
+ }
41611
43110
  /**
41612
43111
  * Validate allocations, call the estimate API, and return estimated intents.
41613
43112
  *
@@ -41615,6 +43114,14 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41615
43114
  * path. Handles forwarder route validation, network compatibility, and the
41616
43115
  * estimate API call.
41617
43116
  *
43117
+ * For auto-allocation (`amountIn`), the greedy allocator reserves a static
43118
+ * per-chain gas fee that can undershoot the API's real fee, draining a source
43119
+ * (typically the destination chain) below `value + maxFee` and triggering a
43120
+ * `BALANCE_INSUFFICIENT_TOKEN` rejection. When the first estimate reveals such
43121
+ * an over-draw, a bounded corrective re-allocation reserves the real gas and
43122
+ * re-estimates until it converges or fails fast (see {@link correctOverdraw}).
43123
+ * Explicit allocations are user-authoritative and never re-allocated.
43124
+ *
41618
43125
  * @param params - Spend parameters.
41619
43126
  * @param destChain - Resolved destination chain with Gateway v1 config.
41620
43127
  * @param recipientAddress - Resolved recipient address.
@@ -41624,15 +43131,24 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
41624
43131
  if (useForwarder) {
41625
43132
  assertForwarderRouteSupport(destChain);
41626
43133
  }
41627
- const { allocations, intents } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
41628
- assertNetworkCompatibility(allocations, destChain);
41629
- if (useForwarder) {
41630
- assertForwarderRouteSupport(destChain, allocations);
43134
+ const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
43135
+ const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder);
43136
+ if (params.amountIn && chainBalances) {
43137
+ const overdrawn = findOverdrawnChains(estimatedIntents, allocations, chainBalances);
43138
+ if (overdrawn.length > 0) {
43139
+ return correctOverdraw({
43140
+ params,
43141
+ destChain,
43142
+ recipientAddress,
43143
+ useForwarder,
43144
+ amountIn: params.amountIn,
43145
+ chainBalances,
43146
+ estimatedIntents,
43147
+ allocations,
43148
+ overdrawn
43149
+ });
43150
+ }
41631
43151
  }
41632
- const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
41633
- const estimateBody = buildEstimateRequestBody(intents);
41634
- const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
41635
- const estimatedIntents = parseEstimateResponse(entries, intents);
41636
43152
  return {
41637
43153
  allocations,
41638
43154
  estimatedIntents,
@@ -42540,8 +44056,10 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
42540
44056
  *
42541
44057
  * - `computeFee` — required function that receives resolved spend params
42542
44058
  * and returns a fee as a string (or `Promise<string>`).
42543
- * - `resolveFeeRecipientAddress` — required function that returns a
42544
- * recipient address as a string (or `Promise<string>`).
44059
+ * - `resolveFeeRecipientAddress` — optional function that returns a
44060
+ * recipient address as a string (or `Promise<string>`). Omit it when
44061
+ * using `setFeeRecipients()`'s declarative map instead — a policy
44062
+ * with neither throws at spend time.
42545
44063
  *
42546
44064
  * @example
42547
44065
  * ```ts
@@ -42554,7 +44072,7 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
42554
44072
  * ```
42555
44073
  */ const customFeePolicySchema = zod.z.object({
42556
44074
  computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
42557
- resolveFeeRecipientAddress: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string())))
44075
+ resolveFeeRecipientAddress: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional()
42558
44076
  }).strict();
42559
44077
  /**
42560
44078
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
@@ -42576,6 +44094,71 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
42576
44094
  validateWithStateTracking(config, customFeePolicySchema, 'UnifiedBalanceKit custom fee policy', assertCustomFeePolicySymbol);
42577
44095
  }
42578
44096
 
44097
+ const assertFeeRecipientsConfigSymbol = Symbol('assertFeeRecipientsConfig');
44098
+ /**
44099
+ * Schema for validating {@link FeeRecipientsConfig}.
44100
+ *
44101
+ * Requires at least one of `evm`/`solana`, non-empty string values for
44102
+ * whichever keys are present, and — mirroring the `depositAccount`
44103
+ * validation in `deposit/validate/assertions` — an address format that
44104
+ * matches the given chain type (EVM hex vs Solana base58).
44105
+ *
44106
+ * @example
44107
+ * ```ts
44108
+ * const config = {
44109
+ * evm: '0x1234567890123456789012345678901234567890',
44110
+ * solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
44111
+ * }
44112
+ * const result = feeRecipientsConfigSchema.safeParse(config)
44113
+ * // result.success === true
44114
+ * ```
44115
+ */ const feeRecipientsConfigSchema = zod.z.object({
44116
+ evm: zod.z.string().min(1, 'Fee recipient address is required.').optional(),
44117
+ solana: zod.z.string().min(1, 'Fee recipient address is required.').optional()
44118
+ }).strict().refine((config)=>Object.keys(config).length > 0, {
44119
+ message: 'At least one fee recipient (evm or solana) is required.'
44120
+ }).superRefine((config, ctx)=>{
44121
+ for (const type of Object.keys(config)){
44122
+ const address = config[type];
44123
+ if (address == null) continue;
44124
+ // `{ name: type, type }` is a placeholder chain identifier — only
44125
+ // `.type` is checked by these two helpers today, `.name` is unused.
44126
+ // No real ChainDefinition exists here, since validation runs before
44127
+ // a destination chain is resolved.
44128
+ if (!isValidAddressForChain(address, {
44129
+ name: type,
44130
+ type
44131
+ })) {
44132
+ const { expectedAddressFormat } = extractChainInfo({
44133
+ name: type,
44134
+ type
44135
+ });
44136
+ ctx.addIssue({
44137
+ code: zod.z.ZodIssueCode.custom,
44138
+ path: [
44139
+ type
44140
+ ],
44141
+ message: `Invalid ${type} address "${address}". Expected ${expectedAddressFormat}.`
44142
+ });
44143
+ }
44144
+ }
44145
+ });
44146
+ /**
44147
+ * Assert that the provided value conforms to {@link FeeRecipientsConfig}.
44148
+ *
44149
+ * Throws a validation error with annotated paths if the configuration is
44150
+ * malformed.
44151
+ *
44152
+ * @param config - The fee recipients map to validate.
44153
+ *
44154
+ * @example
44155
+ * ```ts
44156
+ * assertFeeRecipientsConfig({ evm: '0x1234567890123456789012345678901234567890' })
44157
+ * ```
44158
+ */ function assertFeeRecipientsConfig(config) {
44159
+ validateWithStateTracking(config, feeRecipientsConfigSchema, 'UnifiedBalanceKit fee recipients config', assertFeeRecipientsConfigSymbol);
44160
+ }
44161
+
42579
44162
  function sameChain(a, b) {
42580
44163
  return a.chain !== undefined && a.chain === b.chain;
42581
44164
  }
@@ -43556,6 +45139,105 @@ function assertSourceAccountAddresses(from) {
43556
45139
  config: params.config
43557
45140
  };
43558
45141
  }
45142
+ /**
45143
+ * Tracks, per {@link CustomFeePolicy} instance, which chain types have
45144
+ * already triggered the "falling back to resolveFeeRecipientAddress"
45145
+ * warning, so repeated `spend()`/`estimateSpend()` calls (e.g. live
45146
+ * quoting) warn once per (policy, chain type) pair rather than on every
45147
+ * call.
45148
+ */ const warnedFeeRecipientFallbacks = new WeakMap();
45149
+ /**
45150
+ * Invoke `resolveFeeRecipientAddress` and validate its return value has a
45151
+ * plausible address format for `destChain`, the same check
45152
+ * `setFeeRecipients()` already applies at config time. Unlike the map,
45153
+ * the callback's return value can't be validated ahead of time, so it's
45154
+ * checked here instead — a malformed value throws immediately rather
45155
+ * than silently becoming the fee recipient.
45156
+ *
45157
+ * @internal
45158
+ */ async function resolveFeeRecipientFromCallback(callback, destChain, params) {
45159
+ const address = await callback(destChain, params);
45160
+ if (!isValidAddressForChain(address, destChain)) {
45161
+ throw new KitError({
45162
+ ...InputError.VALIDATION_FAILED,
45163
+ recoverability: 'FATAL',
45164
+ message: `resolveFeeRecipientAddress returned an invalid address ` + `"${address}" for chain type "${destChain.type}" ` + `(resolved destination: ${destChain.name}).`
45165
+ });
45166
+ }
45167
+ return address;
45168
+ }
45169
+ /**
45170
+ * Resolve the single fee recipient address for a spend.
45171
+ *
45172
+ * Every fee burn intent in a spend mints to the same destination
45173
+ * chain regardless of which source chain(s) funded it, so exactly one
45174
+ * recipient address — valid on `destChain` — is ever needed.
45175
+ *
45176
+ * `feeRecipients` (set via `setFeeRecipients`) takes priority over the
45177
+ * policy's `resolveFeeRecipientAddress` callback for any chain type it
45178
+ * has an entry for, since it's a direct lookup and doesn't require
45179
+ * invoking developer code. For a chain type `feeRecipients` doesn't
45180
+ * cover, it falls back to `resolveFeeRecipientAddress` if one is
45181
+ * configured — a warning is logged once per (policy, chain type) pair
45182
+ * so the fallback isn't a silent surprise, without spamming repeated
45183
+ * `estimateSpend()` calls used for live quoting. Throws if neither
45184
+ * resolves `destChain`'s type, or if `resolveFeeRecipientAddress`
45185
+ * resolves it to a malformed address (see
45186
+ * {@link resolveFeeRecipientFromCallback}).
45187
+ *
45188
+ * @internal
45189
+ */ async function resolveFeeRecipient(destChain, policy, feeRecipients, params) {
45190
+ if (feeRecipients) {
45191
+ // `destChain.type` is `@core/chains`' broader `ChainType` union;
45192
+ // `FeeRecipientChainType` is the narrower subset this map supports
45193
+ // today. A type not present as a key simply has no configured
45194
+ // recipient, which is handled below.
45195
+ const type = destChain.type;
45196
+ const recipientAddress = feeRecipients[type];
45197
+ if (recipientAddress) {
45198
+ return recipientAddress;
45199
+ }
45200
+ if (policy.resolveFeeRecipientAddress) {
45201
+ const warnedTypes = warnedFeeRecipientFallbacks.get(policy);
45202
+ if (!warnedTypes?.has(type)) {
45203
+ warnedFeeRecipientFallbacks.set(policy, (warnedTypes ?? new Set()).add(type));
45204
+ 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.`);
45205
+ }
45206
+ return resolveFeeRecipientFromCallback(policy.resolveFeeRecipientAddress, destChain, params);
45207
+ }
45208
+ throw new KitError({
45209
+ ...InputError.VALIDATION_FAILED,
45210
+ recoverability: 'FATAL',
45211
+ 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.`
45212
+ });
45213
+ }
45214
+ if (!policy.resolveFeeRecipientAddress) {
45215
+ throw new KitError({
45216
+ ...InputError.VALIDATION_FAILED,
45217
+ recoverability: 'FATAL',
45218
+ message: 'No fee recipient configured — call setFeeRecipients() or provide ' + 'resolveFeeRecipientAddress on the custom fee policy.'
45219
+ });
45220
+ }
45221
+ return resolveFeeRecipientFromCallback(policy.resolveFeeRecipientAddress, destChain, params);
45222
+ }
45223
+ /**
45224
+ * Guard against a common misconfiguration: a developer sets the
45225
+ * declarative `feeRecipients` map expecting it alone to drive fee
45226
+ * collection, but no fee is ever charged without a `computeFee` from
45227
+ * `customFeePolicy` to determine the amount. Without this check that
45228
+ * misconfiguration fails silently — no fee is charged and no error is
45229
+ * raised.
45230
+ *
45231
+ * @internal
45232
+ */ function assertFeeRecipientsHasPolicy(feeRecipients) {
45233
+ if (feeRecipients) {
45234
+ throw new KitError({
45235
+ ...InputError.VALIDATION_FAILED,
45236
+ recoverability: 'FATAL',
45237
+ 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.'
45238
+ });
45239
+ }
45240
+ }
43559
45241
  /**
43560
45242
  * Apply a {@link CustomFeePolicy} to an adapter-only spend.
43561
45243
  *
@@ -43564,15 +45246,20 @@ function assertSourceAccountAddresses(from) {
43564
45246
  * `config.customFee` so the provider sees it.
43565
45247
  *
43566
45248
  * @internal
43567
- */ async function mergeCustomFeePolicyForAdapterOnly(params, policy) {
43568
- if (params.config?.customFee || !policy) {
45249
+ */ async function mergeCustomFeePolicyForAdapterOnly(params, policy, feeRecipients) {
45250
+ if (params.config?.customFee) {
45251
+ return params;
45252
+ }
45253
+ if (!policy) {
45254
+ assertFeeRecipientsHasPolicy(feeRecipients);
43569
45255
  return params;
43570
45256
  }
43571
45257
  const destChain = resolveChainIdentifier(params.to.chain);
43572
- const [feeValue, recipientAddress] = await Promise.all([
43573
- policy.computeFee(params),
43574
- policy.resolveFeeRecipientAddress(destChain, params)
43575
- ]);
45258
+ // Resolve the recipient before computing the fee: a KitError here
45259
+ // (missing/unresolvable recipient) shouldn't be preceded by an
45260
+ // otherwise-wasted computeFee call, which may be a network request.
45261
+ const recipientAddress = await resolveFeeRecipient(destChain, policy, feeRecipients, params);
45262
+ const feeValue = await policy.computeFee(params);
43576
45263
  return {
43577
45264
  ...params,
43578
45265
  config: {
@@ -43602,18 +45289,35 @@ function assertSourceAccountAddresses(from) {
43602
45289
  });
43603
45290
  }
43604
45291
  }
43605
- async function mergeCustomFeeConfig(resolved, policy) {
43606
- if (resolved.config?.customFee || !policy) {
45292
+ async function mergeCustomFeeConfig(resolved, policy, feeRecipients) {
45293
+ if (resolved.config?.customFee) {
43607
45294
  return resolved;
43608
45295
  }
43609
- const firstSourceChain = resolved.from[0]?.allocations[0]?.chain;
43610
- if (!firstSourceChain) {
45296
+ if (!policy) {
45297
+ assertFeeRecipientsHasPolicy(feeRecipients);
43611
45298
  return resolved;
43612
45299
  }
43613
- const [feeValue, recipientAddress] = await Promise.all([
43614
- policy.computeFee(resolved),
43615
- policy.resolveFeeRecipientAddress(firstSourceChain, resolved)
43616
- ]);
45300
+ // Skip fee resolution when there's no source chain to spend from at
45301
+ // all. This state can't arise from validated input today — the
45302
+ // caller re-checks and throws "No source chain found" right after
45303
+ // this returns — but skipping here isn't dead code: verified that
45304
+ // removing it lets a degenerate zero-allocation resolved value reach
45305
+ // computeFee/assertDeveloperFeeWithinBounds first, which throws a
45306
+ // misleading "Developer fee must be less than the total spend
45307
+ // amount" (0 >= 0 total allocation) instead of the correct "No
45308
+ // source chain found" error — or, for a real developer computeFee
45309
+ // that assumes a non-empty allocation, an uncaught raw exception
45310
+ // instead of any KitError at all. This guard exists to guarantee the
45311
+ // caller's clear error is what actually surfaces, not for
45312
+ // correctness.
45313
+ if (collectSourceChains(resolved).length === 0) {
45314
+ return resolved;
45315
+ }
45316
+ // Resolve the recipient before computing the fee: a KitError here
45317
+ // (missing/unresolvable recipient) shouldn't be preceded by an
45318
+ // otherwise-wasted computeFee call, which may be a network request.
45319
+ const recipientAddress = await resolveFeeRecipient(resolved.to.chain, policy, feeRecipients, resolved);
45320
+ const feeValue = await policy.computeFee(resolved);
43617
45321
  return {
43618
45322
  ...resolved,
43619
45323
  config: {
@@ -43669,14 +45373,14 @@ async function mergeCustomFeeConfig(resolved, policy) {
43669
45373
  }
43670
45374
  const destChain = resolveChainIdentifier(params.to.chain);
43671
45375
  if (!hasExplicitAllocations(params.from)) {
43672
- const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy);
45376
+ const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy, context.feeRecipients);
43673
45377
  assertDeveloperFeeWithinAmount(merged);
43674
45378
  const provider = findProviderForChain(context, normalizeToken(merged.token), destChain);
43675
45379
  return callSpend(provider, toProviderAdapterOnlyParams(merged));
43676
45380
  }
43677
45381
  const resolved = await resolveSpendParams(params);
43678
45382
  assertSpendNetworkCompatibility(resolved);
43679
- const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy);
45383
+ const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy, context.feeRecipients);
43680
45384
  assertDeveloperFeeWithinBounds(withFee);
43681
45385
  const sourceChains = collectSourceChains(withFee);
43682
45386
  if (sourceChains.length === 0) {
@@ -43716,14 +45420,14 @@ async function mergeCustomFeeConfig(resolved, policy) {
43716
45420
  assertSpendParams(params);
43717
45421
  const destChain = resolveChainIdentifier(params.to.chain);
43718
45422
  if (!hasExplicitAllocations(params.from)) {
43719
- const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy);
45423
+ const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy, context.feeRecipients);
43720
45424
  assertDeveloperFeeWithinAmount(merged);
43721
45425
  const provider = findProviderForChain(context, normalizeToken(merged.token), destChain);
43722
45426
  return provider.estimateSpend(toProviderAdapterOnlyParams(merged));
43723
45427
  }
43724
45428
  const resolved = await resolveSpendParams(params);
43725
45429
  assertSpendNetworkCompatibility(resolved);
43726
- const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy);
45430
+ const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy, context.feeRecipients);
43727
45431
  assertDeveloperFeeWithinBounds(withFee);
43728
45432
  const sourceChains = collectSourceChains(withFee);
43729
45433
  if (sourceChains.length === 0) {
@@ -44698,6 +46402,46 @@ const removeFundParamsSchema = zod.z.object({
44698
46402
  */ removeCustomFeePolicy() {
44699
46403
  delete this.context.customFeePolicy;
44700
46404
  }
46405
+ /**
46406
+ * Set a declarative fee recipient map, keyed by chain type. Once set,
46407
+ * `spend()`/`estimateSpend()` resolve the fee recipient by looking up
46408
+ * the spend's destination chain type in this map — taking priority
46409
+ * over `customFeePolicy`'s `resolveFeeRecipientAddress` callback.
46410
+ *
46411
+ * @remarks
46412
+ * This only controls which address a fee is sent to — it does not by
46413
+ * itself cause any fee to be charged. You still need
46414
+ * {@link UnifiedBalanceKit.setCustomFeePolicy}'s `computeFee` to
46415
+ * determine the fee amount; calling `setFeeRecipients` without ever
46416
+ * calling `setCustomFeePolicy` throws at spend time (there is no
46417
+ * `computeFee` to determine an amount).
46418
+ *
46419
+ * @param config - Fee recipient addresses keyed by chain type (e.g.
46420
+ * `{ evm: '0x...', solana: 'Sol...' }`). Provide entries for every
46421
+ * chain type you expect to spend to; spending to a chain type with
46422
+ * no matching entry throws before any fee collection is attempted.
46423
+ *
46424
+ * @example
46425
+ * ```typescript
46426
+ * kit.setFeeRecipients({
46427
+ * evm: '0x1234567890123456789012345678901234567890',
46428
+ * solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
46429
+ * })
46430
+ * ```
46431
+ */ setFeeRecipients(config) {
46432
+ assertFeeRecipientsConfig(config);
46433
+ this.context.feeRecipients = config;
46434
+ }
46435
+ /**
46436
+ * Remove the declarative fee recipient map for the kit.
46437
+ *
46438
+ * @example
46439
+ * ```typescript
46440
+ * kit.removeFeeRecipients()
46441
+ * ```
46442
+ */ removeFeeRecipients() {
46443
+ delete this.context.feeRecipients;
46444
+ }
44701
46445
  }
44702
46446
 
44703
46447
  // Auto-register this kit for user agent tracking
@@ -45024,6 +46768,45 @@ registerKit(`${pkg.name}/${pkg.version}`);
45024
46768
  */ removeCustomFeePolicy() {
45025
46769
  this.kit.removeCustomFeePolicy();
45026
46770
  }
46771
+ /**
46772
+ * Set a declarative fee recipient map, keyed by chain type.
46773
+ *
46774
+ * Once set, `spend()`/`estimateSpend()` resolve the fee recipient by
46775
+ * looking up the spend's destination chain type in this map — taking
46776
+ * priority over `customFeePolicy`'s `resolveFeeRecipientAddress`
46777
+ * callback.
46778
+ *
46779
+ * @remarks
46780
+ * This only controls which address a fee is sent to — it does not by
46781
+ * itself cause any fee to be charged. You still need
46782
+ * `setCustomFeePolicy`'s `computeFee` to determine the fee amount;
46783
+ * calling `setFeeRecipients` without ever calling `setCustomFeePolicy`
46784
+ * throws at spend time (there is no `computeFee` to determine an
46785
+ * amount).
46786
+ *
46787
+ * @param config - Fee recipient addresses keyed by chain type (e.g.
46788
+ * `{ evm: '0x...', solana: 'Sol...' }`).
46789
+ *
46790
+ * @example
46791
+ * ```typescript
46792
+ * kit.unifiedBalance.setFeeRecipients({
46793
+ * evm: '0x1234567890123456789012345678901234567890',
46794
+ * solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
46795
+ * })
46796
+ * ```
46797
+ */ setFeeRecipients(config) {
46798
+ this.kit.setFeeRecipients(config);
46799
+ }
46800
+ /**
46801
+ * Remove the declarative fee recipient map.
46802
+ *
46803
+ * @example
46804
+ * ```typescript
46805
+ * kit.unifiedBalance.removeFeeRecipients()
46806
+ * ```
46807
+ */ removeFeeRecipients() {
46808
+ this.kit.removeFeeRecipients();
46809
+ }
45027
46810
  }
45028
46811
 
45029
46812
  /**
@@ -45175,7 +46958,8 @@ registerKit(`${pkg.name}/${pkg.version}`);
45175
46958
  waitForCrossChainDeposit: async (params)=>waitForCrossChainDeposit(this.context, params),
45176
46959
  getDepositQuote: async (params)=>getDepositQuote(this.context, params),
45177
46960
  getWithdrawalQuote: async (params)=>getWithdrawalQuote(this.context, params),
45178
- getClaimRewardsQuote: async (params)=>getClaimRewardsQuote(this.context, params)
46961
+ getClaimRewardsQuote: async (params)=>getClaimRewardsQuote(this.context, params),
46962
+ retry: async (error)=>retry(this.context, error)
45179
46963
  };
45180
46964
  }
45181
46965
  /**
@@ -45434,7 +47218,8 @@ registerKit(`${pkg.name}/${pkg.version}`);
45434
47218
  * or `'NOT_FOUND'`). Use {@link AppKit.waitForSwap} if you'd rather
45435
47219
  * not write the polling loop yourself.
45436
47220
  *
45437
- * @param params - `txHash`, `chainIn`, optional `chainOut`, and `kitKey`.
47221
+ * @param params - `txHash` and `chainIn`, plus optional `chainOut` and
47222
+ * `kitKey`.
45438
47223
  * @returns A snapshot of the swap's status at the time of the call.
45439
47224
  * @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
45440
47225
  *
@@ -45548,7 +47333,7 @@ registerKit(`${pkg.name}/${pkg.version}`);
45548
47333
  * translates to the chain's native sentinel address — `0xEee…` for EVM,
45549
47334
  * `1111…` for Solana — before querying the service.
45550
47335
  *
45551
- * @param params - `chain`, optional `tokens`, and `kitKey`.
47336
+ * @param params - `chain`, plus optional `tokens` and `kitKey`.
45552
47337
  * @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
45553
47338
  * @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
45554
47339
  * entries, or any entry is neither a registered symbol nor a
@@ -45612,6 +47397,10 @@ registerKit(`${pkg.name}/${pkg.version}`);
45612
47397
  this.context.actions.bridge[action] ??= [];
45613
47398
  this.context.actions.bridge[action].push(typedHandler);
45614
47399
  }
47400
+ if (action === '*' || action.startsWith('earn.')) {
47401
+ this.context.actions.earn[action] ??= [];
47402
+ this.context.actions.earn[action].push(typedHandler);
47403
+ }
45615
47404
  }
45616
47405
  off(actionOrWildCard, handler) {
45617
47406
  const action = actionOrWildCard;
@@ -45621,16 +47410,32 @@ registerKit(`${pkg.name}/${pkg.version}`);
45621
47410
  this.unifiedBalance.off(ubAction, typedHandler);
45622
47411
  }
45623
47412
  if (action === '*' || action.startsWith('bridge.')) {
45624
- const handlers = this.context.actions.bridge[action];
45625
- if (handlers) {
45626
- const index = handlers.indexOf(typedHandler);
45627
- if (index !== -1) {
45628
- handlers.splice(index, 1);
45629
- if (handlers.length === 0) {
45630
- Reflect.deleteProperty(this.context.actions.bridge, action);
45631
- }
45632
- }
45633
- }
47413
+ this.removeStoredActionHandler(this.context.actions.bridge, action, typedHandler);
47414
+ }
47415
+ if (action === '*' || action.startsWith('earn.')) {
47416
+ this.removeStoredActionHandler(this.context.actions.earn, action, typedHandler);
47417
+ }
47418
+ }
47419
+ /**
47420
+ * Remove one handler from a deferred AppKit action bucket.
47421
+ *
47422
+ * Deletes the action key when its handler list becomes empty.
47423
+ *
47424
+ * @param handlers - Action bucket to update (`bridge` or `earn`)
47425
+ * @param action - Stored action key, including namespace or `*`
47426
+ * @param handler - Handler reference previously passed to {@link on}
47427
+ */ removeStoredActionHandler(handlers, action, handler) {
47428
+ const actionHandlers = handlers[action];
47429
+ if (!actionHandlers) {
47430
+ return;
47431
+ }
47432
+ const index = actionHandlers.indexOf(handler);
47433
+ if (index === -1) {
47434
+ return;
47435
+ }
47436
+ actionHandlers.splice(index, 1);
47437
+ if (actionHandlers.length === 0) {
47438
+ Reflect.deleteProperty(handlers, action);
45634
47439
  }
45635
47440
  }
45636
47441
  }