@circle-fin/app-kit 1.11.0 → 1.12.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/earn.cjs CHANGED
@@ -39,6 +39,7 @@ require('bn.js');
39
39
  require('@coral-xyz/anchor');
40
40
  require('bs58');
41
41
  require('@noble/curves/ed25519');
42
+ var viem = require('viem');
42
43
  var units = require('@ethersproject/units');
43
44
  var keccak256 = require('@ethersproject/keccak256');
44
45
 
@@ -679,6 +680,11 @@ class KitError extends Error {
679
680
  name: 'INPUT_INSUFFICIENT_SWAP_AMOUNT',
680
681
  type: 'INPUT'
681
682
  },
683
+ /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
684
+ code: 1008,
685
+ name: 'INPUT_UNSUPPORTED_ACTION',
686
+ type: 'INPUT'
687
+ },
682
688
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
683
689
  code: 1009,
684
690
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -743,6 +749,29 @@ class KitError extends Error {
743
749
  type: 'LIQUIDITY'
744
750
  }
745
751
  };
752
+ /**
753
+ * Standardized error definitions for RPC type errors.
754
+ *
755
+ * RPC errors occur when communicating with blockchain RPC providers,
756
+ * including endpoint failures, invalid responses, and provider-specific issues.
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * import { RpcError } from '@core/errors'
761
+ *
762
+ * const error = new KitError({
763
+ * ...RpcError.ENDPOINT_ERROR,
764
+ * recoverability: 'RETRYABLE',
765
+ * message: 'RPC endpoint unavailable on Ethereum',
766
+ * cause: { trace: { endpoint: 'https://mainnet.infura.io' } }
767
+ * })
768
+ * ```
769
+ */ const RpcError = {
770
+ /** RPC endpoint returned error or is unavailable */ ENDPOINT_ERROR: {
771
+ code: 4001,
772
+ name: 'RPC_ENDPOINT_ERROR',
773
+ type: 'RPC'
774
+ }};
746
775
  /**
747
776
  * Standardized error definitions for NETWORK type errors.
748
777
  *
@@ -2342,6 +2371,8 @@ function getOptionalString(value) {
2342
2371
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2343
2372
  Blockchain["XDC"] = "XDC";
2344
2373
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2374
+ Blockchain["X_Layer"] = "X_Layer";
2375
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2345
2376
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2346
2377
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2347
2378
  })(Blockchain || (Blockchain = {}));
@@ -2395,6 +2426,7 @@ var BridgeChain;
2395
2426
  BridgeChain["Unichain"] = "Unichain";
2396
2427
  BridgeChain["World_Chain"] = "World_Chain";
2397
2428
  BridgeChain["XDC"] = "XDC";
2429
+ BridgeChain["X_Layer"] = "X_Layer";
2398
2430
  // Testnet chains with CCTPv2 support
2399
2431
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2400
2432
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2420,6 +2452,7 @@ var BridgeChain;
2420
2452
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2421
2453
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2422
2454
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2455
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2423
2456
  })(BridgeChain || (BridgeChain = {}));
2424
2457
  var UnifiedBalanceChain;
2425
2458
  (function(UnifiedBalanceChain) {
@@ -4957,7 +4990,8 @@ var EarnChain;
4957
4990
  isTestnet: true,
4958
4991
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4959
4992
  rpcEndpoints: [
4960
- 'https://rpc-amoy.polygon.technology'
4993
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4994
+ 'https://polygon-amoy.drpc.org'
4961
4995
  ],
4962
4996
  eurcAddress: null,
4963
4997
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5822,6 +5856,104 @@ var EarnChain;
5822
5856
  }
5823
5857
  });
5824
5858
 
5859
+ /**
5860
+ * X Layer Mainnet chain definition
5861
+ * @remarks
5862
+ * This represents the official production network for the X Layer blockchain.
5863
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5864
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5865
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5866
+ */ const XLayer = defineChain({
5867
+ type: 'evm',
5868
+ chain: Blockchain.X_Layer,
5869
+ name: 'X Layer',
5870
+ title: 'X Layer Mainnet',
5871
+ nativeCurrency: {
5872
+ name: 'OKB',
5873
+ symbol: 'OKB',
5874
+ decimals: 18
5875
+ },
5876
+ chainId: 196,
5877
+ isTestnet: false,
5878
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5879
+ rpcEndpoints: [
5880
+ 'https://xlayerrpc.okx.com'
5881
+ ],
5882
+ eurcAddress: null,
5883
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5884
+ usdtAddress: null,
5885
+ cctp: {
5886
+ domain: 37,
5887
+ contracts: {
5888
+ v2: {
5889
+ type: 'split',
5890
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5891
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5892
+ confirmations: 65,
5893
+ fastConfirmations: 1
5894
+ }
5895
+ },
5896
+ forwarderSupported: {
5897
+ source: false,
5898
+ destination: false
5899
+ }
5900
+ },
5901
+ kitContracts: {
5902
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5903
+ }
5904
+ });
5905
+
5906
+ /**
5907
+ * X Layer Testnet chain definition
5908
+ * @remarks
5909
+ * This represents the official test network for the X Layer blockchain.
5910
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5911
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5912
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5913
+ */ const XLayerTestnet = defineChain({
5914
+ type: 'evm',
5915
+ chain: Blockchain.X_Layer_Testnet,
5916
+ name: 'X Layer Testnet',
5917
+ title: 'X Layer Testnet',
5918
+ nativeCurrency: {
5919
+ name: 'OKB',
5920
+ symbol: 'OKB',
5921
+ decimals: 18
5922
+ },
5923
+ chainId: 1952,
5924
+ isTestnet: true,
5925
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5926
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5927
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5928
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5929
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5930
+ rpcEndpoints: [
5931
+ 'https://testrpc.xlayer.tech'
5932
+ ],
5933
+ eurcAddress: null,
5934
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5935
+ usdtAddress: null,
5936
+ cctp: {
5937
+ domain: 37,
5938
+ contracts: {
5939
+ v2: {
5940
+ type: 'split',
5941
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5942
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5943
+ confirmations: 65,
5944
+ fastConfirmations: 1
5945
+ }
5946
+ },
5947
+ forwarderSupported: {
5948
+ source: false,
5949
+ destination: false
5950
+ }
5951
+ },
5952
+ kitContracts: {
5953
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5954
+ }
5955
+ });
5956
+
5825
5957
  /**
5826
5958
  * ZKSync Era Mainnet chain definition
5827
5959
  * @remarks
@@ -5941,6 +6073,8 @@ var Chains = {
5941
6073
  WorldChainSepolia: WorldChainSepolia,
5942
6074
  XDC: XDC,
5943
6075
  XDCApothem: XDCApothem,
6076
+ XLayer: XLayer,
6077
+ XLayerTestnet: XLayerTestnet,
5944
6078
  ZKSyncEra: ZKSyncEra,
5945
6079
  ZKSyncEraSepolia: ZKSyncEraSepolia
5946
6080
  };
@@ -7384,6 +7518,7 @@ const swapTokenEnumSchema = zod.z.enum([
7384
7518
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7385
7519
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7386
7520
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7521
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7387
7522
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7388
7523
  // =========================================================================
7389
7524
  // Testnets (alphabetically sorted)
@@ -7418,6 +7553,7 @@ const swapTokenEnumSchema = zod.z.enum([
7418
7553
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7419
7554
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7420
7555
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
7556
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7421
7557
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7422
7558
  }
7423
7559
  };
@@ -8131,6 +8267,52 @@ const swapTokenEnumSchema = zod.z.enum([
8131
8267
  return explorerUrl;
8132
8268
  }
8133
8269
 
8270
+ /**
8271
+ * Assert that a value has type `never` (exhaustive switch helper).
8272
+ *
8273
+ * @remarks
8274
+ * Use in the `default` branch of a switch over a discriminated union.
8275
+ * If all union members are handled, the default is unreachable and TypeScript
8276
+ * narrows the parameter to `never`. If a member is missed, the compiler errors.
8277
+ *
8278
+ * @param _x - The value (typed as `never` when switch is exhaustive).
8279
+ * @returns Never returns; always throws.
8280
+ * @throws Error when the switch is not exhaustive.
8281
+ *
8282
+ * @example
8283
+ * ```typescript
8284
+ * type Foo = { type: 'a'; x: number } | { type: 'b'; y: string }
8285
+ *
8286
+ * function handle(foo: Foo): string {
8287
+ * switch (foo.type) {
8288
+ * case 'a': return String(foo.x)
8289
+ * case 'b': return foo.y
8290
+ * default: return assertNever(foo)
8291
+ * }
8292
+ * }
8293
+ * ```
8294
+ */ function assertNever$2(x) {
8295
+ // Plain `String(x)` collapses non-primitive union members (objects, arrays)
8296
+ // to `'[object Object]'`, which is useless when triaging which discriminant
8297
+ // was missed. Attempt `JSON.stringify` first so the thrown message preserves
8298
+ // the offending shape. Fall back to a minimal `typeof`-based label if
8299
+ // serialization fails (`BigInt` member, circular references, host objects).
8300
+ //
8301
+ // `x` is statically typed as `never` (the whole point of this helper), but
8302
+ // at runtime callers may still pass an unexpected value when the switch is
8303
+ // not actually exhaustive — that's exactly the bug we want to surface. Cast
8304
+ // through `unknown` so the runtime defence is not stripped by the compiler.
8305
+ const value = x;
8306
+ let stringified;
8307
+ try {
8308
+ const json = JSON.stringify(value);
8309
+ stringified = typeof json === 'string' ? json : `<${typeof value}>`;
8310
+ } catch {
8311
+ stringified = `<unstringifiable ${typeof value}>`;
8312
+ }
8313
+ throw new Error(`Unhandled switch case: ${stringified}`);
8314
+ }
8315
+
8134
8316
  /**
8135
8317
  * CCTP forwarding magic bytes prefix.
8136
8318
  *
@@ -8513,7 +8695,7 @@ const swapTokenEnumSchema = zod.z.enum([
8513
8695
  }
8514
8696
 
8515
8697
  var name$3 = "@circle-fin/bridge-kit";
8516
- var version$3 = "1.12.2";
8698
+ var version$3 = "1.13.0";
8517
8699
  var pkg$3 = {
8518
8700
  name: name$3,
8519
8701
  version: version$3};
@@ -8549,13 +8731,21 @@ var pkg$3 = {
8549
8731
  computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
8550
8732
  calculateFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
8551
8733
  resolveFeeRecipientAddress: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string())))
8552
- }).strict().refine((data)=>{
8734
+ }).strict().superRefine((data, ctx)=>{
8553
8735
  const hasComputeFee = data.computeFee !== undefined;
8554
8736
  const hasCalculateFee = data.calculateFee !== undefined;
8555
- // XOR: exactly one must be provided
8556
- return hasComputeFee !== hasCalculateFee;
8557
- }, {
8558
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
8737
+ if (hasComputeFee && hasCalculateFee) {
8738
+ ctx.addIssue({
8739
+ code: zod.z.ZodIssueCode.custom,
8740
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
8741
+ });
8742
+ }
8743
+ if (!hasComputeFee && !hasCalculateFee) {
8744
+ ctx.addIssue({
8745
+ code: zod.z.ZodIssueCode.custom,
8746
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
8747
+ });
8748
+ }
8559
8749
  });
8560
8750
 
8561
8751
  /**
@@ -9381,7 +9571,7 @@ var TransferSpeed;
9381
9571
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
9382
9572
 
9383
9573
  var name$2 = "@circle-fin/swap-kit";
9384
- var version$2 = "1.5.0";
9574
+ var version$2 = "1.5.1";
9385
9575
  var pkg$2 = {
9386
9576
  name: name$2,
9387
9577
  version: version$2};
@@ -10177,6 +10367,311 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
10177
10367
  return apiKeyPattern.test(apiKey);
10178
10368
  };
10179
10369
 
10370
+ /**
10371
+ * IAdapter contract ABI.
10372
+ *
10373
+ * Shared ABI for the on-chain Adapter contract used by multiple kits
10374
+ * (swap, earn) for executing signed instruction sets. The `execute()`
10375
+ * function accepts EIP-712 signed execution parameters, token inputs,
10376
+ * and a signature, then executes the corresponding on-chain
10377
+ * instructions.
10378
+ */ const adapterContractAbi = [
10379
+ {
10380
+ type: 'function',
10381
+ name: 'execute',
10382
+ inputs: [
10383
+ {
10384
+ name: 'params',
10385
+ type: 'tuple',
10386
+ internalType: 'struct IAdapter.ExecutionParams',
10387
+ components: [
10388
+ {
10389
+ name: 'instructions',
10390
+ type: 'tuple[]',
10391
+ internalType: 'struct IAdapter.Instruction[]',
10392
+ components: [
10393
+ {
10394
+ name: 'target',
10395
+ type: 'address',
10396
+ internalType: 'address'
10397
+ },
10398
+ {
10399
+ name: 'data',
10400
+ type: 'bytes',
10401
+ internalType: 'bytes'
10402
+ },
10403
+ {
10404
+ name: 'value',
10405
+ type: 'uint256',
10406
+ internalType: 'uint256'
10407
+ },
10408
+ {
10409
+ name: 'tokenIn',
10410
+ type: 'address',
10411
+ internalType: 'address'
10412
+ },
10413
+ {
10414
+ name: 'amountToApprove',
10415
+ type: 'uint256',
10416
+ internalType: 'uint256'
10417
+ },
10418
+ {
10419
+ name: 'tokenOut',
10420
+ type: 'address',
10421
+ internalType: 'address'
10422
+ },
10423
+ {
10424
+ name: 'minTokenOut',
10425
+ type: 'uint256',
10426
+ internalType: 'uint256'
10427
+ }
10428
+ ]
10429
+ },
10430
+ {
10431
+ name: 'tokens',
10432
+ type: 'tuple[]',
10433
+ internalType: 'struct IAdapter.TokenRecipient[]',
10434
+ components: [
10435
+ {
10436
+ name: 'token',
10437
+ type: 'address',
10438
+ internalType: 'address'
10439
+ },
10440
+ {
10441
+ name: 'beneficiary',
10442
+ type: 'address',
10443
+ internalType: 'address'
10444
+ }
10445
+ ]
10446
+ },
10447
+ {
10448
+ name: 'execId',
10449
+ type: 'uint256',
10450
+ internalType: 'uint256'
10451
+ },
10452
+ {
10453
+ name: 'deadline',
10454
+ type: 'uint256',
10455
+ internalType: 'uint256'
10456
+ },
10457
+ {
10458
+ name: 'metadata',
10459
+ type: 'bytes',
10460
+ internalType: 'bytes'
10461
+ }
10462
+ ]
10463
+ },
10464
+ {
10465
+ name: 'tokenInputs',
10466
+ type: 'tuple[]',
10467
+ internalType: 'struct IAdapter.TokenInput[]',
10468
+ components: [
10469
+ {
10470
+ name: 'permitType',
10471
+ type: 'uint8',
10472
+ internalType: 'enum IAdapter.PermitType'
10473
+ },
10474
+ {
10475
+ name: 'token',
10476
+ type: 'address',
10477
+ internalType: 'address'
10478
+ },
10479
+ {
10480
+ name: 'amount',
10481
+ type: 'uint256',
10482
+ internalType: 'uint256'
10483
+ },
10484
+ {
10485
+ name: 'permitCalldata',
10486
+ type: 'bytes',
10487
+ internalType: 'bytes'
10488
+ }
10489
+ ]
10490
+ },
10491
+ {
10492
+ name: 'signature',
10493
+ type: 'bytes',
10494
+ internalType: 'bytes'
10495
+ }
10496
+ ],
10497
+ outputs: [],
10498
+ stateMutability: 'payable'
10499
+ }
10500
+ ];
10501
+
10502
+ /**
10503
+ * Minimal ERC-4626 tokenized-vault ABI.
10504
+ *
10505
+ * Covers only the mutating methods EarnKit bundles as inner instructions inside
10506
+ * an Adapter `execute()` call: `deposit`, `withdraw`, and `redeem`. It exists so
10507
+ * clients can decode the inner instruction calldata into a human-readable
10508
+ * summary of what a signer is authorizing (asset amount, receiver, owner)
10509
+ * rather than showing opaque bytes. The 4-byte selectors match the calldata the
10510
+ * earn service signs (`deposit(uint256,address)` = `0x6e553f65`,
10511
+ * `withdraw(uint256,address,address)` = `0xb460af94`,
10512
+ * `redeem(uint256,address,address)` = `0xba087652`).
10513
+ */ const erc4626VaultAbi = [
10514
+ {
10515
+ type: 'function',
10516
+ name: 'deposit',
10517
+ stateMutability: 'nonpayable',
10518
+ inputs: [
10519
+ {
10520
+ name: 'assets',
10521
+ type: 'uint256',
10522
+ internalType: 'uint256'
10523
+ },
10524
+ {
10525
+ name: 'receiver',
10526
+ type: 'address',
10527
+ internalType: 'address'
10528
+ }
10529
+ ],
10530
+ outputs: [
10531
+ {
10532
+ name: 'shares',
10533
+ type: 'uint256',
10534
+ internalType: 'uint256'
10535
+ }
10536
+ ]
10537
+ },
10538
+ {
10539
+ type: 'function',
10540
+ name: 'withdraw',
10541
+ stateMutability: 'nonpayable',
10542
+ inputs: [
10543
+ {
10544
+ name: 'assets',
10545
+ type: 'uint256',
10546
+ internalType: 'uint256'
10547
+ },
10548
+ {
10549
+ name: 'receiver',
10550
+ type: 'address',
10551
+ internalType: 'address'
10552
+ },
10553
+ {
10554
+ name: 'owner',
10555
+ type: 'address',
10556
+ internalType: 'address'
10557
+ }
10558
+ ],
10559
+ outputs: [
10560
+ {
10561
+ name: 'shares',
10562
+ type: 'uint256',
10563
+ internalType: 'uint256'
10564
+ }
10565
+ ]
10566
+ },
10567
+ {
10568
+ type: 'function',
10569
+ name: 'redeem',
10570
+ stateMutability: 'nonpayable',
10571
+ inputs: [
10572
+ {
10573
+ name: 'shares',
10574
+ type: 'uint256',
10575
+ internalType: 'uint256'
10576
+ },
10577
+ {
10578
+ name: 'receiver',
10579
+ type: 'address',
10580
+ internalType: 'address'
10581
+ },
10582
+ {
10583
+ name: 'owner',
10584
+ type: 'address',
10585
+ internalType: 'address'
10586
+ }
10587
+ ],
10588
+ outputs: [
10589
+ {
10590
+ name: 'assets',
10591
+ type: 'uint256',
10592
+ internalType: 'uint256'
10593
+ }
10594
+ ]
10595
+ }
10596
+ ];
10597
+
10598
+ /**
10599
+ * Minimal FeeTaker ABI.
10600
+ *
10601
+ * The earn service appends a `takeFeeERC20` instruction to withdraw bundles
10602
+ * when Circle charges a withdrawal fee. This ABI decodes that inner instruction
10603
+ * so the fee (token, beneficiary, amount) is visible in the signing summary
10604
+ * instead of appearing as opaque calldata alongside the redeem/withdraw call.
10605
+ */ const feeTakerAbi = [
10606
+ {
10607
+ type: 'function',
10608
+ name: 'takeFeeERC20',
10609
+ stateMutability: 'nonpayable',
10610
+ inputs: [
10611
+ {
10612
+ name: 'token',
10613
+ type: 'address',
10614
+ internalType: 'address'
10615
+ },
10616
+ {
10617
+ name: 'beneficiary',
10618
+ type: 'address',
10619
+ internalType: 'address'
10620
+ },
10621
+ {
10622
+ name: 'fee',
10623
+ type: 'uint256',
10624
+ internalType: 'uint256'
10625
+ },
10626
+ {
10627
+ name: 'kitType',
10628
+ type: 'bytes8',
10629
+ internalType: 'bytes8'
10630
+ }
10631
+ ],
10632
+ outputs: []
10633
+ }
10634
+ ];
10635
+
10636
+ /**
10637
+ * Minimal Merkl Distributor ABI.
10638
+ *
10639
+ * EarnKit claim-rewards bundles a single `claim` instruction targeting the
10640
+ * Merkl Distributor, batching one entry per reward token. This ABI decodes that
10641
+ * inner instruction so the claimed tokens and amounts are visible in the signing
10642
+ * summary. `claim` uses dynamic array arguments, which is why a real ABI decoder
10643
+ * (rather than fixed-word slicing) is required for the reward instruction.
10644
+ */ const merklDistributorAbi = [
10645
+ {
10646
+ type: 'function',
10647
+ name: 'claim',
10648
+ stateMutability: 'nonpayable',
10649
+ inputs: [
10650
+ {
10651
+ name: 'users',
10652
+ type: 'address[]',
10653
+ internalType: 'address[]'
10654
+ },
10655
+ {
10656
+ name: 'tokens',
10657
+ type: 'address[]',
10658
+ internalType: 'address[]'
10659
+ },
10660
+ {
10661
+ name: 'amounts',
10662
+ type: 'uint256[]',
10663
+ internalType: 'uint256[]'
10664
+ },
10665
+ {
10666
+ name: 'proofs',
10667
+ type: 'bytes32[][]',
10668
+ internalType: 'bytes32[][]'
10669
+ }
10670
+ ],
10671
+ outputs: []
10672
+ }
10673
+ ];
10674
+
10180
10675
  /**
10181
10676
  * Zod schema for validating EVM adapter capabilities.
10182
10677
  *
@@ -12690,7 +13185,7 @@ new Set(Object.values(Blockchain));
12690
13185
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
12691
13186
 
12692
13187
  var name$1 = "@circle-fin/earn-kit";
12693
- var version$1 = "1.4.0";
13188
+ var version$1 = "1.5.0";
12694
13189
  var pkg$1 = {
12695
13190
  name: name$1,
12696
13191
  version: version$1};
@@ -13334,78 +13829,1010 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
13334
13829
  }
13335
13830
 
13336
13831
  /**
13337
- * Prepare an earn adapter action, execute it, wait for confirmation, and
13338
- * throw a structured revert error if the receipt status is `'reverted'`.
13339
- *
13340
- * Wraps the prepareAction, execute, waitForTransaction, and status check
13341
- * sequence so the provider can dispatch any `earn.*` action key with
13342
- * consistent revert handling.
13343
- *
13344
- * Gas estimation: after preparing the action the helper calls
13345
- * {@link estimateBufferedGasLimit}, which applies a safety buffer to the
13346
- * returned gas value. If estimation fails, execution falls through to the
13347
- * adapter's default gas handling.
13348
- *
13349
- * @typeParam TActionKey - Earn action key being executed.
13350
- * @param params - Adapter action, execution context, and revert message.
13351
- * @returns The confirmed on-chain transaction hash and explorer URL.
13352
- * @throws {@link KitError} If the transaction reverts on-chain.
13353
- *
13354
- * @example
13355
- * ```typescript
13356
- * const { txHash, explorerUrl } = await executeEarnAction({
13357
- * adapter,
13358
- * chain,
13359
- * address,
13360
- * actionKey: 'earn.deposit',
13361
- * actionParams: { executeParams, tokenInputs, signature },
13362
- * revertMessage: 'Earn deposit reverted on-chain',
13363
- * })
13364
- * ```
13365
- *
13366
- * @internal
13367
- */ async function executeEarnAction(params) {
13368
- const { adapter, chain, address, actionKey, actionParams, revertMessage } = params;
13369
- const prepared = await adapter.prepareAction(actionKey, actionParams, {
13370
- chain,
13371
- address
13372
- });
13373
- const gasLimitOverride = await estimateBufferedGasLimit(prepared);
13374
- const txHash = prepared.type === 'evm' && gasLimitOverride !== undefined ? await prepared.execute({
13375
- gasLimit: gasLimitOverride
13376
- }) : await prepared.execute();
13377
- const explorerUrl = buildExplorerUrl(chain, txHash);
13378
- const receipt = await adapter.waitForTransaction(txHash, {
13379
- confirmations: 1
13380
- }, chain);
13381
- if (receipt.status === 'reverted') {
13382
- throw createTransactionRevertedError(chain.name, revertMessage, undefined, txHash, explorerUrl);
13832
+ * Combined ABI of every inner instruction EarnKit can bundle inside an Adapter
13833
+ * `execute()` call. `decodeFunctionData` matches an instruction's calldata to
13834
+ * one of these functions by its 4-byte selector.
13835
+ */ const earnInstructionAbi = [
13836
+ ...erc4626VaultAbi,
13837
+ ...feeTakerAbi,
13838
+ ...merklDistributorAbi
13839
+ ];
13840
+ /**
13841
+ * Extract and shallow-validate the `instructions` array from loosely-typed
13842
+ * signed execution params.
13843
+ *
13844
+ * The earn service schema validates `tokenIn`/`amountToApprove` and passes the
13845
+ * remaining instruction fields through untyped, so the params arrive as a plain
13846
+ * record; each accessed field is narrowed at runtime.
13847
+ */ function requireInstructions(executionParams) {
13848
+ const instructions = executionParams['instructions'];
13849
+ if (!Array.isArray(instructions)) {
13850
+ throw decodeMismatchError('execution params are missing an instructions array', {
13851
+ instructions
13852
+ });
13383
13853
  }
13384
- return {
13385
- txHash,
13386
- explorerUrl,
13387
- ...receipt.gasUsed !== undefined && {
13388
- gasUsed: receipt.gasUsed
13389
- },
13390
- ...receipt.effectiveGasPrice !== undefined && {
13391
- effectiveGasPrice: receipt.effectiveGasPrice
13854
+ return instructions.map((instruction, index)=>{
13855
+ if (typeof instruction !== 'object' || instruction === null) {
13856
+ throw decodeMismatchError(`instructions[${index.toString()}] is not an object`, {
13857
+ index
13858
+ });
13392
13859
  }
13393
- };
13860
+ return instruction;
13861
+ });
13394
13862
  }
13395
-
13396
13863
  /**
13397
- * Validate that a service-signed execution payload has not expired before
13398
- * the SDK asks the wallet to broadcast a transaction.
13864
+ * Build a fail-closed {@link KitError} for an earn decode or review failure.
13399
13865
  *
13400
- * @internal
13401
- */ function validateExecutionDeadline(executionParams) {
13402
- const deadline = BigInt(executionParams.deadline);
13403
- const now = BigInt(Math.floor(Date.now() / 1000));
13404
- if (deadline <= now) {
13405
- throw createValidationFailedError('executionParams.deadline', executionParams.deadline, 'Earn execution deadline has expired');
13406
- }
13407
- }
13408
-
13866
+ * Marked non-recoverable: a mismatch between what would be shown and what would
13867
+ * be signed is never safe to retry, so the operation fails fast rather than
13868
+ * presenting misleading decoded data. `messagePrefix` names the failing stage
13869
+ * (calldata decode vs. review construction); callers bind it once and pass the
13870
+ * specific failure as `message`.
13871
+ */ function failClosedEarnError(messagePrefix, message, trace) {
13872
+ return new KitError({
13873
+ ...EarnError.INTERNAL_ERROR,
13874
+ recoverability: 'FATAL',
13875
+ message: `${messagePrefix}: ${message}`,
13876
+ cause: {
13877
+ trace
13878
+ }
13879
+ });
13880
+ }
13881
+ /**
13882
+ * Build a {@link KitError} for a decode or consistency failure.
13883
+ *
13884
+ * Thin wrapper over {@link failClosedEarnError} bound to the decode-stage
13885
+ * message prefix.
13886
+ */ function decodeMismatchError(message, trace) {
13887
+ return failClosedEarnError('Unable to decode earn transaction', message, trace);
13888
+ }
13889
+ /**
13890
+ * Narrow an untyped value to a 0x-prefixed hex string, or fail fast.
13891
+ */ function requireHex(value, path) {
13892
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value)) {
13893
+ return value;
13894
+ }
13895
+ throw decodeMismatchError(`${path} is not a hex string`, {
13896
+ path,
13897
+ value
13898
+ });
13899
+ }
13900
+ /**
13901
+ * Narrow an untyped value to a 20-byte EVM address, or fail fast.
13902
+ */ function requireAddress(value, path) {
13903
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) {
13904
+ return value;
13905
+ }
13906
+ throw decodeMismatchError(`${path} is not an address`, {
13907
+ path,
13908
+ value
13909
+ });
13910
+ }
13911
+ /**
13912
+ * Narrow an untyped `uint256`-like value (decimal string, bigint, or integer)
13913
+ * to a bigint, or fail fast.
13914
+ */ function requireUint(value, path) {
13915
+ if (typeof value === 'bigint') {
13916
+ return value;
13917
+ }
13918
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
13919
+ return BigInt(value);
13920
+ }
13921
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
13922
+ return BigInt(value);
13923
+ }
13924
+ throw decodeMismatchError(`${path} is not a uint256 value`, {
13925
+ path,
13926
+ value
13927
+ });
13928
+ }
13929
+ /**
13930
+ * Decode inner instruction calldata against the earn instruction ABI, mapping a
13931
+ * viem decode failure (unknown selector, malformed args) to a fail-fast error.
13932
+ */ function decodeEarnInstructionData(data, index) {
13933
+ try {
13934
+ return viem.decodeFunctionData({
13935
+ abi: earnInstructionAbi,
13936
+ data
13937
+ });
13938
+ } catch (error) {
13939
+ throw decodeMismatchError(`instructions[${index.toString()}] calldata is not a recognized earn instruction`, {
13940
+ index,
13941
+ selector: data.slice(0, 10),
13942
+ error: String(error)
13943
+ });
13944
+ }
13945
+ }
13946
+ /**
13947
+ * Decode Adapter `execute()` calldata, mapping a viem decode failure to a
13948
+ * fail-fast error.
13949
+ */ function decodeExecuteCalldata(calldata) {
13950
+ try {
13951
+ return viem.decodeFunctionData({
13952
+ abi: adapterContractAbi,
13953
+ data: calldata
13954
+ });
13955
+ } catch (error) {
13956
+ throw decodeMismatchError('encoded calldata is not a valid Adapter execute() call', {
13957
+ error: String(error)
13958
+ });
13959
+ }
13960
+ }
13961
+ /**
13962
+ * Decode one inner instruction's calldata into a typed {@link
13963
+ * DecodedEarnInstruction}.
13964
+ */ function decodeInstruction(instruction, index) {
13965
+ const target = requireAddress(instruction['target'], `instructions[${index.toString()}].target`);
13966
+ const data = requireHex(instruction['data'], `instructions[${index.toString()}].data`);
13967
+ const decoded = decodeEarnInstructionData(data, index);
13968
+ switch(decoded.functionName){
13969
+ case 'deposit':
13970
+ {
13971
+ const [assets, receiver] = decoded.args;
13972
+ return {
13973
+ method: 'deposit',
13974
+ vault: target,
13975
+ assets: assets.toString(),
13976
+ receiver
13977
+ };
13978
+ }
13979
+ case 'withdraw':
13980
+ {
13981
+ const [assets, receiver, owner] = decoded.args;
13982
+ return {
13983
+ method: 'withdraw',
13984
+ vault: target,
13985
+ assets: assets.toString(),
13986
+ receiver,
13987
+ owner
13988
+ };
13989
+ }
13990
+ case 'redeem':
13991
+ {
13992
+ const [shares, receiver, owner] = decoded.args;
13993
+ return {
13994
+ method: 'redeem',
13995
+ vault: target,
13996
+ shares: shares.toString(),
13997
+ receiver,
13998
+ owner
13999
+ };
14000
+ }
14001
+ case 'takeFeeERC20':
14002
+ {
14003
+ const [token, beneficiary, fee, kitType] = decoded.args;
14004
+ return {
14005
+ method: 'takeFeeERC20',
14006
+ feeTaker: target,
14007
+ token,
14008
+ beneficiary,
14009
+ fee: fee.toString(),
14010
+ kitType
14011
+ };
14012
+ }
14013
+ case 'claim':
14014
+ {
14015
+ const users = decoded.args[0];
14016
+ const tokens = decoded.args[1];
14017
+ const amounts = decoded.args[2];
14018
+ // Merkl claim(users, tokens, amounts, proofs) carries parallel arrays,
14019
+ // one entry per reward. Reject any length skew rather than padding with
14020
+ // zero amounts or dropping trailing entries, so the preview can never
14021
+ // misstate what is claimed or for whom.
14022
+ //
14023
+ // Note: Merkl `amounts` are the *cumulative lifetime* total claimable per
14024
+ // (user, token); the Distributor transfers only `amount - alreadyClaimed`.
14025
+ // This decode faithfully surfaces the signed cumulative value, which is
14026
+ // what `DecodedRewardClaim.amount` documents. See that type's doc.
14027
+ if (new Set([
14028
+ users.length,
14029
+ tokens.length,
14030
+ amounts.length
14031
+ ]).size !== 1) {
14032
+ throw decodeMismatchError(`instructions[${index.toString()}] claim has mismatched recipient/token/amount lengths`, {
14033
+ index,
14034
+ users: users.length,
14035
+ tokens: tokens.length,
14036
+ amounts: amounts.length
14037
+ });
14038
+ }
14039
+ const rewards = tokens.map((token, rewardIndex)=>({
14040
+ recipient: requireAddress(users[rewardIndex], `instructions[${index.toString()}].claim.users[${rewardIndex.toString()}]`),
14041
+ address: token,
14042
+ amount: requireUint(amounts[rewardIndex], `instructions[${index.toString()}].claim.amounts[${rewardIndex.toString()}]`).toString()
14043
+ }));
14044
+ return {
14045
+ method: 'claim',
14046
+ distributor: target,
14047
+ rewards
14048
+ };
14049
+ }
14050
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
14051
+ return assertNever$2(decoded);
14052
+ }
14053
+ }
14054
+ /**
14055
+ * Lift the primary values a wallet prompt cares about out of the decoded
14056
+ * instructions into a flat summary.
14057
+ */ function buildSummary(instructions, envelope) {
14058
+ const summary = {};
14059
+ instructions.forEach((instruction, index)=>{
14060
+ switch(instruction.method){
14061
+ case 'deposit':
14062
+ case 'withdraw':
14063
+ case 'redeem':
14064
+ {
14065
+ // The summary lifts a single primary token movement to the top level.
14066
+ // An earn bundle carries exactly one deposit/withdraw/redeem today;
14067
+ // fail fast rather than silently overwriting an earlier one, which
14068
+ // would drop it from the wallet-facing preview.
14069
+ if (summary.token !== undefined) {
14070
+ throw decodeMismatchError('multiple deposit/withdraw/redeem instructions cannot be summarized into a single preview', {
14071
+ index
14072
+ });
14073
+ }
14074
+ // Pair the amount with the token it is actually denominated in so the
14075
+ // preview never folds two units into one entry:
14076
+ // - deposit: `assets` of the underlying asset pulled in (`tokenIn`)
14077
+ // - redeem: `shares` of the vault-share token burned (`tokenIn`)
14078
+ // - withdraw: `assets` of the underlying asset paid out (`tokenOut`).
14079
+ // `withdraw(assets)` counts the underlying received, not the shares
14080
+ // burned to produce it, so `tokenIn` (the share token) would misstate
14081
+ // the unit; the underlying is the instruction's `tokenOut`.
14082
+ const amount = instruction.method === 'redeem' ? instruction.shares : instruction.assets;
14083
+ const tokenField = instruction.method === 'withdraw' ? 'tokenOut' : 'tokenIn';
14084
+ summary.vault = instruction.vault;
14085
+ summary.receiver = instruction.receiver;
14086
+ summary.token = {
14087
+ address: requireAddress(envelope[index]?.[tokenField], `instructions[${index.toString()}].${tokenField}`),
14088
+ amount
14089
+ };
14090
+ break;
14091
+ }
14092
+ case 'takeFeeERC20':
14093
+ {
14094
+ // As with the vault case, a second fee would silently overwrite the
14095
+ // first and understate what is charged; fail fast instead.
14096
+ if (summary.fee !== undefined) {
14097
+ throw decodeMismatchError('multiple fee instructions cannot be summarized into a single preview', {
14098
+ index
14099
+ });
14100
+ }
14101
+ summary.fee = {
14102
+ address: instruction.token,
14103
+ amount: instruction.fee
14104
+ };
14105
+ break;
14106
+ }
14107
+ case 'claim':
14108
+ {
14109
+ // A second claim would silently drop the first from the preview
14110
+ // (rewards are already batched inside one Merkl claim); fail fast.
14111
+ if (summary.rewards !== undefined) {
14112
+ throw decodeMismatchError('multiple claim instructions cannot be summarized into a single preview', {
14113
+ index
14114
+ });
14115
+ }
14116
+ summary.rewards = instruction.rewards;
14117
+ break;
14118
+ }
14119
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
14120
+ assertNever$2(instruction);
14121
+ }
14122
+ });
14123
+ return summary;
14124
+ }
14125
+ /**
14126
+ * Vault/claim instruction methods each declared action may decode to. The
14127
+ * mapping is many-to-one: a full withdrawal decodes to `redeem`, and any action
14128
+ * may carry an auxiliary `takeFeeERC20` alongside its primary instruction.
14129
+ */ const ACTION_ALLOWED_METHODS = {
14130
+ deposit: new Set([
14131
+ 'deposit'
14132
+ ]),
14133
+ withdraw: new Set([
14134
+ 'withdraw',
14135
+ 'redeem'
14136
+ ]),
14137
+ claimRewards: new Set([
14138
+ 'claim'
14139
+ ])
14140
+ };
14141
+ /**
14142
+ * Fail fast when the caller-declared `action` disagrees with the decoded
14143
+ * instructions, so the preview's headline can never mislabel what is signed
14144
+ * (e.g. a `deposit`-labeled call handed withdraw params). `takeFeeERC20` is an
14145
+ * auxiliary Circle-fee instruction and is allowed alongside any action.
14146
+ */ function assertActionMatchesInstructions(action, instructions) {
14147
+ const allowed = ACTION_ALLOWED_METHODS[action];
14148
+ instructions.forEach((instruction, index)=>{
14149
+ if (instruction.method === 'takeFeeERC20') {
14150
+ return;
14151
+ }
14152
+ if (!allowed.has(instruction.method)) {
14153
+ throw decodeMismatchError(`decoded instruction method '${instruction.method}' does not match the '${action}' action`, {
14154
+ action,
14155
+ method: instruction.method,
14156
+ index
14157
+ });
14158
+ }
14159
+ });
14160
+ }
14161
+ /**
14162
+ * Decode a same-chain earn `execute()` bundle into a human-readable summary.
14163
+ *
14164
+ * Decodes every inner instruction in the service-signed `executionParams` — the
14165
+ * same object the SDK ABI-encodes into the transaction — so the returned decode
14166
+ * is a faithful, drift-free view of what the signer is authorizing: input token
14167
+ * and amount, target vault, receiver, any Circle fee, and claimed rewards. Fails
14168
+ * fast with a non-recoverable {@link KitError} if any instruction cannot be
14169
+ * decoded, rather than returning misleading data.
14170
+ *
14171
+ * @param input - Action, chain, adapter, and the signed execution params.
14172
+ * @returns The decoded transaction summary.
14173
+ * @throws {@link KitError} If an instruction's calldata cannot be decoded.
14174
+ *
14175
+ * @example
14176
+ * ```typescript
14177
+ * const decoded = decodeEarnExecute({
14178
+ * action: 'deposit',
14179
+ * chain: 'Arc_Testnet',
14180
+ * adapter: '0x7fb8c7260b63934d8da38af902f87ae6e284a845',
14181
+ * executionParams,
14182
+ * })
14183
+ * // decoded.summary -> { token: { address, amount }, vault, receiver }
14184
+ * ```
14185
+ *
14186
+ * @internal
14187
+ */ function decodeEarnExecute(input) {
14188
+ const { action, chain, adapter, executionParams } = input;
14189
+ const envelope = requireInstructions(executionParams);
14190
+ const instructions = envelope.map((instruction, index)=>decodeInstruction(instruction, index));
14191
+ assertActionMatchesInstructions(action, instructions);
14192
+ return {
14193
+ action,
14194
+ chain,
14195
+ adapter,
14196
+ instructions,
14197
+ summary: buildSummary(instructions, envelope)
14198
+ };
14199
+ }
14200
+ /**
14201
+ * Assert that ABI-encoded Adapter `execute()` calldata encodes the same
14202
+ * instruction set as the service-signed execution params.
14203
+ *
14204
+ * Fail-fast preview check: the SDK encodes `execute(executeParams, ...)` locally,
14205
+ * so decoding those final bytes and comparing every field of each instruction
14206
+ * against the signed params proves the previewed instruction set matches what
14207
+ * will be signed. It compares `instructions[]` only — the outer `tokens`,
14208
+ * `execId`, `deadline`, and `metadata` are not re-compared here. The
14209
+ * authoritative integrity guarantee for the full signed struct is the on-chain
14210
+ * EIP-712 signature verification, which reverts if any signed field is altered.
14211
+ *
14212
+ * @param calldata - Encoded `execute()` calldata about to be signed.
14213
+ * @param executionParams - Service-signed execution params.
14214
+ * @throws {@link KitError} If the calldata is not an `execute()` call or any
14215
+ * instruction field differs from the signed params.
14216
+ *
14217
+ * @example
14218
+ * ```typescript
14219
+ * assertEarnCalldataMatchesExecuteParams(
14220
+ * prepared.getCallData().data,
14221
+ * executionParams,
14222
+ * )
14223
+ * ```
14224
+ *
14225
+ * @internal
14226
+ */ function assertEarnCalldataMatchesExecuteParams(calldata, executionParams) {
14227
+ // adapterContractAbi declares only `execute`, so a successful decode is always
14228
+ // the execute() call; a non-execute selector throws inside
14229
+ // decodeExecuteCalldata above.
14230
+ const decoded = decodeExecuteCalldata(calldata);
14231
+ const encoded = decoded.args[0].instructions;
14232
+ const signed = requireInstructions(executionParams);
14233
+ // Compare each encoded instruction against its signed counterpart. Iterating
14234
+ // the encoded instructions and indexing the signed set keeps both mismatch
14235
+ // branches reachable: a signed set that is too short trips the guard below,
14236
+ // and one that is too long trips the post-loop check.
14237
+ encoded.forEach((instruction, index)=>{
14238
+ const signedInstruction = signed[index];
14239
+ if (signedInstruction === undefined) {
14240
+ throw decodeMismatchError(`signed params are missing instruction ${index.toString()}`, {
14241
+ index,
14242
+ encoded: encoded.length,
14243
+ signed: signed.length
14244
+ });
14245
+ }
14246
+ const path = `instructions[${index.toString()}]`;
14247
+ assertHexEqual(instruction.target, signedInstruction['target'], `${path}.target`);
14248
+ assertHexEqual(instruction.data, signedInstruction['data'], `${path}.data`);
14249
+ assertUintEqual(instruction.value, signedInstruction['value'], `${path}.value`);
14250
+ assertHexEqual(instruction.tokenIn, signedInstruction['tokenIn'], `${path}.tokenIn`);
14251
+ assertUintEqual(instruction.amountToApprove, signedInstruction['amountToApprove'], `${path}.amountToApprove`);
14252
+ assertHexEqual(instruction.tokenOut, signedInstruction['tokenOut'], `${path}.tokenOut`);
14253
+ assertUintEqual(instruction.minTokenOut, signedInstruction['minTokenOut'], `${path}.minTokenOut`);
14254
+ });
14255
+ if (signed.length > encoded.length) {
14256
+ throw decodeMismatchError('signed params contain more instructions than the encoded calldata', {
14257
+ encoded: encoded.length,
14258
+ signed: signed.length
14259
+ });
14260
+ }
14261
+ }
14262
+ /**
14263
+ * Assert two hex values are equal, case-insensitively (addresses and calldata).
14264
+ */ function assertHexEqual(encoded, signed, path) {
14265
+ const signedHex = requireHex(signed, path);
14266
+ if (encoded.toLowerCase() !== signedHex.toLowerCase()) {
14267
+ throw decodeMismatchError(`${path} differs from signed params`, {
14268
+ path,
14269
+ encoded,
14270
+ signed: signedHex
14271
+ });
14272
+ }
14273
+ }
14274
+ /**
14275
+ * Assert an encoded bigint equals a signed `uint256`-like value.
14276
+ */ function assertUintEqual(encoded, signed, path) {
14277
+ const signedUint = requireUint(signed, path);
14278
+ if (encoded !== signedUint) {
14279
+ throw decodeMismatchError(`${path} differs from signed params`, {
14280
+ path,
14281
+ encoded: encoded.toString(),
14282
+ signed: signedUint.toString()
14283
+ });
14284
+ }
14285
+ }
14286
+
14287
+ /**
14288
+ * Namespaced discriminator for the EarnKit authorization review.
14289
+ *
14290
+ * Applications match on this in an adapter `onBeforeAuthorize` hook to decide
14291
+ * whether the request carries EarnKit semantic data. Prefer the
14292
+ * {@link isEarnExecuteReview} type guard over comparing this string directly.
14293
+ *
14294
+ * @example
14295
+ * ```typescript
14296
+ * if (review?.kind === EARN_EXECUTE_REVIEW_KIND) { … }
14297
+ * ```
14298
+ */ const EARN_EXECUTE_REVIEW_KIND = 'earn.execute';
14299
+
14300
+ /**
14301
+ * Build a fail-closed {@link KitError} for a review-construction failure.
14302
+ *
14303
+ * Marked non-recoverable: a review that cannot prove the calldata matches the
14304
+ * signed operation must abort authorization, never retry with misleading data.
14305
+ */ function reviewError(message, trace) {
14306
+ return failClosedEarnError('Unable to build earn authorization review', message, trace);
14307
+ }
14308
+ /**
14309
+ * Narrow the canonical authorization payload to the single Adapter `execute()`
14310
+ * call a same-chain earn operation authorizes.
14311
+ *
14312
+ * Same-chain deposit, withdraw, and claim-rewards each authorize exactly one
14313
+ * `evm-calls` payload carrying one call. Anything else (typed data, a batch,
14314
+ * an empty call list) means this descriptor was attached to the wrong
14315
+ * authorization unit, so fail closed rather than decode misleading data.
14316
+ */ function assertSingleEvmCallPayload(payload) {
14317
+ if (payload.type !== 'evm-calls') {
14318
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
14319
+ type: payload.type
14320
+ });
14321
+ }
14322
+ const [call, ...rest] = payload.calls;
14323
+ if (call === undefined) {
14324
+ throw reviewError('the evm-calls payload contains no calls to review', {
14325
+ callCount: payload.calls.length
14326
+ });
14327
+ }
14328
+ if (rest.length > 0) {
14329
+ throw reviewError('a same-chain earn operation authorizes exactly one Adapter execute() call', {
14330
+ callCount: payload.calls.length
14331
+ });
14332
+ }
14333
+ return call;
14334
+ }
14335
+ /**
14336
+ * Select the final earn `execute()` call from an atomic Earn batch.
14337
+ *
14338
+ * Same-chain batched deposit/withdraw authorizes either `[execute]` when the
14339
+ * current allowance is sufficient, or `[approve, execute]` when a top-up is
14340
+ * required. Any other shape means the descriptor was attached to an
14341
+ * unexpected authorization unit, so fail closed.
14342
+ */ function assertBatchedEarnExecuteCall(payload) {
14343
+ if (payload.type !== 'evm-calls') {
14344
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
14345
+ type: payload.type
14346
+ });
14347
+ }
14348
+ if (payload.calls.length !== 1 && payload.calls.length !== 2) {
14349
+ throw reviewError('a batched earn operation authorizes [execute] or [approve, execute]', {
14350
+ callCount: payload.calls.length
14351
+ });
14352
+ }
14353
+ const executeCall = payload.calls.at(-1);
14354
+ if (executeCall === undefined) {
14355
+ throw reviewError('the earn batch contains no execute call to review', {
14356
+ callCount: payload.calls.length
14357
+ });
14358
+ }
14359
+ return executeCall;
14360
+ }
14361
+ /**
14362
+ * Map a canonical {@link EvmCall} to the {@link EarnEncodedTransaction} preview
14363
+ * shape, failing closed when the earn `execute()` call carries no calldata.
14364
+ */ function toEarnEncodedTransaction(call) {
14365
+ if (call.data === undefined) {
14366
+ throw reviewError('the earn execute() call is missing calldata', {
14367
+ to: call.to
14368
+ });
14369
+ }
14370
+ return {
14371
+ to: call.to,
14372
+ data: call.data,
14373
+ ...call.value !== undefined && {
14374
+ value: call.value
14375
+ }
14376
+ };
14377
+ }
14378
+ /**
14379
+ * Create an Earn authorization descriptor using the supplied canonical-payload
14380
+ * call selector.
14381
+ *
14382
+ * @param input - The action, chain, and service-signed execution params.
14383
+ * @param selectCall - Fail-closed selector for the execute call under review.
14384
+ * @returns A lazy descriptor that decodes and verifies the selected call.
14385
+ *
14386
+ * @internal
14387
+ */ function createEarnExecuteDescriptor(input, selectCall) {
14388
+ const { action, chain, executionParams } = input;
14389
+ const createReview = (payload)=>{
14390
+ const call = selectCall(payload);
14391
+ const encoded = toEarnEncodedTransaction(call);
14392
+ const decoded = decodeEarnExecute({
14393
+ action,
14394
+ chain,
14395
+ adapter: encoded.to,
14396
+ executionParams
14397
+ });
14398
+ // Prove the calldata about to be signed encodes the same instruction set as
14399
+ // the service-signed params. Throwing here aborts before the wallet prompt.
14400
+ assertEarnCalldataMatchesExecuteParams(encoded.data, executionParams);
14401
+ const review = {
14402
+ kind: EARN_EXECUTE_REVIEW_KIND,
14403
+ data: {
14404
+ encoded,
14405
+ decoded
14406
+ }
14407
+ };
14408
+ return review;
14409
+ };
14410
+ return {
14411
+ createReview
14412
+ };
14413
+ }
14414
+ /**
14415
+ * Build the lazy `earn.execute` authorization descriptor for a same-chain earn
14416
+ * action.
14417
+ *
14418
+ * The returned descriptor carries only a `createReview` factory — no intent
14419
+ * override, because the adapter's action system supplies the intent from the
14420
+ * action key. The factory is evaluated at most once, and only when the
14421
+ * application configured an adapter `onBeforeAuthorize` hook. When it runs it:
14422
+ *
14423
+ * 1. narrows the canonical payload to its single Adapter `execute()` call;
14424
+ * 2. lifts that call into an {@link EarnEncodedTransaction};
14425
+ * 3. decodes it into a `DecodedEarnTx`; and
14426
+ * 4. asserts the decoded calldata matches the service-signed params, throwing
14427
+ * (aborting authorization before the wallet or signer) on any mismatch.
14428
+ *
14429
+ * @param input - The action, chain, and service-signed execution params.
14430
+ * @returns An authorization descriptor to pass as the fourth `prepareAction`
14431
+ * argument for the final earn action only (never the allowance approval).
14432
+ * @throws {@link KitError} From the review factory when the payload is not a
14433
+ * single earn `execute()` call or the calldata diverges from the signed
14434
+ * params. The throw surfaces through the adapter gate before authorization.
14435
+ *
14436
+ * @example
14437
+ * ```typescript
14438
+ * const descriptor = buildEarnExecuteDescriptor({
14439
+ * action: 'deposit',
14440
+ * chain: 'Arc_Testnet',
14441
+ * executionParams,
14442
+ * })
14443
+ * await adapter.prepareAction('earn.deposit', actionParams, ctx, {
14444
+ * authorization: descriptor,
14445
+ * })
14446
+ * ```
14447
+ *
14448
+ * @internal
14449
+ */ function buildEarnExecuteDescriptor(input) {
14450
+ return createEarnExecuteDescriptor(input, assertSingleEvmCallPayload);
14451
+ }
14452
+ /**
14453
+ * Build a lazy `earn.execute` authorization descriptor for an atomic Earn
14454
+ * batch containing either `[execute]` or `[approve, execute]`.
14455
+ *
14456
+ * The review always decodes and verifies the final call against the
14457
+ * service-signed execution params. Unexpected payload types and call counts
14458
+ * fail closed before wallet authorization.
14459
+ *
14460
+ * @param input - The action, chain, and service-signed execution params.
14461
+ * @returns A descriptor suitable for `batchExecute` authorization options.
14462
+ * @throws {@link KitError} From the lazy review factory when the batch shape or
14463
+ * final execute calldata cannot be verified.
14464
+ *
14465
+ * @internal
14466
+ */ function buildBatchedEarnExecuteDescriptor(input) {
14467
+ return createEarnExecuteDescriptor(input, assertBatchedEarnExecuteCall);
14468
+ }
14469
+
14470
+ /**
14471
+ * Prepare an earn adapter action, execute it, wait for confirmation, and
14472
+ * throw a structured revert error if the receipt status is `'reverted'`.
14473
+ *
14474
+ * Wraps the prepareAction, execute, waitForTransaction, and status check
14475
+ * sequence so the provider can dispatch any `earn.*` action key with
14476
+ * consistent revert handling.
14477
+ *
14478
+ * Gas estimation: after preparing the action the helper calls
14479
+ * {@link estimateBufferedGasLimit}, which applies a safety buffer to the
14480
+ * returned gas value. If estimation fails, execution falls through to the
14481
+ * adapter's default gas handling.
14482
+ *
14483
+ * @typeParam TActionKey - Earn action key being executed.
14484
+ * @param params - Adapter action, execution context, and revert message.
14485
+ * @returns The confirmed on-chain transaction hash and explorer URL.
14486
+ * @throws {@link KitError} If the transaction reverts on-chain.
14487
+ *
14488
+ * @example
14489
+ * ```typescript
14490
+ * const { txHash, explorerUrl } = await executeEarnAction({
14491
+ * adapter,
14492
+ * chain,
14493
+ * address,
14494
+ * actionKey: 'earn.deposit',
14495
+ * actionParams: { executeParams, tokenInputs, signature },
14496
+ * action: 'deposit',
14497
+ * executionParams,
14498
+ * revertMessage: 'Earn deposit reverted on-chain',
14499
+ * })
14500
+ * ```
14501
+ *
14502
+ * @internal
14503
+ */ async function executeEarnAction(params) {
14504
+ const { adapter, chain, address, actionKey, actionParams, action, executionParams, revertMessage } = params;
14505
+ // Attach the lazy `earn.execute` review to the final earn action only (never
14506
+ // the allowance approval, which runs on a separate path). The adapter's
14507
+ // action system supplies the intent from `actionKey`, so the descriptor
14508
+ // carries only the review factory. The factory is evaluated at most once,
14509
+ // and only when the application configured an `onBeforeAuthorize` hook.
14510
+ const authorization = buildEarnExecuteDescriptor({
14511
+ action,
14512
+ // The provider validates the chain is Earn-supported in
14513
+ // `resolveAdapterContext` before reaching execute, so the concrete chain
14514
+ // identifier is a valid `EarnChainIdentifier`. It is carried through to the
14515
+ // decoded preview's display `chain` field only.
14516
+ chain: chain.chain,
14517
+ executionParams
14518
+ });
14519
+ // The abstract `Adapter.prepareAction` is 3-arg; the fourth authorization
14520
+ // argument lives on the `withLegacyCompat` wrapper that produced the concrete
14521
+ // adapter passed here. Narrow the single seam that threads the descriptor.
14522
+ const prepared = await adapter.prepareAction(actionKey, actionParams, {
14523
+ chain,
14524
+ address
14525
+ }, {
14526
+ authorization
14527
+ });
14528
+ const gasLimitOverride = await estimateBufferedGasLimit(prepared);
14529
+ const txHash = prepared.type === 'evm' && gasLimitOverride !== undefined ? await prepared.execute({
14530
+ gasLimit: gasLimitOverride
14531
+ }) : await prepared.execute();
14532
+ const explorerUrl = buildExplorerUrl(chain, txHash);
14533
+ const receipt = await adapter.waitForTransaction(txHash, {
14534
+ confirmations: 1
14535
+ }, chain);
14536
+ if (receipt.status === 'reverted') {
14537
+ throw createTransactionRevertedError(chain.name, revertMessage, undefined, txHash, explorerUrl);
14538
+ }
14539
+ return {
14540
+ txHash,
14541
+ explorerUrl,
14542
+ ...receipt.gasUsed !== undefined && {
14543
+ gasUsed: receipt.gasUsed
14544
+ },
14545
+ ...receipt.effectiveGasPrice !== undefined && {
14546
+ effectiveGasPrice: receipt.effectiveGasPrice
14547
+ }
14548
+ };
14549
+ }
14550
+
14551
+ /**
14552
+ * Decide whether a same-chain earn action should be submitted as a single
14553
+ * atomic batch.
14554
+ *
14555
+ * Returns `true` only when the consumer has not opted out
14556
+ * (`batchTransactions !== false`), the source chain is EVM, the adapter
14557
+ * structurally exposes the shared batch methods, and the wallet reports atomic
14558
+ * batch support. `address` is forwarded as `fromAddress` so developer-controlled
14559
+ * adapters can probe the specific wallet. Any thrown capability probe is
14560
+ * treated as "no support".
14561
+ *
14562
+ * @param params - Adapter, chain, address, and the resolved `batchTransactions` flag.
14563
+ * @returns `true` when batched execution should be attempted.
14564
+ *
14565
+ * @example
14566
+ * ```typescript
14567
+ * if (await shouldUseBatchedEarnAction({ adapter, chain, address, batchTransactions })) {
14568
+ * // take the batched approve + execute path
14569
+ * }
14570
+ * ```
14571
+ *
14572
+ * @internal
14573
+ */ async function shouldUseBatchedEarnAction(params) {
14574
+ const { adapter, chain, address, batchTransactions } = params;
14575
+ if (batchTransactions === false) {
14576
+ return false;
14577
+ }
14578
+ if (chain.type !== 'evm') {
14579
+ return false;
14580
+ }
14581
+ const candidate = adapter;
14582
+ if (typeof candidate.supportsAtomicBatch !== 'function' || typeof candidate.batchExecute !== 'function') {
14583
+ return false;
14584
+ }
14585
+ try {
14586
+ return await candidate.supportsAtomicBatch(chain, {
14587
+ fromAddress: address
14588
+ });
14589
+ } catch {
14590
+ return false;
14591
+ }
14592
+ }
14593
+ async function buildSuccessfulBatchResult(adapter, chain, receipt, batchId, revertMessage) {
14594
+ const transaction = {
14595
+ txHash: receipt.txHash,
14596
+ explorerUrl: buildExplorerUrl(chain, receipt.txHash)
14597
+ };
14598
+ let confirmed;
14599
+ try {
14600
+ confirmed = await adapter.waitForTransaction(receipt.txHash, {
14601
+ confirmations: 1
14602
+ }, chain);
14603
+ } catch {
14604
+ // The batch adapter already confirmed success. Receipt enrichment is
14605
+ // telemetry-only, so an additional RPC failure must not turn an accepted
14606
+ // money-moving operation into a retryable business failure.
14607
+ return transaction;
14608
+ }
14609
+ if (confirmed.status === 'reverted') {
14610
+ throw createTransactionRevertedError(chain.name, revertMessage, {
14611
+ batchId
14612
+ }, receipt.txHash, transaction.explorerUrl);
14613
+ }
14614
+ return {
14615
+ ...transaction,
14616
+ ...confirmed.gasUsed !== undefined && {
14617
+ gasUsed: confirmed.gasUsed
14618
+ },
14619
+ ...confirmed.effectiveGasPrice !== undefined && {
14620
+ effectiveGasPrice: confirmed.effectiveGasPrice
14621
+ }
14622
+ };
14623
+ }
14624
+ function throwBatchFailure(result, executeReceipt, chain, actionKey, revertMessage) {
14625
+ const cause = result.error;
14626
+ if (result.statusCode === 400) {
14627
+ throw new KitError({
14628
+ ...RpcError.ENDPOINT_ERROR,
14629
+ recoverability: 'RETRYABLE',
14630
+ message: `Batched earn ${actionKey} failed off-chain before inclusion (batch ${result.batchId}).`,
14631
+ cause: {
14632
+ trace: {
14633
+ batchId: result.batchId,
14634
+ statusCode: result.statusCode,
14635
+ cause
14636
+ }
14637
+ }
14638
+ });
14639
+ }
14640
+ const causeTrace = cause instanceof KitError && typeof cause.cause?.trace === 'object' && cause.cause.trace !== null ? cause.cause.trace : undefined;
14641
+ if (cause instanceof KitError && causeTrace?.['kind'] === 'failed_offchain') {
14642
+ throw cause;
14643
+ }
14644
+ 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 !== '';
14645
+ if (isConfirmedRevert) {
14646
+ throw createTransactionRevertedError(chain.name, revertMessage, {
14647
+ batchId: result.batchId,
14648
+ error: cause
14649
+ });
14650
+ }
14651
+ throw new KitError({
14652
+ ...NetworkError.TIMEOUT,
14653
+ recoverability: 'FATAL',
14654
+ message: `Batched earn ${actionKey} was submitted (batch ${result.batchId}) but its outcome could not be confirmed; check the transaction status before retrying.`,
14655
+ cause: {
14656
+ trace: {
14657
+ batchId: result.batchId,
14658
+ cause
14659
+ }
14660
+ }
14661
+ });
14662
+ }
14663
+ /**
14664
+ * Execute the `approve` and `execute` steps of a same-chain earn action as a
14665
+ * single atomic batch.
14666
+ *
14667
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw call
14668
+ * data via `getCallData()`, then submit both through the adapter's shared
14669
+ * `batchExecute`. `address` is forwarded as `opts.fromAddress` so
14670
+ * developer-controlled adapters batch on behalf of the right wallet;
14671
+ * `idempotencyKey` is forwarded for adapters that deduplicate ambiguous
14672
+ * submissions (the Circle developer-controlled adapter reuses the Earn
14673
+ * execution id); other adapters may ignore either option. Reused by both the
14674
+ * deposit and withdraw flows via the `actionKey` parameter.
14675
+ *
14676
+ * @param params - Adapter, chain, action key, signed payload, and approval inputs.
14677
+ * @returns The confirmed execute transaction hash, explorer URL, and receipt
14678
+ * gas data when the adapter can retrieve it.
14679
+ * @throws {@link KitError} when the source chain is not EVM.
14680
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not
14681
+ * supported by the prepared requests.
14682
+ * @throws {@link KitError} when the batch reverts on-chain (a confirmed
14683
+ * terminal revert), carrying `batchId`.
14684
+ * @throws {@link KitError} RETRYABLE when EIP-5792 reports an off-chain
14685
+ * failure carrying `batchId` and status code `400`; no call was included.
14686
+ * @throws {@link KitError} FATAL `NetworkError.TIMEOUT` when the batch was
14687
+ * submitted but its outcome could not be confirmed (poll timeout or any
14688
+ * other non-revert post-submission failure); carries `batchId` so the caller
14689
+ * can check transaction status before retrying.
14690
+ * @remarks
14691
+ * Once the batch has been submitted this function does not fall back to the
14692
+ * sequential path — the batch is already on its way, so a fallback would risk
14693
+ * double-spend. Post-submission failures surface through the adapter's batch
14694
+ * result: a confirmed on-chain revert (Circle: a `TRANSACTION_REVERTED` cause;
14695
+ * Viem: status code `500`/`600`) is thrown as a revert error, status code `400`
14696
+ * is reported as a retryable off-chain failure, and any other unconfirmed
14697
+ * outcome is thrown as a FATAL timeout error carrying `batchId`.
14698
+ *
14699
+ * @example
14700
+ * ```typescript
14701
+ * const { txHash, explorerUrl } = await executeBatchedEarnAction({
14702
+ * adapter,
14703
+ * chain,
14704
+ * address,
14705
+ * actionKey: 'earn.deposit',
14706
+ * executeParams,
14707
+ * tokenInputs,
14708
+ * signature,
14709
+ * approvalToken: usdcAddress,
14710
+ * delegate: adapterContractAddress,
14711
+ * requiredAllowance: 1_000_000n,
14712
+ * idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
14713
+ * revertMessage: 'Earn deposit reverted on-chain',
14714
+ * })
14715
+ * ```
14716
+ *
14717
+ * @internal
14718
+ */ async function executeBatchedEarnAction(params) {
14719
+ const { adapter, chain, address, actionKey, executeParams, tokenInputs, signature, approvalToken, delegate, requiredAllowance, idempotencyKey, revertMessage } = params;
14720
+ if (chain.type !== 'evm') {
14721
+ throw new KitError({
14722
+ ...InputError.INVALID_CHAIN,
14723
+ recoverability: 'FATAL',
14724
+ message: 'Batched earn execution is only supported on EVM chains.'
14725
+ });
14726
+ }
14727
+ const evmChain = chain;
14728
+ const batchAdapter = adapter;
14729
+ // Read the current allowance so the approval tops up only the missing amount.
14730
+ // When the existing allowance already covers the payload, skip the approve
14731
+ // call and batch only the execute — this mirrors the sequential
14732
+ // approveAllowanceIfNeeded guard and avoids an increaseAllowance underflow
14733
+ // (requiredAllowance - currentAllowance would be negative, which reverts as
14734
+ // an out-of-range uint256).
14735
+ const allowancePrepared = await adapter.prepareAction('token.allowance', {
14736
+ tokenAddress: approvalToken,
14737
+ delegate
14738
+ }, {
14739
+ chain,
14740
+ address
14741
+ });
14742
+ const currentAllowance = parseAllowanceResponse(await allowancePrepared.execute());
14743
+ const approvalNeeded = currentAllowance < requiredAllowance;
14744
+ const executePrepared = await adapter.prepareAction(actionKey, {
14745
+ executeParams,
14746
+ tokenInputs,
14747
+ signature
14748
+ }, {
14749
+ chain,
14750
+ address
14751
+ });
14752
+ const approvePrepared = approvalNeeded ? await prepareApprovalAction({
14753
+ adapter,
14754
+ chain,
14755
+ address,
14756
+ tokenAddress: approvalToken,
14757
+ delegate,
14758
+ currentAllowance,
14759
+ requiredAllowance
14760
+ }) : undefined;
14761
+ if (executePrepared.type !== 'evm' || !executePrepared.getCallData) {
14762
+ throw new KitError({
14763
+ ...InputError.UNSUPPORTED_ACTION,
14764
+ recoverability: 'FATAL',
14765
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
14766
+ });
14767
+ }
14768
+ if (approvePrepared !== undefined && (approvePrepared.type !== 'evm' || !approvePrepared.getCallData)) {
14769
+ throw new KitError({
14770
+ ...InputError.UNSUPPORTED_ACTION,
14771
+ recoverability: 'FATAL',
14772
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
14773
+ });
14774
+ }
14775
+ const executeCallData = executePrepared.getCallData();
14776
+ // Prepend the approve call only when an allowance top-up is required.
14777
+ const calls = approvePrepared?.type === 'evm' && approvePrepared.getCallData ? [
14778
+ approvePrepared.getCallData(),
14779
+ executeCallData
14780
+ ] : [
14781
+ executeCallData
14782
+ ];
14783
+ const authorization = buildBatchedEarnExecuteDescriptor({
14784
+ action: actionKey === 'earn.deposit' ? 'deposit' : 'withdraw',
14785
+ chain: evmChain.chain,
14786
+ executionParams: executeParams
14787
+ });
14788
+ const result = await batchAdapter.batchExecute(calls, evmChain, {
14789
+ fromAddress: address,
14790
+ idempotencyKey,
14791
+ atomicRequired: true,
14792
+ authorization
14793
+ });
14794
+ // Success fans one confirmed hash across every receipt; the execute call is
14795
+ // the last one (approve, if present, precedes it). On failure a confirming
14796
+ // adapter returns no receipts, so a missing/non-success last receipt — or a
14797
+ // populated `result.error` — means the batch failed after submission (point
14798
+ // of no return). We never fall back, which would double-spend.
14799
+ const receiptCountMatches = result.receipts.length === calls.length;
14800
+ const executeReceipt = receiptCountMatches ? result.receipts[calls.length - 1] : undefined;
14801
+ const succeeded = receiptCountMatches && (result.statusCode === undefined || result.statusCode === 200) && result.error === undefined && executeReceipt?.status === 'success' && executeReceipt.txHash !== '';
14802
+ if (succeeded) {
14803
+ return buildSuccessfulBatchResult(adapter, evmChain, executeReceipt, result.batchId, revertMessage);
14804
+ }
14805
+ // Distinguish an off-chain rejection, a confirmed on-chain revert, and an
14806
+ // unknown outcome across both adapter conventions that share this contract:
14807
+ // - Circle SCA: no receipts + `error`; its trace kind identifies an
14808
+ // off-chain rejection, confirmed revert, or unconfirmed outcome.
14809
+ // - Viem EIP-5792: statusCode 500/600 explicitly confirms an on-chain
14810
+ // full/partial revert.
14811
+ // - Legacy/string-status wallets: a real-hash error receipt with no cause
14812
+ // is the best available confirmed-revert signal.
14813
+ // statusCode 400 is terminal but off-chain: the wallet confirms no call was
14814
+ // included, so it must not be labeled as a revert or unknown outcome.
14815
+ // Anything else — a poll timeout or any other post-submission failure with no
14816
+ // confirmed-revert signal — means the batch was submitted but its fate is
14817
+ // unconfirmed. Surface that as a FATAL (non-auto-retry) error carrying
14818
+ // `batchId` so the caller checks status before retrying, rather than
14819
+ // mislabeling it a revert.
14820
+ return throwBatchFailure(result, executeReceipt, evmChain, actionKey, revertMessage);
14821
+ }
14822
+
14823
+ /**
14824
+ * Validate that a service-signed execution payload has not expired before
14825
+ * the SDK asks the wallet to broadcast a transaction.
14826
+ *
14827
+ * @internal
14828
+ */ function validateExecutionDeadline(executionParams) {
14829
+ const deadline = BigInt(executionParams.deadline);
14830
+ const now = BigInt(Math.floor(Date.now() / 1000));
14831
+ if (deadline <= now) {
14832
+ throw createValidationFailedError('executionParams.deadline', executionParams.deadline, 'Earn execution deadline has expired');
14833
+ }
14834
+ }
14835
+
13409
14836
  /** Base discriminators shared by every earn action payload. */ const EARN_ACTION_BASE = {
13410
14837
  protocol: 'earn',
13411
14838
  service: 'earn-service'
@@ -14689,7 +16116,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
14689
16116
  }
