@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.mjs CHANGED
@@ -18,13 +18,14 @@
18
18
 
19
19
  import { z } from 'zod';
20
20
  import 'pino';
21
+ import { hexlify, hexZeroPad } from '@ethersproject/bytes';
22
+ import '@ethersproject/abi';
23
+ import { getAddress } from '@ethersproject/address';
21
24
  import { PublicKey } from '@solana/web3.js';
22
25
  import 'bn.js';
23
26
  import '@coral-xyz/anchor';
24
27
  import bs58 from 'bs58';
25
28
  import '@noble/curves/ed25519';
26
- import { hexlify, hexZeroPad } from '@ethersproject/bytes';
27
- import { getAddress } from '@ethersproject/address';
28
29
  import { formatUnits as formatUnits$1 } from '@ethersproject/units';
29
30
  import { keccak256 } from '@ethersproject/keccak256';
30
31
 
@@ -3527,7 +3528,10 @@ var EarnChain;
3527
3528
  contracts: {
3528
3529
  v1: {
3529
3530
  wallet: GATEWAY_WALLET_EVM_TESTNET,
3530
- minter: GATEWAY_MINTER_EVM_TESTNET
3531
+ minter: GATEWAY_MINTER_EVM_TESTNET,
3532
+ // DepositForHandler the GenericExecutor calls to run a fast cross-chain
3533
+ // deposit into the GatewayWallet above.
3534
+ depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
3531
3535
  }
3532
3536
  },
3533
3537
  forwarderSupported: {
@@ -6595,7 +6599,10 @@ var Chains = /*#__PURE__*/Object.freeze({
6595
6599
  minter: z.string({
6596
6600
  required_error: 'Gateway minter address is required. Please provide a valid contract address.',
6597
6601
  invalid_type_error: 'Gateway minter address must be a string.'
6598
- }).min(1, 'Gateway minter address cannot be empty.')
6602
+ }).min(1, 'Gateway minter address cannot be empty.'),
6603
+ depositForHandler: z.string({
6604
+ invalid_type_error: 'Gateway depositForHandler address must be a string.'
6605
+ }).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
6599
6606
  }).strict() // Reject any additional properties not defined in the schema
6600
6607
  ;
6601
6608
  /**
@@ -9480,6 +9487,13 @@ const swapTokenEnumSchema = z.enum([
9480
9487
  return explorerUrl;
9481
9488
  }
9482
9489
 
9490
+ /**
9491
+ * CCTP forwarding magic bytes prefix.
9492
+ *
9493
+ * The ASCII string "cctp-forward" (12 bytes) that identifies a forwarding request.
9494
+ * This prefix is right-padded to 24 bytes in the final hookData.
9495
+ */ const CCTP_FORWARD_MAGIC_PREFIX = 'cctp-forward';
9496
+
9483
9497
  /**
9484
9498
  * Project an arbitrary payload onto the exact set of fields the telemetry
9485
9499
  * endpoint accepts.
@@ -9856,7 +9870,7 @@ const swapTokenEnumSchema = z.enum([
9856
9870
  }
9857
9871
 
9858
9872
  var name$2 = "@circle-fin/bridge-kit";
9859
- var version$2 = "1.12.0";
9873
+ var version$2 = "1.12.1";
9860
9874
  var pkg$2 = {
9861
9875
  name: name$2,
9862
9876
  version: version$2};
@@ -10714,6 +10728,11 @@ var TransferSpeed;
10714
10728
  clock: z.any().optional()
10715
10729
  }).passthrough();
10716
10730
 
10731
+ /**
10732
+ * The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
10733
+ * hookData must start with.
10734
+ */ Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
10735
+
10717
10736
  /**
10718
10737
  * The minimum finality threshold for CCTPv2 transfers.
10719
10738
  *
@@ -10746,7 +10765,7 @@ var TransferSpeed;
10746
10765
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
10747
10766
 
10748
10767
  var name$1 = "@circle-fin/swap-kit";
10749
- var version$1 = "1.3.2";
10768
+ var version$1 = "1.4.0";
10750
10769
  var pkg$1 = {
10751
10770
  name: name$1,
10752
10771
  version: version$1};
@@ -10811,7 +10830,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
10811
10830
  }).min(1, 'kitKey must be a non-empty string').optional(),
10812
10831
  provider: z.string({
10813
10832
  invalid_type_error: 'provider must be a string'
10814
- }).min(1, 'provider must be a non-empty string').optional()
10833
+ }).min(1, 'provider must be a non-empty string').optional(),
10834
+ batchTransactions: z.boolean({
10835
+ invalid_type_error: 'batchTransactions must be a boolean'
10836
+ }).optional()
10815
10837
  });
10816
10838
  /**
10817
10839
  * Zod schema for adapter context.
@@ -11342,7 +11364,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11342
11364
  /**
11343
11365
  * Circle Stablecoin Service API Key.
11344
11366
  * Must be a valid API key format.
11345
- */ apiKey: apiKeySchema
11367
+ */ apiKey: apiKeySchema.optional()
11346
11368
  }).superRefine(requireCrossChainQuoteToAddress);
11347
11369
  /**
11348
11370
  * Zod schema for validating CreateSwapRequest parameters.
@@ -11400,7 +11422,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11400
11422
  /**
11401
11423
  * Circle Stablecoin Service API Key.
11402
11424
  * Must be a valid API key format.
11403
- */ apiKey: apiKeySchema
11425
+ */ apiKey: apiKeySchema.optional()
11404
11426
  });
