@circle-fin/app-kit 1.11.0 → 1.12.1

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/earn.mjs CHANGED
@@ -37,6 +37,7 @@ import 'bn.js';
37
37
  import '@coral-xyz/anchor';
38
38
  import 'bs58';
39
39
  import '@noble/curves/ed25519';
40
+ import { decodeFunctionData } from 'viem';
40
41
  import { formatUnits as formatUnits$1 } from '@ethersproject/units';
41
42
  import { keccak256 } from '@ethersproject/keccak256';
42
43
 
@@ -677,6 +678,11 @@ class KitError extends Error {
677
678
  name: 'INPUT_INSUFFICIENT_SWAP_AMOUNT',
678
679
  type: 'INPUT'
679
680
  },
681
+ /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
682
+ code: 1008,
683
+ name: 'INPUT_UNSUPPORTED_ACTION',
684
+ type: 'INPUT'
685
+ },
680
686
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
681
687
  code: 1009,
682
688
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -741,6 +747,29 @@ class KitError extends Error {
741
747
  type: 'LIQUIDITY'
742
748
  }
743
749
  };
750
+ /**
751
+ * Standardized error definitions for RPC type errors.
752
+ *
753
+ * RPC errors occur when communicating with blockchain RPC providers,
754
+ * including endpoint failures, invalid responses, and provider-specific issues.
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * import { RpcError } from '@core/errors'
759
+ *
760
+ * const error = new KitError({
761
+ * ...RpcError.ENDPOINT_ERROR,
762
+ * recoverability: 'RETRYABLE',
763
+ * message: 'RPC endpoint unavailable on Ethereum',
764
+ * cause: { trace: { endpoint: 'https://mainnet.infura.io' } }
765
+ * })
766
+ * ```
767
+ */ const RpcError = {
768
+ /** RPC endpoint returned error or is unavailable */ ENDPOINT_ERROR: {
769
+ code: 4001,
770
+ name: 'RPC_ENDPOINT_ERROR',
771
+ type: 'RPC'
772
+ }};
744
773
  /**
745
774
  * Standardized error definitions for NETWORK type errors.
746
775
  *
@@ -2340,6 +2369,8 @@ function getOptionalString(value) {
2340
2369
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2341
2370
  Blockchain["XDC"] = "XDC";
2342
2371
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2372
+ Blockchain["X_Layer"] = "X_Layer";
2373
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2343
2374
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2344
2375
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2345
2376
  })(Blockchain || (Blockchain = {}));
@@ -2393,6 +2424,7 @@ var BridgeChain;
2393
2424
  BridgeChain["Unichain"] = "Unichain";
2394
2425
  BridgeChain["World_Chain"] = "World_Chain";
2395
2426
  BridgeChain["XDC"] = "XDC";
2427
+ BridgeChain["X_Layer"] = "X_Layer";
2396
2428
  // Testnet chains with CCTPv2 support
2397
2429
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2398
2430
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2418,6 +2450,7 @@ var BridgeChain;
2418
2450
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2419
2451
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2420
2452
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2453
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2421
2454
  })(BridgeChain || (BridgeChain = {}));
2422
2455
  var UnifiedBalanceChain;
2423
2456
  (function(UnifiedBalanceChain) {
@@ -4955,7 +4988,8 @@ var EarnChain;
4955
4988
  isTestnet: true,
4956
4989
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4957
4990
  rpcEndpoints: [
4958
- 'https://rpc-amoy.polygon.technology'
4991
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4992
+ 'https://polygon-amoy.drpc.org'
4959
4993
  ],
4960
4994
  eurcAddress: null,
4961
4995
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5820,6 +5854,104 @@ var EarnChain;
5820
5854
  }
5821
5855
  });
5822
5856
 
5857
+ /**
5858
+ * X Layer Mainnet chain definition
5859
+ * @remarks
5860
+ * This represents the official production network for the X Layer blockchain.
5861
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5862
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5863
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5864
+ */ const XLayer = defineChain({
5865
+ type: 'evm',
5866
+ chain: Blockchain.X_Layer,
5867
+ name: 'X Layer',
5868
+ title: 'X Layer Mainnet',
5869
+ nativeCurrency: {
5870
+ name: 'OKB',
5871
+ symbol: 'OKB',
5872
+ decimals: 18
5873
+ },
5874
+ chainId: 196,
5875
+ isTestnet: false,
5876
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5877
+ rpcEndpoints: [
5878
+ 'https://xlayerrpc.okx.com'
5879
+ ],
5880
+ eurcAddress: null,
5881
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5882
+ usdtAddress: null,
5883
+ cctp: {
5884
+ domain: 37,
5885
+ contracts: {
5886
+ v2: {
5887
+ type: 'split',
5888
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5889
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5890
+ confirmations: 65,
5891
+ fastConfirmations: 1
5892
+ }
5893
+ },
5894
+ forwarderSupported: {
5895
+ source: false,
5896
+ destination: false
5897
+ }
5898
+ },
5899
+ kitContracts: {
5900
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5901
+ }
5902
+ });
5903
+
5904
+ /**
5905
+ * X Layer Testnet chain definition
5906
+ * @remarks
5907
+ * This represents the official test network for the X Layer blockchain.
5908
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5909
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5910
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5911
+ */ const XLayerTestnet = defineChain({
5912
+ type: 'evm',
5913
+ chain: Blockchain.X_Layer_Testnet,
5914
+ name: 'X Layer Testnet',
5915
+ title: 'X Layer Testnet',
5916
+ nativeCurrency: {
5917
+ name: 'OKB',
5918
+ symbol: 'OKB',
5919
+ decimals: 18
5920
+ },
5921
+ chainId: 1952,
5922
+ isTestnet: true,
5923
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5924
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5925
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5926
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5927
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5928
+ rpcEndpoints: [
5929
+ 'https://testrpc.xlayer.tech'
5930
+ ],
5931
+ eurcAddress: null,
5932
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5933
+ usdtAddress: null,
5934
+ cctp: {
5935
+ domain: 37,
5936
+ contracts: {
5937
+ v2: {
5938
+ type: 'split',
5939
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5940
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5941
+ confirmations: 65,
5942
+ fastConfirmations: 1
5943
+ }
5944
+ },
5945
+ forwarderSupported: {
5946
+ source: false,
5947
+ destination: false
5948
+ }
5949
+ },
5950
+ kitContracts: {
5951
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5952
+ }
5953
+ });
5954
+
5823
5955
  /**
5824
5956
  * ZKSync Era Mainnet chain definition
5825
5957
  * @remarks
@@ -5939,6 +6071,8 @@ var Chains = /*#__PURE__*/Object.freeze({
5939
6071
  WorldChainSepolia: WorldChainSepolia,
5940
6072
  XDC: XDC,
5941
6073
  XDCApothem: XDCApothem,
6074
+ XLayer: XLayer,
6075
+ XLayerTestnet: XLayerTestnet,
5942
6076
  ZKSyncEra: ZKSyncEra,
5943
6077
  ZKSyncEraSepolia: ZKSyncEraSepolia
5944
6078
  });