14690
16117
 
14691
16118
  var name = "@circle-fin/provider-earn-service";
14692
- var version = "1.3.1";
16119
+ var version = "1.4.0";
14693
16120
  var pkg = {
14694
16121
  name: name,
14695
16122
  version: version};
@@ -16317,6 +17744,46 @@ function finishElapsedWait(lastStatus, lastError) {
16317
17744
  const approvalToken = resolveEarnApprovalToken(executionParams);
16318
17745
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
16319
17746
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
17747
+ const approvalNeeded = !options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n;
17748
+ // Batch-capable wallets bundle approve + deposit into one atomic
17749
+ // submission. Only attempt this when an approval is actually needed.
17750
+ if (approvalNeeded && approvalToken !== undefined && await shouldUseBatchedEarnAction({
17751
+ adapter,
17752
+ chain,
17753
+ address,
17754
+ batchTransactions: config?.batchTransactions
17755
+ })) {
17756
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
17757
+ try {
17758
+ const result = await executeBatchedEarnAction({
17759
+ adapter,
17760
+ chain,
17761
+ address,
17762
+ actionKey: 'earn.deposit',
17763
+ executeParams: executionParams,
17764
+ tokenInputs,
17765
+ signature,
17766
+ approvalToken,
17767
+ delegate: adapterContractAddress,
17768
+ requiredAllowance,
17769
+ idempotencyKey: execId,
17770
+ revertMessage: 'Earn deposit reverted on-chain'
17771
+ });
17772
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
17773
+ return result;
17774
+ } catch (error) {
17775
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
17776
+ throw error;
17777
+ }
17778
+ }, ({ txHash })=>txHash);
17779
+ return {
17780
+ kind: 'same-chain',
17781
+ txHash,
17782
+ explorerUrl,
17783
+ vaultAddress,
17784
+ amount: params.amount
17785
+ };
17786
+ }
16320
17787
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
16321
17788
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16322
17789
  try {
@@ -16349,6 +17816,8 @@ function finishElapsedWait(lastStatus, lastError) {
16349
17816
  tokenInputs,
16350
17817
  signature
16351
17818
  },
17819
+ action: 'deposit',
17820
+ executionParams,
16352
17821
  revertMessage: 'Earn deposit reverted on-chain'
16353
17822
  });