11405
11427
  /**
11406
11428
  * Zod schema for validating GetSwapStatusResponse data.
@@ -11436,7 +11458,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11436
11458
  toChain: z.string({
11437
11459
  invalid_type_error: 'toChain must be a string'
11438
11460
  }).min(1, 'toChain must be a non-empty string if provided').optional(),
11439
- apiKey: apiKeySchema
11461
+ apiKey: apiKeySchema.optional()
11440
11462
  });
11441
11463
  /**
11442
11464
  * Zod schema for validating CreateSwapResponse payloads.
@@ -11445,13 +11467,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11445
11467
  required_error: 'fee token is required',
11446
11468
  invalid_type_error: 'fee token must be a string'
11447
11469
  }).min(1, 'fee token must be a non-empty string'),
11448
- amount: feeAmountSchema
11470
+ amount: feeAmountSchema,
11471
+ decimals: z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
11472
+ symbol: z.string({
11473
+ invalid_type_error: 'fee token symbol must be a string'
11474
+ }).min(1, 'fee token symbol must be a non-empty string').optional()
11449
11475
  });
11450
11476
  /**
11451
11477
  * Developer fee item schema with basis field.
11452
- */ const createSwapDeveloperFeeItemSchema = z.object({
11453
- token: z.string().min(1, 'fee token must be a non-empty string'),
11454
- amount: feeAmountSchema,
11478
+ */ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
11455
11479
  basis: z.enum([
11456
11480
  'inputAmount',
11457
11481
  'estimatedAmount'
@@ -11543,7 +11567,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11543
11567
  addresses: z.array(z.string({
11544
11568
  invalid_type_error: 'addresses entries must be strings'
11545
11569
  }).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(),
11546
- apiKey: apiKeySchema
11570
+ apiKey: apiKeySchema.optional()
11547
11571
  });
11548
11572
  /**
11549
11573
  * Zod schema for validating GetTokenRatesResponse payloads.
@@ -11721,7 +11745,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11721
11745
  ...DEFAULT_CONFIG,
11722
11746
  headers: {
11723
11747
  ...DEFAULT_CONFIG.headers,
11724
- Authorization: `Bearer ${apiKey}`
11748
+ // Permissionless mode: no Authorization header when the kit key is absent.
11749
+ ...apiKey !== undefined && {
11750
+ Authorization: `Bearer ${apiKey}`
11751
+ }
11725
11752
  }
11726
11753
  };
11727
11754
  try {
@@ -11875,7 +11902,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11875
11902
  ...DEFAULT_CONFIG,
11876
11903
  headers: {
11877
11904
  ...DEFAULT_CONFIG.headers,
11878
- Authorization: `Bearer ${validatedParams.apiKey}`
11905
+ // Permissionless mode: no Authorization header when the kit key is absent.
11906
+ ...validatedParams.apiKey !== undefined && {
11907
+ Authorization: `Bearer ${validatedParams.apiKey}`
11908
+ }
11879
11909
  }
11880
11910
  };
11881
11911
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
@@ -11930,7 +11960,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11930
11960
  const validatedParams = {
11931
11961
  txHash: result.data.txHash,
11932
11962
  chain: result.data.chain,
11933
- apiKey: result.data.apiKey,
11963
+ ...result.data.apiKey !== undefined && {
11964
+ apiKey: result.data.apiKey
11965
+ },
11934
11966
  ...result.data.toChain !== undefined && {
11935
11967
  toChain: result.data.toChain
11936
11968
  }
@@ -11940,7 +11972,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
11940
11972
  ...DEFAULT_CONFIG,
11941
11973
  headers: {
11942
11974
  ...DEFAULT_CONFIG.headers,
11943
- Authorization: `Bearer ${validatedParams.apiKey}`
11975
+ // Permissionless mode: no Authorization header when the kit key is absent.
11976
+ ...validatedParams.apiKey !== undefined && {
11977
+ Authorization: `Bearer ${validatedParams.apiKey}`
11978
+ }
11944
11979
  }
11945
11980
  };
11946
11981
  return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
@@ -12029,7 +12064,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12029
12064
  }
12030
12065
  const validatedParams = {
12031
12066
  chain: result.data.chain,
12032
- apiKey: result.data.apiKey,
12067
+ ...result.data.apiKey !== undefined && {
12068
+ apiKey: result.data.apiKey
12069
+ },
12033
12070
  ...result.data.addresses !== undefined && {
12034
12071
  addresses: result.data.addresses
12035
12072
  }
@@ -12039,7 +12076,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
12039
12076
  ...DEFAULT_CONFIG,
12040
12077
  headers: {
12041
12078
  ...DEFAULT_CONFIG.headers,
12042
- Authorization: `Bearer ${validatedParams.apiKey}`
12079
+ // Permissionless mode: no Authorization header when the kit key is absent.
12080
+ ...validatedParams.apiKey !== undefined && {
12081
+ Authorization: `Bearer ${validatedParams.apiKey}`
12082
+ }
12043
12083
  }
12044
12084
  };
12045
12085
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
@@ -12849,6 +12889,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
12849
12889
  */ function hasSignTypedData(adapter) {
12850
12890
  return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
12851
12891
  }
12892
+ /**
12893
+ * Type guard to check if an adapter can actually produce an EIP-712
12894
+ * typed-data signature.
12895
+ *
12896
+ * @remarks
12897
+ * Strengthens {@link hasSignTypedData}: having a `signTypedData` method
12898
+ * does not guarantee it can succeed. Adapters whose signer is delegated
12899
+ * (e.g. through a signing strategy backed by a smart contract account)
12900
+ * expose the method but reject typed-data payloads at runtime. Such
12901
+ * adapters report their real capability through an optional
12902
+ * `supportsSignTypedData()` method, which this guard consults when
12903
+ * present. Adapters without the capability method are assumed able to
12904
+ * sign, preserving the previous duck-typing behavior.
12905
+ *
12906
+ * @param adapter - The adapter to check
12907
+ * @returns True if calling `signTypedData` can be expected to succeed
12908
+ *
12909
+ * @example
12910
+ * ```typescript
12911
+ * import { canSignTypedData } from '@core/adapter-evm'
12912
+ *
12913
+ * if (canSignTypedData(adapter)) {
12914
+ * const signature = await adapter.signTypedData(typedData, context)
12915
+ * } else {
12916
+ * // take an on-chain approval path instead of a permit signature
12917
+ * }
12918
+ * ```
12919
+ */ function canSignTypedData(adapter) {
12920
+ if (!hasSignTypedData(adapter)) {
12921
+ return false;
12922
+ }
12923
+ if (typeof adapter.supportsSignTypedData === 'function') {
12924
+ // The value is `boolean` per the interface, but a plain-JS adapter may
12925
+ // return anything; treat it as untrusted and coerce to a strict
12926
+ // boolean. Comparing an `unknown` (not a `boolean`) also keeps the
12927
+ // lint autofix from stripping this as a redundant `=== true`.
12928
+ const supported = adapter.supportsSignTypedData();
12929
+ return supported === true;
12930
+ }
12931
+ return true;
12932
+ }
12852
12933
 
12853
12934
  /**
12854
12935
  * Build EIP-2612 typed data for permit signing.
@@ -13271,10 +13352,13 @@ enc.encode('used_transfer_spec_hash');
13271
13352
  * at usage time rather than construction time.
13272
13353
  *
13273
13354
  * Validates:
13274
- * - Kit key is present and matches required format (KIT_KEY:id:secret)
13355
+ * - Kit key matches the required format (KIT_KEY:id:secret) when provided.
13356
+ * An absent or empty kit key is permitted (permissionless mode) — the swap
13357
+ * service now treats the key as optional.
13275
13358
  *
13276
- * @param kitKey - The inline kit key from the swap operation config
13277
- * @throws KitError with VALIDATION_FAILED if kit key is invalid or missing
13359
+ * @param kitKey - The inline kit key from the swap operation config (optional)
13360
+ * @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
13361
+ * match the KIT_KEY:<keyId>:<keySecret> format
13278
13362
  *
13279
13363
  * @example
13280
13364
  * ```typescript
@@ -13286,9 +13370,11 @@ enc.encode('used_transfer_spec_hash');
13286
13370
  * assertKitKey(kitKey)
13287
13371
  * ```
13288
13372
  */ function assertKitKey(kitKey) {
13289
- // Validate API key format using existing schema from service-client
13373
+ // Permissionless mode: the swap service treats the kit key as optional, so an
13374
+ // absent (or empty) key is valid. Only validate the format when a key is
13375
+ // actually provided.
13290
13376
  if (!kitKey) {
13291
- 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');
13377
+ return;
13292
13378
  }
13293
13379
  const apiKeyResult = apiKeySchema.safeParse(kitKey);
13294
13380
  if (!apiKeyResult.success) {
@@ -13585,8 +13671,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13585
13671
  validateResolvedAddress(resolvedTokenInAddress, chain);
13586
13672
  validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
13587
13673
  validateResolvedAddress(to, destinationChain);
13588
- const kitKey = config?.kitKey ?? '';
13589
- // Validates the kit key
13674
+ const kitKey = config?.kitKey;
13675
+ // Validate the kit key format when one is provided (permissionless otherwise).
13590
13676
  assertKitKey(kitKey);
13591
13677
  // Validate custom fee configuration if present
13592
13678
  const customFee = config?.customFee;
@@ -13631,7 +13717,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
13631
13717
  }
13632
13718
  }
13633
13719
  },
13634
- apiKey: kitKey
13720
+ // Map kitKey → apiKey for the service client; omitted in permissionless mode.
13721
+ ...kitKey ? {
13722
+ apiKey: kitKey
13723
+ } : {}
13635
13724
  };
13636
13725
  }
