@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/swap.cjs CHANGED
@@ -20,13 +20,14 @@
20
20
 
21
21
  var zod = require('zod');
22
22
  require('pino');
23
+ var bytes = require('@ethersproject/bytes');
24
+ require('@ethersproject/abi');
25
+ var address = require('@ethersproject/address');
23
26
  var web3_js = require('@solana/web3.js');
24
27
  require('bn.js');
25
28
  require('@coral-xyz/anchor');
26
29
  var bs58 = require('bs58');
27
30
  require('@noble/curves/ed25519');
28
- var bytes = require('@ethersproject/bytes');
29
- var address = require('@ethersproject/address');
30
31
  var units = require('@ethersproject/units');
31
32
  var keccak256 = require('@ethersproject/keccak256');
32
33
 
@@ -3533,7 +3534,10 @@ var EarnChain;
3533
3534
  contracts: {
3534
3535
  v1: {
3535
3536
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3536
- minter: GATEWAY_MINTER_EVM_TESTNET
3537
+ minter: GATEWAY_MINTER_EVM_TESTNET,
3538
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3539
+ // deposit into the GatewayWallet above.
3540
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3537
3541
  }
3538
3542
  },
3539
3543
  forwarderSupported: {
@@ -6601,7 +6605,10 @@ var Chains = {
6601
6605
  minter: zod.z.string({
6602
6606
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
6603
6607
  invalid_type_error: 'Gateway minter address must be a string.'
6604
- }).min(1, 'Gateway minter address cannot be empty.')
6608
+ }).min(1, 'Gateway minter address cannot be empty.'),
6609
+ depositForHandler: zod.z.string({
6610
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
6611
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
6605
6612
  }).strict() // Reject any additional properties not defined in the schema
6606
6613
  ;
6607
6614
  /**
@@ -9486,6 +9493,13 @@ const swapTokenEnumSchema = zod.z.enum([
9486
9493
  return explorerUrl;
9487
9494
  }
9488
9495
 
9496
+ /**
9497
+ * CCTP forwarding magic bytes prefix.
9498
+ *
9499
+ * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9500
+ * This prefix is right-padded to 24 bytes in the final hookData.
9501
+ */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
9502
+
9489
9503
  /**
9490
9504
  * Project an arbitrary payload onto the exact set of fields the telemetry
9491
9505
  * endpoint accepts.
@@ -9862,7 +9876,7 @@ const swapTokenEnumSchema = zod.z.enum([
9862
9876
  }
9863
9877
 
9864
9878
  var name$2 = "@circle-fin/bridge-kit";
9865
- var version$2 = "1.12.0";
9879
+ var version$2 = "1.12.1";
9866
9880
  var pkg$2 = {
9867
9881
  name: name$2,
9868
9882
  version: version$2};
@@ -10720,6 +10734,11 @@ var TransferSpeed;
10720
10734
  clock: zod.z.any().optional()
10721
10735
  }).passthrough();
10722
10736
 
10737
+ /**
10738
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
10739
+ * hookData must start with.
10740
+ */ Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
10741
+
10723
10742
  /**
10724
10743
  * The minimum finality threshold for CCTPv2 transfers.
10725
10744
  *
@@ -10752,7 +10771,7 @@ var TransferSpeed;
10752
10771
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
10753
10772
 
10754
10773
  var name$1 = "@circle-fin/swap-kit";
10755
- var version$1 = "1.3.2";
10774
+ var version$1 = "1.4.0";
10756
10775
  var pkg$1 = {
10757
10776
  name: name$1,
10758
10777
  version: version$1};
@@ -10817,7 +10836,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
10817
10836
  }).min(1, 'kitKey must be a non-empty string').optional(),
10818
10837
  provider: zod.z.string({
10819
10838
  invalid_type_error: 'provider must be a string'
10820
- }).min(1, 'provider must be a non-empty string').optional()
10839
+ }).min(1, 'provider must be a non-empty string').optional(),
10840
+ batchTransactions: zod.z.boolean({
10841
+ invalid_type_error: 'batchTransactions must be a boolean'
10842
+ }).optional()
10821
10843
  });
10822
10844
  /**
10823
10845
  * Zod schema for adapter context.
@@ -11348,7 +11370,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11348
11370
  /**
11349
11371
  * Circle Stablecoin Service API Key.
11350
11372
  * Must be a valid API key format.
11351
- */ apiKey: apiKeySchema
11373
+ */ apiKey: apiKeySchema.optional()
11352
11374
  }).superRefine(requireCrossChainQuoteToAddress);
11353
11375
  /**
11354
11376
  * Zod schema for validating CreateSwapRequest parameters.
@@ -11406,7 +11428,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11406
11428
  /**
11407
11429
  * Circle Stablecoin Service API Key.
11408
11430
  * Must be a valid API key format.
11409
- */ apiKey: apiKeySchema
11431
+ */ apiKey: apiKeySchema.optional()
11410
11432
  });