16354
17823
  reportTransactionSuccess(transactionReportContext, 'Deposit', result);
@@ -16480,6 +17949,44 @@ function finishElapsedWait(lastStatus, lastError) {
16480
17949
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
16481
17950
  const approvalToken = tokenInputs[0]?.token;
16482
17951
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
17952
+ // Batch-capable wallets bundle approve + withdraw into one atomic
17953
+ // submission. Only attempt this when an approval is actually needed.
17954
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n && await shouldUseBatchedEarnAction({
17955
+ adapter,
17956
+ chain,
17957
+ address,
17958
+ batchTransactions: config?.batchTransactions
17959
+ })) {
17960
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
17961
+ try {
17962
+ const result = await executeBatchedEarnAction({
17963
+ adapter,
17964
+ chain,
17965
+ address,
17966
+ actionKey: 'earn.withdraw',
17967
+ executeParams: executionParams,
17968
+ tokenInputs,
17969
+ signature,
17970
+ approvalToken,
17971
+ delegate: adapterContractAddress,
17972
+ requiredAllowance,
17973
+ idempotencyKey: execId,
17974
+ revertMessage: 'Earn withdraw reverted on-chain'
17975
+ });
17976
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
17977
+ return result;
17978
+ } catch (error) {
17979
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
17980
+ throw error;
17981
+ }
17982
+ }, ({ txHash })=>txHash);
17983
+ return {
17984
+ txHash,
17985
+ explorerUrl,
17986
+ vaultAddress,
17987
+ amount: params.amount
17988
+ };
17989
+ }
16483
17990
  if (!options.skipApprove && approvalToken !== undefined) {
16484
17991
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
16485
17992
  try {
@@ -16512,6 +18019,8 @@ function finishElapsedWait(lastStatus, lastError) {
16512
18019
  tokenInputs,
16513
18020
  signature
16514
18021
  },
18022
+ action: 'withdraw',
18023
+ executionParams,
16515
18024
  revertMessage: 'Earn withdraw reverted on-chain'
16516
18025
  });