13637
13726
 
@@ -14285,6 +14374,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14285
14374
  }
14286
14375
  }
14287
14376
 
14377
+ /**
14378
+ * Determine whether an adapter can produce an EIP-2612 permit signature.
14379
+ *
14380
+ * @remarks
14381
+ * A gasless permit needs two adapter capabilities: fetching the token's
14382
+ * EIP-2612 nonce and producing an EIP-712 typed-data signature. The
14383
+ * typed-data check uses {@link canSignTypedData} rather than a bare
14384
+ * `hasSignTypedData` guard so that an adapter routed through a signing
14385
+ * strategy that cannot produce typed-data signatures — one whose manifest
14386
+ * omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
14387
+ * — is correctly excluded. Such an adapter falls back to an on-chain approval
14388
+ * (batched into a single submission when it supports atomic execution) instead
14389
+ * of attempting a permit its strategy would reject.
14390
+ *
14391
+ * @param adapter - The source adapter to inspect.
14392
+ * @returns `true` when the adapter can both fetch a nonce and sign typed data.
14393
+ *
14394
+ * @example
14395
+ * ```typescript
14396
+ * import { adapterSupportsPermit } from './utils'
14397
+ *
14398
+ * if (adapterSupportsPermit(adapter)) {
14399
+ * // gasless permit path — fold the approval into the swap transaction
14400
+ * } else {
14401
+ * // on-chain approval path (batched when supportsAtomicBatch is true)
14402
+ * }
14403
+ * ```
14404
+ */ function adapterSupportsPermit(adapter) {
14405
+ return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
14406
+ }
14407
+
14288
14408
  /**
14289
14409
  * Generate EIP-2612 permit signature for token approval.
14290
14410
  *
@@ -14420,8 +14540,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
14420
14540
  }
14421
14541
  // Skip permit generation if the adapter lacks the required capabilities.
14422
14542
  // handleEvmTokenApproval will have already sent an on-chain approval in this case.
14423
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
14424
- if (!adapterSupportsPermit) {
14543
+ if (!adapterSupportsPermit(adapter)) {
14425
14544
  return [
14426
14545
  createFallbackTokenInput(tokenInAddress, inputAmount)
14427
14546
  ];
@@ -14980,6 +15099,65 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
14980
15099
  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.`;
14981
15100
  }
14982
15101
 
15102
+ /**
15103
+ * Determine which chain a fee token should be resolved and formatted against.
15104
+ *
15105
+ * @remarks
15106
+ * Fees returned by the service may be denominated in either the input token
15107
+ * (on the source chain) or the output token (on the destination chain). A
15108
+ * contract address only resolves on the chain it belongs to, so formatting a
15109
+ * destination-denominated fee against the source chain causes
15110
+ * {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
15111
+ * units (e.g. a cross-chain swap charging a fee in the destination output
15112
+ * token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
15113
+ * resolved against the source chain). This is the fallback for fee items that
15114
+ * are not self-described with their own `decimals`/`chain`.
15115
+ *
15116
+ * Prefer the source chain (covers same-chain swaps and input-denominated
15117
+ * fees), then fall back to the destination chain when the token only resolves
15118
+ * there. When neither chain recognises the token, default to the source chain
15119
+ * so existing on-chain decimal lookups via the source adapter still apply.
15120
+ *
15121
+ * Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
15122
+ * source-first preference keeps them on the source chain. That is correct for
15123
+ * registry stablecoins, and for `'NATIVE'` only when both chains share native
15124
+ * decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
15125
+ * `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
15126
+ * mis-scaled. This is latent — providers emit the address form, and
15127
+ * self-describing fee items carry their own `decimals` and never reach this
15128
+ * helper — so the gap only opens for a future `'NATIVE'` fee that arrives
15129
+ * without `decimals` on a cross-native-decimal route.
15130
+ *
15131
+ * @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
15132
+ * @param sourceChain - The chain the swap originates from.
15133
+ * @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
15134
+ * @returns The chain definition the fee token should be resolved against.
15135
+ *
15136
+ * @example
15137
+ * ```typescript
15138
+ * import { resolveFeeChain } from './resolveFeeChain'
15139
+ * import { Ethereum, Base } from '@core/chains'
15140
+ *
15141
+ * // Cross-chain swap fee charged in the destination (output) token
15142
+ * resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
15143
+ * // => Base (EURC resolves on Base, not Ethereum)
15144
+ *
15145
+ * // Symbol or source-token fees stay on the source chain
15146
+ * resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
15147
+ * ```
15148
+ */ function resolveFeeChain(token, sourceChain, destinationChain) {
15149
+ if (sourceChain.chain === destinationChain.chain) {
15150
+ return sourceChain;
15151
+ }
15152
+ if (resolveTokenSymbol(token, sourceChain) !== null) {
15153
+ return sourceChain;
15154
+ }
15155
+ if (resolveTokenSymbol(token, destinationChain) !== null) {
15156
+ return destinationChain;
15157
+ }
15158
+ return sourceChain;
15159
+ }
15160
+
14983
15161
  const TOKEN_REGISTRY$1 = createTokenRegistry();