11411
11433
  /**
11412
11434
  * Zod schema for validating GetSwapStatusResponse data.
@@ -11442,7 +11464,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11442
11464
  toChain: zod.z.string({
11443
11465
  invalid_type_error: 'toChain must be a string'
11444
11466
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
11445
- apiKey: apiKeySchema
11467
+ apiKey: apiKeySchema.optional()
11446
11468
  });
11447
11469
  /**
11448
11470
  * Zod schema for validating CreateSwapResponse payloads.
@@ -11451,13 +11473,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11451
11473
  required_error: 'fee token is required',
11452
11474
  invalid_type_error: 'fee token must be a string'
11453
11475
  }).min(1, 'fee token must be a non-empty string'),
11454
- amount: feeAmountSchema
11476
+ amount: feeAmountSchema,
11477
+ decimals: zod.z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
11478
+ symbol: zod.z.string({
11479
+ invalid_type_error: 'fee token symbol must be a string'
11480
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
11455
11481
  });
11456
11482
  /**
11457
11483
  * Developer fee item schema with basis field.
11458
- */ const createSwapDeveloperFeeItemSchema = zod.z.object({
11459
- token: zod.z.string().min(1, 'fee token must be a non-empty string'),
11460
- amount: feeAmountSchema,
11484
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
11461
11485
  basis: zod.z.enum([
11462
11486
  'inputAmount',
11463
11487
  'estimatedAmount'
@@ -11549,7 +11573,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11549
11573
  addresses: zod.z.array(zod.z.string({
11550
11574
  invalid_type_error: 'addresses entries must be strings'
11551
11575
  }).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(),
11552
- apiKey: apiKeySchema
11576
+ apiKey: apiKeySchema.optional()
11553
11577
  });
11554
11578
  /**
11555
11579
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -11727,7 +11751,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11727
11751
  ...DEFAULT_CONFIG,
11728
11752
  headers: {
11729
11753
  ...DEFAULT_CONFIG.headers,
11730
- Authorization: `Bearer ${apiKey}`
11754
+ // Permissionless mode: no Authorization header when the kit key is absent.
11755
+ ...apiKey !== undefined && {
11756
+ Authorization: `Bearer ${apiKey}`
11757
+ }
11731
11758
  }
11732
11759
  };
11733
11760
  try {
@@ -11881,7 +11908,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11881
11908
  ...DEFAULT_CONFIG,
11882
11909
  headers: {
11883
11910
  ...DEFAULT_CONFIG.headers,
11884
- Authorization: `Bearer ${validatedParams.apiKey}`
11911
+ // Permissionless mode: no Authorization header when the kit key is absent.
11912
+ ...validatedParams.apiKey !== undefined && {
11913
+ Authorization: `Bearer ${validatedParams.apiKey}`
11914
+ }
11885
11915
  }
11886
11916
  };
11887
11917
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
@@ -11936,7 +11966,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11936
11966
  const validatedParams = {
11937
11967
  txHash: result.data.txHash,
11938
11968
  chain: result.data.chain,
11939
- apiKey: result.data.apiKey,
11969
+ ...result.data.apiKey !== undefined && {
11970
+ apiKey: result.data.apiKey
11971
+ },
11940
11972
  ...result.data.toChain !== undefined && {
11941
11973
  toChain: result.data.toChain
11942
11974
  }
@@ -11946,7 +11978,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11946
11978
  ...DEFAULT_CONFIG,
11947
11979
  headers: {
11948
11980
  ...DEFAULT_CONFIG.headers,
11949
- Authorization: `Bearer ${validatedParams.apiKey}`
11981
+ // Permissionless mode: no Authorization header when the kit key is absent.
11982
+ ...validatedParams.apiKey !== undefined && {
11983
+ Authorization: `Bearer ${validatedParams.apiKey}`
11984
+ }
11950
11985
  }
11951
11986
  };
11952
11987
  return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
@@ -12035,7 +12070,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12035
12070
  }
12036
12071
  const validatedParams = {
12037
12072
  chain: result.data.chain,
12038
- apiKey: result.data.apiKey,
12073
+ ...result.data.apiKey !== undefined && {
12074
+ apiKey: result.data.apiKey
12075
+ },
12039
12076
  ...result.data.addresses !== undefined && {
12040
12077
  addresses: result.data.addresses
12041
12078
  }
@@ -12045,7 +12082,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12045
12082
  ...DEFAULT_CONFIG,
12046
12083
  headers: {
12047
12084
  ...DEFAULT_CONFIG.headers,
12048
- Authorization: `Bearer ${validatedParams.apiKey}`
12085
+ // Permissionless mode: no Authorization header when the kit key is absent.
12086
+ ...validatedParams.apiKey !== undefined && {
12087
+ Authorization: `Bearer ${validatedParams.apiKey}`
12088
+ }
12049
12089
  }
12050
12090
  };
12051
12091
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
@@ -12855,6 +12895,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
12855
12895
  */ function hasSignTypedData(adapter) {
12856
12896
  return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
12857
12897
  }
12898
+ /**
12899
+ * Type guard to check if an adapter can actually produce an EIP-712
12900
+ * typed-data signature.
12901
+ *
12902
+ * @remarks
12903
+ * Strengthens {@link hasSignTypedData}: having a `signTypedData` method
12904
+ * does not guarantee it can succeed. Adapters whose signer is delegated
12905
+ * (e.g. through a signing strategy backed by a smart contract account)
12906
+ * expose the method but reject typed-data payloads at runtime. Such
12907
+ * adapters report their real capability through an optional
12908
+ * `supportsSignTypedData()` method, which this guard consults when
12909
+ * present. Adapters without the capability method are assumed able to
12910
+ * sign, preserving the previous duck-typing behavior.
12911
+ *
12912
+ * @param adapter - The adapter to check
12913
+ * @returns True if calling `signTypedData` can be expected to succeed
12914
+ *
12915
+ * @example
12916
+ * ```typescript
12917
+ * import { canSignTypedData } from '@core/adapter-evm'
12918
+ *
12919
+ * if (canSignTypedData(adapter)) {
12920
+ * const signature = await adapter.signTypedData(typedData, context)
12921
+ * } else {
12922
+ * // take an on-chain approval path instead of a permit signature
12923
+ * }
12924
+ * ```
12925
+ */ function canSignTypedData(adapter) {
12926
+ if (!hasSignTypedData(adapter)) {
12927
+ return false;
12928
+ }
12929
+ if (typeof adapter.supportsSignTypedData === 'function') {
12930
+ // The value is `boolean` per the interface, but a plain-JS adapter may
12931
+ // return anything; treat it as untrusted and coerce to a strict
12932
+ // boolean. Comparing an `unknown` (not a `boolean`) also keeps the
12933
+ // lint autofix from stripping this as a redundant `=== true`.
12934
+ const supported = adapter.supportsSignTypedData();
12935
+ return supported === true;
12936
+ }
12937
+ return true;
12938
+ }
12858
12939
 
12859
12940
  /**
12860
12941
  * Build EIP-2612 typed data for permit signing.
@@ -13277,10 +13358,13 @@ enc.encode('used_transfer_spec_hash');
13277
13358
  * at usage time rather than construction time.
13278
13359
  *
13279
13360
  * Validates:
13280
- * - Kit key is present and matches required format (KIT_KEY:id:secret)
13361
+ * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
13362
+ * An absent or empty kit key is permitted (permissionless mode) — the swap
13363
+ * service now treats the key as optional.
13281
13364
  *
13282
- * @param kitKey - The inline kit key from the swap operation config
13283
- * @throws KitError with VALIDATION_FAILED if kit key is invalid or missing
13365
+ * @param kitKey - The inline kit key from the swap operation config (optional)
13366
+ * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
13367
+ * match the KIT_KEY:<keyId>:<keySecret> format
13284
13368
  *
13285
13369
  * @example
13286
13370
  * ```typescript
@@ -13292,9 +13376,11 @@ enc.encode('used_transfer_spec_hash');
13292
13376
  * assertKitKey(kitKey)
13293
13377
  * ```
13294
13378
  */ function assertKitKey(kitKey) {
13295
- // Validate API key format using existing schema from service-client
13379
+ // Permissionless mode: the swap service treats the kit key as optional, so an
13380
+ // absent (or empty) key is valid. Only validate the format when a key is
13381
+ // actually provided.
13296
13382
  if (!kitKey) {
13297
- 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');
13383
+ return;
13298
13384
  }
13299
13385
  const apiKeyResult = apiKeySchema.safeParse(kitKey);
13300
13386
  if (!apiKeyResult.success) {
@@ -13591,8 +13677,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13591
13677
  validateResolvedAddress(resolvedTokenInAddress, chain);
13592
13678
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
13593
13679
  validateResolvedAddress(to, destinationChain);
13594
- const kitKey = config?.kitKey ?? '';
13595
- // Validates the kit key
13680
+ const kitKey = config?.kitKey;
13681
+ // Validate the kit key format when one is provided (permissionless otherwise).
13596
13682
  assertKitKey(kitKey);
13597
13683
  // Validate custom fee configuration if present
13598
13684
  const customFee = config?.customFee;
@@ -13637,7 +13723,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13637
13723
  }
13638
13724
  }
13639
13725
  },
13640
- apiKey: kitKey
13726
+ // Map kitKey → apiKey for the service client; omitted in permissionless mode.
13727
+ ...kitKey ? {
13728
+ apiKey: kitKey
13729
+ } : {}
13641
13730
  };
13642
13731
  }