@@ -7382,6 +7516,7 @@ const swapTokenEnumSchema = z.enum([
7382
7516
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7383
7517
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7384
7518
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7519
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7385
7520
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7386
7521
  // =========================================================================
7387
7522
  // Testnets (alphabetically sorted)
@@ -7390,6 +7525,7 @@ const swapTokenEnumSchema = z.enum([
7390
7525
  [Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
7391
7526
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
7392
7527
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
7528
+ [Blockchain.Celo_Alfajores_Testnet]: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B',
7393
7529
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
7394
7530
  [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
7395
7531
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
@@ -7416,6 +7552,7 @@ const swapTokenEnumSchema = z.enum([
7416
7552
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7417
7553
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7418
7554
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
7555
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7419
7556
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7420
7557
  }
7421
7558
  };
@@ -8129,6 +8266,52 @@ const swapTokenEnumSchema = z.enum([
8129
8266
  return explorerUrl;
8130
8267
  }
8131
8268
 
8269
+ /**
8270
+ * Assert that a value has type `never` (exhaustive switch helper).
8271
+ *
8272
+ * @remarks
8273
+ * Use in the `default` branch of a switch over a discriminated union.
8274
+ * If all union members are handled, the default is unreachable and TypeScript
8275
+ * narrows the parameter to `never`. If a member is missed, the compiler errors.
8276
+ *
8277
+ * @param _x - The value (typed as `never` when switch is exhaustive).
8278
+ * @returns Never returns; always throws.
8279
+ * @throws Error when the switch is not exhaustive.
8280
+ *
8281
+ * @example
8282
+ * ```typescript
8283
+ * type Foo = { type: 'a'; x: number } | { type: 'b'; y: string }
8284
+ *
8285
+ * function handle(foo: Foo): string {
8286
+ * switch (foo.type) {
8287
+ * case 'a': return String(foo.x)
8288
+ * case 'b': return foo.y
8289
+ * default: return assertNever(foo)
8290
+ * }
8291
+ * }
8292
+ * ```
8293
+ */ function assertNever$2(x) {
8294
+ // Plain `String(x)` collapses non-primitive union members (objects, arrays)
8295
+ // to `'[object Object]'`, which is useless when triaging which discriminant
8296
+ // was missed. Attempt `JSON.stringify` first so the thrown message preserves
8297
+ // the offending shape. Fall back to a minimal `typeof`-based label if
8298
+ // serialization fails (`BigInt` member, circular references, host objects).
8299
+ //
8300
+ // `x` is statically typed as `never` (the whole point of this helper), but
8301
+ // at runtime callers may still pass an unexpected value when the switch is
8302
+ // not actually exhaustive — that's exactly the bug we want to surface. Cast
8303
+ // through `unknown` so the runtime defence is not stripped by the compiler.
8304
+ const value = x;
8305
+ let stringified;
8306
+ try {
8307
+ const json = JSON.stringify(value);
8308
+ stringified = typeof json === 'string' ? json : `<${typeof value}>`;
8309
+ } catch {
8310
+ stringified = `<unstringifiable ${typeof value}>`;
8311
+ }
8312
+ throw new Error(`Unhandled switch case: ${stringified}`);
8313
+ }
8314
+
8132
8315
  /**
8133
8316
  * CCTP forwarding magic bytes prefix.
8134
8317
  *
@@ -8511,7 +8694,7 @@ const swapTokenEnumSchema = z.enum([
8511
8694
  }
8512
8695
 
8513
8696
  var name$3 = "@circle-fin/bridge-kit";
8514
- var version$3 = "1.12.2";
8697
+ var version$3 = "1.13.0";
8515
8698
  var pkg$3 = {
8516
8699
  name: name$3,
8517
8700
  version: version$3};
@@ -8547,15 +8730,196 @@ var pkg$3 = {
8547
8730
  computeFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
8548
8731
  calculateFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
8549
8732
  resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
8550
- }).strict().refine((data)=>{
8733
+ }).strict().superRefine((data, ctx)=>{
8551
8734
  const hasComputeFee = data.computeFee !== undefined;
8552
8735
  const hasCalculateFee = data.calculateFee !== undefined;
8553
- // XOR: exactly one must be provided
8554
- return hasComputeFee !== hasCalculateFee;
8555
- }, {
8556
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
8736
+ if (hasComputeFee && hasCalculateFee) {
8737
+ ctx.addIssue({
8738
+ code: z.ZodIssueCode.custom,
8739
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
8740
+ });
8741
+ }
8742
+ if (!hasComputeFee && !hasCalculateFee) {
8743
+ ctx.addIssue({
8744
+ code: z.ZodIssueCode.custom,
8745
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
8746
+ });
8747
+ }
8557
8748
  });
8558
8749
 
8750
+ /**
8751
+ * Canonical list of actions that do not prepare or submit transactions.
8752
+ *
8753
+ * @internal
8754
+ */ const READ_ACTION_KEYS = [
8755
+ 'token.allowance',
8756
+ 'token.balanceOf',
8757
+ 'token.name',
8758
+ 'native.balanceOf',
8759
+ 'usdc.allowance',
8760
+ 'usdc.balanceOf',
8761
+ 'usdc.name',
8762
+ 'gateway.v1.isDelegate',
8763
+ 'gateway.v1.withdrawingBalance',
8764
+ 'gateway.v1.withdrawalBlock',
8765
+ 'gateway.v1.signBurnIntents'
8766
+ ];
8767
+ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
8768
+ /**
8769
+ * Check whether a runtime value identifies a read action.
8770
+ *
8771
+ * @param action - The value to classify.
8772
+ * @returns Whether the value is a registered read-action key.
8773
+ *
8774
+ * @example
8775
+ * ```typescript
8776
+ * import { isReadActionKey } from '@core/adapter'
8777
+ *
8778
+ * if (isReadActionKey(value)) {
8779
+ * await adapter.readAction(value, params, context)
8780
+ * }
8781
+ * ```
8782
+ *
8783
+ * @internal
8784
+ */ function isReadActionKey(action) {
8785
+ return READ_ACTION_KEY_SET.has(action);
8786
+ }
8787
+
8788
+ /**
8789
+ * Create the standard error for a missing or non-read action.
8790
+ *
8791
+ * @param action - The unsupported action value.
8792
+ * @returns A fatal unsupported-action error.
8793
+ *
8794
+ * @internal
8795
+ */ function createUnsupportedReadActionError(action) {
8796
+ return new KitError({
8797
+ ...InputError.UNSUPPORTED_ACTION,
8798
+ recoverability: 'FATAL',
8799
+ message: `Read action "${String(action)}" is not registered in this adapter.`
8800
+ });
8801
+ }
8802
+ /**
8803
+ * Execute a read through the adapter's dedicated read seam when available.
8804
+ *
8805
+ * @remarks
8806
+ * Fall back to the legacy `prepareAction().execute()` contract so providers
8807
+ * remain runtime-compatible with adapter versions released before `readAction`.
8808
+ * Consumers must upgrade their adapter package for reads to bypass custom
8809
+ * `prepareAction` wrappers.
8810
+ *
8811
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
8812
+ * @typeParam TActionKey - The read action key.
8813
+ * @param adapter - The adapter that owns the read action.
8814
+ * @param action - The read action to execute.
8815
+ * @param params - The parameters for the read action.
8816
+ * @param ctx - The operation context.
8817
+ * @returns The raw read-action result.
8818
+ * @throws {KitError} When `action` is not a supported read-action key.
8819
+ *
8820
+ * @example
8821
+ * ```typescript
8822
+ * import { executeAdapterReadAction } from '@core/adapter'
8823
+ * import { Ethereum } from '@core/chains'
8824
+ *
8825
+ * const allowance = await executeAdapterReadAction(
8826
+ * adapter,
8827
+ * 'token.allowance',
8828
+ * { tokenAddress, delegate },
8829
+ * { chain: Ethereum },
8830
+ * )
8831
+ * ```
8832
+ *
8833
+ * @internal
8834
+ */ async function executeAdapterReadAction(adapter, action, params, ctx) {
8835
+ if (!isReadActionKey(action)) {
8836
+ throw createUnsupportedReadActionError(action);
8837
+ }
8838
+ const runtimeAdapter = adapter;
8839
+ if (typeof runtimeAdapter.readAction === 'function') {
8840
+ return runtimeAdapter.readAction(action, params, ctx);
8841
+ }
8842
+ let request;
8843
+ try {
8844
+ request = await adapter.prepareAction(action, params, ctx);
8845
+ } catch (error) {
8846
+ if (error instanceof Error && error.message === `Action ${action} is not supported`) {
8847
+ throw createUnsupportedReadActionError(action);
8848
+ }
8849
+ throw error;
8850
+ }
8851
+ return request.execute();
8852
+ }
8853
+ /**
8854
+ * Read and parse a token allowance while supporting older adapter versions.
8855
+ *
8856
+ * @remarks
8857
+ * Prefer the adapter's dedicated read seam and fall back to the legacy
8858
+ * `prepareAction().execute()` contract when the runtime adapter predates it.
8859
+ *
8860
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
8861
+ * @param adapter - The adapter that owns the allowance action.
8862
+ * @param params - The token and delegate whose allowance is being read.
8863
+ * @param ctx - The operation context.
8864
+ * @returns The non-negative allowance in token base units.
8865
+ * @throws {KitError} When the action is unsupported or its response is malformed.
8866
+ *
8867
+ * @example
8868
+ * ```typescript
8869
+ * import { readTokenAllowance } from '@core/adapter'
8870
+ * import { Ethereum } from '@core/chains'
8871
+ *
8872
+ * const allowance = await readTokenAllowance(
8873
+ * adapter,
8874
+ * { tokenAddress, delegate },
8875
+ * { chain: Ethereum },
8876
+ * )
8877
+ * ```
8878
+ *
8879
+ * @internal
8880
+ */ async function readTokenAllowance(adapter, params, ctx) {
8881
+ return parseAllowanceResponse(await executeAdapterReadAction(adapter, 'token.allowance', params, ctx));
8882
+ }
8883
+ /**
8884
+ * Parse a raw token allowance response into base units.
8885
+ *
8886
+ * @param allowanceRaw - The adapter response, optionally wrapped as an Amount output.
8887
+ * @returns The non-negative allowance as a bigint, or zero for a missing value.
8888
+ * @throws {KitError} When the response cannot represent a non-negative bigint.
8889
+ *
8890
+ * @example
8891
+ * ```typescript
8892
+ * import { parseAllowanceResponse } from '@core/adapter'
8893
+ *
8894
+ * const allowance = parseAllowanceResponse({ amount: { raw: 1000000n } })
8895
+ * ```
8896
+ */ function parseAllowanceResponse(allowanceRaw) {
8897
+ let value = allowanceRaw;
8898
+ if (typeof value === 'object' && value !== null && 'amount' in value) {
8899
+ const amount = value.amount;
8900
+ if (typeof amount === 'object' && amount !== null && 'raw' in amount) {
8901
+ value = amount.raw;
8902
+ }
8903
+ }
8904
+ if (value === undefined || value === null) {
8905
+ return 0n;
8906
+ }
8907
+ let allowance;
8908
+ if (typeof value === 'bigint') {
8909
+ allowance = value;
8910
+ } else if (typeof value === 'string') {
8911
+ try {
8912
+ allowance = BigInt(value);
8913
+ } catch {
8914
+ allowance = undefined;
8915
+ }
8916
+ }
8917
+ if (allowance === undefined || allowance < 0n) {
8918
+ throw createValidationFailedError('token.allowance', value, 'token.allowance response must be a non-negative bigint-compatible string or bigint');
8919
+ }
8920
+ return allowance;
8921
+ }
8922
+
8559
8923
  /**
8560
8924
  * Schema for validating hexadecimal strings with '0x' prefix.
8561
8925
  *
@@ -9379,7 +9743,7 @@ var TransferSpeed;
9379
9743
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
9380
9744
 
9381
9745
  var name$2 = "@circle-fin/swap-kit";
9382
- var version$2 = "1.5.0";
9746
+ var version$2 = "1.5.2";
9383
9747
  var pkg$2 = {
9384
9748
  name: name$2,
9385
9749
  version: version$2};
@@ -10176,25 +10540,330 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
10176
10540
  };
10177
10541
 
10178
10542
  /**
10179
- * Zod schema for validating EVM adapter capabilities.
10180
- *
10181
- * This schema ensures that EVM adapter capabilities meet the requirements for the
10182
- * OperationContext pattern by validating the addressContext property and ensuring
10183
- * the supportedChains property contains only EVM-compatible chains.
10184
- *
10185
- * @remarks
10186
- * This schema enforces that all supported chains are EVM type, making it specific
10187
- * to EVM adapters (Ethers, Viem, etc.). For non-EVM adapters, a different validation
10188
- * schema should be used.
10543
+ * IAdapter contract ABI.
10189
10544
  *
10190
- * When used with the validate() function from @core/utils, this throws KitError
10191
- * with INPUT_VALIDATION_FAILED code if validation fails, with details about which
10192
- * properties failed.
10545
+ * Shared ABI for the on-chain Adapter contract used by multiple kits
10546
+ * (swap, earn) for executing signed instruction sets. The `execute()`
10547
+ * function accepts EIP-712 signed execution parameters, token inputs,
10548
+ * and a signature, then executes the corresponding on-chain
10549
+ * instructions.
10550
+ */ const adapterContractAbi = [
10551
+ {
10552
+ type: 'function',
10553
+ name: 'execute',
10554
+ inputs: [
10555
+ {
10556
+ name: 'params',
10557
+ type: 'tuple',
10558
+ internalType: 'struct IAdapter.ExecutionParams',
10559
+ components: [
10560
+ {
10561
+ name: 'instructions',
10562
+ type: 'tuple[]',
10563
+ internalType: 'struct IAdapter.Instruction[]',
10564
+ components: [
10565
+ {
10566
+ name: 'target',
10567
+ type: 'address',
10568
+ internalType: 'address'
10569
+ },
10570
+ {
10571
+ name: 'data',
10572
+ type: 'bytes',
10573
+ internalType: 'bytes'
10574
+ },
10575
+ {
10576
+ name: 'value',
10577
+ type: 'uint256',
10578
+ internalType: 'uint256'
10579
+ },
10580
+ {
10581
+ name: 'tokenIn',
10582
+ type: 'address',
10583
+ internalType: 'address'
10584
+ },
10585
+ {
10586
+ name: 'amountToApprove',
10587
+ type: 'uint256',
10588
+ internalType: 'uint256'
10589
+ },
10590
+ {
10591
+ name: 'tokenOut',
10592
+ type: 'address',
10593
+ internalType: 'address'
10594
+ },
10595
+ {
10596
+ name: 'minTokenOut',
10597
+ type: 'uint256',
10598
+ internalType: 'uint256'
10599
+ }
10600
+ ]
10601
+ },
10602
+ {
10603
+ name: 'tokens',
10604
+ type: 'tuple[]',
10605
+ internalType: 'struct IAdapter.TokenRecipient[]',
10606
+ components: [
10607
+ {
10608
+ name: 'token',
10609
+ type: 'address',
10610
+ internalType: 'address'
10611
+ },
10612
+ {
10613
+ name: 'beneficiary',
10614
+ type: 'address',
10615
+ internalType: 'address'
10616
+ }
10617
+ ]
10618
+ },
10619
+ {
10620
+ name: 'execId',
10621
+ type: 'uint256',
10622
+ internalType: 'uint256'
10623
+ },
10624
+ {
10625
+ name: 'deadline',
10626
+ type: 'uint256',
10627
+ internalType: 'uint256'
10628
+ },
10629
+ {
10630
+ name: 'metadata',
10631
+ type: 'bytes',
10632
+ internalType: 'bytes'
10633
+ }
10634
+ ]
10635
+ },
10636
+ {
10637
+ name: 'tokenInputs',
10638
+ type: 'tuple[]',
10639
+ internalType: 'struct IAdapter.TokenInput[]',
10640
+ components: [
10641
+ {
10642
+ name: 'permitType',
10643
+ type: 'uint8',
10644
+ internalType: 'enum IAdapter.PermitType'
10645
+ },
10646
+ {
10647
+ name: 'token',
10648
+ type: 'address',
10649
+ internalType: 'address'
10650
+ },
10651
+ {
10652
+ name: 'amount',
10653
+ type: 'uint256',
10654
+ internalType: 'uint256'
10655
+ },
10656
+ {
10657
+ name: 'permitCalldata',
10658
+ type: 'bytes',
10659
+ internalType: 'bytes'
10660
+ }
10661
+ ]
10662
+ },
10663
+ {
10664
+ name: 'signature',
10665
+ type: 'bytes',
10666
+ internalType: 'bytes'
10667
+ }
10668
+ ],
10669
+ outputs: [],
10670
+ stateMutability: 'payable'
10671
+ }
10672
+ ];
10673
+
10674
+ /**
10675
+ * Minimal ERC-4626 tokenized-vault ABI.
10676
+ *
10677
+ * Covers only the mutating methods EarnKit bundles as inner instructions inside
10678
+ * an Adapter `execute()` call: `deposit`, `withdraw`, and `redeem`. It exists so
10679
+ * clients can decode the inner instruction calldata into a human-readable
10680
+ * summary of what a signer is authorizing (asset amount, receiver, owner)
10681
+ * rather than showing opaque bytes. The 4-byte selectors match the calldata the
10682
+ * earn service signs (`deposit(uint256,address)` = `0x6e553f65`,
10683
+ * `withdraw(uint256,address,address)` = `0xb460af94`,
10684
+ * `redeem(uint256,address,address)` = `0xba087652`).
10685
+ */ const erc4626VaultAbi = [
10686
+ {
10687
+ type: 'function',
10688
+ name: 'deposit',
10689
+ stateMutability: 'nonpayable',
10690
+ inputs: [
10691
+ {
10692
+ name: 'assets',
10693
+ type: 'uint256',
10694
+ internalType: 'uint256'
10695
+ },
10696
+ {
10697
+ name: 'receiver',
10698
+ type: 'address',
10699
+ internalType: 'address'
10700
+ }
10701
+ ],
10702
+ outputs: [
10703
+ {
10704
+ name: 'shares',
10705
+ type: 'uint256',
10706
+ internalType: 'uint256'
10707
+ }
10708
+ ]
10709
+ },
10710
+ {
10711
+ type: 'function',
10712
+ name: 'withdraw',
10713
+ stateMutability: 'nonpayable',
10714
+ inputs: [
10715
+ {
10716
+ name: 'assets',
10717
+ type: 'uint256',
10718
+ internalType: 'uint256'
10719
+ },
10720
+ {
10721
+ name: 'receiver',
10722
+ type: 'address',
10723
+ internalType: 'address'
10724
+ },
10725
+ {
10726
+ name: 'owner',
10727
+ type: 'address',
10728
+ internalType: 'address'
10729
+ }
10730
+ ],
10731
+ outputs: [
10732
+ {
10733
+ name: 'shares',
10734
+ type: 'uint256',
10735
+ internalType: 'uint256'
10736
+ }
10737
+ ]
10738
+ },
10739
+ {
10740
+ type: 'function',
10741
+ name: 'redeem',
10742
+ stateMutability: 'nonpayable',
10743
+ inputs: [
10744
+ {
10745
+ name: 'shares',
10746
+ type: 'uint256',
10747
+ internalType: 'uint256'
10748
+ },
10749
+ {
10750
+ name: 'receiver',
10751
+ type: 'address',
10752
+ internalType: 'address'
10753
+ },
10754
+ {
10755
+ name: 'owner',
10756
+ type: 'address',
10757
+ internalType: 'address'
10758
+ }
10759
+ ],
10760
+ outputs: [
10761
+ {
10762
+ name: 'assets',
10763
+ type: 'uint256',
10764
+ internalType: 'uint256'
10765
+ }
10766
+ ]
10767
+ }
10768
+ ];
10769
+
10770
+ /**
10771
+ * Minimal FeeTaker ABI.
10193
10772
  *
10194
- * @example
10195
- * ```typescript
10196
- * import { AdapterCapabilitiesSchema } from '@core/adapter-evm/validation'
10197
- * import { Ethereum, Base } from '@core/chains'
10773
+ * The earn service appends a `takeFeeERC20` instruction to withdraw bundles
10774
+ * when Circle charges a withdrawal fee. This ABI decodes that inner instruction
10775
+ * so the fee (token, beneficiary, amount) is visible in the signing summary
10776
+ * instead of appearing as opaque calldata alongside the redeem/withdraw call.
10777
+ */ const feeTakerAbi = [
10778
+ {
10779
+ type: 'function',
10780
+ name: 'takeFeeERC20',
10781
+ stateMutability: 'nonpayable',
10782
+ inputs: [
10783
+ {
10784
+ name: 'token',
10785
+ type: 'address',
10786
+ internalType: 'address'
10787
+ },
10788
+ {
10789
+ name: 'beneficiary',
10790
+ type: 'address',
10791
+ internalType: 'address'
10792
+ },
10793
+ {
10794
+ name: 'fee',
10795
+ type: 'uint256',
10796
+ internalType: 'uint256'
10797
+ },
10798
+ {
10799
+ name: 'kitType',
10800
+ type: 'bytes8',
10801
+ internalType: 'bytes8'
10802
+ }
10803
+ ],
10804
+ outputs: []
10805
+ }
10806
+ ];
10807
+
10808
+ /**
10809
+ * Minimal Merkl Distributor ABI.
10810
+ *
10811
+ * EarnKit claim-rewards bundles a single `claim` instruction targeting the
10812
+ * Merkl Distributor, batching one entry per reward token. This ABI decodes that
10813
+ * inner instruction so the claimed tokens and amounts are visible in the signing
10814
+ * summary. `claim` uses dynamic array arguments, which is why a real ABI decoder
10815
+ * (rather than fixed-word slicing) is required for the reward instruction.
10816
+ */ const merklDistributorAbi = [
10817
+ {
10818
+ type: 'function',
10819
+ name: 'claim',
10820
+ stateMutability: 'nonpayable',
10821
+ inputs: [
10822
+ {
10823
+ name: 'users',
10824
+ type: 'address[]',
10825
+ internalType: 'address[]'
10826
+ },
10827
+ {
10828
+ name: 'tokens',
10829
+ type: 'address[]',
10830
+ internalType: 'address[]'
10831
+ },
10832
+ {
10833
+ name: 'amounts',
10834
+ type: 'uint256[]',
10835
+ internalType: 'uint256[]'
10836
+ },
10837
+ {
10838
+ name: 'proofs',
10839
+ type: 'bytes32[][]',
10840
+ internalType: 'bytes32[][]'
10841
+ }
10842
+ ],
10843
+ outputs: []
10844
+ }
10845
+ ];
10846
+
10847
+ /**
10848
+ * Zod schema for validating EVM adapter capabilities.
10849
+ *
10850
+ * This schema ensures that EVM adapter capabilities meet the requirements for the
10851
+ * OperationContext pattern by validating the addressContext property and ensuring
10852
+ * the supportedChains property contains only EVM-compatible chains.
10853
+ *
10854
+ * @remarks
10855
+ * This schema enforces that all supported chains are EVM type, making it specific
10856
+ * to EVM adapters (Ethers, Viem, etc.). For non-EVM adapters, a different validation
10857
+ * schema should be used.
10858
+ *
10859
+ * When used with the validate() function from @core/utils, this throws KitError
10860
+ * with INPUT_VALIDATION_FAILED code if validation fails, with details about which
10861
+ * properties failed.
10862
+ *
10863
+ * @example
10864
+ * ```typescript
10865
+ * import { AdapterCapabilitiesSchema } from '@core/adapter-evm/validation'
10866
+ * import { Ethereum, Base } from '@core/chains'
10198
10867
  *
10199
10868
  * // Valid user-controlled capabilities
10200
10869
  * const userCapabilities = {
@@ -12688,7 +13357,7 @@ new Set(Object.values(Blockchain));
12688
13357
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
12689
13358
 
12690
13359
  var name$1 = "@circle-fin/earn-kit";
12691
- var version$1 = "1.4.0";
13360
+ var version$1 = "1.5.1";
12692
13361
  var pkg$1 = {
12693
13362
  name: name$1,
12694
13363
  version: version$1};
@@ -12935,30 +13604,6 @@ function toSdkChain(chain) {
12935
13604
  return assertHexAddress('chain.kitContracts.adapter', adapterContractAddress, `Adapter contract for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
12936
13605
  }
12937
13606
 
12938
- /**
12939
- * Parse the raw `token.allowance` adapter response into a bigint.
12940
- *
12941
- * @internal
12942
- */ function parseAllowanceResponse(allowanceRaw) {
12943
- if (allowanceRaw === undefined || allowanceRaw === null) {
12944
- return 0n;
12945
- }
12946
- let allowance;
12947
- if (typeof allowanceRaw === 'bigint') {
12948
- allowance = allowanceRaw;
12949
- } else if (typeof allowanceRaw === 'string') {
12950
- try {
12951
- allowance = BigInt(allowanceRaw);
12952
- } catch {
12953
- allowance = undefined;
12954
- }
12955
- }
12956
- if (allowance === undefined || allowance < 0n) {
12957
- throw createValidationFailedError('token.allowance', allowanceRaw, 'token.allowance response must be a non-negative bigint-compatible string or bigint');
12958
- }
12959
- return allowance;
12960
- }
12961
-
12962
13607
  /**
12963
13608
  * Safety multiplier applied to locally estimated gas for earn transactions.
12964
13609
  *
@@ -13180,17 +13825,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
13180
13825
  if (requiredAllowance <= 0n) {
13181
13826
  return undefined;
13182
13827
  }
13183
- const allowancePrepared = await adapter.prepareAction('token.allowance', {
13184
- tokenAddress,
13185
- delegate
13186
- }, {
13187
- chain,
13188
- address
13189
- });
13190
- // Each execute() is a fresh allowance read at `latest`, so the same prepared
13191
- // action serves both the pre-approval decision read and the post-approval
13192
- // propagation polls.
13193
- const readAllowance = async ()=>parseAllowanceResponse(await allowancePrepared.execute());
13828
+ // Each call is a fresh allowance read at `latest`, so the same function serves
13829
+ // both the pre-approval decision and post-approval propagation polls.
13830
+ const readAllowance = async ()=>readTokenAllowance(adapter, {
13831
+ tokenAddress,
13832
+ delegate
13833
+ }, {
13834
+ chain,
13835
+ address
13836
+ });
13194
13837
  const currentAllowance = await readAllowance();
13195
13838
  if (currentAllowance >= requiredAllowance) {
13196
13839
  return undefined;
@@ -13331,6 +13974,645 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
13331
13974
  return approvedToken;
13332
13975
  }
13333
13976
 
13977
+ /**
13978
+ * Combined ABI of every inner instruction EarnKit can bundle inside an Adapter
13979
+ * `execute()` call. `decodeFunctionData` matches an instruction's calldata to
13980
+ * one of these functions by its 4-byte selector.
13981
+ */ const earnInstructionAbi = [
13982
+ ...erc4626VaultAbi,
13983
+ ...feeTakerAbi,
13984
+ ...merklDistributorAbi
13985
+ ];
13986
+ /**
13987
+ * Extract and shallow-validate the `instructions` array from loosely-typed
13988
+ * signed execution params.
13989
+ *
13990
+ * The earn service schema validates `tokenIn`/`amountToApprove` and passes the
13991
+ * remaining instruction fields through untyped, so the params arrive as a plain
13992
+ * record; each accessed field is narrowed at runtime.
13993
+ */ function requireInstructions(executionParams) {
13994
+ const instructions = executionParams['instructions'];
13995
+ if (!Array.isArray(instructions)) {
13996
+ throw decodeMismatchError('execution params are missing an instructions array', {
13997
+ instructions
13998
+ });
13999
+ }
14000
+ return instructions.map((instruction, index)=>{
14001
+ if (typeof instruction !== 'object' || instruction === null) {
14002
+ throw decodeMismatchError(`instructions[${index.toString()}] is not an object`, {
14003
+ index
14004
+ });
14005
+ }
14006
+ return instruction;
14007
+ });
14008
+ }
14009
+ /**
14010
+ * Build a fail-closed {@link KitError} for an earn decode or review failure.
14011
+ *
14012
+ * Marked non-recoverable: a mismatch between what would be shown and what would
14013
+ * be signed is never safe to retry, so the operation fails fast rather than
14014
+ * presenting misleading decoded data. `messagePrefix` names the failing stage
14015
+ * (calldata decode vs. review construction); callers bind it once and pass the
14016
+ * specific failure as `message`.
14017
+ */ function failClosedEarnError(messagePrefix, message, trace) {
14018
+ return new KitError({
14019
+ ...EarnError.INTERNAL_ERROR,
14020
+ recoverability: 'FATAL',
14021
+ message: `${messagePrefix}: ${message}`,
14022
+ cause: {
14023
+ trace
14024
+ }
14025
+ });
14026
+ }
14027
+ /**
14028
+ * Build a {@link KitError} for a decode or consistency failure.
14029
+ *
14030
+ * Thin wrapper over {@link failClosedEarnError} bound to the decode-stage
14031
+ * message prefix.
14032
+ */ function decodeMismatchError(message, trace) {
14033
+ return failClosedEarnError('Unable to decode earn transaction', message, trace);
14034
+ }
14035
+ /**
14036
+ * Narrow an untyped value to a 0x-prefixed hex string, or fail fast.
14037
+ */ function requireHex(value, path) {
14038
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value)) {
14039
+ return value;
14040
+ }
14041
+ throw decodeMismatchError(`${path} is not a hex string`, {
14042
+ path,
14043
+ value
14044
+ });
14045
+ }
14046
+ /**
14047
+ * Narrow an untyped value to a 20-byte EVM address, or fail fast.
14048
+ */ function requireAddress(value, path) {
14049
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) {
14050
+ return value;
14051
+ }
14052
+ throw decodeMismatchError(`${path} is not an address`, {
14053
+ path,
14054
+ value
14055
+ });
14056
+ }
14057
+ /**
14058
+ * Narrow an untyped `uint256`-like value (decimal string, bigint, or integer)
14059
+ * to a bigint, or fail fast.
14060
+ */ function requireUint(value, path) {
14061
+ if (typeof value === 'bigint') {
14062
+ return value;
14063
+ }
14064
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
14065
+ return BigInt(value);
14066
+ }
14067
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
14068
+ return BigInt(value);
14069
+ }
14070
+ throw decodeMismatchError(`${path} is not a uint256 value`, {
14071
+ path,
14072
+ value
14073
+ });
14074
+ }
14075
+ /**
14076
+ * Decode inner instruction calldata against the earn instruction ABI, mapping a
14077
+ * viem decode failure (unknown selector, malformed args) to a fail-fast error.
14078
+ */ function decodeEarnInstructionData(data, index) {
14079
+ try {
14080
+ return decodeFunctionData({
14081
+ abi: earnInstructionAbi,
14082
+ data
14083
+ });
14084
+ } catch (error) {
14085
+ throw decodeMismatchError(`instructions[${index.toString()}] calldata is not a recognized earn instruction`, {
14086
+ index,
14087
+ selector: data.slice(0, 10),
14088
+ error: String(error)
14089
+ });
14090
+ }
14091
+ }
14092
+ /**
14093
+ * Decode Adapter `execute()` calldata, mapping a viem decode failure to a
14094
+ * fail-fast error.
14095
+ */ function decodeExecuteCalldata(calldata) {
14096
+ try {
14097
+ return decodeFunctionData({
14098
+ abi: adapterContractAbi,
14099
+ data: calldata
14100
+ });
14101
+ } catch (error) {
14102
+ throw decodeMismatchError('encoded calldata is not a valid Adapter execute() call', {
14103
+ error: String(error)
14104
+ });
14105
+ }
14106
+ }
14107
+ /**
14108
+ * Decode one inner instruction's calldata into a typed {@link
14109
+ * DecodedEarnInstruction}.
14110
+ */ function decodeInstruction(instruction, index) {
14111
+ const target = requireAddress(instruction['target'], `instructions[${index.toString()}].target`);
14112
+ const data = requireHex(instruction['data'], `instructions[${index.toString()}].data`);
14113
+ const decoded = decodeEarnInstructionData(data, index);
14114
+ switch(decoded.functionName){
14115
+ case 'deposit':
14116
+ {
14117
+ const [assets, receiver] = decoded.args;
14118
+ return {
14119
+ method: 'deposit',
14120
+ vault: target,
14121
+ assets: assets.toString(),
14122
+ receiver
14123
+ };
14124
+ }
14125
+ case 'withdraw':
14126
+ {
14127
+ const [assets, receiver, owner] = decoded.args;
14128
+ return {
14129
+ method: 'withdraw',
14130
+ vault: target,
14131
+ assets: assets.toString(),
14132
+ receiver,
14133
+ owner
14134
+ };
14135
+ }
14136
+ case 'redeem':
14137
+ {
14138
+ const [shares, receiver, owner] = decoded.args;
14139
+ return {
14140
+ method: 'redeem',
14141
+ vault: target,
14142
+ shares: shares.toString(),
14143
+ receiver,
14144
+ owner
14145
+ };
14146
+ }
14147
+ case 'takeFeeERC20':
14148
+ {
14149
+ const [token, beneficiary, fee, kitType] = decoded.args;
14150
+ return {
14151
+ method: 'takeFeeERC20',
14152
+ feeTaker: target,
14153
+ token,
14154
+ beneficiary,
14155
+ fee: fee.toString(),
14156
+ kitType
14157
+ };
14158
+ }
14159
+ case 'claim':
14160
+ {
14161
+ const users = decoded.args[0];
14162
+ const tokens = decoded.args[1];
14163
+ const amounts = decoded.args[2];
14164
+ // Merkl claim(users, tokens, amounts, proofs) carries parallel arrays,
14165
+ // one entry per reward. Reject any length skew rather than padding with
14166
+ // zero amounts or dropping trailing entries, so the preview can never
14167
+ // misstate what is claimed or for whom.
14168
+ //
14169
+ // Note: Merkl `amounts` are the *cumulative lifetime* total claimable per
14170
+ // (user, token); the Distributor transfers only `amount - alreadyClaimed`.
14171
+ // This decode faithfully surfaces the signed cumulative value, which is
14172
+ // what `DecodedRewardClaim.amount` documents. See that type's doc.
14173
+ if (new Set([
14174
+ users.length,
14175
+ tokens.length,
14176
+ amounts.length
14177
+ ]).size !== 1) {
14178
+ throw decodeMismatchError(`instructions[${index.toString()}] claim has mismatched recipient/token/amount lengths`, {
14179
+ index,
14180
+ users: users.length,
14181
+ tokens: tokens.length,
14182
+ amounts: amounts.length
14183
+ });
14184
+ }
14185
+ const rewards = tokens.map((token, rewardIndex)=>({
14186
+ recipient: requireAddress(users[rewardIndex], `instructions[${index.toString()}].claim.users[${rewardIndex.toString()}]`),
14187
+ address: token,
14188
+ amount: requireUint(amounts[rewardIndex], `instructions[${index.toString()}].claim.amounts[${rewardIndex.toString()}]`).toString()
14189
+ }));
14190
+ return {
14191
+ method: 'claim',
14192
+ distributor: target,
14193
+ rewards
14194
+ };
14195
+ }
14196
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
14197
+ return assertNever$2(decoded);
14198
+ }
14199
+ }
14200
+ /**
14201
+ * Lift the primary values a wallet prompt cares about out of the decoded
14202
+ * instructions into a flat summary.
14203
+ */ function buildSummary(instructions, envelope) {
14204
+ const summary = {};
14205
+ instructions.forEach((instruction, index)=>{
14206
+ switch(instruction.method){
14207
+ case 'deposit':
14208
+ case 'withdraw':
14209
+ case 'redeem':
14210
+ {
14211
+ // The summary lifts a single primary token movement to the top level.
14212
+ // An earn bundle carries exactly one deposit/withdraw/redeem today;
14213
+ // fail fast rather than silently overwriting an earlier one, which
14214
+ // would drop it from the wallet-facing preview.
14215
+ if (summary.token !== undefined) {
14216
+ throw decodeMismatchError('multiple deposit/withdraw/redeem instructions cannot be summarized into a single preview', {
14217
+ index
14218
+ });
14219
+ }
14220
+ // Pair the amount with the token it is actually denominated in so the
14221
+ // preview never folds two units into one entry:
14222
+ // - deposit: `assets` of the underlying asset pulled in (`tokenIn`)
14223
+ // - redeem: `shares` of the vault-share token burned (`tokenIn`)
14224
+ // - withdraw: `assets` of the underlying asset paid out (`tokenOut`).
14225
+ // `withdraw(assets)` counts the underlying received, not the shares
14226
+ // burned to produce it, so `tokenIn` (the share token) would misstate
14227
+ // the unit; the underlying is the instruction's `tokenOut`.
14228
+ const amount = instruction.method === 'redeem' ? instruction.shares : instruction.assets;
14229
+ const tokenField = instruction.method === 'withdraw' ? 'tokenOut' : 'tokenIn';
14230
+ summary.vault = instruction.vault;
14231
+ summary.receiver = instruction.receiver;
14232
+ summary.token = {
14233
+ address: requireAddress(envelope[index]?.[tokenField], `instructions[${index.toString()}].${tokenField}`),
14234
+ amount
14235
+ };
14236
+ break;
14237
+ }
14238
+ case 'takeFeeERC20':
14239
+ {
14240
+ // As with the vault case, a second fee would silently overwrite the
14241
+ // first and understate what is charged; fail fast instead.
14242
+ if (summary.fee !== undefined) {
14243
+ throw decodeMismatchError('multiple fee instructions cannot be summarized into a single preview', {
14244
+ index
14245
+ });
14246
+ }
14247
+ summary.fee = {
14248
+ address: instruction.token,
14249
+ amount: instruction.fee
14250
+ };
14251
+ break;
14252
+ }
14253
+ case 'claim':
14254
+ {
14255
+ // A second claim would silently drop the first from the preview
14256
+ // (rewards are already batched inside one Merkl claim); fail fast.
14257
+ if (summary.rewards !== undefined) {
14258
+ throw decodeMismatchError('multiple claim instructions cannot be summarized into a single preview', {
14259
+ index
14260
+ });
14261
+ }
14262
+ summary.rewards = instruction.rewards;
14263
+ break;
14264
+ }
14265
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
14266
+ assertNever$2(instruction);
14267
+ }
14268
+ });
14269
+ return summary;
14270
+ }
14271
+ /**
14272
+ * Vault/claim instruction methods each declared action may decode to. The
14273
+ * mapping is many-to-one: a full withdrawal decodes to `redeem`, and any action
14274
+ * may carry an auxiliary `takeFeeERC20` alongside its primary instruction.
14275
+ */ const ACTION_ALLOWED_METHODS = {
14276
+ deposit: new Set([
14277
+ 'deposit'
14278
+ ]),
14279
+ withdraw: new Set([
14280
+ 'withdraw',
14281
+ 'redeem'
14282
+ ]),
14283
+ claimRewards: new Set([
14284
+ 'claim'
14285
+ ])
14286
+ };
14287
+ /**
14288
+ * Fail fast when the caller-declared `action` disagrees with the decoded
14289
+ * instructions, so the preview's headline can never mislabel what is signed
14290
+ * (e.g. a `deposit`-labeled call handed withdraw params). `takeFeeERC20` is an
14291
+ * auxiliary Circle-fee instruction and is allowed alongside any action.
14292
+ */ function assertActionMatchesInstructions(action, instructions) {
14293
+ const allowed = ACTION_ALLOWED_METHODS[action];
14294
+ instructions.forEach((instruction, index)=>{
14295
+ if (instruction.method === 'takeFeeERC20') {
14296
+ return;
14297
+ }
14298
+ if (!allowed.has(instruction.method)) {
14299
+ throw decodeMismatchError(`decoded instruction method '${instruction.method}' does not match the '${action}' action`, {
14300
+ action,
14301
+ method: instruction.method,
14302
+ index
14303
+ });
14304
+ }
14305
+ });
14306
+ }
14307
+ /**
14308
+ * Decode a same-chain earn `execute()` bundle into a human-readable summary.
14309
+ *
14310
+ * Decodes every inner instruction in the service-signed `executionParams` — the
14311
+ * same object the SDK ABI-encodes into the transaction — so the returned decode
14312
+ * is a faithful, drift-free view of what the signer is authorizing: input token
14313
+ * and amount, target vault, receiver, any Circle fee, and claimed rewards. Fails
14314
+ * fast with a non-recoverable {@link KitError} if any instruction cannot be
14315
+ * decoded, rather than returning misleading data.
14316
+ *
14317
+ * @param input - Action, chain, adapter, and the signed execution params.
14318
+ * @returns The decoded transaction summary.
14319
+ * @throws {@link KitError} If an instruction's calldata cannot be decoded.
14320
+ *
14321
+ * @example
14322
+ * ```typescript
14323
+ * const decoded = decodeEarnExecute({
14324
+ * action: 'deposit',
14325
+ * chain: 'Arc_Testnet',
14326
+ * adapter: '0x7fb8c7260b63934d8da38af902f87ae6e284a845',
14327
+ * executionParams,
14328
+ * })
14329
+ * // decoded.summary -> { token: { address, amount }, vault, receiver }
14330
+ * ```
14331
+ *
14332
+ * @internal
14333
+ */ function decodeEarnExecute(input) {
14334
+ const { action, chain, adapter, executionParams } = input;
14335
+ const envelope = requireInstructions(executionParams);
14336
+ const instructions = envelope.map((instruction, index)=>decodeInstruction(instruction, index));
14337
+ assertActionMatchesInstructions(action, instructions);
14338
+ return {
14339
+ action,
14340
+ chain,
14341
+ adapter,
14342
+ instructions,
14343
+ summary: buildSummary(instructions, envelope)
14344
+ };
14345
+ }
14346
+ /**
14347
+ * Assert that ABI-encoded Adapter `execute()` calldata encodes the same
14348
+ * instruction set as the service-signed execution params.
14349
+ *
14350
+ * Fail-fast preview check: the SDK encodes `execute(executeParams, ...)` locally,
14351
+ * so decoding those final bytes and comparing every field of each instruction
14352
+ * against the signed params proves the previewed instruction set matches what
14353
+ * will be signed. It compares `instructions[]` only — the outer `tokens`,
14354
+ * `execId`, `deadline`, and `metadata` are not re-compared here. The
14355
+ * authoritative integrity guarantee for the full signed struct is the on-chain
14356
+ * EIP-712 signature verification, which reverts if any signed field is altered.
14357
+ *
14358
+ * @param calldata - Encoded `execute()` calldata about to be signed.
14359
+ * @param executionParams - Service-signed execution params.
14360
+ * @throws {@link KitError} If the calldata is not an `execute()` call or any
14361
+ * instruction field differs from the signed params.
14362
+ *
14363
+ * @example
14364
+ * ```typescript
14365
+ * assertEarnCalldataMatchesExecuteParams(
14366
+ * prepared.getCallData().data,
14367
+ * executionParams,
14368
+ * )
14369
+ * ```
14370
+ *
14371
+ * @internal
14372
+ */ function assertEarnCalldataMatchesExecuteParams(calldata, executionParams) {
14373
+ // adapterContractAbi declares only `execute`, so a successful decode is always
14374
+ // the execute() call; a non-execute selector throws inside
14375
+ // decodeExecuteCalldata above.
14376
+ const decoded = decodeExecuteCalldata(calldata);
14377
+ const encoded = decoded.args[0].instructions;
14378
+ const signed = requireInstructions(executionParams);
14379
+ // Compare each encoded instruction against its signed counterpart. Iterating
14380
+ // the encoded instructions and indexing the signed set keeps both mismatch
14381
+ // branches reachable: a signed set that is too short trips the guard below,
14382
+ // and one that is too long trips the post-loop check.
14383
+ encoded.forEach((instruction, index)=>{
14384
+ const signedInstruction = signed[index];
14385
+ if (signedInstruction === undefined) {
14386
+ throw decodeMismatchError(`signed params are missing instruction ${index.toString()}`, {
14387
+ index,
14388
+ encoded: encoded.length,
14389
+ signed: signed.length
14390
+ });
14391
+ }
14392
+ const path = `instructions[${index.toString()}]`;
14393
+ assertHexEqual(instruction.target, signedInstruction['target'], `${path}.target`);
14394
+ assertHexEqual(instruction.data, signedInstruction['data'], `${path}.data`);
14395
+ assertUintEqual(instruction.value, signedInstruction['value'], `${path}.value`);
14396
+ assertHexEqual(instruction.tokenIn, signedInstruction['tokenIn'], `${path}.tokenIn`);
14397
+ assertUintEqual(instruction.amountToApprove, signedInstruction['amountToApprove'], `${path}.amountToApprove`);
14398
+ assertHexEqual(instruction.tokenOut, signedInstruction['tokenOut'], `${path}.tokenOut`);
14399
+ assertUintEqual(instruction.minTokenOut, signedInstruction['minTokenOut'], `${path}.minTokenOut`);
14400
+ });
14401
+ if (signed.length > encoded.length) {
14402
+ throw decodeMismatchError('signed params contain more instructions than the encoded calldata', {
14403
+ encoded: encoded.length,
14404
+ signed: signed.length
14405
+ });
14406
+ }
14407
+ }
14408
+ /**
14409
+ * Assert two hex values are equal, case-insensitively (addresses and calldata).
14410
+ */ function assertHexEqual(encoded, signed, path) {
14411
+ const signedHex = requireHex(signed, path);
14412
+ if (encoded.toLowerCase() !== signedHex.toLowerCase()) {
14413
+ throw decodeMismatchError(`${path} differs from signed params`, {
14414
+ path,
14415
+ encoded,
14416
+ signed: signedHex
14417
+ });
14418
+ }
14419
+ }
14420
+ /**
14421
+ * Assert an encoded bigint equals a signed `uint256`-like value.
14422
+ */ function assertUintEqual(encoded, signed, path) {
14423
+ const signedUint = requireUint(signed, path);
14424
+ if (encoded !== signedUint) {
14425
+ throw decodeMismatchError(`${path} differs from signed params`, {
14426
+ path,
14427
+ encoded: encoded.toString(),
14428
+ signed: signedUint.toString()
14429
+ });
14430
+ }
14431
+ }
14432
+
14433
+ /**
14434
+ * Namespaced discriminator for the EarnKit authorization review.
14435
+ *
14436
+ * Applications match on this in an adapter `onBeforeAuthorize` hook to decide
14437
+ * whether the request carries EarnKit semantic data. Prefer the
14438
+ * {@link isEarnExecuteReview} type guard over comparing this string directly.
14439
+ *
14440
+ * @example
14441
+ * ```typescript
14442
+ * if (review?.kind === EARN_EXECUTE_REVIEW_KIND) { … }
14443
+ * ```
14444
+ */ const EARN_EXECUTE_REVIEW_KIND = 'earn.execute';
14445
+
14446
+ /**
14447
+ * Build a fail-closed {@link KitError} for a review-construction failure.
14448
+ *
14449
+ * Marked non-recoverable: a review that cannot prove the calldata matches the
14450
+ * signed operation must abort authorization, never retry with misleading data.
14451
+ */ function reviewError(message, trace) {
14452
+ return failClosedEarnError('Unable to build earn authorization review', message, trace);
14453
+ }
14454
+ /**
14455
+ * Narrow the canonical authorization payload to the single Adapter `execute()`
14456
+ * call a same-chain earn operation authorizes.
14457
+ *
14458
+ * Same-chain deposit, withdraw, and claim-rewards each authorize exactly one
14459
+ * `evm-calls` payload carrying one call. Anything else (typed data, a batch,
14460
+ * an empty call list) means this descriptor was attached to the wrong
14461
+ * authorization unit, so fail closed rather than decode misleading data.
14462
+ */ function assertSingleEvmCallPayload(payload) {
14463
+ if (payload.type !== 'evm-calls') {
14464
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
14465
+ type: payload.type
14466
+ });
14467
+ }
14468
+ const [call, ...rest] = payload.calls;
14469
+ if (call === undefined) {
14470
+ throw reviewError('the evm-calls payload contains no calls to review', {
14471
+ callCount: payload.calls.length
14472
+ });
14473
+ }
14474
+ if (rest.length > 0) {
14475
+ throw reviewError('a same-chain earn operation authorizes exactly one Adapter execute() call', {
14476
+ callCount: payload.calls.length
14477
+ });
14478
+ }
14479
+ return call;
14480
+ }
14481
+ /**
14482
+ * Select the final earn `execute()` call from an atomic Earn batch.
14483
+ *
14484
+ * Same-chain batched deposit/withdraw authorizes either `[execute]` when the
14485
+ * current allowance is sufficient, or `[approve, execute]` when a top-up is
14486
+ * required. Any other shape means the descriptor was attached to an
14487
+ * unexpected authorization unit, so fail closed.
14488
+ */ function assertBatchedEarnExecuteCall(payload) {
14489
+ if (payload.type !== 'evm-calls') {
14490
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
14491
+ type: payload.type
14492
+ });
14493
+ }
14494
+ if (payload.calls.length !== 1 && payload.calls.length !== 2) {
14495
+ throw reviewError('a batched earn operation authorizes [execute] or [approve, execute]', {
14496
+ callCount: payload.calls.length
14497
+ });
14498
+ }
14499
+ const executeCall = payload.calls.at(-1);
14500
+ if (executeCall === undefined) {
14501
+ throw reviewError('the earn batch contains no execute call to review', {
14502
+ callCount: payload.calls.length
14503
+ });
14504
+ }
14505
+ return executeCall;
14506
+ }
14507
+ /**
14508
+ * Map a canonical {@link EvmCall} to the {@link EarnEncodedTransaction} preview
14509
+ * shape, failing closed when the earn `execute()` call carries no calldata.
14510
+ */ function toEarnEncodedTransaction(call) {
14511
+ if (call.data === undefined) {
14512
+ throw reviewError('the earn execute() call is missing calldata', {
14513
+ to: call.to
14514
+ });
14515
+ }
14516
+ return {
14517
+ to: call.to,
14518
+ data: call.data,
14519
+ ...call.value !== undefined && {
14520
+ value: call.value
14521
+ }
14522
+ };
14523
+ }
14524
+ /**
14525
+ * Create an Earn authorization descriptor using the supplied canonical-payload
14526
+ * call selector.
14527
+ *
14528
+ * @param input - The action, chain, and service-signed execution params.
14529
+ * @param selectCall - Fail-closed selector for the execute call under review.
14530
+ * @returns A lazy descriptor that decodes and verifies the selected call.
14531
+ *
14532
+ * @internal
14533
+ */ function createEarnExecuteDescriptor(input, selectCall) {
14534
+ const { action, chain, executionParams } = input;
14535
+ const createReview = (payload)=>{
14536
+ const call = selectCall(payload);
14537
+ const encoded = toEarnEncodedTransaction(call);
14538
+ const decoded = decodeEarnExecute({
14539
+ action,
14540
+ chain,
14541
+ adapter: encoded.to,
14542
+ executionParams
14543
+ });
14544
+ // Prove the calldata about to be signed encodes the same instruction set as
14545
+ // the service-signed params. Throwing here aborts before the wallet prompt.
14546
+ assertEarnCalldataMatchesExecuteParams(encoded.data, executionParams);
14547
+ const review = {
14548
+ kind: EARN_EXECUTE_REVIEW_KIND,
14549
+ data: {
14550
+ encoded,
14551
+ decoded
14552
+ }
14553
+ };
14554
+ return review;
14555
+ };
14556
+ return {
14557
+ createReview
14558
+ };
14559
+ }
14560
+ /**
14561
+ * Build the lazy `earn.execute` authorization descriptor for a same-chain earn
14562
+ * action.
14563
+ *
14564
+ * The returned descriptor carries only a `createReview` factory — no intent
14565
+ * override, because the adapter's action system supplies the intent from the
14566
+ * action key. The factory is evaluated at most once, and only when the
14567
+ * application configured an adapter `onBeforeAuthorize` hook. When it runs it:
14568
+ *
14569
+ * 1. narrows the canonical payload to its single Adapter `execute()` call;
14570
+ * 2. lifts that call into an {@link EarnEncodedTransaction};
14571
+ * 3. decodes it into a `DecodedEarnTx`; and
14572
+ * 4. asserts the decoded calldata matches the service-signed params, throwing
14573
+ * (aborting authorization before the wallet or signer) on any mismatch.
14574
+ *
14575
+ * @param input - The action, chain, and service-signed execution params.
14576
+ * @returns An authorization descriptor to pass as the fourth `prepareAction`
14577
+ * argument for the final earn action only (never the allowance approval).
14578
+ * @throws {@link KitError} From the review factory when the payload is not a
14579
+ * single earn `execute()` call or the calldata diverges from the signed
14580
+ * params. The throw surfaces through the adapter gate before authorization.
14581
+ *
14582
+ * @example
14583
+ * ```typescript
14584
+ * const descriptor = buildEarnExecuteDescriptor({
14585
+ * action: 'deposit',
14586
+ * chain: 'Arc_Testnet',
14587
+ * executionParams,
14588
+ * })
14589
+ * await adapter.prepareAction('earn.deposit', actionParams, ctx, {
14590
+ * authorization: descriptor,
14591
+ * })
14592
+ * ```
14593
+ *
14594
+ * @internal
14595
+ */ function buildEarnExecuteDescriptor(input) {
14596
+ return createEarnExecuteDescriptor(input, assertSingleEvmCallPayload);
14597
+ }
14598
+ /**
14599
+ * Build a lazy `earn.execute` authorization descriptor for an atomic Earn
14600
+ * batch containing either `[execute]` or `[approve, execute]`.
14601
+ *
14602
+ * The review always decodes and verifies the final call against the
14603
+ * service-signed execution params. Unexpected payload types and call counts
14604
+ * fail closed before wallet authorization.
14605
+ *
14606
+ * @param input - The action, chain, and service-signed execution params.
14607
+ * @returns A descriptor suitable for `batchExecute` authorization options.
14608
+ * @throws {@link KitError} From the lazy review factory when the batch shape or
14609
+ * final execute calldata cannot be verified.
14610
+ *
14611
+ * @internal
14612
+ */ function buildBatchedEarnExecuteDescriptor(input) {
14613
+ return createEarnExecuteDescriptor(input, assertBatchedEarnExecuteCall);
14614
+ }
14615
+
13334
14616
  /**
13335
14617
  * Prepare an earn adapter action, execute it, wait for confirmation, and
13336
14618
  * throw a structured revert error if the receipt status is `'reverted'`.
@@ -13357,16 +14639,37 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
13357
14639
  * address,
13358
14640
  * actionKey: 'earn.deposit',
13359
14641
  * actionParams: { executeParams, tokenInputs, signature },
14642
+ * action: 'deposit',
14643
+ * executionParams,
13360
14644
  * revertMessage: 'Earn deposit reverted on-chain',
13361
14645
  * })
13362
14646
  * ```
13363
14647
  *
13364
14648
  * @internal
13365
14649
  */ async function executeEarnAction(params) {
13366
- const { adapter, chain, address, actionKey, actionParams, revertMessage } = params;
14650
+ const { adapter, chain, address, actionKey, actionParams, action, executionParams, revertMessage } = params;
14651
+ // Attach the lazy `earn.execute` review to the final earn action only (never
14652
+ // the allowance approval, which runs on a separate path). The adapter's
14653
+ // action system supplies the intent from `actionKey`, so the descriptor
14654
+ // carries only the review factory. The factory is evaluated at most once,
14655
+ // and only when the application configured an `onBeforeAuthorize` hook.
14656
+ const authorization = buildEarnExecuteDescriptor({
14657
+ action,
14658
+ // The provider validates the chain is Earn-supported in
14659
+ // `resolveAdapterContext` before reaching execute, so the concrete chain
14660
+ // identifier is a valid `EarnChainIdentifier`. It is carried through to the
14661
+ // decoded preview's display `chain` field only.
14662
+ chain: chain.chain,
14663
+ executionParams
14664
+ });
14665
+ // The abstract `Adapter.prepareAction` is 3-arg; the fourth authorization
14666
+ // argument lives on the `withLegacyCompat` wrapper that produced the concrete
14667
+ // adapter passed here. Narrow the single seam that threads the descriptor.
13367
14668
  const prepared = await adapter.prepareAction(actionKey, actionParams, {
13368
14669
  chain,
13369
14670
  address
14671
+ }, {
14672
+ authorization
13370
14673
  });
13371
14674
  const gasLimitOverride = await estimateBufferedGasLimit(prepared);
13372
14675
  const txHash = prepared.type === 'evm' && gasLimitOverride !== undefined ? await prepared.execute({
@@ -13391,6 +14694,277 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
13391
14694
  };
13392
14695
  }
13393
14696
 
14697
+ /**
14698
+ * Decide whether a same-chain earn action should be submitted as a single
14699
+ * atomic batch.
14700
+ *
14701
+ * Returns `true` only when the consumer has not opted out
14702
+ * (`batchTransactions !== false`), the source chain is EVM, the adapter
14703
+ * structurally exposes the shared batch methods, and the wallet reports atomic
14704
+ * batch support. `address` is forwarded as `fromAddress` so developer-controlled
14705
+ * adapters can probe the specific wallet. Any thrown capability probe is
14706
+ * treated as "no support".
14707
+ *
14708
+ * @param params - Adapter, chain, address, and the resolved `batchTransactions` flag.
14709
+ * @returns `true` when batched execution should be attempted.
14710
+ *
14711
+ * @example
14712
+ * ```typescript
14713
+ * if (await shouldUseBatchedEarnAction({ adapter, chain, address, batchTransactions })) {
14714
+ * // take the batched approve + execute path
14715
+ * }
14716
+ * ```
14717
+ *
14718
+ * @internal
14719
+ */ async function shouldUseBatchedEarnAction(params) {
14720
+ const { adapter, chain, address, batchTransactions } = params;
14721
+ if (batchTransactions === false) {
14722
+ return false;
14723
+ }
14724
+ if (chain.type !== 'evm') {
14725
+ return false;
14726
+ }
14727
+ const candidate = adapter;
14728
+ if (typeof candidate.supportsAtomicBatch !== 'function' || typeof candidate.batchExecute !== 'function') {
14729
+ return false;
14730
+ }
14731
+ try {
14732
+ return await candidate.supportsAtomicBatch(chain, {
14733
+ fromAddress: address
14734
+ });
14735
+ } catch {
14736
+ return false;
14737
+ }
14738
+ }
14739
+ async function buildSuccessfulBatchResult(adapter, chain, receipt, batchId, revertMessage) {
14740
+ const transaction = {
14741
+ txHash: receipt.txHash,
14742
+ explorerUrl: buildExplorerUrl(chain, receipt.txHash)
14743
+ };
14744
+ let confirmed;
14745
+ try {
14746
+ confirmed = await adapter.waitForTransaction(receipt.txHash, {
14747
+ confirmations: 1
14748
+ }, chain);
14749
+ } catch {
14750
+ // The batch adapter already confirmed success. Receipt enrichment is
14751
+ // telemetry-only, so an additional RPC failure must not turn an accepted
14752
+ // money-moving operation into a retryable business failure.
14753
+ return transaction;
14754
+ }
14755
+ if (confirmed.status === 'reverted') {
14756
+ throw createTransactionRevertedError(chain.name, revertMessage, {
14757
+ batchId
14758
+ }, receipt.txHash, transaction.explorerUrl);
14759
+ }
14760
+ return {
14761
+ ...transaction,
14762
+ ...confirmed.gasUsed !== undefined && {
14763
+ gasUsed: confirmed.gasUsed
14764
+ },
14765
+ ...confirmed.effectiveGasPrice !== undefined && {
14766
+ effectiveGasPrice: confirmed.effectiveGasPrice
14767
+ }
14768
+ };
14769
+ }
14770
+ function throwBatchFailure(result, executeReceipt, chain, actionKey, revertMessage) {
14771
+ const cause = result.error;
14772
+ if (result.statusCode === 400) {
14773
+ throw new KitError({
14774
+ ...RpcError.ENDPOINT_ERROR,
14775
+ recoverability: 'RETRYABLE',
14776
+ message: `Batched earn ${actionKey} failed off-chain before inclusion (batch ${result.batchId}).`,
14777
+ cause: {
14778
+ trace: {
14779
+ batchId: result.batchId,
14780
+ statusCode: result.statusCode,
14781
+ cause
14782
+ }
14783
+ }
14784
+ });
14785
+ }
14786
+ const causeTrace = cause instanceof KitError && typeof cause.cause?.trace === 'object' && cause.cause.trace !== null ? cause.cause.trace : undefined;
14787
+ if (cause instanceof KitError && causeTrace?.['kind'] === 'failed_offchain') {
14788
+ throw cause;
14789
+ }
14790
+ const isConfirmedRevert = cause instanceof KitError && cause.name === OnchainError.TRANSACTION_REVERTED.name || result.statusCode === 500 || result.statusCode === 600 || result.statusCode === undefined && cause === undefined && executeReceipt?.status === 'error' && executeReceipt.txHash !== '';
14791
+ if (isConfirmedRevert) {
14792
+ throw createTransactionRevertedError(chain.name, revertMessage, {
14793
+ batchId: result.batchId,
14794
+ error: cause
14795
+ });
14796
+ }
14797
+ throw new KitError({
14798
+ ...NetworkError.TIMEOUT,
14799
+ recoverability: 'FATAL',
14800
+ message: `Batched earn ${actionKey} was submitted (batch ${result.batchId}) but its outcome could not be confirmed; check the transaction status before retrying.`,
14801
+ cause: {
14802
+ trace: {
14803
+ batchId: result.batchId,
14804
+ cause
14805
+ }
14806
+ }
14807
+ });
14808
+ }
14809
+ /**
14810
+ * Execute the `approve` and `execute` steps of a same-chain earn action as a
14811
+ * single atomic batch.
14812
+ *
14813
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw call
14814
+ * data via `getCallData()`, then submit both through the adapter's shared
14815
+ * `batchExecute`. `address` is forwarded as `opts.fromAddress` so
14816
+ * developer-controlled adapters batch on behalf of the right wallet;
14817
+ * `idempotencyKey` is forwarded for adapters that deduplicate ambiguous
14818
+ * submissions (the Circle developer-controlled adapter reuses the Earn
14819
+ * execution id); other adapters may ignore either option. Reused by both the
14820
+ * deposit and withdraw flows via the `actionKey` parameter.
14821
+ *
14822
+ * @param params - Adapter, chain, action key, signed payload, and approval inputs.
14823
+ * @returns The confirmed execute transaction hash, explorer URL, and receipt
14824
+ * gas data when the adapter can retrieve it.
14825
+ * @throws {@link KitError} when the source chain is not EVM.
14826
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not
14827
+ * supported by the prepared requests.
14828
+ * @throws {@link KitError} when the batch reverts on-chain (a confirmed
14829
+ * terminal revert), carrying `batchId`.
14830
+ * @throws {@link KitError} RETRYABLE when EIP-5792 reports an off-chain
14831
+ * failure carrying `batchId` and status code `400`; no call was included.
14832
+ * @throws {@link KitError} FATAL `NetworkError.TIMEOUT` when the batch was
14833
+ * submitted but its outcome could not be confirmed (poll timeout or any
14834
+ * other non-revert post-submission failure); carries `batchId` so the caller
14835
+ * can check transaction status before retrying.
14836
+ * @remarks
14837
+ * Once the batch has been submitted this function does not fall back to the
14838
+ * sequential path — the batch is already on its way, so a fallback would risk
14839
+ * double-spend. Post-submission failures surface through the adapter's batch
14840
+ * result: a confirmed on-chain revert (Circle: a `TRANSACTION_REVERTED` cause;
14841
+ * Viem: status code `500`/`600`) is thrown as a revert error, status code `400`
14842
+ * is reported as a retryable off-chain failure, and any other unconfirmed
14843
+ * outcome is thrown as a FATAL timeout error carrying `batchId`.
14844
+ *
14845
+ * @example
14846
+ * ```typescript
14847
+ * const { txHash, explorerUrl } = await executeBatchedEarnAction({
14848
+ * adapter,
14849
+ * chain,
14850
+ * address,
14851
+ * actionKey: 'earn.deposit',
14852
+ * executeParams,
14853
+ * tokenInputs,
14854
+ * signature,
14855
+ * approvalToken: usdcAddress,
14856
+ * delegate: adapterContractAddress,
14857
+ * requiredAllowance: 1_000_000n,
14858
+ * idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
14859
+ * revertMessage: 'Earn deposit reverted on-chain',
14860
+ * })
14861
+ * ```
14862
+ *
14863
+ * @internal
14864
+ */ async function executeBatchedEarnAction(params) {
14865
+ const { adapter, chain, address, actionKey, executeParams, tokenInputs, signature, approvalToken, delegate, requiredAllowance, idempotencyKey, revertMessage } = params;
14866
+ if (chain.type !== 'evm') {
14867
+ throw new KitError({
14868
+ ...InputError.INVALID_CHAIN,
14869
+ recoverability: 'FATAL',
14870
+ message: 'Batched earn execution is only supported on EVM chains.'
14871
+ });
14872
+ }
14873
+ const evmChain = chain;
14874
+ const batchAdapter = adapter;
14875
+ // Read the current allowance so the approval tops up only the missing amount.
14876
+ // When the existing allowance already covers the payload, skip the approve
14877
+ // call and batch only the execute — this mirrors the sequential
14878
+ // approveAllowanceIfNeeded guard and avoids an increaseAllowance underflow
14879
+ // (requiredAllowance - currentAllowance would be negative, which reverts as
14880
+ // an out-of-range uint256).
14881
+ const currentAllowance = await readTokenAllowance(adapter, {
14882
+ tokenAddress: approvalToken,
14883
+ delegate
14884
+ }, {
14885
+ chain,
14886
+ address
14887
+ });
14888
+ const approvalNeeded = currentAllowance < requiredAllowance;
14889
+ const executePrepared = await adapter.prepareAction(actionKey, {
14890
+ executeParams,
14891
+ tokenInputs,
14892
+ signature
14893
+ }, {
14894
+ chain,
14895
+ address
14896
+ });
14897
+ const approvePrepared = approvalNeeded ? await prepareApprovalAction({
14898
+ adapter,
14899
+ chain,
14900
+ address,
14901
+ tokenAddress: approvalToken,
14902
+ delegate,
14903
+ currentAllowance,
14904
+ requiredAllowance
14905
+ }) : undefined;
14906
+ if (executePrepared.type !== 'evm' || !executePrepared.getCallData) {
14907
+ throw new KitError({
14908
+ ...InputError.UNSUPPORTED_ACTION,
14909
+ recoverability: 'FATAL',
14910
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
14911
+ });
14912
+ }
14913
+ if (approvePrepared !== undefined && (approvePrepared.type !== 'evm' || !approvePrepared.getCallData)) {
14914
+ throw new KitError({
14915
+ ...InputError.UNSUPPORTED_ACTION,
14916
+ recoverability: 'FATAL',
14917
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
14918
+ });
14919
+ }
14920
+ const executeCallData = executePrepared.getCallData();
14921
+ // Prepend the approve call only when an allowance top-up is required.
14922
+ const calls = approvePrepared?.type === 'evm' && approvePrepared.getCallData ? [
14923
+ approvePrepared.getCallData(),
14924
+ executeCallData
14925
+ ] : [
14926
+ executeCallData
14927
+ ];
14928
+ const authorization = buildBatchedEarnExecuteDescriptor({
14929
+ action: actionKey === 'earn.deposit' ? 'deposit' : 'withdraw',
14930
+ chain: evmChain.chain,
14931
+ executionParams: executeParams
14932
+ });
14933
+ const result = await batchAdapter.batchExecute(calls, evmChain, {
14934
+ fromAddress: address,
14935
+ idempotencyKey,
14936
+ atomicRequired: true,
14937
+ authorization
14938
+ });
14939
+ // Success fans one confirmed hash across every receipt; the execute call is
14940
+ // the last one (approve, if present, precedes it). On failure a confirming
14941
+ // adapter returns no receipts, so a missing/non-success last receipt — or a
14942
+ // populated `result.error` — means the batch failed after submission (point
14943
+ // of no return). We never fall back, which would double-spend.
14944
+ const receiptCountMatches = result.receipts.length === calls.length;
14945
+ const executeReceipt = receiptCountMatches ? result.receipts[calls.length - 1] : undefined;
14946
+ const succeeded = receiptCountMatches && (result.statusCode === undefined || result.statusCode === 200) && result.error === undefined && executeReceipt?.status === 'success' && executeReceipt.txHash !== '';
14947
+ if (succeeded) {
14948
+ return buildSuccessfulBatchResult(adapter, evmChain, executeReceipt, result.batchId, revertMessage);
14949
+ }
14950
+ // Distinguish an off-chain rejection, a confirmed on-chain revert, and an
14951
+ // unknown outcome across both adapter conventions that share this contract:
14952
+ // - Circle SCA: no receipts + `error`; its trace kind identifies an
14953
+ // off-chain rejection, confirmed revert, or unconfirmed outcome.
14954
+ // - Viem EIP-5792: statusCode 500/600 explicitly confirms an on-chain
14955
+ // full/partial revert.
14956
+ // - Legacy/string-status wallets: a real-hash error receipt with no cause
14957
+ // is the best available confirmed-revert signal.
14958
+ // statusCode 400 is terminal but off-chain: the wallet confirms no call was
14959
+ // included, so it must not be labeled as a revert or unknown outcome.
14960
+ // Anything else — a poll timeout or any other post-submission failure with no
14961
+ // confirmed-revert signal — means the batch was submitted but its fate is
14962
+ // unconfirmed. Surface that as a FATAL (non-auto-retry) error carrying
14963
+ // `batchId` so the caller checks status before retrying, rather than
14964
+ // mislabeling it a revert.
14965
+ return throwBatchFailure(result, executeReceipt, evmChain, actionKey, revertMessage);
14966
+ }
14967
+
13394
14968
  /**
13395
14969
  * Validate that a service-signed execution payload has not expired before
13396
14970
  * the SDK asks the wallet to broadcast a transaction.
@@ -14687,7 +16261,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
14687
16261
  }
14688
16262
 
14689
16263
  var name = "@circle-fin/provider-earn-service";
14690
- var version = "1.3.1";
16264
+ var version = "1.4.1";
14691
16265
  var pkg = {
14692
16266
  name: name,
14693
16267
  version: version};
@@ -16315,6 +17889,46 @@ function finishElapsedWait(lastStatus, lastError) {
16315
17889
  const approvalToken = resolveEarnApprovalToken(executionParams);
16316
17890
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
16317
17891
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
17892
+ const approvalNeeded = !options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n;
17893
+ // Batch-capable wallets bundle approve + deposit into one atomic
17894
+ // submission. Only attempt this when an approval is actually needed.
17895
+ if (approvalNeeded && approvalToken !== undefined && await shouldUseBatchedEarnAction({
17896
+ adapter,
17897
+ chain,
17898
+ address,
17899
+ batchTransactions: config?.batchTransactions
17900
+ })) {
17901
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
17902
+ try {
17903
+ const result = await executeBatchedEarnAction({
17904
+ adapter,
17905
+ chain,
17906
+ address,
17907
+ actionKey: 'earn.deposit',
17908
+ executeParams: executionParams,
17909
+ tokenInputs,
17910
+ signature,
17911
+ approvalToken,
17912
+ delegate: adapterContractAddress,
17913
+ requiredAllowance,
17914
+ idempotencyKey: execId,
17915
+ revertMessage: 'Earn deposit reverted on-chain'
17916
+ });
17917
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
17918
+ return result;
17919
+ } catch (error) {
17920
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
17921
+ throw error;
17922
+ }
17923
+ }, ({ txHash })=>txHash);
17924
+ return {
17925
+ kind: 'same-chain',
17926
+ txHash,
17927
+ explorerUrl,
17928
+ vaultAddress,
17929
+ amount: params.amount
17930
+ };
17931
+ }
16318
17932
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
16319
17933
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16320
17934
  try {
@@ -16347,6 +17961,8 @@ function finishElapsedWait(lastStatus, lastError) {
16347
17961
  tokenInputs,
16348
17962
  signature
16349
17963
  },
17964
+ action: 'deposit',
17965
+ executionParams,
16350
17966
  revertMessage: 'Earn deposit reverted on-chain'
16351
17967
  });
16352
17968
  reportTransactionSuccess(transactionReportContext, 'Deposit', result);
@@ -16478,6 +18094,44 @@ function finishElapsedWait(lastStatus, lastError) {
16478
18094
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
16479
18095
  const approvalToken = tokenInputs[0]?.token;
16480
18096
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
18097
+ // Batch-capable wallets bundle approve + withdraw into one atomic
18098
+ // submission. Only attempt this when an approval is actually needed.
18099
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n && await shouldUseBatchedEarnAction({
18100
+ adapter,
18101
+ chain,
18102
+ address,
18103
+ batchTransactions: config?.batchTransactions
18104
+ })) {
18105
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
18106
+ try {
18107
+ const result = await executeBatchedEarnAction({
18108
+ adapter,
18109
+ chain,
18110
+ address,
18111
+ actionKey: 'earn.withdraw',
18112
+ executeParams: executionParams,
18113
+ tokenInputs,
18114
+ signature,
18115
+ approvalToken,
18116
+ delegate: adapterContractAddress,
18117
+ requiredAllowance,
18118
+ idempotencyKey: execId,
18119
+ revertMessage: 'Earn withdraw reverted on-chain'
18120
+ });
18121
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
18122
+ return result;
18123
+ } catch (error) {
18124
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
18125
+ throw error;
18126
+ }
18127
+ }, ({ txHash })=>txHash);
18128
+ return {
18129
+ txHash,
18130
+ explorerUrl,
18131
+ vaultAddress,
18132
+ amount: params.amount
18133
+ };
18134
+ }
16481
18135
  if (!options.skipApprove && approvalToken !== undefined) {
16482
18136
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16483
18137
  try {
@@ -16510,6 +18164,8 @@ function finishElapsedWait(lastStatus, lastError) {
16510
18164
  tokenInputs,
16511
18165
  signature
16512
18166
  },
18167
+ action: 'withdraw',
18168
+ executionParams,
16513
18169
  revertMessage: 'Earn withdraw reverted on-chain'
16514
18170
  });
16515
18171
  reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
@@ -16589,6 +18245,8 @@ function finishElapsedWait(lastStatus, lastError) {
16589
18245
  tokenInputs: [],
16590
18246
  signature
16591
18247
  },
18248
+ action: 'claimRewards',
18249
+ executionParams,
16592
18250
  revertMessage: 'Earn claim rewards reverted on-chain'
16593
18251
  }), ({ txHash })=>txHash);
16594
18252
  return {
@@ -17272,11 +18930,16 @@ const sourceAdapterContextSchema = z.object({
17272
18930
  *
17273
18931
  * Validate the optional Kit Key field using the standard `apiKeySchema`
17274
18932
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
17275
- * operates in permissionless mode.
18933
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
18934
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
18935
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
18936
+ * schema must accept them or a valid config object is rejected.
17276
18937
  *
17277
18938
  * @internal
17278
18939
  */ const earnConfigSchema = z.object({
17279
- kitKey: apiKeySchema.optional()
18940
+ kitKey: apiKeySchema.optional(),
18941
+ baseUrl: z.string().optional(),
18942
+ batchTransactions: z.boolean().optional()
17280
18943
  }).strict();
17281
18944
  /**
17282
18945
  * Canonical decimal form: a leading digit with no leading zeros (a single