14984
15162
  /**
14985
15163
  * Format a raw base-unit amount into a human-readable decimal string.
@@ -15070,6 +15248,186 @@ const TOKEN_REGISTRY$1 = createTokenRegistry();
15070
15248
  }
15071
15249
  }
15072
15250
 
15251
+ /**
15252
+ * Runtime guard for {@link BatchCapableSwapAdapter}.
15253
+ *
15254
+ * @param adapter - The adapter to inspect.
15255
+ * @returns `true` when the adapter exposes both batch methods.
15256
+ *
15257
+ * @example
15258
+ * ```typescript
15259
+ * if (isBatchCapableSwapAdapter(adapter)) {
15260
+ * // adapter.supportsAtomicBatch / adapter.batchExecute are available
15261
+ * }
15262
+ * ```
15263
+ */ function isBatchCapableSwapAdapter(adapter) {
15264
+ return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
15265
+ }
15266
+ /**
15267
+ * Decide whether the EVM swap should take the batched approve-and-swap path.
15268
+ *
15269
+ * @remarks
15270
+ * Batching only helps when an on-chain approval would otherwise be required, so
15271
+ * it is skipped for native tokens (no approval) and for the gasless permit path
15272
+ * (already a single transaction). USDT is skipped because its reset-to-zero
15273
+ * allowance flow cannot be expressed as a fixed approve+swap pair. When those
15274
+ * gates pass, the adapter's actual atomic-batch capability is queried; any
15275
+ * failure resolves to `false` so the swap falls back to the sequential path.
15276
+ *
15277
+ * @param args - The decision inputs.
15278
+ * @param args.adapter - The source adapter.
15279
+ * @param args.chain - The source chain definition.
15280
+ * @param args.tokenInAddress - The resolved input-token address.
15281
+ * @param args.allowanceStrategy - Optional allowance strategy override.
15282
+ * @param args.batchTransactions - Optional explicit opt-out (`false` disables).
15283
+ * @returns `true` when the batched approve-and-swap path should be used.
15284
+ *
15285
+ * @example
15286
+ * ```typescript
15287
+ * const useBatched = await shouldUseBatchedSwap({
15288
+ * adapter,
15289
+ * chain,
15290
+ * tokenInAddress: '0xA0b8...',
15291
+ * allowanceStrategy: config?.allowanceStrategy,
15292
+ * batchTransactions: config?.batchTransactions,
15293
+ * })
15294
+ * ```
15295
+ */ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
15296
+ // Explicit opt-out.
15297
+ if (batchTransactions === false) {
15298
+ return false;
15299
+ }
15300
+ // Batching is an EVM capability (EIP-5792 or a signing strategy).
15301
+ if (chain.type !== 'evm') {
15302
+ return false;
15303
+ }
15304
+ // Native tokens need no approval — the swap is already a single transaction.
15305
+ if (isNativeEvmAddress(tokenInAddress)) {
15306
+ return false;
15307
+ }
15308
+ // A gasless permit folds the approval into the swap transaction, so there is
15309
+ // nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
15310
+ const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
15311
+ if (canUsePermit) {
15312
+ return false;
15313
+ }
15314
+ // USDT's reset-to-zero allowance dance cannot be expressed as a fixed
15315
+ // approve+swap pair; leave it on the sequential path.
15316
+ const usdt = chain.usdtAddress?.toLowerCase();
15317
+ if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
15318
+ return false;
15319
+ }
15320
+ if (!isBatchCapableSwapAdapter(adapter)) {
15321
+ return false;
15322
+ }
15323
+ try {
15324
+ return await adapter.supportsAtomicBatch(chain);
15325
+ } catch {
15326
+ return false;
15327
+ }
15328
+ }
15329
+ /**
15330
+ * Execute the approval and swap as a single atomic batch.
15331
+ *
15332
+ * @remarks
15333
+ * Extracts the raw call data from both prepared requests, submits them as one
15334
+ * batch via `adapter.batchExecute`, and maps the swap receipt back to a
15335
+ * transaction hash. The `fromAddress` is threaded for adapters routed through a
15336
+ * signing strategy (which have no wallet account to read the sender from); the
15337
+ * wallet-client path ignores it.
15338
+ *
15339
+ * Following the batch contract, `batchExecute` never throws once the batch is
15340
+ * submitted — a missing or failed swap receipt is surfaced here as a thrown
15341
+ * {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
15342
+ * batch and double-swap.
15343
+ *
15344
+ * @param args - The execution inputs.
15345
+ * @param args.adapter - The batch-capable source adapter.
15346
+ * @param args.chain - The EVM chain to execute on.
15347
+ * @param args.approveRequest - The prepared ERC-20 approval request.
15348
+ * @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
15349
+ * @param args.fromAddress - The address authorizing the batch.
15350
+ * @returns The swap transaction hash and the executed approval + swap records.
15351
+ * @throws {@link KitError} when the prepared requests cannot yield call data.
15352
+ * @throws {@link KitError} when the batch does not confirm or the swap reverts.
15353
+ *
15354
+ * @example
15355
+ * ```typescript
15356
+ * const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
15357
+ * adapter,
15358
+ * chain,
15359
+ * approveRequest,
15360
+ * swapRequest,
15361
+ * fromAddress: '0x742d...',
15362
+ * })
15363
+ * ```
15364
+ */ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
15365
+ if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
15366
+ throw new KitError({
15367
+ ...InputError.UNSUPPORTED_ACTION,
15368
+ recoverability: 'FATAL',
15369
+ message: 'Batched swap requires EVM prepared requests with getCallData() support.'
15370
+ });
15371
+ }
15372
+ const approveCallData = approveRequest.getCallData();
15373
+ const swapCallData = swapRequest.getCallData();
15374
+ const batchResult = await adapter.batchExecute([
15375
+ approveCallData,
15376
+ swapCallData
15377
+ ], chain, {
15378
+ fromAddress
15379
+ });
15380
+ const swapReceipt = batchResult.receipts[1];
15381
+ // A missing swap receipt means the batch never confirmed (polling timed out
15382
+ // or the wallet returned fewer receipts than calls). Re-throw the underlying
15383
+ // error when present (already FATAL); otherwise surface a FATAL timeout so the
15384
+ // caller checks the batch status rather than resubmitting.
15385
+ if (swapReceipt === undefined || swapReceipt.txHash === '') {
15386
+ if (isKitError(batchResult.error)) {
15387
+ throw batchResult.error;
15388
+ }
15389
+ throw new KitError({
15390
+ ...NetworkError.TIMEOUT,
15391
+ recoverability: 'FATAL',
15392
+ message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
15393
+ // Preserve the underlying confirmation failure when it isn't a KitError —
15394
+ // the signing-strategy path returns a raw viem error (e.g. a dropped or
15395
+ // replaced tx) — so the root cause survives behind the generic timeout.
15396
+ cause: {
15397
+ trace: {
15398
+ batchId: batchResult.batchId,
15399
+ ...batchResult.error != null && {
15400
+ error: batchResult.error
15401
+ }
15402
+ }
15403
+ }
15404
+ });
15405
+ }
15406
+ if (swapReceipt.status !== 'success') {
15407
+ throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
15408
+ }
15409
+ const executedTransactions = [];
15410
+ const approveReceipt = batchResult.receipts[0];
15411
+ // An atomic batch is a single on-chain transaction, so the approve and swap
15412
+ // receipts share one hash. Only surface a distinct approval record when it is
15413
+ // genuinely a separate transaction; otherwise the lone swap record represents
15414
+ // the batch, avoiding a phantom duplicate tx in executedTransactions.
15415
+ if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
15416
+ executedTransactions.push({
15417
+ type: 'approval',
15418
+ txHash: approveReceipt.txHash
15419
+ });
15420
+ }
15421
+ executedTransactions.push({
15422
+ type: 'swap',
15423
+ txHash: swapReceipt.txHash
15424
+ });
15425
+ return {
15426
+ swapTxHash: swapReceipt.txHash,
15427
+ executedTransactions
15428
+ };
15429
+ }
15430
+
15073
15431
  /**
15074
15432
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
15075
15433
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -15196,7 +15554,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15196
15554
  const statusResult = await getSwapStatus$1({
15197
15555
  txHash,
15198
15556
  chain: chain.chain,
15199
- apiKey
15557
+ ...apiKey !== undefined && {
15558
+ apiKey
15559
+ }
15200
15560
  });
15201
15561
  if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
15202
15562
  return {
@@ -15787,8 +16147,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
15787
16147
  // Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
15788
16148
  const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
15789
16149
  const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
15790
- const adapterSupportsPermit = hasEIP2612NonceFetching(adapter) && hasSignTypedData(adapter);
15791
- const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
16150
+ const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
15792
16151
  const needsApproval = !isNativeToken && !canUsePermitFlow;
15793
16152
  if (!needsApproval) {
15794
16153
  return;
@@ -16067,34 +16426,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16067
16426
  const serviceResponse = await createSwap(serviceParams);
16068
16427
  // Track executed transactions
16069
16428
  const executedTransactions = [];
16070
- // Prepare swap action based on chain type
16071
- let preparedAction;
16072
- if (chain.type === 'solana') {
16073
- // Solana: No approval needed, directly prepare swap action
16074
- preparedAction = await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext);
16075
- } else {
16076
- // EVM chains: Handle token approval if needed
16077
- await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16078
- // EVM chains: prepareEvmSwapAction handles EIP-2612 permit generation
16079
- // Adapter contract address is read from chain.kitContracts.adapter
16080
- // Use the already-resolved context from above
16081
- preparedAction = await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy);
16082
- }
16429
+ // Prepare swap action(s) based on chain type and batch capability.
16430
+ // Returns either a single prepared action (Solana / sequential EVM) or a
16431
+ // batched approve+swap plan (EVM atomic-batch path).
16432
+ const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
16433
+ adapter,
16434
+ chain,
16435
+ serviceResponse,
16436
+ resolvedContext,
16437
+ executionCtx,
16438
+ config,
16439
+ executedTransactions
16440
+ });
16083
16441
  // Execute swap transaction via adapter
16084
16442
  // For EVM chains, use gas limit from proxy service API
16085
16443
  let txHash;
16086
16444
  const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
16087
16445
  try {
16088
- txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16089
- executedTransactions.push({
16090
- type: 'swap',
16091
- txHash
16092
- });
16093
- // Wait for transaction confirmation and verify success
16094
- const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16095
- if (txReceipt.status === 'reverted') {
16096
- const explorerUrl = buildExplorerUrl(chain, txHash);
16097
- throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16446
+ if (batchedSwapPlan) {
16447
+ // Approve + swap submitted as one atomic batch. batchExecute confirms
16448
+ // the swap internally, so no separate waitForTransaction is needed.
16449
+ const batched = await executeBatchedApproveAndSwap({
16450
+ adapter: adapter,
16451
+ chain: chain,
16452
+ approveRequest: batchedSwapPlan.approveRequest,
16453
+ swapRequest: batchedSwapPlan.swapRequest,
16454
+ fromAddress: executionCtx.fromAddress
16455
+ });
16456
+ txHash = batched.swapTxHash;
16457
+ executedTransactions.push(...batched.executedTransactions);
16458
+ } else if (preparedAction) {
16459
+ txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
16460
+ executedTransactions.push({
16461
+ type: 'swap',
16462
+ txHash
16463
+ });
16464
+ // Wait for transaction confirmation and verify success
16465
+ const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
16466
+ if (txReceipt.status === 'reverted') {
16467
+ const explorerUrl = buildExplorerUrl(chain, txHash);
16468
+ throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
16469
+ }
16470
+ } else {
16471
+ // Unreachable: the preparation step always yields either a batched plan
16472
+ // or a prepared action.
16473
+ throw new KitError({
16474
+ ...InputError.UNSUPPORTED_ACTION,
16475
+ recoverability: 'FATAL',
16476
+ message: 'No swap execution path was prepared.'
16477
+ });
16098
16478
  }
16099
16479
  } catch (err) {
16100
16480
  handleSwapExecutionError(err, txHash, chain);
@@ -16113,7 +16493,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16113
16493
  isCrossChainSwap,
16114
16494
  txHash,
16115
16495
  chain,
16116
- apiKey: serviceParams.apiKey
16496
+ ...serviceParams.apiKey !== undefined && {
16497
+ apiKey: serviceParams.apiKey
16498
+ }
16117
16499
  });
16118
16500
  // Build and return SwapResult
16119
16501
  return {
@@ -16138,6 +16520,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16138
16520
  };
16139
16521
  }
16140
16522
  /**
16523
+ * Prepare the swap execution request(s) for the source wallet's chain.
16524
+ *
16525
+ * Produces either a single {@link PreparedChainRequest} (Solana, or the
16526
+ * sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
16527
+ * atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
16528
+ * batching). The caller executes whichever field is populated. Any on-chain
16529
+ * approval sent on the sequential path is appended to `executedTransactions`.
16530
+ *
16531
+ * @typeParam TFromAdapterCapabilities - Source-adapter capability set.
16532
+ * @param args - Inputs derived from the validated swap request.
16533
+ * @param args.adapter - Source-chain wallet adapter.
16534
+ * @param args.chain - Source chain definition.
16535
+ * @param args.serviceResponse - Validated createSwap response.
16536
+ * @param args.resolvedContext - Resolved operation context.
16537
+ * @param args.executionCtx - Minimal on-chain execution context.
16538
+ * @param args.config - Optional swap configuration (allowance/batch flags).
16539
+ * @param args.executedTransactions - Array appended with any sent approval.
16540
+ * @returns The prepared action or the batched approve+swap plan.
16541
+ * @throws KitError when the EVM atomic-batch path is selected but the chain
16542
+ * has no configured adapter contract.
16543
+ */ async prepareSwapRequests(args) {
16544
+ const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
16545
+ if (chain.type === 'solana') {
16546
+ // Solana: No approval needed, directly prepare swap action
16547
+ return {
16548
+ preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
16549
+ };
16550
+ }
16551
+ const useBatch = await shouldUseBatchedSwap({
16552
+ adapter,
16553
+ chain,
16554
+ tokenInAddress: executionCtx.tokenInAddress,
16555
+ allowanceStrategy: config?.allowanceStrategy,
16556
+ batchTransactions: config?.batchTransactions
16557
+ });
16558
+ if (useBatch) {
16559
+ // EVM chains: fuse the ERC-20 approval and the swap into a single atomic
16560
+ // batch (one signing challenge for smart-contract wallets). Force the
16561
+ // swap onto the pre-approval (PermitType.NONE) path since the approval
16562
+ // rides in the same batch.
16563
+ const adapterContractAddress = chain.kitContracts?.adapter;
16564
+ if (!adapterContractAddress) {
16565
+ throw new KitError({
16566
+ ...InputError.VALIDATION_FAILED,
16567
+ recoverability: 'FATAL',
16568
+ message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
16569
+ cause: {
16570
+ trace: {
16571
+ chain: chain.name
16572
+ }
16573
+ }
16574
+ });
16575
+ }
16576
+ const [approveRequest, swapRequest] = await Promise.all([
16577
+ this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
16578
+ prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
16579
+ ]);
16580
+ return {
16581
+ batchedSwapPlan: {
16582
+ approveRequest,
16583
+ swapRequest
16584
+ }
16585
+ };
16586
+ }
16587
+ // EVM chains: Handle token approval if needed, then prepare the swap.
16588
+ // prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
16589
+ // contract address is read from chain.kitContracts.adapter.
16590
+ await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
16591
+ return {
16592
+ preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
16593
+ };
16594
+ }
16595
+ /**
16141
16596
  * Executes a swap transaction with the appropriate gas limit for the chain type.
16142
16597
  *
16143
16598
  * For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
@@ -16186,8 +16641,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16186
16641
  */ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