13643
13732
 
@@ -14291,6 +14380,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14291
14380
  }
14292
14381
  }
14293
14382
 
14383
+ /**
14384
+ * Determine whether an adapter can produce an EIP-2612 permit signature.
14385
+ *
14386
+ * @remarks
14387
+ * A gasless permit needs two adapter capabilities: fetching the token's
14388
+ * EIP-2612 nonce and producing an EIP-712 typed-data signature. The
14389
+ * typed-data check uses {@link canSignTypedData} rather than a bare
14390
+ * `hasSignTypedData` guard so that an adapter routed through a signing
14391
+ * strategy that cannot produce typed-data signatures — one whose manifest
14392
+ * omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
14393
+ * — is correctly excluded. Such an adapter falls back to an on-chain approval
14394
+ * (batched into a single submission when it supports atomic execution) instead
14395
+ * of attempting a permit its strategy would reject.
14396
+ *
14397
+ * @param adapter - The source adapter to inspect.
14398
+ * @returns `true` when the adapter can both fetch a nonce and sign typed data.
14399
+ *
14400
+ * @example
14401
+ * ```typescript
14402
+ * import { adapterSupportsPermit } from './utils'
14403
+ *
14404
+ * if (adapterSupportsPermit(adapter)) {
14405
+ * // gasless permit path — fold the approval into the swap transaction
14406
+ * } else {
14407
+ * // on-chain approval path (batched when supportsAtomicBatch is true)
14408
+ * }
14409
+ * ```
14410
+ */ function adapterSupportsPermit(adapter) {
14411
+ return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
14412
+ }
14413
+
14294
14414
  /**
14295
14415
  * Generate EIP-2612 permit signature for token approval.
14296
14416
  *
@@ -14426,8 +14546,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14426
14546
  }
14427
14547
  // Skip permit generation if the adapter lacks the required capabilities.
14428
14548
  // handleEvmTokenApproval will have already sent an on-chain approval in this case.
14429
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
14430
- if (!adapterSupportsPermit) {
14549
+ if (!adapterSupportsPermit(adapter)) {
14431
14550
  return [
14432
14551
  createFallbackTokenInput(tokenInAddress, inputAmount)
14433
14552
  ];
@@ -14986,6 +15105,65 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
14986
15105
  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.`;
14987
15106
  }
14988
15107
 
15108
+ /**
15109
+ * Determine which chain a fee token should be resolved and formatted against.
15110
+ *
15111
+ * @remarks
15112
+ * Fees returned by the service may be denominated in either the input token
15113
+ * (on the source chain) or the output token (on the destination chain). A
15114
+ * contract address only resolves on the chain it belongs to, so formatting a
15115
+ * destination-denominated fee against the source chain causes
15116
+ * {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
15117
+ * units (e.g. a cross-chain swap charging a fee in the destination output
15118
+ * token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
15119
+ * resolved against the source chain). This is the fallback for fee items that
15120
+ * are not self-described with their own `decimals`/`chain`.
15121
+ *
15122
+ * Prefer the source chain (covers same-chain swaps and input-denominated
15123
+ * fees), then fall back to the destination chain when the token only resolves
15124
+ * there. When neither chain recognises the token, default to the source chain
15125
+ * so existing on-chain decimal lookups via the source adapter still apply.
15126
+ *
15127
+ * Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
15128
+ * source-first preference keeps them on the source chain. That is correct for
15129
+ * registry stablecoins, and for `'NATIVE'` only when both chains share native
15130
+ * decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
15131
+ * `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
15132
+ * mis-scaled. This is latent — providers emit the address form, and
15133
+ * self-describing fee items carry their own `decimals` and never reach this
15134
+ * helper — so the gap only opens for a future `'NATIVE'` fee that arrives
15135
+ * without `decimals` on a cross-native-decimal route.
15136
+ *
15137
+ * @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
15138
+ * @param sourceChain - The chain the swap originates from.
15139
+ * @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
15140
+ * @returns The chain definition the fee token should be resolved against.
15141
+ *
15142
+ * @example
15143
+ * ```typescript
15144
+ * import { resolveFeeChain } from './resolveFeeChain'
15145
+ * import { Ethereum, Base } from '@core/chains'
15146
+ *
15147
+ * // Cross-chain swap fee charged in the destination (output) token
15148
+ * resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
15149
+ * // => Base (EURC resolves on Base, not Ethereum)
15150
+ *
15151
+ * // Symbol or source-token fees stay on the source chain
15152
+ * resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
15153
+ * ```
15154
+ */ function resolveFeeChain(token, sourceChain, destinationChain) {
15155
+ if (sourceChain.chain === destinationChain.chain) {
15156
+ return sourceChain;
15157
+ }
15158
+ if (resolveTokenSymbol(token, sourceChain) !== null) {
15159
+ return sourceChain;
15160
+ }
15161
+ if (resolveTokenSymbol(token, destinationChain) !== null) {
15162
+ return destinationChain;
15163
+ }
15164
+ return sourceChain;
15165
+ }
15166
+
14989
15167
  const TOKEN_REGISTRY$1 = createTokenRegistry();