16517
18026
  reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
@@ -16591,6 +18100,8 @@ function finishElapsedWait(lastStatus, lastError) {
16591
18100
  tokenInputs: [],
16592
18101
  signature
16593
18102
  },
18103
+ action: 'claimRewards',
18104
+ executionParams,
16594
18105
  revertMessage: 'Earn claim rewards reverted on-chain'
16595
18106
  }), ({ txHash })=>txHash);
16596
18107
  return {
@@ -17274,11 +18785,16 @@ const sourceAdapterContextSchema = zod.z.object({
17274
18785
  *
17275
18786
  * Validate the optional Kit Key field using the standard `apiKeySchema`
17276
18787
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
17277
- * operates in permissionless mode.
18788
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
18789
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
18790
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
18791
+ * schema must accept them or a valid config object is rejected.
17278
18792
  *
17279
18793
  * @internal
17280
18794
  */ const earnConfigSchema = zod.z.object({
17281
- kitKey: apiKeySchema.optional()
18795
+ kitKey: apiKeySchema.optional(),
18796
+ baseUrl: zod.z.string().optional(),
18797
+ batchTransactions: zod.z.boolean().optional()
17282
18798
  }).strict();
17283
18799
  /**
17284
18800
  * Canonical decimal form: a leading digit with no leading zeros (a single