16187
16642
  if (!fees) return [];
16188
16643
  const [providerFees, swapFees, developerFees] = await Promise.all([
16189
- this.formatServiceFees(fees.provider, chain, 'provider', adapter),
16190
- this.formatServiceFees(fees.swap, chain, 'swap', adapter),
16644
+ this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
16645
+ this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
16191
16646
  recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
16192
16647
  ]);
16193
16648
  return [
@@ -16197,6 +16652,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16197
16652
  ];
16198
16653
  }
16199
16654
  /**
16655
+ * Resolve a single fee item to its display token and human-readable amount.
16656
+ *
16657
+ * @remarks
16658
+ * Prefer the self-describing metadata the service attaches to each fee:
16659
+ * `decimals` (and `symbol`) come straight from the provider quote, so they
16660
+ * are authoritative even for a token absent from the SDK registry on both
16661
+ * chains. That is the case {@link resolveFeeChain} cannot recover — a
16662
+ * destination-denominated fee token resolves on neither the source registry
16663
+ * nor the source-bound adapter, leaving the amount as raw base units. When
16664
+ * the service omits `decimals` (optional during rollout), fall back to
16665
+ * inferring the fee token's chain and resolving via the registry/adapter.
16666
+ *
16667
+ * Like {@link formatTokenValue}, this never throws: fee display is cosmetic
16668
+ * and must not fail an estimate/swap. A malformed self-describing `decimals`
16669
+ * (e.g. a non-numeric `amount` or invalid decimal count that makes
16670
+ * {@link formatUnits} throw) falls through to chain-based resolution rather
16671
+ * than propagating out of {@link buildFormattedFees}.
16672
+ *
16673
+ * @param fee - The fee item from the service response.
16674
+ * @param chain - The source chain definition.
16675
+ * @param destinationChain - The destination chain definition.
16676
+ * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16677
+ * @returns Promise resolving to the formatted amount and display token.
16678
+ */ async formatFeeValue(fee, chain, destinationChain, adapter) {
16679
+ if (fee.decimals != null) {
16680
+ try {
16681
+ return {
16682
+ amount: formatUnits(fee.amount, fee.decimals),
16683
+ token: fee.symbol ?? fee.token
16684
+ };
16685
+ } catch {
16686
+ // Malformed service metadata — fall through to chain-based resolution,
16687
+ // which never throws (worst case: raw passthrough).
16688
+ }
16689
+ }
16690
+ const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
16691
+ return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16692
+ }
16693
+ /**
16200
16694
  * Format service fee items into the SDK's ServiceSwapFee structure.
16201
16695
  *
16202
16696
  * @remarks
@@ -16207,14 +16701,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16207
16701
  * - Raw passthrough only when both registry and adapter fail
16208
16702
  *
16209
16703
  * @param feeItems - Array of fee items from the service response.
16210
- * @param chain - The chain definition for token resolution and formatting.
16704
+ * @param chain - The source chain definition for token resolution and formatting.
16705
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16211
16706
  * @param type - The fee type to assign ('provider' or 'swap').
16212
16707
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16213
16708
  * @returns Promise resolving to formatted ServiceSwapFee array.
16214
- */ async formatServiceFees(feeItems, chain, type, adapter) {
16709
+ */ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
16215
16710
  if (!feeItems) return [];
16216
16711
  return Promise.all(feeItems.map(async (fee)=>{
16217
- const formatted = await formatTokenValue(fee.amount, fee.token, chain, adapter);
16712
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16218
16713
  return {
16219
16714
  token: formatted.token,
16220
16715
  amount: formatted.amount,
@@ -16226,16 +16721,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
16226
16721
  * Format developer fee items into the SDK's ServiceSwapFee structure.
16227
16722
  *
16228
16723
  * @param feeItems - Array of developer fee items from the service response.
16229
- * @param chain - The chain definition for token resolution and formatting.
16724
+ * @param chain - The source chain definition for token resolution and formatting.
16725
+ * @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
16230
16726
  * @param recipientAddress - The developer's fee recipient address from config.
16231
16727
  * @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
16232
16728
  * @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
16233
16729
  */ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
16234
16730
  if (!feeItems) return [];
16235
- const isCrossChainSwap = destinationChain.chain !== chain.chain;
16236
16731
  return Promise.all(feeItems.map(async (fee)=>{
16237
- const feeChain = !isCrossChainSwap && fee.basis === 'estimatedAmount' ? destinationChain : chain;
16238
- const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
16732
+ const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
16239
16733
  return {
16240
16734
  token: formatted.token,
16241
16735
  amount: formatted.amount,
@@ -18630,22 +19124,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18630
19124
  try {
18631
19125
  // Step 1: Build quote params directly (no need for buildServiceParams)
18632
19126
  // Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
19127
+ // The kit key is optional (permissionless mode); when absent the quote is
19128
+ // fetched without an Authorization header.
18633
19129
  const kitKey = params.config?.kitKey;
18634
- if (!kitKey) {
18635
- throw new KitError({
18636
- code: 1098,
18637
- name: 'INPUT_VALIDATION_FAILED',
18638
- type: 'INPUT',
18639
- recoverability: 'FATAL',
18640
- message: 'kitKey is required in config for callback-based fees',
18641
- cause: {
18642
- trace: {
18643
- operation: 'handleOutputFeeCallback',
18644
- params
18645
- }
18646
- }
18647
- });
18648
- }
18649
19130
  // Resolve token aliases to addresses for the quote API
18650
19131
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
18651
19132
  const chain = params.from.chain;
@@ -18670,7 +19151,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
18670
19151
  ...params.config?.slippageBps !== undefined && {
18671
19152
  slippageBps: params.config.slippageBps
18672
19153
  },
18673
- apiKey: kitKey
19154
+ ...kitKey ? {
19155
+ apiKey: kitKey
19156
+ } : {}
18674
19157
  };
18675
19158
  // Step 2: Get quote from service
18676
19159
  const quoteResponse = await getQuote(quoteParams);
@@ -19112,7 +19595,9 @@ const sleep$1 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
19112
19595
  ...isCrossChain && {
19113
19596
  toChain: chainOut
19114
19597
  },
19115
- apiKey: params.kitKey
19598
+ ...params.kitKey ? {
19599
+ apiKey: params.kitKey
19600
+ } : {}
19116
19601
  };
19117
19602
  let raw = await getSwapStatus$1(request);
19118
19603
  // When the service hasn't finished indexing a just-submitted swap it
@@ -19252,7 +19737,9 @@ const isResultShape = (params)=>'result' in params;
19252
19737
  ...chainOut !== undefined && {
19253
19738
  chainOut
19254
19739
  },
19255
- kitKey: params.kitKey
19740
+ ...params.kitKey ? {
19741
+ kitKey: params.kitKey
19742
+ } : {}
19256
19743
  };
19257
19744
  const deadline = Date.now() + timeoutMs;
19258
19745
  let pollIndex = 0;
@@ -19413,7 +19900,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
19413
19900
  const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
19414
19901
  return getTokenRates$1({
19415
19902
  chain,
19416
- apiKey: params.kitKey,
19903
+ ...params.kitKey ? {
19904
+ apiKey: params.kitKey
19905
+ } : {},
19417
19906
  ...resolvedAddresses !== undefined && {
19418
19907
  addresses: resolvedAddresses
19419
19908
  }
@@ -20410,7 +20899,7 @@ registerKit(`${pkg$1.name}/${pkg$1.version}`);
20410
20899
  };
20411
20900
 
20412
20901
  var name = "@circle-fin/earn-kit";
20413
- var version = "1.2.2";
20902
+ var version = "1.3.0";
20414
20903
  var pkg = {
20415
20904
  name: name,
20416
20905
  version: version};
@@ -20540,7 +21029,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20540
21029
  asset: z.string(),
20541
21030
  assetAddress: z.string(),
20542
21031
  lltv: z.number(),
20543
- supplyUsd: z.number()
21032
+ supplyUsd: z.number(),
21033
+ // Optional during the expand/contract window (a backend that predates the
21034
+ // field omits the key), mirroring the `.optional()` facets on the base
21035
+ // schema; `null` when the product exposes no per-market allocation (V2).
21036
+ allocationPct: z.number().nullable().optional()
20544
21037
  });
20545
21038
  /**
20546
21039
  * Zod schema for a Morpho vault warning in the API response.
@@ -20554,7 +21047,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20554
21047
  ])
20555
21048
  });
20556
21049
  /**
20557
- * Zod schema for a single vault info object in the API response.
21050
+ * Zod schema for the manager (curator) facet in the API response.
21051
+ *
21052
+ * @internal
21053
+ */ const managerSchema = z.object({
21054
+ name: z.string(),
21055
+ address: z.string().optional(),
21056
+ // Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
21057
+ // are added here as the providers that emit them land, rather than shipped
21058
+ // speculatively.
21059
+ type: z.enum([
21060
+ 'curator'
21061
+ ])
21062
+ });
21063
+ /**
21064
+ * Zod schema for the APY profile facet in the API response.
21065
+ *
21066
+ * @internal
21067
+ */ const apyProfileSchema = z.object({
21068
+ current: z.number(),
21069
+ native: z.number().nullable(),
21070
+ d7: z.number().nullable(),
21071
+ d30: z.number().nullable(),
21072
+ d90: z.number().nullable(),
21073
+ rewardShare: z.number().nullable(),
21074
+ source: z.string().optional(),
21075
+ asOf: z.string().optional()
21076
+ });
21077
+ /**
21078
+ * Zod schema for the fee split facet in the API response.
21079
+ *
21080
+ * @internal
21081
+ */ const feeInfoSchema = z.object({
21082
+ performance: z.number().nullable(),
21083
+ management: z.number().nullable()
21084
+ });
21085
+ /**
21086
+ * Zod schema for the liquidity profile facet in the API response.
21087
+ *
21088
+ * `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
21089
+ * it is validated as a raw JSON amount, like `totalDeposits`/`available`.
21090
+ *
21091
+ * @internal
21092
+ */ const liquidityProfileSchema = z.object({
21093
+ totalDeposits: amountJsonSchema,
21094
+ available: amountJsonSchema,
21095
+ totalSupply: amountJsonSchema,
21096
+ status: z.enum([
21097
+ 'active',
21098
+ 'low_liquidity'
21099
+ ])
21100
+ });
21101
+ /**
21102
+ * Zod schema for the risk signals facet in the API response.
21103
+ *
21104
+ * @internal
21105
+ */ const riskSignalsSchema = z.object({
21106
+ circleSentinel: z.boolean(),
21107
+ warnings: z.array(vaultWarningSchema).optional(),
21108
+ earnKitWarnings: z.array(z.string()).optional()
21109
+ });
21110
+ /**
21111
+ * Zod schema for the universal earn-opportunity base in the API response.
21112
+ *
21113
+ * Retains every existing deprecated flat field (kept validated through the
21114
+ * expand/contract window so default-strip does not drop them) and adds the
21115
+ * new nested facets. The nested facets are `.optional()` during the
21116
+ * transition so the SDK still validates against a not-yet-fully-deployed
21117
+ * backend; they become required after Expand ships.
20558
21118
  *
20559
21119
  * @internal
20560
21120
  */ const vaultInfoResponseSchema = z.object({
@@ -20579,6 +21139,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
20579
21139
  warnings: z.array(vaultWarningSchema).optional(),
20580
21140
  earnKitWarnings: z.array(z.string()).optional()
20581
21141
  });
21142
+ /**
21143
+ * Shared base schema: existing flat fields (kept) plus the new nested
21144
+ * facets and neutral identity. Facets are `.optional()` during the
21145
+ * transition; flip to required once the backend is confirmed emitting.
21146
+ *
21147
+ * @internal
21148
+ */ const earnBaseSchema = vaultInfoResponseSchema.extend({
21149
+ address: z.string().optional(),
21150
+ asOf: z.string().optional(),
21151
+ manager: managerSchema.nullable().optional(),
21152
+ apyProfile: apyProfileSchema.optional(),
21153
+ fee: feeInfoSchema.optional(),
21154
+ liquidityProfile: liquidityProfileSchema.optional(),
21155
+ riskSignals: riskSignalsSchema.optional()
21156
+ });
21157
+ /**
21158
+ * Zod schema for the `vault` opportunity variant.
21159
+ *
21160
+ * @internal
21161
+ */ const vaultOpportunitySchema = earnBaseSchema.extend({
21162
+ productType: z.literal('vault'),
21163
+ collateral: z.array(collateralSchema)
21164
+ });
21165
+ /**
21166
+ * Discriminated union over `productType`. Add union members here as new
21167
+ * product types (e.g. `lending_market`, `rwa_token`) land.
21168
+ *
21169
+ * @internal
21170
+ */ const earnOpportunityVariants = [
21171
+ vaultOpportunitySchema
21172
+ ];
21173
+ /** @internal */ const earnOpportunitySchema = z.discriminatedUnion('productType', earnOpportunityVariants);
21174
+ /** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
21175
+ /**
21176
+ * Tolerant list parser for earn opportunities.
21177
+ *
21178
+ * `z.discriminatedUnion` throws on an unrecognized discriminant and
21179
+ * `z.array` fails the whole array if any element fails. Two migration-window
21180
+ * cases are smoothed over here so neither breaks an already-shipped SDK:
21181
+ *
21182
+ * - A backend that predates `productType` omits it entirely. `'vault'` was the
21183
+ * only opportunity type then, so default a missing discriminant to `'vault'`
21184
+ * rather than dropping every vault the backend returns.
21185
+ * - A future backend adds a *second* `productType` this SDK version does not
21186
+ * know. Drop those elements (a present-but-unrecognized discriminant) instead
21187
+ * of rejecting the whole list.
21188
+ *
21189
+ * Only the drop above is a *tolerant* case. Anything that is not a plain object
21190
+ * with a present-but-unknown string `productType` — `null`, `undefined`,
21191
+ * primitives, or an object whose `productType` is malformed — is passed through
21192
+ * untouched so `z.array(earnOpportunitySchema)` reports it as a normal
21193
+ * validation failure. It is deliberately not silently dropped (which would hide
21194
+ * malformed backend data) and never throws here (an unguarded property read on
21195
+ * a non-object would escape `safeParse` as a raw `TypeError` instead of a
21196
+ * `ZodError`).
21197
+ *
21198
+ * @internal
21199
+ */ const earnOpportunityListSchema = z.preprocess((raw)=>{
21200
+ if (!Array.isArray(raw)) {
21201
+ return raw;
21202
+ }
21203
+ // Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
21204
+ // map/filter chain stays type-safe and no `any` leaks into the return.
21205
+ const entries = raw;
21206
+ return entries.map((entry)=>{
21207
+ // Only touch plain objects; non-objects fall through to fail validation.
21208
+ if (typeof entry !== 'object' || entry === null) {
21209
+ return entry;
21210
+ }
21211
+ const record = entry;
21212
+ // Older backend predating productType: default to the only type then.
21213
+ return record.productType === undefined ? {
21214
+ ...record,
21215
+ productType: 'vault'
21216
+ } : record;
21217
+ }).filter((entry)=>{
21218
+ // Drop ONLY a present-but-unknown string discriminant (a future
21219
+ // productType this SDK version doesn't know). Everything else —
21220
+ // non-objects, a non-string productType — flows through to
21221
+ // z.array(earnOpportunitySchema) and fails/passes validation normally.
21222
+ if (typeof entry !== 'object' || entry === null) {
21223
+ return true;
21224
+ }
21225
+ const productType = entry.productType;
21226
+ if (typeof productType !== 'string') {
21227
+ return true;
21228
+ }
21229
+ return knownProductTypes.has(productType);
21230
+ });
21231
+ }, z.array(earnOpportunitySchema));
20582
21232
  // ---------------------------------------------------------------------------
20583
21233
  // Position response schema
20584
21234
  // ---------------------------------------------------------------------------
@@ -20708,6 +21358,7 @@ const positionPnlSchema = z.discriminatedUnion('status', [
20708
21358
  *
20709
21359
  * @internal
20710
21360
  */ const depositPayloadSchema = z.object({
21361
+ execId: bridgeDepositExecIdSchema,
20711
21362
  executionParams: depositExecutionParamsSchema,
20712
21363
  signature: hexSignatureSchema
20713
21364
  });
@@ -20799,6 +21450,21 @@ const bridgeDepositPrepareReviewSchema = z.object({
20799
21450
  amount: amountJsonSchema,
20800
21451
  vaultAddress: hexAddressSchema
20801
21452
  }).passthrough();
21453
+ /** @internal */ const bridgeQuoteExpirySchema = z.discriminatedUnion('mode', [
21454
+ z.object({
21455
+ mode: z.literal('TIMESTAMP'),
21456
+ expiresAt: z.string().datetime({
21457
+ offset: true
21458
+ })
21459
+ }),
21460
+ z.object({
21461
+ mode: z.literal('BLOCK_NUMBER'),
21462
+ expiresAtBlock: z.number().int(),
21463
+ blockEstimatedAt: z.string().datetime({
21464
+ offset: true
21465
+ }).optional()
21466
+ })
21467
+ ]).optional().catch(undefined);
20802
21468
  /**
20803
21469
  * Zod schema for the bridge deposit prepare payload.
20804
21470
  *
@@ -20810,6 +21476,10 @@ const bridgeDepositPrepareReviewSchema = z.object({
20810
21476
  execId: bridgeDepositExecIdSchema,
20811
21477
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
20812
21478
  expiresAt: z.string().datetime(),
21479
+ quoteIssuedAt: z.string().datetime({
21480
+ offset: true
21481
+ }).optional().catch(undefined),
21482
+ quoteExpiry: bridgeQuoteExpirySchema,
20813
21483
  review: bridgeDepositPrepareReviewSchema
20814
21484
  });
20815
21485
  /**
@@ -20875,6 +21545,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
20875
21545
  *
20876
21546
  * @internal
20877
21547
  */ const withdrawPayloadSchema = z.object({
21548
+ execId: bridgeDepositExecIdSchema,
20878
21549
  executionParams: withdrawExecutionParamsSchema,
20879
21550
  signature: hexSignatureSchema
20880
21551
  });
@@ -20888,6 +21559,27 @@ const bridgeDepositPrepareReviewSchema = z.object({
20888
21559
  data: withdrawPayloadSchema
20889
21560
  });
20890
21561
  // ---------------------------------------------------------------------------
21562
+ // Transaction report response schema
21563
+ // ---------------------------------------------------------------------------
21564
+ /**
21565
+ * Zod schema for the transaction report payload inside the API `data` envelope.
21566
+ *
21567
+ * The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
21568
+ * schema accepts any object shape and does not require specific fields.
21569
+ *
21570
+ * @internal
21571
+ */ const transactionReportPayloadSchema = z.object({}).passthrough();
21572
+ /**
21573
+ * Zod schema for the `POST /v1/earnKit/transactions/report` API response.
21574
+ *
21575
+ * The Earn Service API wraps the transaction report payload in a `data`
21576
+ * envelope.
21577
+ *
21578
+ * @internal
21579
+ */ z.object({
21580
+ data: transactionReportPayloadSchema
21581
+ });
21582
+ // ---------------------------------------------------------------------------
20891
21583
  // Claim rewards response schema
20892
21584
  // ---------------------------------------------------------------------------
20893
21585
  /**
@@ -20948,6 +21640,30 @@ const bridgeDepositPrepareReviewSchema = z.object({
20948
21640
  token: z.string(),
20949
21641
  amount: amountJsonSchema
20950
21642
  });
21643
+ /**
21644
+ * Zod schema for a native gas-fee entry in an EarnKit quote response.
21645
+ *
21646
+ * The Earn Service backend estimates gas server-side and returns one entry per
21647
+ * action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
21648
+ * `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
21649
+ * integer string in the chain's native base units. When the backend cannot
21650
+ * estimate an action it returns `fees: null` with an `error` message instead.
21651
+ *
21652
+ * The schema deliberately validates almost nothing beyond the envelope: `name`
21653
+ * is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
21654
+ * of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
21655
+ * `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
21656
+ * which degrades a malformed entry to a `fees: null` soft failure. This is
21657
+ * intentional: gas is best-effort, so a single unparseable gas entry (a wrong
21658
+ * type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
21659
+ * `fee`) must never fail Zod validation and reject the entire quote.
21660
+ *
21661
+ * @internal
21662
+ */ const quoteGasFeeSchema = z.object({
21663
+ name: z.string().optional(),
21664
+ fees: z.unknown(),
21665
+ error: z.string().optional()
21666
+ }).passthrough();
20951
21667
  /**
20952
21668
  * Zod schema for the inner deposit quote payload.
20953
21669
  *
@@ -20963,7 +21679,8 @@ const bridgeDepositPrepareReviewSchema = z.object({
20963
21679
  expectedShares: amountJsonSchema,
20964
21680
  sharePrice: z.string(),
20965
21681
  currentApy: z.number(),
20966
- fees: z.array(feeSchema).optional()
21682
+ fees: z.array(feeSchema).optional(),
21683
+ gasFees: z.array(quoteGasFeeSchema).optional()
20967
21684
  });
20968
21685
  /**
20969
21686
  * Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
@@ -20990,6 +21707,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
20990
21707
  sharePrice: z.string(),
20991
21708
  maxWithdrawable: amountJsonSchema,
20992
21709
  fees: z.array(feeSchema),
21710
+ gasFees: z.array(quoteGasFeeSchema).optional(),
20993
21711
  warnings: z.array(z.string()).optional()
20994
21712
  });
20995
21713
  /**
@@ -21047,7 +21765,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
21047
21765
  *
21048
21766
  * @internal
21049
21767
  */ const getVaultsPayloadSchema = z.object({
21050
- vaults: z.array(vaultInfoResponseSchema),
21768
+ vaults: earnOpportunityListSchema,
21051
21769
  errors: z.array(vaultErrorSchema)
21052
21770
  });
21053
21771
  /**
@@ -21077,7 +21795,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
21077
21795
  *
21078
21796
  * @internal
21079
21797
  */ const exploreVaultsPayloadSchema = z.object({
21080
- vaults: z.array(vaultInfoResponseSchema),
21798
+ vaults: earnOpportunityListSchema,
21081
21799
  pagination: explorePaginationSchema
21082
21800
  });
21083
21801
  /**