14990
15168
  /**
14991
15169
  * Format a raw base-unit amount into a human-readable decimal string.
@@ -15076,6 +15254,186 @@ const TOKEN_REGISTRY$1 = createTokenRegistry();
15076
15254
  }
15077
15255
  }
15078
15256
 
15257
+ /**
15258
+ * Runtime guard for {@link BatchCapableSwapAdapter}.
15259
+ *
15260
+ * @param adapter - The adapter to inspect.
15261
+ * @returns `true` when the adapter exposes both batch methods.
15262
+ *
15263
+ * @example
15264
+ * ```typescript
15265
+ * if (isBatchCapableSwapAdapter(adapter)) {
15266
+ * // adapter.supportsAtomicBatch / adapter.batchExecute are available
15267
+ * }
15268
+ * ```
15269
+ */ function isBatchCapableSwapAdapter(adapter) {
15270
+ return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
15271
+ }
15272
+ /**
15273
+ * Decide whether the EVM swap should take the batched approve-and-swap path.
15274
+ *
15275
+ * @remarks
15276
+ * Batching only helps when an on-chain approval would otherwise be required, so
15277
+ * it is skipped for native tokens (no approval) and for the gasless permit path
15278
+ * (already a single transaction). USDT is skipped because its reset-to-zero
15279
+ * allowance flow cannot be expressed as a fixed approve+swap pair. When those
15280
+ * gates pass, the adapter's actual atomic-batch capability is queried; any
15281
+ * failure resolves to `false` so the swap falls back to the sequential path.
15282
+ *
15283
+ * @param args - The decision inputs.
15284
+ * @param args.adapter - The source adapter.
15285
+ * @param args.chain - The source chain definition.
15286
+ * @param args.tokenInAddress - The resolved input-token address.
15287
+ * @param args.allowanceStrategy - Optional allowance strategy override.
15288
+ * @param args.batchTransactions - Optional explicit opt-out (`false` disables).
15289
+ * @returns `true` when the batched approve-and-swap path should be used.
15290
+ *
15291
+ * @example
15292
+ * ```typescript
15293
+ * const useBatched = await shouldUseBatchedSwap({
15294
+ * adapter,
15295
+ * chain,
15296
+ * tokenInAddress: '0xA0b8...',
15297
+ * allowanceStrategy: config?.allowanceStrategy,
15298
+ * batchTransactions: config?.batchTransactions,
15299
+ * })
15300
+ * ```
15301
+ */ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
15302
+ // Explicit opt-out.
15303
+ if (batchTransactions === false) {
15304
+ return false;
15305
+ }
15306
+ // Batching is an EVM capability (EIP-5792 or a signing strategy).
15307
+ if (chain.type !== 'evm') {
15308
+ return false;
15309
+ }
15310
+ // Native tokens need no approval — the swap is already a single transaction.
15311
+ if (isNativeEvmAddress(tokenInAddress)) {
15312
+ return false;
15313
+ }
15314
+ // A gasless permit folds the approval into the swap transaction, so there is
15315
+ // nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
15316
+ const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
15317
+ if (canUsePermit) {
15318
+ return false;
15319
+ }
15320
+ // USDT's reset-to-zero allowance dance cannot be expressed as a fixed
15321
+ // approve+swap pair; leave it on the sequential path.
15322
+ const usdt = chain.usdtAddress?.toLowerCase();
15323
+ if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
15324
+ return false;
15325
+ }
15326
+ if (!isBatchCapableSwapAdapter(adapter)) {
15327
+ return false;
15328
+ }
15329
+ try {
15330
+ return await adapter.supportsAtomicBatch(chain);
15331
+ } catch {
15332
+ return false;
15333
+ }
15334
+ }
15335
+ /**
15336
+ * Execute the approval and swap as a single atomic batch.
15337
+ *
15338
+ * @remarks
15339
+ * Extracts the raw call data from both prepared requests, submits them as one
15340
+ * batch via `adapter.batchExecute`, and maps the swap receipt back to a
15341
+ * transaction hash. The `fromAddress` is threaded for adapters routed through a
15342
+ * signing strategy (which have no wallet account to read the sender from); the
15343
+ * wallet-client path ignores it.
15344
+ *
15345
+ * Following the batch contract, `batchExecute` never throws once the batch is
15346
+ * submitted — a missing or failed swap receipt is surfaced here as a thrown
15347
+ * {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
15348
+ * batch and double-swap.
15349
+ *
15350
+ * @param args - The execution inputs.
15351
+ * @param args.adapter - The batch-capable source adapter.
15352
+ * @param args.chain - The EVM chain to execute on.
15353
+ * @param args.approveRequest - The prepared ERC-20 approval request.
15354
+ * @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
15355
+ * @param args.fromAddress - The address authorizing the batch.
15356
+ * @returns The swap transaction hash and the executed approval + swap records.
15357
+ * @throws {@link KitError} when the prepared requests cannot yield call data.
15358
+ * @throws {@link KitError} when the batch does not confirm or the swap reverts.
15359
+ *
15360
+ * @example
15361
+ * ```typescript
15362
+ * const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
15363
+ * adapter,
15364
+ * chain,
15365
+ * approveRequest,
15366
+ * swapRequest,
15367
+ * fromAddress: '0x742d...',
15368
+ * })
15369
+ * ```
15370
+ */ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
15371
+ if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
15372
+ throw new KitError({
15373
+ ...InputError.UNSUPPORTED_ACTION,
15374
+ recoverability: 'FATAL',
15375
+ message: 'Batched swap requires EVM prepared requests with getCallData() support.'
15376
+ });
15377
+ }
15378
+ const approveCallData = approveRequest.getCallData();
15379
+ const swapCallData = swapRequest.getCallData();
15380
+ const batchResult = await adapter.batchExecute([
15381
+ approveCallData,
15382
+ swapCallData
15383
+ ], chain, {
15384
+ fromAddress
15385
+ });
15386
+ const swapReceipt = batchResult.receipts[1];
15387
+ // A missing swap receipt means the batch never confirmed (polling timed out
15388
+ // or the wallet returned fewer receipts than calls). Re-throw the underlying
15389
+ // error when present (already FATAL); otherwise surface a FATAL timeout so the
15390
+ // caller checks the batch status rather than resubmitting.
15391
+ if (swapReceipt === undefined || swapReceipt.txHash === '') {
15392
+ if (isKitError(batchResult.error)) {
15393
+ throw batchResult.error;
15394
+ }
15395
+ throw new KitError({
15396
+ ...NetworkError.TIMEOUT,
15397
+ recoverability: 'FATAL',
15398
+ message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
15399
+ // Preserve the underlying confirmation failure when it isn't a KitError —
15400
+ // the signing-strategy path returns a raw viem error (e.g. a dropped or
15401
+ // replaced tx) — so the root cause survives behind the generic timeout.
15402
+ cause: {
15403
+ trace: {
15404
+ batchId: batchResult.batchId,
15405
+ ...batchResult.error != null && {
15406
+ error: batchResult.error
15407
+ }
15408
+ }
15409
+ }
15410
+ });
15411
+ }
15412
+ if (swapReceipt.status !== 'success') {
15413
+ throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
15414
+ }
15415
+ const executedTransactions = [];
15416
+ const approveReceipt = batchResult.receipts[0];
15417
+ // An atomic batch is a single on-chain transaction, so the approve and swap
15418
+ // receipts share one hash. Only surface a distinct approval record when it is
15419
+ // genuinely a separate transaction; otherwise the lone swap record represents
15420
+ // the batch, avoiding a phantom duplicate tx in executedTransactions.
15421
+ if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
15422
+ executedTransactions.push({
15423
+ type: 'approval',
15424
+ txHash: approveReceipt.txHash
15425
+ });
15426
+ }
15427
+ executedTransactions.push({
15428
+ type: 'swap',
15429
+ txHash: swapReceipt.txHash
15430
+ });
15431
+ return {
15432
+ swapTxHash: swapReceipt.txHash,
15433
+ executedTransactions
15434
+ };
15435
+ }
15436
+
15079
15437
  /**
15080
15438
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
15081
15439
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -15202,7 +15560,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15202
15560
  const statusResult = await getSwapStatus$1({
15203
15561
  txHash,
15204
15562
  chain: chain.chain,
15205
- apiKey
15563
+ ...apiKey !== undefined && {
15564
+ apiKey
15565
+ }
15206
15566
  });
15207
15567
  if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
15208
15568
  return {
@@ -15793,8 +16153,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15793
16153
  // Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
15794
16154
  const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
15795
16155
  const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
15796
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
15797
- const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
16156
+ const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
15798
16157
  const needsApproval = !isNativeToken && !canUsePermitFlow;
15799
16158
  if (!needsApproval) {
15800
16159
  return;
@@ -16073,34 +16432,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16073
16432
  const serviceResponse = await createSwap(serviceParams);
16074
16433
  // Track executed transactions
16075
16434
  const executedTransactions = [];
16076
- // Prepare swap action based on chain type
16077
- let preparedAction;
16078
- if (chain.type === 'solana') {
16079
- // Solana: No approval needed, directly prepare swap action
16080
- preparedAction = await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext);
16081
- } else {
16082
- // EVM chains: Handle token approval if needed
16083
- await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16084
- // EVM chains: prepareEvmSwapAction handles EIP-2612 permit generation
16085
- // Adapter contract address is read from chain.kitContracts.adapter
16086
- // Use the already-resolved context from above
16087
- preparedAction = await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy);
16088
- }
16435
+ // Prepare swap action(s) based on chain type and batch capability.
16436
+ // Returns either a single prepared action (Solana / sequential EVM) or a
16437
+ // batched approve+swap plan (EVM atomic-batch path).
16438
+ const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
16439
+ adapter,
16440
+ chain,
16441
+ serviceResponse,
16442
+ resolvedContext,
16443
+ executionCtx,
16444
+ config,
16445
+ executedTransactions
16446
+ });
16089
16447
  // Execute swap transaction via adapter
16090
16448
  // For EVM chains, use gas limit from proxy service API
16091
16449
  let txHash;
16092
16450
  const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
16093
16451
  try {
16094
- txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16095
- executedTransactions.push({
16096
- type: 'swap',
16097
- txHash
16098
- });
16099
- // Wait for transaction confirmation and verify success
16100
- const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16101
- if (txReceipt.status === 'reverted') {
16102
- const explorerUrl = buildExplorerUrl(chain, txHash);
16103
- throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16452
+ if (batchedSwapPlan) {
16453
+ // Approve + swap submitted as one atomic batch. batchExecute confirms
16454
+ // the swap internally, so no separate waitForTransaction is needed.
16455
+ const batched = await executeBatchedApproveAndSwap({
16456
+ adapter: adapter,
16457
+ chain: chain,
16458
+ approveRequest: batchedSwapPlan.approveRequest,
16459
+ swapRequest: batchedSwapPlan.swapRequest,
16460
+ fromAddress: executionCtx.fromAddress
16461
+ });
16462
+ txHash = batched.swapTxHash;
16463
+ executedTransactions.push(...batched.executedTransactions);
16464
+ } else if (preparedAction) {
16465
+ txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16466
+ executedTransactions.push({
16467
+ type: 'swap',
16468
+ txHash
16469
+ });
16470
+ // Wait for transaction confirmation and verify success
16471
+ const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16472
+ if (txReceipt.status === 'reverted') {
16473
+ const explorerUrl = buildExplorerUrl(chain, txHash);
16474
+ throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16475
+ }
16476
+ } else {
16477
+ // Unreachable: the preparation step always yields either a batched plan
16478
+ // or a prepared action.
16479
+ throw new KitError({
16480
+ ...InputError.UNSUPPORTED_ACTION,
16481
+ recoverability: 'FATAL',
16482
+ message: 'No swap execution path was prepared.'
16483
+ });
16104
16484
  }
16105
16485
  } catch (err) {
16106
16486
  handleSwapExecutionError(err, txHash, chain);
@@ -16119,7 +16499,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16119
16499
  isCrossChainSwap,
16120
16500
  txHash,
16121
16501
  chain,
16122
- apiKey: serviceParams.apiKey
16502
+ ...serviceParams.apiKey !== undefined && {
16503
+ apiKey: serviceParams.apiKey
16504
+ }
16123
16505
  });
16124
16506
  // Build and return SwapResult
16125
16507
  return {
@@ -16144,6 +16526,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16144
16526
  };
16145
16527
  }
16146
16528
  /**
16529
+ * Prepare the swap execution request(s) for the source wallet's chain.
16530
+ *
16531
+ * Produces either a single {@link PreparedChainRequest} (Solana, or the
16532
+ * sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
16533
+ * atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
16534
+ * batching). The caller executes whichever field is populated. Any on-chain
16535
+ * approval sent on the sequential path is appended to `executedTransactions`.
16536
+ *
16537
+ * @typeParam TFromAdapterCapabilities - Source-adapter capability set.
16538
+ * @param args - Inputs derived from the validated swap request.
16539
+ * @param args.adapter - Source-chain wallet adapter.
16540
+ * @param args.chain - Source chain definition.
16541
+ * @param args.serviceResponse - Validated createSwap response.
16542
+ * @param args.resolvedContext - Resolved operation context.
16543
+ * @param args.executionCtx - Minimal on-chain execution context.
16544
+ * @param args.config - Optional swap configuration (allowance/batch flags).
16545
+ * @param args.executedTransactions - Array appended with any sent approval.
16546
+ * @returns The prepared action or the batched approve+swap plan.
16547
+ * @throws KitError when the EVM atomic-batch path is selected but the chain
16548
+ * has no configured adapter contract.
16549
+ */ async prepareSwapRequests(args) {
16550
+ const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
16551
+ if (chain.type === 'solana') {
16552
+ // Solana: No approval needed, directly prepare swap action
16553
+ return {
16554
+ preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
16555
+ };
16556
+ }
16557
+ const useBatch = await shouldUseBatchedSwap({
16558
+ adapter,
16559
+ chain,
16560
+ tokenInAddress: executionCtx.tokenInAddress,
16561
+ allowanceStrategy: config?.allowanceStrategy,
16562
+ batchTransactions: config?.batchTransactions
16563
+ });
16564
+ if (useBatch) {
16565
+ // EVM chains: fuse the ERC-20 approval and the swap into a single atomic
16566
+ // batch (one signing challenge for smart-contract wallets). Force the
16567
+ // swap onto the pre-approval (PermitType.NONE) path since the approval
16568
+ // rides in the same batch.
16569
+ const adapterContractAddress = chain.kitContracts?.adapter;
16570
+ if (!adapterContractAddress) {
16571
+ throw new KitError({
16572
+ ...InputError.VALIDATION_FAILED,
16573
+ recoverability: 'FATAL',
16574
+ message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
16575
+ cause: {
16576
+ trace: {
16577
+ chain: chain.name
16578
+ }
16579
+ }
16580
+ });
16581
+ }
16582
+ const [approveRequest, swapRequest] = await Promise.all([
16583
+ this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
16584
+ prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
16585
+ ]);
16586
+ return {
16587
+ batchedSwapPlan: {
16588
+ approveRequest,
16589
+ swapRequest
16590
+ }
16591
+ };
16592
+ }
16593
+ // EVM chains: Handle token approval if needed, then prepare the swap.
16594
+ // prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
16595
+ // contract address is read from chain.kitContracts.adapter.
16596
+ await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16597
+ return {
16598
+ preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
16599
+ };
16600
+ }
16601
+ /**
16147
16602
  * Executes a swap transaction with the appropriate gas limit for the chain type.
16148
16603
  *
16149
16604
  * For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
@@ -16192,8 +16647,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16192
16647
  */ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
16193
16648
  if (!fees) return [];
16194
16649
  const [providerFees, swapFees, developerFees] = await Promise.all([
16195
- this.formatServiceFees(fees.provider, chain, 'provider', adapter),
16196
- this.formatServiceFees(fees.swap, chain, 'swap', adapter),
16650
+ this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
16651
+ this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
16197
16652
  recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
16198
16653
  ]);
16199
16654
  return [
@@ -16203,6 +16658,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16203
16658
  ];
16204
16659
  }
16205
16660
  /**
16661
+ * Resolve a single fee item to its display token and human-readable amount.
16662
+ *
16663
+ * @remarks
16664
+ * Prefer the self-describing metadata the service attaches to each fee:
16665
+ * `decimals` (and `symbol`) come straight from the provider quote, so they
16666
+ * are authoritative even for a token absent from the SDK registry on both
16667
+ * chains. That is the case {@link resolveFeeChain} cannot recover — a
16668
+ * destination-denominated fee token resolves on neither the source registry
16669
+ * nor the source-bound adapter, leaving the amount as raw base units. When
16670
+ * the service omits `decimals` (optional during rollout), fall back to
16671
+ * inferring the fee token's chain and resolving via the registry/adapter.
16672
+ *
16673
+ * Like {@link formatTokenValue}, this never throws: fee display is cosmetic
16674
+ * and must not fail an estimate/swap. A malformed self-describing `decimals`
16675
+ * (e.g. a non-numeric `amount` or invalid decimal count that makes
16676
+ * {@link formatUnits} throw) falls through to chain-based resolution rather
16677
+ * than propagating out of {@link buildFormattedFees}.
16678
+ *
16679
+ * @param fee - The fee item from the service response.
16680
+ * @param chain - The source chain definition.
16681
+ * @param destinationChain - The destination chain definition.
16682
+ * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16683
+ * @returns Promise resolving to the formatted amount and display token.
16684
+ */ async formatFeeValue(fee, chain, destinationChain, adapter) {
16685
+ if (fee.decimals != null) {
16686
+ try {
16687
+ return {
16688
+ amount: formatUnits(fee.amount, fee.decimals),
16689
+ token: fee.symbol ?? fee.token
16690
+ };
16691
+ } catch {
16692
+ // Malformed service metadata — fall through to chain-based resolution,
16693
+ // which never throws (worst case: raw passthrough).
16694
+ }
16695
+ }
16696
+ const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
16697
+ return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16698
+ }
16699
+ /**
16206
16700
  * Format service fee items into the SDK's ServiceSwapFee structure.
16207
16701
  *
16208
16702
  * @remarks
@@ -16213,14 +16707,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16213
16707
  * - Raw passthrough only when both registry and adapter fail
16214
16708
  *
16215
16709
  * @param feeItems - Array of fee items from the service response.
16216
- * @param chain - The chain definition for token resolution and formatting.
16710
+ * @param chain - The source chain definition for token resolution and formatting.
16711
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16217
16712
  * @param type - The fee type to assign ('provider' or 'swap').
16218
16713
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16219
16714
  * @returns Promise resolving to formatted ServiceSwapFee array.
16220
- */ async formatServiceFees(feeItems, chain, type, adapter) {
16715
+ */ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
16221
16716
  if (!feeItems) return [];
16222
16717
  return Promise.all(feeItems.map(async (fee)=>{
16223
- const formatted = await formatTokenValue(fee.amount, fee.token, chain, adapter);
16718
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16224
16719
  return {
16225
16720
  token: formatted.token,
16226
16721
  amount: formatted.amount,
@@ -16232,16 +16727,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16232
16727
  * Format developer fee items into the SDK's ServiceSwapFee structure.
16233
16728
  *
16234
16729
  * @param feeItems - Array of developer fee items from the service response.
16235
- * @param chain - The chain definition for token resolution and formatting.
16730
+ * @param chain - The source chain definition for token resolution and formatting.
16731
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16236
16732
  * @param recipientAddress - The developer's fee recipient address from config.
16237
16733
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16238
16734
  * @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
16239
16735
  */ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
16240
16736
  if (!feeItems) return [];
16241
- const isCrossChainSwap = destinationChain.chain !== chain.chain;
16242
16737
  return Promise.all(feeItems.map(async (fee)=>{
16243
- const feeChain = !isCrossChainSwap && fee.basis === 'estimatedAmount' ? destinationChain : chain;
16244
- const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16738
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16245
16739
  return {
16246
16740
  token: formatted.token,
16247
16741
  amount: formatted.amount,
@@ -18636,22 +19130,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18636
19130
  try {
18637
19131
  // Step 1: Build quote params directly (no need for buildServiceParams)
18638
19132
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
19133
+ // The kit key is optional (permissionless mode); when absent the quote is
19134
+ // fetched without an Authorization header.
18639
19135
  const kitKey = params.config?.kitKey;
18640
- if (!kitKey) {
18641
- throw new KitError({
18642
- code: 1098,
18643
- name: 'INPUT_VALIDATION_FAILED',
18644
- type: 'INPUT',
18645
- recoverability: 'FATAL',
18646
- message: 'kitKey is required in config for callback-based fees',
18647
- cause: {
18648
- trace: {
18649
- operation: 'handleOutputFeeCallback',
18650
- params
18651
- }
18652
- }
18653
- });
18654
- }
18655
19136
  // Resolve token aliases to addresses for the quote API
18656
19137
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
18657
19138
  const chain = params.from.chain;
@@ -18676,7 +19157,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18676
19157
  ...params.config?.slippageBps !== undefined && {
18677
19158
  slippageBps: params.config.slippageBps
18678
19159
  },
18679
- apiKey: kitKey
19160
+ ...kitKey ? {
19161
+ apiKey: kitKey
19162
+ } : {}
18680
19163
  };
18681
19164
  // Step 2: Get quote from service
18682
19165
  const quoteResponse = await getQuote(quoteParams);
@@ -19118,7 +19601,9 @@ const sleep$1 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
19118
19601
  ...isCrossChain && {
19119
19602
  toChain: chainOut
19120
19603
  },
19121
- apiKey: params.kitKey
19604
+ ...params.kitKey ? {
19605
+ apiKey: params.kitKey
19606
+ } : {}
19122
19607
  };
19123
19608
  let raw = await getSwapStatus$1(request);
19124
19609
  // When the service hasn't finished indexing a just-submitted swap it
@@ -19258,7 +19743,9 @@ const isResultShape = (params)=>'result' in params;
19258
19743
  ...chainOut !== undefined && {
19259
19744
  chainOut
19260
19745
  },
19261
- kitKey: params.kitKey
19746
+ ...params.kitKey ? {
19747
+ kitKey: params.kitKey
19748
+ } : {}
19262
19749
  };
19263
19750
  const deadline = Date.now() + timeoutMs;
19264
19751
  let pollIndex = 0;
@@ -19419,7 +19906,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19419
19906
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
19420
19907
  return getTokenRates$1({
19421
19908
  chain,
19422
- apiKey: params.kitKey,
19909
+ ...params.kitKey ? {
19910
+ apiKey: params.kitKey
19911
+ } : {},
19423
19912
  ...resolvedAddresses !== undefined && {
19424
19913
  addresses: resolvedAddresses
19425
19914
  }
@@ -20416,7 +20905,7 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20416
20905
  };
20417
20906
 
20418
20907
  var name = "@circle-fin/earn-kit";
20419
- var version = "1.2.2";
20908
+ var version = "1.3.0";
20420
20909
  var pkg = {
20421
20910
  name: name,
20422
20911
  version: version};
@@ -20546,7 +21035,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20546
21035
  asset: zod.z.string(),
20547
21036
  assetAddress: zod.z.string(),
20548
21037
  lltv: zod.z.number(),
20549
- supplyUsd: zod.z.number()
21038
+ supplyUsd: zod.z.number(),
21039
+ // Optional during the expand/contract window (a backend that predates the
21040
+ // field omits the key), mirroring the `.optional()` facets on the base
21041
+ // schema; `null` when the product exposes no per-market allocation (V2).
21042
+ allocationPct: zod.z.number().nullable().optional()
20550
21043
  });
20551
21044
  /**
20552
21045
  * Zod schema for a Morpho vault warning in the API response.
@@ -20560,7 +21053,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20560
21053
  ])
20561
21054
  });
20562
21055
  /**
20563
- * Zod schema for a single vault info object in the API response.
21056
+ * Zod schema for the manager (curator) facet in the API response.
21057
+ *
21058
+ * @internal
21059
+ */ const managerSchema = zod.z.object({
21060
+ name: zod.z.string(),
21061
+ address: zod.z.string().optional(),
21062
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
21063
+ // are added here as the providers that emit them land, rather than shipped
21064
+ // speculatively.
21065
+ type: zod.z.enum([
21066
+ 'curator'
21067
+ ])
21068
+ });
21069
+ /**
21070
+ * Zod schema for the APY profile facet in the API response.
21071
+ *
21072
+ * @internal
21073
+ */ const apyProfileSchema = zod.z.object({
21074
+ current: zod.z.number(),
21075
+ native: zod.z.number().nullable(),
21076
+ d7: zod.z.number().nullable(),
21077
+ d30: zod.z.number().nullable(),
21078
+ d90: zod.z.number().nullable(),
21079
+ rewardShare: zod.z.number().nullable(),
21080
+ source: zod.z.string().optional(),
21081
+ asOf: zod.z.string().optional()
21082
+ });
21083
+ /**
21084
+ * Zod schema for the fee split facet in the API response.
21085
+ *
21086
+ * @internal
21087
+ */ const feeInfoSchema = zod.z.object({
21088
+ performance: zod.z.number().nullable(),
21089
+ management: zod.z.number().nullable()
21090
+ });
21091
+ /**
21092
+ * Zod schema for the liquidity profile facet in the API response.
21093
+ *
21094
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
21095
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
21096
+ *
21097
+ * @internal
21098
+ */ const liquidityProfileSchema = zod.z.object({
21099
+ totalDeposits: amountJsonSchema,
21100
+ available: amountJsonSchema,
21101
+ totalSupply: amountJsonSchema,
21102
+ status: zod.z.enum([
21103
+ 'active',
21104
+ 'low_liquidity'
21105
+ ])
21106
+ });
21107
+ /**
21108
+ * Zod schema for the risk signals facet in the API response.
21109
+ *
21110
+ * @internal
21111
+ */ const riskSignalsSchema = zod.z.object({
21112
+ circleSentinel: zod.z.boolean(),
21113
+ warnings: zod.z.array(vaultWarningSchema).optional(),
21114
+ earnKitWarnings: zod.z.array(zod.z.string()).optional()
21115
+ });
21116
+ /**
21117
+ * Zod schema for the universal earn-opportunity base in the API response.
21118
+ *
21119
+ * Retains every existing deprecated flat field (kept validated through the
21120
+ * expand/contract window so default-strip does not drop them) and adds the
21121
+ * new nested facets. The nested facets are `.optional()` during the
21122
+ * transition so the SDK still validates against a not-yet-fully-deployed
21123
+ * backend; they become required after Expand ships.
20564
21124
  *
20565
21125
  * @internal
20566
21126
  */ const vaultInfoResponseSchema = zod.z.object({
@@ -20585,6 +21145,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20585
21145
  warnings: zod.z.array(vaultWarningSchema).optional(),
20586
21146
  earnKitWarnings: zod.z.array(zod.z.string()).optional()
20587
21147
  });
21148
+ /**
21149
+ * Shared base schema: existing flat fields (kept) plus the new nested
21150
+ * facets and neutral identity. Facets are `.optional()` during the
21151
+ * transition; flip to required once the backend is confirmed emitting.
21152
+ *
21153
+ * @internal
21154
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
21155
+ address: zod.z.string().optional(),
21156
+ asOf: zod.z.string().optional(),
21157
+ manager: managerSchema.nullable().optional(),
21158
+ apyProfile: apyProfileSchema.optional(),
21159
+ fee: feeInfoSchema.optional(),
21160
+ liquidityProfile: liquidityProfileSchema.optional(),
21161
+ riskSignals: riskSignalsSchema.optional()
21162
+ });
21163
+ /**
21164
+ * Zod schema for the `vault` opportunity variant.
21165
+ *
21166
+ * @internal
21167
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
21168
+ productType: zod.z.literal('vault'),
21169
+ collateral: zod.z.array(collateralSchema)
21170
+ });
21171
+ /**
21172
+ * Discriminated union over `productType`. Add union members here as new
21173
+ * product types (e.g. `lending_market`, `rwa_token`) land.
21174
+ *
21175
+ * @internal
21176
+ */ const earnOpportunityVariants = [
21177
+ vaultOpportunitySchema
21178
+ ];
21179
+ /** @internal */ const earnOpportunitySchema = zod.z.discriminatedUnion('productType', earnOpportunityVariants);
21180
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
21181
+ /**
21182
+ * Tolerant list parser for earn opportunities.
21183
+ *
21184
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
21185
+ * `z.array` fails the whole array if any element fails. Two migration-window
21186
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
21187
+ *
21188
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
21189
+ * only opportunity type then, so default a missing discriminant to `'vault'`
21190
+ * rather than dropping every vault the backend returns.
21191
+ * - A future backend adds a *second* `productType` this SDK version does not
21192
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
21193
+ * of rejecting the whole list.
21194
+ *
21195
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
21196
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
21197
+ * primitives, or an object whose `productType` is malformed — is passed through
21198
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
21199
+ * validation failure. It is deliberately not silently dropped (which would hide
21200
+ * malformed backend data) and never throws here (an unguarded property read on
21201
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
21202
+ * `ZodError`).
21203
+ *
21204
+ * @internal
21205
+ */ const earnOpportunityListSchema = zod.z.preprocess((raw)=>{
21206
+ if (!Array.isArray(raw)) {
21207
+ return raw;
21208
+ }
21209
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
21210
+ // map/filter chain stays type-safe and no `any` leaks into the return.
21211
+ const entries = raw;
21212
+ return entries.map((entry)=>{
21213
+ // Only touch plain objects; non-objects fall through to fail validation.
21214
+ if (typeof entry !== 'object' || entry === null) {
21215
+ return entry;
21216
+ }
21217
+ const record = entry;
21218
+ // Older backend predating productType: default to the only type then.
21219
+ return record.productType === undefined ? {
21220
+ ...record,
21221
+ productType: 'vault'
21222
+ } : record;
21223
+ }).filter((entry)=>{
21224
+ // Drop ONLY a present-but-unknown string discriminant (a future
21225
+ // productType this SDK version doesn't know). Everything else —
21226
+ // non-objects, a non-string productType — flows through to
21227
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
21228
+ if (typeof entry !== 'object' || entry === null) {
21229
+ return true;
21230
+ }
21231
+ const productType = entry.productType;
21232
+ if (typeof productType !== 'string') {
21233
+ return true;
21234
+ }
21235
+ return knownProductTypes.has(productType);
21236
+ });
21237
+ }, zod.z.array(earnOpportunitySchema));
20588
21238
  // ---------------------------------------------------------------------------
20589
21239
  // Position response schema
20590
21240
  // ---------------------------------------------------------------------------
@@ -20714,6 +21364,7 @@ const positionPnlSchema = zod.z.discriminatedUnion('status', [
20714
21364
  *
20715
21365
  * @internal
20716
21366
  */ const depositPayloadSchema = zod.z.object({
21367
+ execId: bridgeDepositExecIdSchema,
20717
21368
  executionParams: depositExecutionParamsSchema,
20718
21369
  signature: hexSignatureSchema
20719
21370
  });
@@ -20805,6 +21456,21 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20805
21456
  amount: amountJsonSchema,
20806
21457
  vaultAddress: hexAddressSchema
20807
21458
  }).passthrough();
21459
+ /** @internal */ const bridgeQuoteExpirySchema = zod.z.discriminatedUnion('mode', [
21460
+ zod.z.object({
21461
+ mode: zod.z.literal('TIMESTAMP'),
21462
+ expiresAt: zod.z.string().datetime({
21463
+ offset: true
21464
+ })
21465
+ }),
21466
+ zod.z.object({
21467
+ mode: zod.z.literal('BLOCK_NUMBER'),
21468
+ expiresAtBlock: zod.z.number().int(),
21469
+ blockEstimatedAt: zod.z.string().datetime({
21470
+ offset: true
21471
+ }).optional()
21472
+ })
21473
+ ]).optional().catch(undefined);
20808
21474
  /**
20809
21475
  * Zod schema for the bridge deposit prepare payload.
20810
21476
  *
@@ -20816,6 +21482,10 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20816
21482
  execId: bridgeDepositExecIdSchema,
20817
21483
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
20818
21484
  expiresAt: zod.z.string().datetime(),
21485
+ quoteIssuedAt: zod.z.string().datetime({
21486
+ offset: true
21487
+ }).optional().catch(undefined),
21488
+ quoteExpiry: bridgeQuoteExpirySchema,
20819
21489
  review: bridgeDepositPrepareReviewSchema
20820
21490
  });
20821
21491
  /**
@@ -20881,6 +21551,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20881
21551
  *
20882
21552
  * @internal
20883
21553
  */ const withdrawPayloadSchema = zod.z.object({
21554
+ execId: bridgeDepositExecIdSchema,
20884
21555
  executionParams: withdrawExecutionParamsSchema,
20885
21556
  signature: hexSignatureSchema
20886
21557
  });
@@ -20894,6 +21565,27 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20894
21565
  data: withdrawPayloadSchema
20895
21566
  });
20896
21567
  // ---------------------------------------------------------------------------
21568
+ // Transaction report response schema
21569
+ // ---------------------------------------------------------------------------
21570
+ /**
21571
+ * Zod schema for the transaction report payload inside the API `data` envelope.
21572
+ *
21573
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
21574
+ * schema accepts any object shape and does not require specific fields.
21575
+ *
21576
+ * @internal
21577
+ */ const transactionReportPayloadSchema = zod.z.object({}).passthrough();
21578
+ /**
21579
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
21580
+ *
21581
+ * The Earn Service API wraps the transaction report payload in a `data`
21582
+ * envelope.
21583
+ *
21584
+ * @internal
21585
+ */ zod.z.object({
21586
+ data: transactionReportPayloadSchema
21587
+ });
21588
+ // ---------------------------------------------------------------------------
20897
21589
  // Claim rewards response schema
20898
21590
  // ---------------------------------------------------------------------------
20899
21591
  /**
@@ -20954,6 +21646,30 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20954
21646
  token: zod.z.string(),
20955
21647
  amount: amountJsonSchema
20956
21648
  });
21649
+ /**
21650
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
21651
+ *
21652
+ * The Earn Service backend estimates gas server-side and returns one entry per
21653
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
21654
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
21655
+ * integer string in the chain's native base units. When the backend cannot
21656
+ * estimate an action it returns `fees: null` with an `error` message instead.
21657
+ *
21658
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
21659
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
21660
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
21661
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
21662
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
21663
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
21664
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
21665
+ * `fee`) must never fail Zod validation and reject the entire quote.
21666
+ *
21667
+ * @internal
21668
+ */ const quoteGasFeeSchema = zod.z.object({
21669
+ name: zod.z.string().optional(),
21670
+ fees: zod.z.unknown(),
21671
+ error: zod.z.string().optional()
21672
+ }).passthrough();
20957
21673
  /**
20958
21674
  * Zod schema for the inner deposit quote payload.
20959
21675
  *
@@ -20969,7 +21685,8 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20969
21685
  expectedShares: amountJsonSchema,
20970
21686
  sharePrice: zod.z.string(),
20971
21687
  currentApy: zod.z.number(),
20972
- fees: zod.z.array(feeSchema).optional()
21688
+ fees: zod.z.array(feeSchema).optional(),
21689
+ gasFees: zod.z.array(quoteGasFeeSchema).optional()
20973
21690
  });
20974
21691
  /**
20975
21692
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -20996,6 +21713,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
20996
21713
  sharePrice: zod.z.string(),
20997
21714
  maxWithdrawable: amountJsonSchema,
20998
21715
  fees: zod.z.array(feeSchema),
21716
+ gasFees: zod.z.array(quoteGasFeeSchema).optional(),
20999
21717
  warnings: zod.z.array(zod.z.string()).optional()
21000
21718
  });
21001
21719
  /**
@@ -21053,7 +21771,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
21053
21771
  *
21054
21772
  * @internal
21055
21773
  */ const getVaultsPayloadSchema = zod.z.object({
21056
- vaults: zod.z.array(vaultInfoResponseSchema),
21774
+ vaults: earnOpportunityListSchema,
21057
21775
  errors: zod.z.array(vaultErrorSchema)
21058
21776
  });
21059
21777
  /**
@@ -21083,7 +21801,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
21083
21801
  *
21084
21802
  * @internal
21085
21803
  */ const exploreVaultsPayloadSchema = zod.z.object({
21086
- vaults: zod.z.array(vaultInfoResponseSchema),
21804
+ vaults: earnOpportunityListSchema,
21087
21805
  pagination: explorePaginationSchema
21088
21806
  });
21089
21807
  /**