@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/index.cjs CHANGED
@@ -40,6 +40,7 @@ var web3_js = require('@solana/web3.js');
40
40
  require('bn.js');
41
41
  require('@coral-xyz/anchor');
42
42
  require('@noble/curves/ed25519');
43
+ var viem = require('viem');
43
44
  var keccak256 = require('@ethersproject/keccak256');
44
45
 
45
46
  function _interopDefault (e) { return e && e.__esModule ? e.default : e; }
@@ -47,44 +48,6 @@ function _interopDefault (e) { return e && e.__esModule ? e.default : e; }
47
48
  var pino__default = /*#__PURE__*/_interopDefault(pino);
48
49
  var bs58__default = /*#__PURE__*/_interopDefault(bs58);
49
50
 
50
- /**
51
- * Creates a AppKit context.
52
- *
53
- * This function constructs a context object, initializes the actions registry
54
- * used for event handlers, and merges in any custom implementations provided
55
- * via params.
56
- *
57
- * @param params - Optional custom implementations to override defaults
58
- * @returns A AppKitContext
59
- *
60
- * @example
61
- * ```typescript
62
- * // Create context with all defaults
63
- * const defaultContext = createContext()
64
- *
65
- * // Create context with custom fee calculation
66
- * const customContext = createContext({
67
- * getFee: async (type, params) => {
68
- * if (type === 'bridge') {
69
- * // Custom bridge fee logic
70
- * return await calculateBridgeFee(params)
71
- * }
72
- * // Use default for other types
73
- * return defaultFeeCalculation(type, params)
74
- * }
75
- * })
76
- * ```
77
- */ const createContext = (params = {})=>{
78
- return {
79
- ...params,
80
- actions: {
81
- bridge: {},
82
- earn: {},
83
- ...params.actions
84
- }
85
- };
86
- };
87
-
88
51
  // Import global type declarations
89
52
  /**
90
53
  * Check whether the current runtime is Node.js.
@@ -4130,6 +4093,8 @@ function getOptionalString(value) {
4130
4093
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
4131
4094
  Blockchain["XDC"] = "XDC";
4132
4095
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
4096
+ Blockchain["X_Layer"] = "X_Layer";
4097
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
4133
4098
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
4134
4099
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
4135
4100
  })(exports.Blockchain || (exports.Blockchain = {}));
@@ -4183,6 +4148,7 @@ exports.BridgeChain = void 0;
4183
4148
  BridgeChain["Unichain"] = "Unichain";
4184
4149
  BridgeChain["World_Chain"] = "World_Chain";
4185
4150
  BridgeChain["XDC"] = "XDC";
4151
+ BridgeChain["X_Layer"] = "X_Layer";
4186
4152
  // Testnet chains with CCTPv2 support
4187
4153
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
4188
4154
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -4208,6 +4174,7 @@ exports.BridgeChain = void 0;
4208
4174
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
4209
4175
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
4210
4176
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
4177
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
4211
4178
  })(exports.BridgeChain || (exports.BridgeChain = {}));
4212
4179
  exports.UnifiedBalanceChain = void 0;
4213
4180
  (function(UnifiedBalanceChain) {
@@ -6766,7 +6733,8 @@ exports.EarnChain = void 0;
6766
6733
  isTestnet: true,
6767
6734
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
6768
6735
  rpcEndpoints: [
6769
- 'https://rpc-amoy.polygon.technology'
6736
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
6737
+ 'https://polygon-amoy.drpc.org'
6770
6738
  ],
6771
6739
  eurcAddress: null,
6772
6740
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -7631,6 +7599,104 @@ exports.EarnChain = void 0;
7631
7599
  }
7632
7600
  });
7633
7601
 
7602
+ /**
7603
+ * X Layer Mainnet chain definition
7604
+ * @remarks
7605
+ * This represents the official production network for the X Layer blockchain.
7606
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
7607
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
7608
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
7609
+ */ const XLayer = defineChain({
7610
+ type: 'evm',
7611
+ chain: exports.Blockchain.X_Layer,
7612
+ name: 'X Layer',
7613
+ title: 'X Layer Mainnet',
7614
+ nativeCurrency: {
7615
+ name: 'OKB',
7616
+ symbol: 'OKB',
7617
+ decimals: 18
7618
+ },
7619
+ chainId: 196,
7620
+ isTestnet: false,
7621
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
7622
+ rpcEndpoints: [
7623
+ 'https://xlayerrpc.okx.com'
7624
+ ],
7625
+ eurcAddress: null,
7626
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7627
+ usdtAddress: null,
7628
+ cctp: {
7629
+ domain: 37,
7630
+ contracts: {
7631
+ v2: {
7632
+ type: 'split',
7633
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7634
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7635
+ confirmations: 65,
7636
+ fastConfirmations: 1
7637
+ }
7638
+ },
7639
+ forwarderSupported: {
7640
+ source: false,
7641
+ destination: false
7642
+ }
7643
+ },
7644
+ kitContracts: {
7645
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
7646
+ }
7647
+ });
7648
+
7649
+ /**
7650
+ * X Layer Testnet chain definition
7651
+ * @remarks
7652
+ * This represents the official test network for the X Layer blockchain.
7653
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
7654
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
7655
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
7656
+ */ const XLayerTestnet = defineChain({
7657
+ type: 'evm',
7658
+ chain: exports.Blockchain.X_Layer_Testnet,
7659
+ name: 'X Layer Testnet',
7660
+ title: 'X Layer Testnet',
7661
+ nativeCurrency: {
7662
+ name: 'OKB',
7663
+ symbol: 'OKB',
7664
+ decimals: 18
7665
+ },
7666
+ chainId: 1952,
7667
+ isTestnet: true,
7668
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
7669
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
7670
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
7671
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
7672
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
7673
+ rpcEndpoints: [
7674
+ 'https://testrpc.xlayer.tech'
7675
+ ],
7676
+ eurcAddress: null,
7677
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7678
+ usdtAddress: null,
7679
+ cctp: {
7680
+ domain: 37,
7681
+ contracts: {
7682
+ v2: {
7683
+ type: 'split',
7684
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7685
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
7686
+ confirmations: 65,
7687
+ fastConfirmations: 1
7688
+ }
7689
+ },
7690
+ forwarderSupported: {
7691
+ source: false,
7692
+ destination: false
7693
+ }
7694
+ },
7695
+ kitContracts: {
7696
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
7697
+ }
7698
+ });
7699
+
7634
7700
  /**
7635
7701
  * ZKSync Era Mainnet chain definition
7636
7702
  * @remarks
@@ -7750,6 +7816,8 @@ var Chains = {
7750
7816
  WorldChainSepolia: WorldChainSepolia,
7751
7817
  XDC: XDC,
7752
7818
  XDCApothem: XDCApothem,
7819
+ XLayer: XLayer,
7820
+ XLayerTestnet: XLayerTestnet,
7753
7821
  ZKSyncEra: ZKSyncEra,
7754
7822
  ZKSyncEraSepolia: ZKSyncEraSepolia
7755
7823
  };
@@ -10113,6 +10181,7 @@ function parseOrThrow(value, schema, context) {
10113
10181
  [exports.Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
10114
10182
  [exports.Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
10115
10183
  [exports.Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
10184
+ [exports.Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
10116
10185
  [exports.Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
10117
10186
  // =========================================================================
10118
10187
  // Testnets (alphabetically sorted)
@@ -10147,6 +10216,7 @@ function parseOrThrow(value, schema, context) {
10147
10216
  [exports.Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
10148
10217
  [exports.Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
10149
10218
  [exports.Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
10219
+ [exports.Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
10150
10220
  [exports.Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
10151
10221
  }
10152
10222
  };
@@ -11096,6 +11166,52 @@ function parseOrThrow(value, schema, context) {
11096
11166
  return explorerUrl;
11097
11167
  }
11098
11168
 
11169
+ /**
11170
+ * Assert that a value has type `never` (exhaustive switch helper).
11171
+ *
11172
+ * @remarks
11173
+ * Use in the `default` branch of a switch over a discriminated union.
11174
+ * If all union members are handled, the default is unreachable and TypeScript
11175
+ * narrows the parameter to `never`. If a member is missed, the compiler errors.
11176
+ *
11177
+ * @param _x - The value (typed as `never` when switch is exhaustive).
11178
+ * @returns Never returns; always throws.
11179
+ * @throws Error when the switch is not exhaustive.
11180
+ *
11181
+ * @example
11182
+ * ```typescript
11183
+ * type Foo = { type: 'a'; x: number } | { type: 'b'; y: string }
11184
+ *
11185
+ * function handle(foo: Foo): string {
11186
+ * switch (foo.type) {
11187
+ * case 'a': return String(foo.x)
11188
+ * case 'b': return foo.y
11189
+ * default: return assertNever(foo)
11190
+ * }
11191
+ * }
11192
+ * ```
11193
+ */ function assertNever$2(x) {
11194
+ // Plain `String(x)` collapses non-primitive union members (objects, arrays)
11195
+ // to `'[object Object]'`, which is useless when triaging which discriminant
11196
+ // was missed. Attempt `JSON.stringify` first so the thrown message preserves
11197
+ // the offending shape. Fall back to a minimal `typeof`-based label if
11198
+ // serialization fails (`BigInt` member, circular references, host objects).
11199
+ //
11200
+ // `x` is statically typed as `never` (the whole point of this helper), but
11201
+ // at runtime callers may still pass an unexpected value when the switch is
11202
+ // not actually exhaustive — that's exactly the bug we want to surface. Cast
11203
+ // through `unknown` so the runtime defence is not stripped by the compiler.
11204
+ const value = x;
11205
+ let stringified;
11206
+ try {
11207
+ const json = JSON.stringify(value);
11208
+ stringified = typeof json === 'string' ? json : `<${typeof value}>`;
11209
+ } catch {
11210
+ stringified = `<unstringifiable ${typeof value}>`;
11211
+ }
11212
+ throw new Error(`Unhandled switch case: ${stringified}`);
11213
+ }
11214
+
11099
11215
  /**
11100
11216
  * CCTP forwarding magic bytes prefix.
11101
11217
  *
@@ -11756,7 +11872,7 @@ function resolveOptions(options) {
11756
11872
  }
11757
11873
 
11758
11874
  var name$4 = "@circle-fin/bridge-kit";
11759
- var version$5 = "1.12.2";
11875
+ var version$5 = "1.13.0";
11760
11876
  var pkg$5 = {
11761
11877
  name: name$4,
11762
11878
  version: version$5};
@@ -11793,13 +11909,21 @@ const assertCustomFeePolicySymbol$2 = Symbol('assertCustomFeePolicy');
11793
11909
  computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
11794
11910
  calculateFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
11795
11911
  resolveFeeRecipientAddress: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string())))
11796
- }).strict().refine((data)=>{
11912
+ }).strict().superRefine((data, ctx)=>{
11797
11913
  const hasComputeFee = data.computeFee !== undefined;
11798
11914
  const hasCalculateFee = data.calculateFee !== undefined;
11799
- // XOR: exactly one must be provided
11800
- return hasComputeFee !== hasCalculateFee;
11801
- }, {
11802
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
11915
+ if (hasComputeFee && hasCalculateFee) {
11916
+ ctx.addIssue({
11917
+ code: zod.z.ZodIssueCode.custom,
11918
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
11919
+ });
11920
+ }
11921
+ if (!hasComputeFee && !hasCalculateFee) {
11922
+ ctx.addIssue({
11923
+ code: zod.z.ZodIssueCode.custom,
11924
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
11925
+ });
11926
+ }
11803
11927
  });
11804
11928
  /**
11805
11929
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
@@ -14599,14 +14723,32 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
14599
14723
  ;
14600
14724
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
14601
14725
  ;
14602
- // Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
14603
- // chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
14604
- // Kept separate from the fee-estimate averages above.
14605
- const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
14726
+ // Gas FLOORS, not ceilings kept separate from the fee-estimate averages
14727
+ // above. `executePreparedChainRequest` submits
14728
+ // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
14729
+ // is covered by its own estimate, and a chain whose estimator under-reports
14730
+ // (Cronos: returns 30_600 where the EIP-7623 calldata floor is 45_000) is
14731
+ // covered by the floor.
14732
+ //
14733
+ // Two distinct chain surcharges drive these numbers, both measured live:
14734
+ // Sei — ~+51_500 per NEWLY CREATED storage slot (73_595 vs vanilla 22_100);
14735
+ // no flat per-tx surcharge (31_535, identical to Base).
14736
+ // Edge — ~+53_200 flat on EVERY tx (84_751 vs Base 31_535); storage priced
14737
+ // normally. Edge therefore fails warm as well as cold.
14738
+ // A floor must clear the worst COLD cost, since a slot that exists at estimate
14739
+ // time can be consumed before inclusion and cost a full step more on execution.
14740
+ // Each floor is therefore derived from the worst observed estimate *after* the
14741
+ // 1.25x buffer, plus headroom — sizing it below the buffered value would leave
14742
+ // the estimate governing and defeat the point of the floor.
14743
+ //
14744
+ // The `*_GAS_LIMIT_EVM` names are kept despite these being floors: they are
14745
+ // exported, so renaming to `*_GAS_FLOOR_EVM` would be a breaking change for
14746
+ // consumers. Read "LIMIT" here as "the limit we submit", never as a ceiling.
14747
+ const APPROVE_GAS_LIMIT_EVM = 150_000n // buffered worst cold 149_355 (Edge Testnet 119_484 x 1.25) + drift headroom
14606
14748
  ;
14607
- const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
14749
+ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 500_000n // buffered worst 474_078 (Sei 379_263 x 1.25) + ~26k headroom
14608
14750
  ;
14609
- const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
14751
+ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears Cronos' calldata floor ~10x
14610
14752
  ;
14611
14753
  /**
14612
14754
  * The minimum finality threshold for CCTPv2 transfers.
@@ -16071,6 +16213,63 @@ function hasPendingState(analysis, result) {
16071
16213
  return waitForPendingTransaction(pendingStep, adapter, chain);
16072
16214
  }
16073
16215
 
16216
+ /**
16217
+ * Multiplier applied to a successful gas estimate before it is submitted.
16218
+ *
16219
+ * Estimates are exact, not padded: Sei returns 109_739 for an approve that
16220
+ * consumes 107_717 (1.9% headroom). Chains that price storage in large steps
16221
+ * can exceed the estimate if state changes between estimation and inclusion,
16222
+ * so the estimate is padded before use.
16223
+ *
16224
+ * @remarks
16225
+ * This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
16226
+ * scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
16227
+ * covers a slot that exists at estimation time and is consumed before
16228
+ * inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
16229
+ * the estimate covers it. For burn the buffer does cover a step (25% of
16230
+ * ~300_000 exceeds 51_500).
16231
+ */ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
16232
+ /**
16233
+ * Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
16234
+ *
16235
+ * Estimates first so chains whose real cost exceeds the floor are covered by
16236
+ * their own measurement, and falls back to the floor whenever estimation is
16237
+ * unavailable or under-reports. Estimation failure is never fatal here: before
16238
+ * floors existed these requests were submitted with a pinned limit and no
16239
+ * estimate at all, so degrading to the floor is never worse than the previous
16240
+ * behaviour.
16241
+ *
16242
+ * @param request - The prepared EVM request to size a gas limit for
16243
+ * @param gasFloor - The minimum gas limit to submit, in gas units
16244
+ * @returns The gas limit to submit, in gas units
16245
+ * @throws Never — estimation failures degrade to `gasFloor`
16246
+ *
16247
+ * @example
16248
+ * ```typescript
16249
+ * const gasLimit = await resolveGasLimit(request, 150_000)
16250
+ * ```
16251
+ */ const resolveGasLimit = async (request, gasFloor)=>{
16252
+ try {
16253
+ // Deliberately called without a `fallback`: both the viem and ethers
16254
+ // adapters *return* the supplied fallback object when estimation reverts
16255
+ // rather than throwing, which would set the estimate to the floor and then
16256
+ // multiply it by the buffer below. Omitting it routes reverts through the
16257
+ // catch, so a failed estimate degrades to exactly the floor.
16258
+ const estimate = await request.estimate();
16259
+ // The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
16260
+ // typed `bigint`, but adapters are a public extension point and may be
16261
+ // implemented in plain JS, so a non-bigint `gas` would throw here
16262
+ // ("Cannot mix BigInt and other types"). Guarding it keeps the documented
16263
+ // contract — estimation never aborts a step, it degrades to the floor.
16264
+ const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
16265
+ // Convert before comparing: Math.max throws on BigInt operands, and gas
16266
+ // units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
16267
+ return Math.max(Number(buffered), gasFloor);
16268
+ } catch {
16269
+ // Estimation is best-effort; the floor is the known-safe value.
16270
+ return gasFloor;
16271
+ }
16272
+ };
16074
16273
  /**
16075
16274
  * Executes a prepared chain request and returns the result as a bridge step.
16076
16275
  *
@@ -16084,8 +16283,8 @@ function hasPendingState(analysis, result) {
16084
16283
  * - `adapter`: The adapter that will execute the transaction
16085
16284
  * - `confirmations`: The number of confirmations to wait for (defaults to 1)
16086
16285
  * - `timeout`: The timeout for the request in milliseconds
16087
- * - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
16088
- * bypassing `eth_estimateGas`; ignored for non-EVM requests
16286
+ * - `gasFloor`: Optional minimum gas limit (number); the request is submitted
16287
+ * with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
16089
16288
  * @returns The bridge step with the transaction details and explorer URL
16090
16289
  * @throws If the transaction execution fails
16091
16290
  *
@@ -16100,7 +16299,7 @@ function hasPendingState(analysis, result) {
16100
16299
  * })
16101
16300
  * console.log('Transaction hash:', step.txHash)
16102
16301
  * ```
16103
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
16302
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
16104
16303
  const step = {
16105
16304
  name,
16106
16305
  state: 'pending'
@@ -16113,8 +16312,8 @@ function hasPendingState(analysis, result) {
16113
16312
  step.state = 'noop';
16114
16313
  return step;
16115
16314
  }
16116
- const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
16117
- gasLimit
16315
+ const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
16316
+ gasLimit: await resolveGasLimit(request, gasFloor)
16118
16317
  }) : await request.execute();
16119
16318
  step.txHash = txHash;
16120
16319
  const retryOptions = {
@@ -16188,7 +16387,7 @@ function hasPendingState(analysis, result) {
16188
16387
  adapter: params.source.adapter,
16189
16388
  chain: params.source.chain,
16190
16389
  request: await provider.approve(params.source, approvalAmount),
16191
- gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
16390
+ gasFloor: Number(APPROVE_GAS_LIMIT_EVM)
16192
16391
  });
16193
16392
  }
16194
16393
 
@@ -16216,7 +16415,7 @@ function hasPendingState(analysis, result) {
16216
16415
  adapter: params.source.adapter,
16217
16416
  chain: params.source.chain,
16218
16417
  request: await provider.burn(params),
16219
- gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
16418
+ gasFloor: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
16220
16419
  });
16221
16420
  }
16222
16421
 
@@ -16310,10 +16509,9 @@ function hasPendingState(analysis, result) {
16310
16509
  request: mintRequest,
16311
16510
  // Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
16312
16511
  // eth_estimateGas does not account for, returning a below-floor value
16313
- // without reverting. Pinning to a value above the observed execution max
16314
- // (310_839) bypasses re-estimation and guarantees we clear both the floor
16315
- // and the actual execution cost.
16316
- gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
16512
+ // without reverting. The floor covers those; chains that cost more than the
16513
+ // floor are covered by their own estimate.
16514
+ gasFloor: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
16317
16515
  });
16318
16516
  // Add forwarded: false for non-relayer mints
16319
16517
  return {
@@ -16836,7 +17034,7 @@ const mockAttestationMessage = {
16836
17034
  return step;
16837
17035
  }
16838
17036
 
16839
- var version$4 = "1.10.1";
17037
+ var version$4 = "1.10.2";
16840
17038
  var pkg$4 = {
16841
17039
  version: version$4};
16842
17040
 
@@ -19382,75 +19580,8 @@ function assertCCTPV2Config(config) {
19382
19580
  // Auto-register this kit for user agent tracking
19383
19581
  registerKit(`${pkg$5.name}/${pkg$5.version}`);
19384
19582
 
19385
- /**
19386
- * Create a BridgeKit instance with optional developer fee configuration.
19387
- *
19388
- * This utility creates a BridgeKit instance that optionally includes developer
19389
- * fee configuration based on the provided AppKit context. If the context
19390
- * provides both `getFee` and `getFeeRecipient` methods, they will be configured
19391
- * as developer fees in the BridgeKit instance using the `setCustomFeePolicy` method.
19392
- *
19393
- * The fee integration transforms string-based fees from the context into the
19394
- * format expected by BridgeKit, enabling seamless fee calculation across both kits.
19395
- *
19396
- * @param context - The AppKit context containing optional fee methods
19397
- * @returns A configured BridgeKit instance with or without developer fees
19398
- *
19399
- * @example
19400
- * ```typescript
19401
- * import { createBridgeKit } from '@circle-fin/app-kit/utils'
19402
- * import { createContext } from '@circle-fin/app-kit/context'
19403
- *
19404
- * // Create context with fee methods
19405
- * const context = createContext({
19406
- * getFee: async (type, params) => '1000000', // 1 USDC in micro-units
19407
- * getFeeRecipient: async (type, info) => '0x742d35Cc4634C0532925a3b8D1d7'
19408
- * })
19409
- *
19410
- * // Create BridgeKit with developer fees
19411
- * const bridgeKit = createBridgeKit(context)
19412
- * ```
19413
- *
19414
- * @example
19415
- * ```typescript
19416
- * import { createBridgeKit } from '@circle-fin/app-kit/utils'
19417
- * import { createContext } from '@circle-fin/app-kit/context'
19418
- *
19419
- * // Create context without fee methods
19420
- * const context = createContext()
19421
- *
19422
- * // Create standard BridgeKit instance
19423
- * const bridgeKit = createBridgeKit(context)
19424
- * ```
19425
- */ const createBridgeKit = (context)=>{
19426
- const getFee = context.getFee?.bind(context);
19427
- const getFeeRecipient = context.getFeeRecipient?.bind(context);
19428
- const hasBoth = typeof getFee === 'function' && typeof getFeeRecipient === 'function';
19429
- const kit = new BridgeKit({
19430
- ...context.disableErrorReporting != null && {
19431
- disableErrorReporting: context.disableErrorReporting
19432
- },
19433
- ...context.headers != null && {
19434
- headers: context.headers
19435
- }
19436
- });
19437
- if (hasBoth) {
19438
- kit.setCustomFeePolicy({
19439
- calculateFee: async (params)=>{
19440
- const feeStr = await getFee('bridge', params);
19441
- return feeStr;
19442
- },
19443
- resolveFeeRecipientAddress: async (chain, params)=>await getFeeRecipient('bridge', {
19444
- chain,
19445
- params: params || {}
19446
- })
19447
- });
19448
- }
19449
- return kit;
19450
- };
19451
-
19452
19583
  var name$3 = "@circle-fin/swap-kit";
19453
- var version$3 = "1.5.0";
19584
+ var version$3 = "1.5.1";
19454
19585
  var pkg$3 = {
19455
19586
  name: name$3,
19456
19587
  version: version$3};
@@ -20866,6 +20997,138 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20866
20997
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
20867
20998
  };
20868
20999
 
21000
+ /**
21001
+ * IAdapter contract ABI.
21002
+ *
21003
+ * Shared ABI for the on-chain Adapter contract used by multiple kits
21004
+ * (swap, earn) for executing signed instruction sets. The `execute()`
21005
+ * function accepts EIP-712 signed execution parameters, token inputs,
21006
+ * and a signature, then executes the corresponding on-chain
21007
+ * instructions.
21008
+ */ const adapterContractAbi = [
21009
+ {
21010
+ type: 'function',
21011
+ name: 'execute',
21012
+ inputs: [
21013
+ {
21014
+ name: 'params',
21015
+ type: 'tuple',
21016
+ internalType: 'struct IAdapter.ExecutionParams',
21017
+ components: [
21018
+ {
21019
+ name: 'instructions',
21020
+ type: 'tuple[]',
21021
+ internalType: 'struct IAdapter.Instruction[]',
21022
+ components: [
21023
+ {
21024
+ name: 'target',
21025
+ type: 'address',
21026
+ internalType: 'address'
21027
+ },
21028
+ {
21029
+ name: 'data',
21030
+ type: 'bytes',
21031
+ internalType: 'bytes'
21032
+ },
21033
+ {
21034
+ name: 'value',
21035
+ type: 'uint256',
21036
+ internalType: 'uint256'
21037
+ },
21038
+ {
21039
+ name: 'tokenIn',
21040
+ type: 'address',
21041
+ internalType: 'address'
21042
+ },
21043
+ {
21044
+ name: 'amountToApprove',
21045
+ type: 'uint256',
21046
+ internalType: 'uint256'
21047
+ },
21048
+ {
21049
+ name: 'tokenOut',
21050
+ type: 'address',
21051
+ internalType: 'address'
21052
+ },
21053
+ {
21054
+ name: 'minTokenOut',
21055
+ type: 'uint256',
21056
+ internalType: 'uint256'
21057
+ }
21058
+ ]
21059
+ },
21060
+ {
21061
+ name: 'tokens',
21062
+ type: 'tuple[]',
21063
+ internalType: 'struct IAdapter.TokenRecipient[]',
21064
+ components: [
21065
+ {
21066
+ name: 'token',
21067
+ type: 'address',
21068
+ internalType: 'address'
21069
+ },
21070
+ {
21071
+ name: 'beneficiary',
21072
+ type: 'address',
21073
+ internalType: 'address'
21074
+ }
21075
+ ]
21076
+ },
21077
+ {
21078
+ name: 'execId',
21079
+ type: 'uint256',
21080
+ internalType: 'uint256'
21081
+ },
21082
+ {
21083
+ name: 'deadline',
21084
+ type: 'uint256',
21085
+ internalType: 'uint256'
21086
+ },
21087
+ {
21088
+ name: 'metadata',
21089
+ type: 'bytes',
21090
+ internalType: 'bytes'
21091
+ }
21092
+ ]
21093
+ },
21094
+ {
21095
+ name: 'tokenInputs',
21096
+ type: 'tuple[]',
21097
+ internalType: 'struct IAdapter.TokenInput[]',
21098
+ components: [
21099
+ {
21100
+ name: 'permitType',
21101
+ type: 'uint8',
21102
+ internalType: 'enum IAdapter.PermitType'
21103
+ },
21104
+ {
21105
+ name: 'token',
21106
+ type: 'address',
21107
+ internalType: 'address'
21108
+ },
21109
+ {
21110
+ name: 'amount',
21111
+ type: 'uint256',
21112
+ internalType: 'uint256'
21113
+ },
21114
+ {
21115
+ name: 'permitCalldata',
21116
+ type: 'bytes',
21117
+ internalType: 'bytes'
21118
+ }
21119
+ ]
21120
+ },
21121
+ {
21122
+ name: 'signature',
21123
+ type: 'bytes',
21124
+ internalType: 'bytes'
21125
+ }
21126
+ ],
21127
+ outputs: [],
21128
+ stateMutability: 'payable'
21129
+ }
21130
+ ];
21131
+
20869
21132
  /**
20870
21133
  * USDC ABI
20871
21134
  *
@@ -22101,6 +22364,179 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
22101
22364
  }
22102
22365
  ];
22103
22366
 
22367
+ /**
22368
+ * Minimal ERC-4626 tokenized-vault ABI.
22369
+ *
22370
+ * Covers only the mutating methods EarnKit bundles as inner instructions inside
22371
+ * an Adapter `execute()` call: `deposit`, `withdraw`, and `redeem`. It exists so
22372
+ * clients can decode the inner instruction calldata into a human-readable
22373
+ * summary of what a signer is authorizing (asset amount, receiver, owner)
22374
+ * rather than showing opaque bytes. The 4-byte selectors match the calldata the
22375
+ * earn service signs (`deposit(uint256,address)` = `0x6e553f65`,
22376
+ * `withdraw(uint256,address,address)` = `0xb460af94`,
22377
+ * `redeem(uint256,address,address)` = `0xba087652`).
22378
+ */ const erc4626VaultAbi = [
22379
+ {
22380
+ type: 'function',
22381
+ name: 'deposit',
22382
+ stateMutability: 'nonpayable',
22383
+ inputs: [
22384
+ {
22385
+ name: 'assets',
22386
+ type: 'uint256',
22387
+ internalType: 'uint256'
22388
+ },
22389
+ {
22390
+ name: 'receiver',
22391
+ type: 'address',
22392
+ internalType: 'address'
22393
+ }
22394
+ ],
22395
+ outputs: [
22396
+ {
22397
+ name: 'shares',
22398
+ type: 'uint256',
22399
+ internalType: 'uint256'
22400
+ }
22401
+ ]
22402
+ },
22403
+ {
22404
+ type: 'function',
22405
+ name: 'withdraw',
22406
+ stateMutability: 'nonpayable',
22407
+ inputs: [
22408
+ {
22409
+ name: 'assets',
22410
+ type: 'uint256',
22411
+ internalType: 'uint256'
22412
+ },
22413
+ {
22414
+ name: 'receiver',
22415
+ type: 'address',
22416
+ internalType: 'address'
22417
+ },
22418
+ {
22419
+ name: 'owner',
22420
+ type: 'address',
22421
+ internalType: 'address'
22422
+ }
22423
+ ],
22424
+ outputs: [
22425
+ {
22426
+ name: 'shares',
22427
+ type: 'uint256',
22428
+ internalType: 'uint256'
22429
+ }
22430
+ ]
22431
+ },
22432
+ {
22433
+ type: 'function',
22434
+ name: 'redeem',
22435
+ stateMutability: 'nonpayable',
22436
+ inputs: [
22437
+ {
22438
+ name: 'shares',
22439
+ type: 'uint256',
22440
+ internalType: 'uint256'
22441
+ },
22442
+ {
22443
+ name: 'receiver',
22444
+ type: 'address',
22445
+ internalType: 'address'
22446
+ },
22447
+ {
22448
+ name: 'owner',
22449
+ type: 'address',
22450
+ internalType: 'address'
22451
+ }
22452
+ ],
22453
+ outputs: [
22454
+ {
22455
+ name: 'assets',
22456
+ type: 'uint256',
22457
+ internalType: 'uint256'
22458
+ }
22459
+ ]
22460
+ }
22461
+ ];
22462
+
22463
+ /**
22464
+ * Minimal FeeTaker ABI.
22465
+ *
22466
+ * The earn service appends a `takeFeeERC20` instruction to withdraw bundles
22467
+ * when Circle charges a withdrawal fee. This ABI decodes that inner instruction
22468
+ * so the fee (token, beneficiary, amount) is visible in the signing summary
22469
+ * instead of appearing as opaque calldata alongside the redeem/withdraw call.
22470
+ */ const feeTakerAbi = [
22471
+ {
22472
+ type: 'function',
22473
+ name: 'takeFeeERC20',
22474
+ stateMutability: 'nonpayable',
22475
+ inputs: [
22476
+ {
22477
+ name: 'token',
22478
+ type: 'address',
22479
+ internalType: 'address'
22480
+ },
22481
+ {
22482
+ name: 'beneficiary',
22483
+ type: 'address',
22484
+ internalType: 'address'
22485
+ },
22486
+ {
22487
+ name: 'fee',
22488
+ type: 'uint256',
22489
+ internalType: 'uint256'
22490
+ },
22491
+ {
22492
+ name: 'kitType',
22493
+ type: 'bytes8',
22494
+ internalType: 'bytes8'
22495
+ }
22496
+ ],
22497
+ outputs: []
22498
+ }
22499
+ ];
22500
+
22501
+ /**
22502
+ * Minimal Merkl Distributor ABI.
22503
+ *
22504
+ * EarnKit claim-rewards bundles a single `claim` instruction targeting the
22505
+ * Merkl Distributor, batching one entry per reward token. This ABI decodes that
22506
+ * inner instruction so the claimed tokens and amounts are visible in the signing
22507
+ * summary. `claim` uses dynamic array arguments, which is why a real ABI decoder
22508
+ * (rather than fixed-word slicing) is required for the reward instruction.
22509
+ */ const merklDistributorAbi = [
22510
+ {
22511
+ type: 'function',
22512
+ name: 'claim',
22513
+ stateMutability: 'nonpayable',
22514
+ inputs: [
22515
+ {
22516
+ name: 'users',
22517
+ type: 'address[]',
22518
+ internalType: 'address[]'
22519
+ },
22520
+ {
22521
+ name: 'tokens',
22522
+ type: 'address[]',
22523
+ internalType: 'address[]'
22524
+ },
22525
+ {
22526
+ name: 'amounts',
22527
+ type: 'uint256[]',
22528
+ internalType: 'uint256[]'
22529
+ },
22530
+ {
22531
+ name: 'proofs',
22532
+ type: 'bytes32[][]',
22533
+ internalType: 'bytes32[][]'
22534
+ }
22535
+ ],
22536
+ outputs: []
22537
+ }
22538
+ ];
22539
+
22104
22540
  /**
22105
22541
  * Zod schema for validating EVM adapter capabilities.
22106
22542
  *
@@ -23609,72 +24045,55 @@ function evmSigningData(burnIntent) {
23609
24045
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
23610
24046
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
23611
24047
  * signatures, so for Gateway's purposes a 7702-delegated address is
23612
- * an EOA, not an SCA.
24048
+ * an EOA, not a contract signer.
23613
24049
  *
23614
24050
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
23615
24051
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
23616
24052
  /**
23617
- * Assert that `address` on `chain` can sign Gateway burn intents.
24053
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
24054
+ * rather than as an EOA.
23618
24055
  *
23619
- * Gateway verifies burn-intent signatures with plain `ecrecover` (see
23620
- * `evm-gateway-contracts/src/lib/EIP712Domain.sol`). Smart-contract
23621
- * accounts (SCAs) produce signatures over wrapped hashes (ERC-1271 /
23622
- * ERC-6492 / ERC-6900 replay-safe hashes) that Gateway cannot verify.
23623
- * Additionally, the Circle Wallets backend rejects SCA typed-data signing
23624
- * against Gateway's chainId-less domain with an opaque
23625
- * `invalid integer value <nil>/<nil> for type uint256` error.
24056
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
24057
+ * check for EOAs, and — for requests that carry `contractSigner: true`
24058
+ * an offchain `isValidSignature` simulation against the signing contract
24059
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
24060
+ * declare it. This detects the contract case from on-chain bytecode.
23626
24061
  *
23627
- * EIP-7702-delegated EOAs are exempt: they expose non-empty bytecode
23628
- * (`0xef0100<delegate>`) but the underlying secp256k1 key still produces
23629
- * `ecrecover`-verifiable signatures, so Gateway accepts them.
24062
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
24063
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
24064
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
24065
+ * stays correct for them.
23630
24066
  *
23631
- * When the signer is a true SCA, raises an `INPUT_UNSUPPORTED_ACTION`
23632
- * error directing the caller to register an EOA delegate against the
23633
- * SCA and then submit the spend with the delegate EOA as the signer
23634
- * and the SCA as the source account. See the unified-balance / Gateway
23635
- * docs for the exact API.
23636
- *
23637
- * If bytecode cannot be read (RPC failure, etc.) the pre-check is
23638
- * skipped and downstream signing surfaces its own error — a warning is
23639
- * logged so the skip is diagnosable.
24067
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
24068
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
24069
+ * genuine contract signer misreported this way is rejected by Gateway with
24070
+ * an invalid-signature error rather than silently mis-attested.
23640
24071
  *
23641
24072
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
23642
- * @param address - Signer address to validate.
24073
+ * @param address - Signer address to classify.
23643
24074
  * @param chain - EVM chain where the signer lives.
23644
- * @throws {KitError} INPUT_UNSUPPORTED_ACTION when `address` is an SCA.
24075
+ * @returns `true` when the signer is a contract account and the transfer
24076
+ * request must set `contractSigner: true`; `false` otherwise.
23645
24077
  *
23646
24078
  * @example
23647
24079
  * ```typescript
23648
- * import { assertSignerIsEoa } from '@core/adapter-evm'
24080
+ * import { isContractSigner } from '@core/adapter-evm'
23649
24081
  * import { Ethereum } from '@core/chains'
23650
24082
  *
23651
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
24083
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
23652
24084
  * ```
23653
- */ async function assertSignerIsEoa(adapter, address, chain) {
24085
+ */ async function isContractSigner(adapter, address, chain) {
23654
24086
  let code;
23655
24087
  try {
23656
24088
  code = await adapter.readBytecode(address, chain);
23657
24089
  } catch (err) {
23658
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
23659
- return;
24090
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
24091
+ return false;
23660
24092
  }
23661
24093
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
23662
- return;
24094
+ return false;
23663
24095
  }
23664
- throw new KitError({
23665
- ...InputError.UNSUPPORTED_ACTION,
23666
- recoverability: 'FATAL',
23667
- message: `Gateway burn-intent signing requires an EOA signer (Gateway ` + `verifies signatures with ecrecover and does not support ERC-1271). ` + `The signer ${address} on ${chain.name} has on-chain bytecode, ` + `indicating it is a smart-contract account (SCA). Register an EOA ` + `delegate against the SCA, then submit the spend with the delegate ` + `EOA as the signer and the SCA as the source account. See DEVX-2774.`,
23668
- cause: {
23669
- trace: {
23670
- operation: 'signEvmIntentGroup.assertSignerIsEoa',
23671
- address,
23672
- chain: chain.name,
23673
- bytecodeBytes: (code.length - 2) / 2,
23674
- bytecodePrefix: code.slice(0, 12)
23675
- }
23676
- }
23677
- });
24096
+ return true;
23678
24097
  }
23679
24098
 
23680
24099
  /**
@@ -23701,78 +24120,177 @@ function evmSigningData(burnIntent) {
23701
24120
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
23702
24121
  }
23703
24122
 
24123
+ function resolveIntentChain(group, intent) {
24124
+ const sourceDomain = intent.spec.sourceDomain;
24125
+ const chain = group.chainsByDomain.get(sourceDomain);
24126
+ if (chain !== undefined) return chain;
24127
+ throw createValidationFailedError$1('intent.spec.sourceDomain', sourceDomain, `No source chain found for Gateway domain ${String(sourceDomain)}`);
24128
+ }
24129
+ function normalizeSignatureResult(result) {
24130
+ if (typeof result === 'string') {
24131
+ return {
24132
+ signature: result,
24133
+ contractSigner: false
24134
+ };
24135
+ }
24136
+ if (typeof result === 'object' && result !== null && 'signature' in result && typeof result.signature === 'string') {
24137
+ return {
24138
+ signature: result.signature,
24139
+ contractSigner: 'contractSigner' in result && result.contractSigner === true
24140
+ };
24141
+ }
24142
+ throw createValidationFailedError$1('signature', result, 'must be a signature string or an object containing a signature string');
24143
+ }
24144
+ function validateGroupIntents(intents) {
24145
+ evmSigningData(intents);
24146
+ }
24147
+ function collectChainsByDomain(group) {
24148
+ const chainsByDomain = new Map();
24149
+ for (const intent of group.intents){
24150
+ chainsByDomain.set(intent.spec.sourceDomain, resolveIntentChain(group, intent));
24151
+ }
24152
+ return chainsByDomain;
24153
+ }
24154
+ async function classifySignerTypes(group, chainsByDomain) {
24155
+ const { adapter, address } = group;
24156
+ // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
24157
+ // each consumer package bundles its own copy of the base class and the
24158
+ // `instanceof` identity check fails across package boundaries.
24159
+ // Empty strings are rejected to avoid calling eth_getCode('') on the RPC.
24160
+ const hasResolvedSigner = typeof address === 'string' && address.length > 0;
24161
+ const signerTypes = await Promise.all([
24162
+ ...chainsByDomain
24163
+ ].map(async ([sourceDomain, sourceChain])=>{
24164
+ const contractSigner = hasResolvedSigner && sourceChain.type === 'evm' && isEvmAdapterLike(adapter) ? await isContractSigner(adapter, address, sourceChain) : false;
24165
+ return [
24166
+ sourceDomain,
24167
+ contractSigner
24168
+ ];
24169
+ }));
24170
+ return new Map(signerTypes);
24171
+ }
24172
+ function createSigningUnits(group, signerTypeByDomain) {
24173
+ const contractUnitsByDomain = new Map();
24174
+ let eoaUnit;
24175
+ for (const [index, intent] of group.intents.entries()){
24176
+ const sourceDomain = intent.spec.sourceDomain;
24177
+ const contractSigner = signerTypeByDomain.get(sourceDomain) ?? false;
24178
+ if (contractSigner) {
24179
+ const existingUnit = contractUnitsByDomain.get(sourceDomain);
24180
+ if (existingUnit === undefined) {
24181
+ contractUnitsByDomain.set(sourceDomain, {
24182
+ intents: [
24183
+ intent
24184
+ ],
24185
+ chain: resolveIntentChain(group, intent),
24186
+ contractSigner: true,
24187
+ firstIntentIndex: index
24188
+ });
24189
+ } else {
24190
+ existingUnit.intents.push(intent);
24191
+ }
24192
+ } else {
24193
+ eoaUnit ??= {
24194
+ intents: [],
24195
+ chain: resolveIntentChain(group, intent),
24196
+ contractSigner: false,
24197
+ firstIntentIndex: index
24198
+ };
24199
+ eoaUnit.intents.push(intent);
24200
+ }
24201
+ }
24202
+ const signingUnits = [
24203
+ ...contractUnitsByDomain.values()
24204
+ ];
24205
+ if (eoaUnit !== undefined) signingUnits.push(eoaUnit);
24206
+ signingUnits.sort((a, b)=>a.firstIntentIndex - b.firstIntentIndex);
24207
+ return signingUnits;
24208
+ }
24209
+ async function signUnit(group, unit) {
24210
+ const { adapter, address } = group;
24211
+ const firstIntent = unit.intents[0];
24212
+ const typedData = unit.intents.length === 1 && firstIntent !== undefined ? evmSigningData(firstIntent) : evmSigningData(unit.intents);
24213
+ const operationContext = address === undefined ? {
24214
+ chain: unit.chain
24215
+ } : {
24216
+ chain: unit.chain,
24217
+ address
24218
+ };
24219
+ const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
24220
+ typedData,
24221
+ chain: unit.chain
24222
+ }, operationContext);
24223
+ const result = normalizeSignatureResult(await signRequest.execute());
24224
+ return {
24225
+ intents: unit.intents,
24226
+ signature: result.signature,
24227
+ contractSigner: result.contractSigner || unit.contractSigner
24228
+ };
24229
+ }
24230
+ async function signUnits(group, signingUnits) {
24231
+ const signedSets = [];
24232
+ // Keep wallet prompts deterministic. Multiple adapter groups can still sign
24233
+ // in parallel, but one signer is asked for its chain-bound signatures in
24234
+ // source-intent order.
24235
+ for (const unit of signingUnits){
24236
+ signedSets.push(await signUnit(group, unit));
24237
+ }
24238
+ return signedSets;
24239
+ }
23704
24240
  /**
23705
- * Sign an EVM adapter group: batches all intents and produces a single
23706
- * EIP-712 ECDSA signature.
24241
+ * Sign an EVM adapter group.
23707
24242
  *
23708
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
23709
- * For multi-intent groups, `primaryType` is `'BurnIntentSet'`.
24243
+ * EOA intents remain batched into one EIP-712 `BurnIntentSet`. ERC-1271
24244
+ * intents are grouped and signed per source chain because smart accounts
24245
+ * commonly include `chainId` in their replay-safe signature hash.
24246
+ * All returned entries can still be submitted together in one atomic Gateway
24247
+ * transfer request.
23710
24248
  *
23711
- * Before signing, asserts that the signer address is an EOA. Gateway
23712
- * verifies burn-intent signatures with plain `ecrecover` (no ERC-1271
23713
- * fallback), so signatures produced by smart-contract accounts (SCAs)
23714
- * cannot be verified. When an SCA is detected, a clear error is raised
23715
- * directing the caller to the delegate workflow (DEVX-2774).
24249
+ * Before signing, classifies the signer as an EOA or a contract account.
24250
+ * Gateway validates EOA signatures with `ecrecover` and contract-account
24251
+ * signatures with ERC-1271, but it does not infer which one applies — the
24252
+ * transfer request has to declare it. The returned `contractSigner` flag
24253
+ * carries that decision through to `buildTransferRequestBody`.
23716
24254
  *
23717
24255
  * @param group - The adapter group containing the adapter, chain, and
23718
24256
  * burn intents to sign.
23719
- * @returns A signed set with the intents and the ECDSA signature.
24257
+ * @returns Signed entries with their intents, signatures, and Gateway signer
24258
+ * validation mode.
24259
+ * @throws KitError when an intent has no source-chain mapping or a signing
24260
+ * action returns an invalid signature shape.
23720
24261
  *
23721
24262
  * @example
23722
24263
  * ```typescript
23723
24264
  * import { signEvmIntentGroup } from '@core/adapter-evm'
23724
24265
  *
23725
- * const signedSet = await signEvmIntentGroup({
24266
+ * const signedSets = await signEvmIntentGroup({
23726
24267
  * adapter: evmAdapter,
23727
24268
  * chain: ethereumChain,
23728
24269
  * intents: [burnIntent1, burnIntent2],
24270
+ * chainsByDomain: new Map([
24271
+ * [0, ethereumChain],
24272
+ * [6, baseChain],
24273
+ * ]),
23729
24274
  * address: '0x...',
23730
24275
  * })
23731
- * console.log(signedSet.signature)
24276
+ * console.log(signedSets)
23732
24277
  * ```
23733
24278
  */ async function signEvmIntentGroup(group) {
23734
- const { adapter, intents: groupIntents, chain, address } = group;
23735
- const operationContext = address === undefined ? {
23736
- chain
23737
- } : {
23738
- chain,
23739
- address
23740
- };
23741
- // Gateway verifies burn-intent signatures with plain ecrecover. An SCA
23742
- // signer silently produces a signature over a wrapped hash that Gateway
23743
- // cannot verify, and Circle Wallets' KMS rejects the typed data up front
23744
- // with an opaque `<nil>/<nil>` error. Short-circuit with a clear message
23745
- // when we can detect bytecode at the signer address. See DEVX-2774.
23746
- //
23747
- // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
23748
- // each consumer package bundles its own copy of the base class and the
23749
- // `instanceof` identity check fails across package boundaries.
23750
- //
23751
- // Empty string is defended against because assertSignerIsEoa would
23752
- // otherwise call eth_getCode('') on the RPC.
23753
- const hasResolvedSigner = typeof address === 'string' && address.length > 0;
23754
- if (hasResolvedSigner && chain.type === 'evm' && isEvmAdapterLike(adapter)) {
23755
- await assertSignerIsEoa(adapter, address, chain);
23756
- }
23757
- const firstIntent = groupIntents[0];
23758
- const typedData = groupIntents.length === 1 && firstIntent ? evmSigningData(firstIntent) : evmSigningData(groupIntents);
23759
- const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
23760
- typedData,
23761
- chain
23762
- }, operationContext);
23763
- const sig = await signRequest.execute();
23764
- return {
23765
- intents: groupIntents,
23766
- signature: sig
23767
- };
24279
+ // Validate the collection before doing bytecode reads or asking a wallet
24280
+ // to sign. evmSigningData owns the canonical BurnIntent validation.
24281
+ validateGroupIntents(group.intents);
24282
+ const chainsByDomain = collectChainsByDomain(group);
24283
+ const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
24284
+ const signingUnits = createSigningUnits(group, signerTypeByDomain);
24285
+ return await signUnits(group, signingUnits);
23768
24286
  }
23769
24287
 
23770
24288
  /**
23771
24289
  * Add an EVM intent into the batched EVM group map.
23772
24290
  *
23773
24291
  * On EVM, all intents for the same adapter are batched into a single
23774
- * group so that they can be signed in one EIP-712 `BurnIntentSet`
23775
- * operation.
24292
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
24293
+ * while signing ERC-1271 intents separately on their source chains.
23776
24294
  *
23777
24295
  * @param intent - The burn intent to group.
23778
24296
  * @param alloc - The allocation that resolved to this intent.
@@ -23789,6 +24307,7 @@ function evmSigningData(burnIntent) {
23789
24307
  const existing = evmGroups.get(alloc.adapter);
23790
24308
  if (existing) {
23791
24309
  existing.intents.push(intent);
24310
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
23792
24311
  } else {
23793
24312
  evmGroups.set(alloc.adapter, {
23794
24313
  adapter: alloc.adapter,
@@ -23796,6 +24315,12 @@ function evmSigningData(burnIntent) {
23796
24315
  intents: [
23797
24316
  intent
23798
24317
  ],
24318
+ chainsByDomain: new Map([
24319
+ [
24320
+ alloc.chain.gateway.domain,
24321
+ alloc.chain
24322
+ ]
24323
+ ]),
23799
24324
  address: alloc.sourceSigner
23800
24325
  });
23801
24326
  }
@@ -32001,6 +32526,113 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
32001
32526
  // Auto-register this kit for user agent tracking
32002
32527
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
32003
32528
 
32529
+ /**
32530
+ * Creates a AppKit context.
32531
+ *
32532
+ * This function constructs a context object, initializes the actions registry
32533
+ * used for event handlers, and merges in any custom implementations provided
32534
+ * via params.
32535
+ *
32536
+ * @param params - Optional custom implementations to override defaults
32537
+ * @returns A AppKitContext
32538
+ *
32539
+ * @example
32540
+ * ```typescript
32541
+ * // Create context with all defaults
32542
+ * const defaultContext = createContext()
32543
+ *
32544
+ * // Create context with custom fee calculation
32545
+ * const customContext = createContext({
32546
+ * getFee: async (type, params) => {
32547
+ * if (type === 'bridge') {
32548
+ * // Custom bridge fee logic
32549
+ * return await calculateBridgeFee(params)
32550
+ * }
32551
+ * // Use default for other types
32552
+ * return defaultFeeCalculation(type, params)
32553
+ * }
32554
+ * })
32555
+ * ```
32556
+ */ const createContext = (params = {})=>{
32557
+ return {
32558
+ ...params,
32559
+ actions: {
32560
+ bridge: {},
32561
+ earn: {},
32562
+ ...params.actions
32563
+ }
32564
+ };
32565
+ };
32566
+
32567
+ /**
32568
+ * Create a BridgeKit instance with optional developer fee configuration.
32569
+ *
32570
+ * This utility creates a BridgeKit instance that optionally includes developer
32571
+ * fee configuration based on the provided AppKit context. If the context
32572
+ * provides both `getFee` and `getFeeRecipient` methods, they will be configured
32573
+ * as developer fees in the BridgeKit instance using the `setCustomFeePolicy` method.
32574
+ *
32575
+ * The fee integration transforms string-based fees from the context into the
32576
+ * format expected by BridgeKit, enabling seamless fee calculation across both kits.
32577
+ *
32578
+ * @param context - The AppKit context containing optional fee methods
32579
+ * @returns A configured BridgeKit instance with or without developer fees
32580
+ *
32581
+ * @example
32582
+ * ```typescript
32583
+ * import { createBridgeKit } from '@circle-fin/app-kit/utils'
32584
+ * import { createContext } from '@circle-fin/app-kit/context'
32585
+ *
32586
+ * // Create context with fee methods
32587
+ * const context = createContext({
32588
+ * getFee: async (type, params) => '1000000', // 1 USDC in micro-units
32589
+ * getFeeRecipient: async (type, info) => '0x742d35Cc4634C0532925a3b8D1d7'
32590
+ * })
32591
+ *
32592
+ * // Create BridgeKit with developer fees
32593
+ * const bridgeKit = createBridgeKit(context)
32594
+ * ```
32595
+ *
32596
+ * @example
32597
+ * ```typescript
32598
+ * import { createBridgeKit } from '@circle-fin/app-kit/utils'
32599
+ * import { createContext } from '@circle-fin/app-kit/context'
32600
+ *
32601
+ * // Create context without fee methods
32602
+ * const context = createContext()
32603
+ *
32604
+ * // Create standard BridgeKit instance
32605
+ * const bridgeKit = createBridgeKit(context)
32606
+ * ```
32607
+ */ const createBridgeKit = (context)=>{
32608
+ const getFee = context.getFee?.bind(context);
32609
+ const getFeeRecipient = context.getFeeRecipient?.bind(context);
32610
+ const hasBoth = typeof getFee === 'function' && typeof getFeeRecipient === 'function';
32611
+ const kit = new BridgeKit({
32612
+ ...context.disableErrorReporting != null && {
32613
+ disableErrorReporting: context.disableErrorReporting
32614
+ },
32615
+ ...context.headers != null && {
32616
+ headers: context.headers
32617
+ }
32618
+ });
32619
+ if (context.customFeePolicy?.bridge != null) {
32620
+ kit.setCustomFeePolicy(context.customFeePolicy.bridge);
32621
+ } else if (hasBoth) {
32622
+ kit.setCustomFeePolicy({
32623
+ calculateFee: async (params)=>{
32624
+ const feeStr = await getFee('bridge', params);
32625
+ return feeStr;
32626
+ },
32627
+ resolveFeeRecipientAddress: async (chain, params)=>await getFeeRecipient('bridge', {
32628
+ chain,
32629
+ params: params || {}
32630
+ })
32631
+ });
32632
+ }
32633
+ return kit;
32634
+ };
32635
+
32004
32636
  /**
32005
32637
  * Create a SwapKit instance with optional developer fee configuration.
32006
32638
  *
@@ -32039,7 +32671,9 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32039
32671
  disableAnalytics: context.disableAnalytics
32040
32672
  }
32041
32673
  });
32042
- if (hasBoth) {
32674
+ if (context.customFeePolicy?.swap != null) {
32675
+ kit.setCustomFeePolicy(context.customFeePolicy.swap);
32676
+ } else if (hasBoth) {
32043
32677
  kit.setCustomFeePolicy({
32044
32678
  computeFee: async (params)=>{
32045
32679
  // Adapt provider-level params (with tokenIn/tokenOut)
@@ -32098,7 +32732,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32098
32732
  };
32099
32733
 
32100
32734
  var name$2 = "@circle-fin/earn-kit";
32101
- var version$2 = "1.4.0";
32735
+ var version$2 = "1.5.0";
32102
32736
  var pkg$2 = {
32103
32737
  name: name$2,
32104
32738
  version: version$2};
@@ -32741,6 +33375,683 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
32741
33375
  return approvedToken;
32742
33376
  }
32743
33377
 
33378
+ /**
33379
+ * Combined ABI of every inner instruction EarnKit can bundle inside an Adapter
33380
+ * `execute()` call. `decodeFunctionData` matches an instruction's calldata to
33381
+ * one of these functions by its 4-byte selector.
33382
+ */ const earnInstructionAbi = [
33383
+ ...erc4626VaultAbi,
33384
+ ...feeTakerAbi,
33385
+ ...merklDistributorAbi
33386
+ ];
33387
+ /**
33388
+ * Extract and shallow-validate the `instructions` array from loosely-typed
33389
+ * signed execution params.
33390
+ *
33391
+ * The earn service schema validates `tokenIn`/`amountToApprove` and passes the
33392
+ * remaining instruction fields through untyped, so the params arrive as a plain
33393
+ * record; each accessed field is narrowed at runtime.
33394
+ */ function requireInstructions(executionParams) {
33395
+ const instructions = executionParams['instructions'];
33396
+ if (!Array.isArray(instructions)) {
33397
+ throw decodeMismatchError('execution params are missing an instructions array', {
33398
+ instructions
33399
+ });
33400
+ }
33401
+ return instructions.map((instruction, index)=>{
33402
+ if (typeof instruction !== 'object' || instruction === null) {
33403
+ throw decodeMismatchError(`instructions[${index.toString()}] is not an object`, {
33404
+ index
33405
+ });
33406
+ }
33407
+ return instruction;
33408
+ });
33409
+ }
33410
+ /**
33411
+ * Build a fail-closed {@link KitError} for an earn decode or review failure.
33412
+ *
33413
+ * Marked non-recoverable: a mismatch between what would be shown and what would
33414
+ * be signed is never safe to retry, so the operation fails fast rather than
33415
+ * presenting misleading decoded data. `messagePrefix` names the failing stage
33416
+ * (calldata decode vs. review construction); callers bind it once and pass the
33417
+ * specific failure as `message`.
33418
+ */ function failClosedEarnError(messagePrefix, message, trace) {
33419
+ return new KitError({
33420
+ ...EarnError.INTERNAL_ERROR,
33421
+ recoverability: 'FATAL',
33422
+ message: `${messagePrefix}: ${message}`,
33423
+ cause: {
33424
+ trace
33425
+ }
33426
+ });
33427
+ }
33428
+ /**
33429
+ * Build a {@link KitError} for a decode or consistency failure.
33430
+ *
33431
+ * Thin wrapper over {@link failClosedEarnError} bound to the decode-stage
33432
+ * message prefix.
33433
+ */ function decodeMismatchError(message, trace) {
33434
+ return failClosedEarnError('Unable to decode earn transaction', message, trace);
33435
+ }
33436
+ /**
33437
+ * Narrow an untyped value to a 0x-prefixed hex string, or fail fast.
33438
+ */ function requireHex(value, path) {
33439
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value)) {
33440
+ return value;
33441
+ }
33442
+ throw decodeMismatchError(`${path} is not a hex string`, {
33443
+ path,
33444
+ value
33445
+ });
33446
+ }
33447
+ /**
33448
+ * Narrow an untyped value to a 20-byte EVM address, or fail fast.
33449
+ */ function requireAddress(value, path) {
33450
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) {
33451
+ return value;
33452
+ }
33453
+ throw decodeMismatchError(`${path} is not an address`, {
33454
+ path,
33455
+ value
33456
+ });
33457
+ }
33458
+ /**
33459
+ * Narrow an untyped `uint256`-like value (decimal string, bigint, or integer)
33460
+ * to a bigint, or fail fast.
33461
+ */ function requireUint(value, path) {
33462
+ if (typeof value === 'bigint') {
33463
+ return value;
33464
+ }
33465
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
33466
+ return BigInt(value);
33467
+ }
33468
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
33469
+ return BigInt(value);
33470
+ }
33471
+ throw decodeMismatchError(`${path} is not a uint256 value`, {
33472
+ path,
33473
+ value
33474
+ });
33475
+ }
33476
+ /**
33477
+ * Decode inner instruction calldata against the earn instruction ABI, mapping a
33478
+ * viem decode failure (unknown selector, malformed args) to a fail-fast error.
33479
+ */ function decodeEarnInstructionData(data, index) {
33480
+ try {
33481
+ return viem.decodeFunctionData({
33482
+ abi: earnInstructionAbi,
33483
+ data
33484
+ });
33485
+ } catch (error) {
33486
+ throw decodeMismatchError(`instructions[${index.toString()}] calldata is not a recognized earn instruction`, {
33487
+ index,
33488
+ selector: data.slice(0, 10),
33489
+ error: String(error)
33490
+ });
33491
+ }
33492
+ }
33493
+ /**
33494
+ * Decode Adapter `execute()` calldata, mapping a viem decode failure to a
33495
+ * fail-fast error.
33496
+ */ function decodeExecuteCalldata(calldata) {
33497
+ try {
33498
+ return viem.decodeFunctionData({
33499
+ abi: adapterContractAbi,
33500
+ data: calldata
33501
+ });
33502
+ } catch (error) {
33503
+ throw decodeMismatchError('encoded calldata is not a valid Adapter execute() call', {
33504
+ error: String(error)
33505
+ });
33506
+ }
33507
+ }
33508
+ /**
33509
+ * Decode one inner instruction's calldata into a typed {@link
33510
+ * DecodedEarnInstruction}.
33511
+ */ function decodeInstruction(instruction, index) {
33512
+ const target = requireAddress(instruction['target'], `instructions[${index.toString()}].target`);
33513
+ const data = requireHex(instruction['data'], `instructions[${index.toString()}].data`);
33514
+ const decoded = decodeEarnInstructionData(data, index);
33515
+ switch(decoded.functionName){
33516
+ case 'deposit':
33517
+ {
33518
+ const [assets, receiver] = decoded.args;
33519
+ return {
33520
+ method: 'deposit',
33521
+ vault: target,
33522
+ assets: assets.toString(),
33523
+ receiver
33524
+ };
33525
+ }
33526
+ case 'withdraw':
33527
+ {
33528
+ const [assets, receiver, owner] = decoded.args;
33529
+ return {
33530
+ method: 'withdraw',
33531
+ vault: target,
33532
+ assets: assets.toString(),
33533
+ receiver,
33534
+ owner
33535
+ };
33536
+ }
33537
+ case 'redeem':
33538
+ {
33539
+ const [shares, receiver, owner] = decoded.args;
33540
+ return {
33541
+ method: 'redeem',
33542
+ vault: target,
33543
+ shares: shares.toString(),
33544
+ receiver,
33545
+ owner
33546
+ };
33547
+ }
33548
+ case 'takeFeeERC20':
33549
+ {
33550
+ const [token, beneficiary, fee, kitType] = decoded.args;
33551
+ return {
33552
+ method: 'takeFeeERC20',
33553
+ feeTaker: target,
33554
+ token,
33555
+ beneficiary,
33556
+ fee: fee.toString(),
33557
+ kitType
33558
+ };
33559
+ }
33560
+ case 'claim':
33561
+ {
33562
+ const users = decoded.args[0];
33563
+ const tokens = decoded.args[1];
33564
+ const amounts = decoded.args[2];
33565
+ // Merkl claim(users, tokens, amounts, proofs) carries parallel arrays,
33566
+ // one entry per reward. Reject any length skew rather than padding with
33567
+ // zero amounts or dropping trailing entries, so the preview can never
33568
+ // misstate what is claimed or for whom.
33569
+ //
33570
+ // Note: Merkl `amounts` are the *cumulative lifetime* total claimable per
33571
+ // (user, token); the Distributor transfers only `amount - alreadyClaimed`.
33572
+ // This decode faithfully surfaces the signed cumulative value, which is
33573
+ // what `DecodedRewardClaim.amount` documents. See that type's doc.
33574
+ if (new Set([
33575
+ users.length,
33576
+ tokens.length,
33577
+ amounts.length
33578
+ ]).size !== 1) {
33579
+ throw decodeMismatchError(`instructions[${index.toString()}] claim has mismatched recipient/token/amount lengths`, {
33580
+ index,
33581
+ users: users.length,
33582
+ tokens: tokens.length,
33583
+ amounts: amounts.length
33584
+ });
33585
+ }
33586
+ const rewards = tokens.map((token, rewardIndex)=>({
33587
+ recipient: requireAddress(users[rewardIndex], `instructions[${index.toString()}].claim.users[${rewardIndex.toString()}]`),
33588
+ address: token,
33589
+ amount: requireUint(amounts[rewardIndex], `instructions[${index.toString()}].claim.amounts[${rewardIndex.toString()}]`).toString()
33590
+ }));
33591
+ return {
33592
+ method: 'claim',
33593
+ distributor: target,
33594
+ rewards
33595
+ };
33596
+ }
33597
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
33598
+ return assertNever$2(decoded);
33599
+ }
33600
+ }
33601
+ /**
33602
+ * Lift the primary values a wallet prompt cares about out of the decoded
33603
+ * instructions into a flat summary.
33604
+ */ function buildSummary(instructions, envelope) {
33605
+ const summary = {};
33606
+ instructions.forEach((instruction, index)=>{
33607
+ switch(instruction.method){
33608
+ case 'deposit':
33609
+ case 'withdraw':
33610
+ case 'redeem':
33611
+ {
33612
+ // The summary lifts a single primary token movement to the top level.
33613
+ // An earn bundle carries exactly one deposit/withdraw/redeem today;
33614
+ // fail fast rather than silently overwriting an earlier one, which
33615
+ // would drop it from the wallet-facing preview.
33616
+ if (summary.token !== undefined) {
33617
+ throw decodeMismatchError('multiple deposit/withdraw/redeem instructions cannot be summarized into a single preview', {
33618
+ index
33619
+ });
33620
+ }
33621
+ // Pair the amount with the token it is actually denominated in so the
33622
+ // preview never folds two units into one entry:
33623
+ // - deposit: `assets` of the underlying asset pulled in (`tokenIn`)
33624
+ // - redeem: `shares` of the vault-share token burned (`tokenIn`)
33625
+ // - withdraw: `assets` of the underlying asset paid out (`tokenOut`).
33626
+ // `withdraw(assets)` counts the underlying received, not the shares
33627
+ // burned to produce it, so `tokenIn` (the share token) would misstate
33628
+ // the unit; the underlying is the instruction's `tokenOut`.
33629
+ const amount = instruction.method === 'redeem' ? instruction.shares : instruction.assets;
33630
+ const tokenField = instruction.method === 'withdraw' ? 'tokenOut' : 'tokenIn';
33631
+ summary.vault = instruction.vault;
33632
+ summary.receiver = instruction.receiver;
33633
+ summary.token = {
33634
+ address: requireAddress(envelope[index]?.[tokenField], `instructions[${index.toString()}].${tokenField}`),
33635
+ amount
33636
+ };
33637
+ break;
33638
+ }
33639
+ case 'takeFeeERC20':
33640
+ {
33641
+ // As with the vault case, a second fee would silently overwrite the
33642
+ // first and understate what is charged; fail fast instead.
33643
+ if (summary.fee !== undefined) {
33644
+ throw decodeMismatchError('multiple fee instructions cannot be summarized into a single preview', {
33645
+ index
33646
+ });
33647
+ }
33648
+ summary.fee = {
33649
+ address: instruction.token,
33650
+ amount: instruction.fee
33651
+ };
33652
+ break;
33653
+ }
33654
+ case 'claim':
33655
+ {
33656
+ // A second claim would silently drop the first from the preview
33657
+ // (rewards are already batched inside one Merkl claim); fail fast.
33658
+ if (summary.rewards !== undefined) {
33659
+ throw decodeMismatchError('multiple claim instructions cannot be summarized into a single preview', {
33660
+ index
33661
+ });
33662
+ }
33663
+ summary.rewards = instruction.rewards;
33664
+ break;
33665
+ }
33666
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
33667
+ assertNever$2(instruction);
33668
+ }
33669
+ });
33670
+ return summary;
33671
+ }
33672
+ /**
33673
+ * Vault/claim instruction methods each declared action may decode to. The
33674
+ * mapping is many-to-one: a full withdrawal decodes to `redeem`, and any action
33675
+ * may carry an auxiliary `takeFeeERC20` alongside its primary instruction.
33676
+ */ const ACTION_ALLOWED_METHODS = {
33677
+ deposit: new Set([
33678
+ 'deposit'
33679
+ ]),
33680
+ withdraw: new Set([
33681
+ 'withdraw',
33682
+ 'redeem'
33683
+ ]),
33684
+ claimRewards: new Set([
33685
+ 'claim'
33686
+ ])
33687
+ };
33688
+ /**
33689
+ * Fail fast when the caller-declared `action` disagrees with the decoded
33690
+ * instructions, so the preview's headline can never mislabel what is signed
33691
+ * (e.g. a `deposit`-labeled call handed withdraw params). `takeFeeERC20` is an
33692
+ * auxiliary Circle-fee instruction and is allowed alongside any action.
33693
+ */ function assertActionMatchesInstructions(action, instructions) {
33694
+ const allowed = ACTION_ALLOWED_METHODS[action];
33695
+ instructions.forEach((instruction, index)=>{
33696
+ if (instruction.method === 'takeFeeERC20') {
33697
+ return;
33698
+ }
33699
+ if (!allowed.has(instruction.method)) {
33700
+ throw decodeMismatchError(`decoded instruction method '${instruction.method}' does not match the '${action}' action`, {
33701
+ action,
33702
+ method: instruction.method,
33703
+ index
33704
+ });
33705
+ }
33706
+ });
33707
+ }
33708
+ /**
33709
+ * Decode a same-chain earn `execute()` bundle into a human-readable summary.
33710
+ *
33711
+ * Decodes every inner instruction in the service-signed `executionParams` — the
33712
+ * same object the SDK ABI-encodes into the transaction — so the returned decode
33713
+ * is a faithful, drift-free view of what the signer is authorizing: input token
33714
+ * and amount, target vault, receiver, any Circle fee, and claimed rewards. Fails
33715
+ * fast with a non-recoverable {@link KitError} if any instruction cannot be
33716
+ * decoded, rather than returning misleading data.
33717
+ *
33718
+ * @param input - Action, chain, adapter, and the signed execution params.
33719
+ * @returns The decoded transaction summary.
33720
+ * @throws {@link KitError} If an instruction's calldata cannot be decoded.
33721
+ *
33722
+ * @example
33723
+ * ```typescript
33724
+ * const decoded = decodeEarnExecute({
33725
+ * action: 'deposit',
33726
+ * chain: 'Arc_Testnet',
33727
+ * adapter: '0x7fb8c7260b63934d8da38af902f87ae6e284a845',
33728
+ * executionParams,
33729
+ * })
33730
+ * // decoded.summary -> { token: { address, amount }, vault, receiver }
33731
+ * ```
33732
+ *
33733
+ * @internal
33734
+ */ function decodeEarnExecute(input) {
33735
+ const { action, chain, adapter, executionParams } = input;
33736
+ const envelope = requireInstructions(executionParams);
33737
+ const instructions = envelope.map((instruction, index)=>decodeInstruction(instruction, index));
33738
+ assertActionMatchesInstructions(action, instructions);
33739
+ return {
33740
+ action,
33741
+ chain,
33742
+ adapter,
33743
+ instructions,
33744
+ summary: buildSummary(instructions, envelope)
33745
+ };
33746
+ }
33747
+ /**
33748
+ * Assert that ABI-encoded Adapter `execute()` calldata encodes the same
33749
+ * instruction set as the service-signed execution params.
33750
+ *
33751
+ * Fail-fast preview check: the SDK encodes `execute(executeParams, ...)` locally,
33752
+ * so decoding those final bytes and comparing every field of each instruction
33753
+ * against the signed params proves the previewed instruction set matches what
33754
+ * will be signed. It compares `instructions[]` only — the outer `tokens`,
33755
+ * `execId`, `deadline`, and `metadata` are not re-compared here. The
33756
+ * authoritative integrity guarantee for the full signed struct is the on-chain
33757
+ * EIP-712 signature verification, which reverts if any signed field is altered.
33758
+ *
33759
+ * @param calldata - Encoded `execute()` calldata about to be signed.
33760
+ * @param executionParams - Service-signed execution params.
33761
+ * @throws {@link KitError} If the calldata is not an `execute()` call or any
33762
+ * instruction field differs from the signed params.
33763
+ *
33764
+ * @example
33765
+ * ```typescript
33766
+ * assertEarnCalldataMatchesExecuteParams(
33767
+ * prepared.getCallData().data,
33768
+ * executionParams,
33769
+ * )
33770
+ * ```
33771
+ *
33772
+ * @internal
33773
+ */ function assertEarnCalldataMatchesExecuteParams(calldata, executionParams) {
33774
+ // adapterContractAbi declares only `execute`, so a successful decode is always
33775
+ // the execute() call; a non-execute selector throws inside
33776
+ // decodeExecuteCalldata above.
33777
+ const decoded = decodeExecuteCalldata(calldata);
33778
+ const encoded = decoded.args[0].instructions;
33779
+ const signed = requireInstructions(executionParams);
33780
+ // Compare each encoded instruction against its signed counterpart. Iterating
33781
+ // the encoded instructions and indexing the signed set keeps both mismatch
33782
+ // branches reachable: a signed set that is too short trips the guard below,
33783
+ // and one that is too long trips the post-loop check.
33784
+ encoded.forEach((instruction, index)=>{
33785
+ const signedInstruction = signed[index];
33786
+ if (signedInstruction === undefined) {
33787
+ throw decodeMismatchError(`signed params are missing instruction ${index.toString()}`, {
33788
+ index,
33789
+ encoded: encoded.length,
33790
+ signed: signed.length
33791
+ });
33792
+ }
33793
+ const path = `instructions[${index.toString()}]`;
33794
+ assertHexEqual(instruction.target, signedInstruction['target'], `${path}.target`);
33795
+ assertHexEqual(instruction.data, signedInstruction['data'], `${path}.data`);
33796
+ assertUintEqual(instruction.value, signedInstruction['value'], `${path}.value`);
33797
+ assertHexEqual(instruction.tokenIn, signedInstruction['tokenIn'], `${path}.tokenIn`);
33798
+ assertUintEqual(instruction.amountToApprove, signedInstruction['amountToApprove'], `${path}.amountToApprove`);
33799
+ assertHexEqual(instruction.tokenOut, signedInstruction['tokenOut'], `${path}.tokenOut`);
33800
+ assertUintEqual(instruction.minTokenOut, signedInstruction['minTokenOut'], `${path}.minTokenOut`);
33801
+ });
33802
+ if (signed.length > encoded.length) {
33803
+ throw decodeMismatchError('signed params contain more instructions than the encoded calldata', {
33804
+ encoded: encoded.length,
33805
+ signed: signed.length
33806
+ });
33807
+ }
33808
+ }
33809
+ /**
33810
+ * Assert two hex values are equal, case-insensitively (addresses and calldata).
33811
+ */ function assertHexEqual(encoded, signed, path) {
33812
+ const signedHex = requireHex(signed, path);
33813
+ if (encoded.toLowerCase() !== signedHex.toLowerCase()) {
33814
+ throw decodeMismatchError(`${path} differs from signed params`, {
33815
+ path,
33816
+ encoded,
33817
+ signed: signedHex
33818
+ });
33819
+ }
33820
+ }
33821
+ /**
33822
+ * Assert an encoded bigint equals a signed `uint256`-like value.
33823
+ */ function assertUintEqual(encoded, signed, path) {
33824
+ const signedUint = requireUint(signed, path);
33825
+ if (encoded !== signedUint) {
33826
+ throw decodeMismatchError(`${path} differs from signed params`, {
33827
+ path,
33828
+ encoded: encoded.toString(),
33829
+ signed: signedUint.toString()
33830
+ });
33831
+ }
33832
+ }
33833
+
33834
+ /**
33835
+ * Namespaced discriminator for the EarnKit authorization review.
33836
+ *
33837
+ * Applications match on this in an adapter `onBeforeAuthorize` hook to decide
33838
+ * whether the request carries EarnKit semantic data. Prefer the
33839
+ * {@link isEarnExecuteReview} type guard over comparing this string directly.
33840
+ *
33841
+ * @example
33842
+ * ```typescript
33843
+ * if (review?.kind === EARN_EXECUTE_REVIEW_KIND) { … }
33844
+ * ```
33845
+ */ const EARN_EXECUTE_REVIEW_KIND = 'earn.execute';
33846
+
33847
+ /**
33848
+ * Build a fail-closed {@link KitError} for a review-construction failure.
33849
+ *
33850
+ * Marked non-recoverable: a review that cannot prove the calldata matches the
33851
+ * signed operation must abort authorization, never retry with misleading data.
33852
+ */ function reviewError(message, trace) {
33853
+ return failClosedEarnError('Unable to build earn authorization review', message, trace);
33854
+ }
33855
+ /**
33856
+ * Narrow the canonical authorization payload to the single Adapter `execute()`
33857
+ * call a same-chain earn operation authorizes.
33858
+ *
33859
+ * Same-chain deposit, withdraw, and claim-rewards each authorize exactly one
33860
+ * `evm-calls` payload carrying one call. Anything else (typed data, a batch,
33861
+ * an empty call list) means this descriptor was attached to the wrong
33862
+ * authorization unit, so fail closed rather than decode misleading data.
33863
+ */ function assertSingleEvmCallPayload(payload) {
33864
+ if (payload.type !== 'evm-calls') {
33865
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
33866
+ type: payload.type
33867
+ });
33868
+ }
33869
+ const [call, ...rest] = payload.calls;
33870
+ if (call === undefined) {
33871
+ throw reviewError('the evm-calls payload contains no calls to review', {
33872
+ callCount: payload.calls.length
33873
+ });
33874
+ }
33875
+ if (rest.length > 0) {
33876
+ throw reviewError('a same-chain earn operation authorizes exactly one Adapter execute() call', {
33877
+ callCount: payload.calls.length
33878
+ });
33879
+ }
33880
+ return call;
33881
+ }
33882
+ /**
33883
+ * Select the final earn `execute()` call from an atomic Earn batch.
33884
+ *
33885
+ * Same-chain batched deposit/withdraw authorizes either `[execute]` when the
33886
+ * current allowance is sufficient, or `[approve, execute]` when a top-up is
33887
+ * required. Any other shape means the descriptor was attached to an
33888
+ * unexpected authorization unit, so fail closed.
33889
+ */ function assertBatchedEarnExecuteCall(payload) {
33890
+ if (payload.type !== 'evm-calls') {
33891
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
33892
+ type: payload.type
33893
+ });
33894
+ }
33895
+ if (payload.calls.length !== 1 && payload.calls.length !== 2) {
33896
+ throw reviewError('a batched earn operation authorizes [execute] or [approve, execute]', {
33897
+ callCount: payload.calls.length
33898
+ });
33899
+ }
33900
+ const executeCall = payload.calls.at(-1);
33901
+ if (executeCall === undefined) {
33902
+ throw reviewError('the earn batch contains no execute call to review', {
33903
+ callCount: payload.calls.length
33904
+ });
33905
+ }
33906
+ return executeCall;
33907
+ }
33908
+ /**
33909
+ * Map a canonical {@link EvmCall} to the {@link EarnEncodedTransaction} preview
33910
+ * shape, failing closed when the earn `execute()` call carries no calldata.
33911
+ */ function toEarnEncodedTransaction(call) {
33912
+ if (call.data === undefined) {
33913
+ throw reviewError('the earn execute() call is missing calldata', {
33914
+ to: call.to
33915
+ });
33916
+ }
33917
+ return {
33918
+ to: call.to,
33919
+ data: call.data,
33920
+ ...call.value !== undefined && {
33921
+ value: call.value
33922
+ }
33923
+ };
33924
+ }
33925
+ /**
33926
+ * Create an Earn authorization descriptor using the supplied canonical-payload
33927
+ * call selector.
33928
+ *
33929
+ * @param input - The action, chain, and service-signed execution params.
33930
+ * @param selectCall - Fail-closed selector for the execute call under review.
33931
+ * @returns A lazy descriptor that decodes and verifies the selected call.
33932
+ *
33933
+ * @internal
33934
+ */ function createEarnExecuteDescriptor(input, selectCall) {
33935
+ const { action, chain, executionParams } = input;
33936
+ const createReview = (payload)=>{
33937
+ const call = selectCall(payload);
33938
+ const encoded = toEarnEncodedTransaction(call);
33939
+ const decoded = decodeEarnExecute({
33940
+ action,
33941
+ chain,
33942
+ adapter: encoded.to,
33943
+ executionParams
33944
+ });
33945
+ // Prove the calldata about to be signed encodes the same instruction set as
33946
+ // the service-signed params. Throwing here aborts before the wallet prompt.
33947
+ assertEarnCalldataMatchesExecuteParams(encoded.data, executionParams);
33948
+ const review = {
33949
+ kind: EARN_EXECUTE_REVIEW_KIND,
33950
+ data: {
33951
+ encoded,
33952
+ decoded
33953
+ }
33954
+ };
33955
+ return review;
33956
+ };
33957
+ return {
33958
+ createReview
33959
+ };
33960
+ }
33961
+ /**
33962
+ * Build the lazy `earn.execute` authorization descriptor for a same-chain earn
33963
+ * action.
33964
+ *
33965
+ * The returned descriptor carries only a `createReview` factory — no intent
33966
+ * override, because the adapter's action system supplies the intent from the
33967
+ * action key. The factory is evaluated at most once, and only when the
33968
+ * application configured an adapter `onBeforeAuthorize` hook. When it runs it:
33969
+ *
33970
+ * 1. narrows the canonical payload to its single Adapter `execute()` call;
33971
+ * 2. lifts that call into an {@link EarnEncodedTransaction};
33972
+ * 3. decodes it into a `DecodedEarnTx`; and
33973
+ * 4. asserts the decoded calldata matches the service-signed params, throwing
33974
+ * (aborting authorization before the wallet or signer) on any mismatch.
33975
+ *
33976
+ * @param input - The action, chain, and service-signed execution params.
33977
+ * @returns An authorization descriptor to pass as the fourth `prepareAction`
33978
+ * argument for the final earn action only (never the allowance approval).
33979
+ * @throws {@link KitError} From the review factory when the payload is not a
33980
+ * single earn `execute()` call or the calldata diverges from the signed
33981
+ * params. The throw surfaces through the adapter gate before authorization.
33982
+ *
33983
+ * @example
33984
+ * ```typescript
33985
+ * const descriptor = buildEarnExecuteDescriptor({
33986
+ * action: 'deposit',
33987
+ * chain: 'Arc_Testnet',
33988
+ * executionParams,
33989
+ * })
33990
+ * await adapter.prepareAction('earn.deposit', actionParams, ctx, {
33991
+ * authorization: descriptor,
33992
+ * })
33993
+ * ```
33994
+ *
33995
+ * @internal
33996
+ */ function buildEarnExecuteDescriptor(input) {
33997
+ return createEarnExecuteDescriptor(input, assertSingleEvmCallPayload);
33998
+ }
33999
+ /**
34000
+ * Build a lazy `earn.execute` authorization descriptor for an atomic Earn
34001
+ * batch containing either `[execute]` or `[approve, execute]`.
34002
+ *
34003
+ * The review always decodes and verifies the final call against the
34004
+ * service-signed execution params. Unexpected payload types and call counts
34005
+ * fail closed before wallet authorization.
34006
+ *
34007
+ * @param input - The action, chain, and service-signed execution params.
34008
+ * @returns A descriptor suitable for `batchExecute` authorization options.
34009
+ * @throws {@link KitError} From the lazy review factory when the batch shape or
34010
+ * final execute calldata cannot be verified.
34011
+ *
34012
+ * @internal
34013
+ */ function buildBatchedEarnExecuteDescriptor(input) {
34014
+ return createEarnExecuteDescriptor(input, assertBatchedEarnExecuteCall);
34015
+ }
34016
+ /**
34017
+ * Type guard: narrow an adapter authorization review to an EarnKit
34018
+ * `earn.execute` review.
34019
+ *
34020
+ * Use this inside an adapter `onBeforeAuthorize` hook to detect whether the
34021
+ * request carries EarnKit semantic data before reading it, instead of
34022
+ * comparing `review.kind` by hand. Robust against plain-JavaScript callers:
34023
+ * accepts `unknown` and checks the shape at runtime.
34024
+ *
34025
+ * @param review - The `review` field from an `AuthorizationRequest`, or any
34026
+ * value.
34027
+ * @returns `true` when `review` is an `earn.execute` review with the expected
34028
+ * `{ encoded, decoded }` data shape.
34029
+ *
34030
+ * @example
34031
+ * ```typescript
34032
+ * onBeforeAuthorize: async ({ review }) => {
34033
+ * if (isEarnExecuteReview(review)) {
34034
+ * await showEarnConfirmation(review.data.decoded.summary)
34035
+ * }
34036
+ * return 'approve'
34037
+ * }
34038
+ * ```
34039
+ */ function isEarnExecuteReview(review) {
34040
+ if (typeof review !== 'object' || review === null) {
34041
+ return false;
34042
+ }
34043
+ const candidate = review;
34044
+ if (candidate.kind !== EARN_EXECUTE_REVIEW_KIND) {
34045
+ return false;
34046
+ }
34047
+ const data = candidate.data;
34048
+ if (typeof data !== 'object' || data === null) {
34049
+ return false;
34050
+ }
34051
+ const { encoded, decoded } = data;
34052
+ return typeof encoded === 'object' && encoded !== null && typeof decoded === 'object' && decoded !== null;
34053
+ }
34054
+
32744
34055
  /**
32745
34056
  * Prepare an earn adapter action, execute it, wait for confirmation, and
32746
34057
  * throw a structured revert error if the receipt status is `'reverted'`.
@@ -32767,16 +34078,37 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
32767
34078
  * address,
32768
34079
  * actionKey: 'earn.deposit',
32769
34080
  * actionParams: { executeParams, tokenInputs, signature },
34081
+ * action: 'deposit',
34082
+ * executionParams,
32770
34083
  * revertMessage: 'Earn deposit reverted on-chain',
32771
34084
  * })
32772
34085
  * ```
32773
34086
  *
32774
34087
  * @internal
32775
34088
  */ async function executeEarnAction(params) {
32776
- const { adapter, chain, address, actionKey, actionParams, revertMessage } = params;
34089
+ const { adapter, chain, address, actionKey, actionParams, action, executionParams, revertMessage } = params;
34090
+ // Attach the lazy `earn.execute` review to the final earn action only (never
34091
+ // the allowance approval, which runs on a separate path). The adapter's
34092
+ // action system supplies the intent from `actionKey`, so the descriptor
34093
+ // carries only the review factory. The factory is evaluated at most once,
34094
+ // and only when the application configured an `onBeforeAuthorize` hook.
34095
+ const authorization = buildEarnExecuteDescriptor({
34096
+ action,
34097
+ // The provider validates the chain is Earn-supported in
34098
+ // `resolveAdapterContext` before reaching execute, so the concrete chain
34099
+ // identifier is a valid `EarnChainIdentifier`. It is carried through to the
34100
+ // decoded preview's display `chain` field only.
34101
+ chain: chain.chain,
34102
+ executionParams
34103
+ });
34104
+ // The abstract `Adapter.prepareAction` is 3-arg; the fourth authorization
34105
+ // argument lives on the `withLegacyCompat` wrapper that produced the concrete
34106
+ // adapter passed here. Narrow the single seam that threads the descriptor.
32777
34107
  const prepared = await adapter.prepareAction(actionKey, actionParams, {
32778
34108
  chain,
32779
34109
  address
34110
+ }, {
34111
+ authorization
32780
34112
  });
32781
34113
  const gasLimitOverride = await estimateBufferedGasLimit(prepared);
32782
34114
  const txHash = prepared.type === 'evm' && gasLimitOverride !== undefined ? await prepared.execute({
@@ -32801,6 +34133,278 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
32801
34133
  };
32802
34134
  }
32803
34135
 
34136
+ /**
34137
+ * Decide whether a same-chain earn action should be submitted as a single
34138
+ * atomic batch.
34139
+ *
34140
+ * Returns `true` only when the consumer has not opted out
34141
+ * (`batchTransactions !== false`), the source chain is EVM, the adapter
34142
+ * structurally exposes the shared batch methods, and the wallet reports atomic
34143
+ * batch support. `address` is forwarded as `fromAddress` so developer-controlled
34144
+ * adapters can probe the specific wallet. Any thrown capability probe is
34145
+ * treated as "no support".
34146
+ *
34147
+ * @param params - Adapter, chain, address, and the resolved `batchTransactions` flag.
34148
+ * @returns `true` when batched execution should be attempted.
34149
+ *
34150
+ * @example
34151
+ * ```typescript
34152
+ * if (await shouldUseBatchedEarnAction({ adapter, chain, address, batchTransactions })) {
34153
+ * // take the batched approve + execute path
34154
+ * }
34155
+ * ```
34156
+ *
34157
+ * @internal
34158
+ */ async function shouldUseBatchedEarnAction(params) {
34159
+ const { adapter, chain, address, batchTransactions } = params;
34160
+ if (batchTransactions === false) {
34161
+ return false;
34162
+ }
34163
+ if (chain.type !== 'evm') {
34164
+ return false;
34165
+ }
34166
+ const candidate = adapter;
34167
+ if (typeof candidate.supportsAtomicBatch !== 'function' || typeof candidate.batchExecute !== 'function') {
34168
+ return false;
34169
+ }
34170
+ try {
34171
+ return await candidate.supportsAtomicBatch(chain, {
34172
+ fromAddress: address
34173
+ });
34174
+ } catch {
34175
+ return false;
34176
+ }
34177
+ }
34178
+ async function buildSuccessfulBatchResult(adapter, chain, receipt, batchId, revertMessage) {
34179
+ const transaction = {
34180
+ txHash: receipt.txHash,
34181
+ explorerUrl: buildExplorerUrl(chain, receipt.txHash)
34182
+ };
34183
+ let confirmed;
34184
+ try {
34185
+ confirmed = await adapter.waitForTransaction(receipt.txHash, {
34186
+ confirmations: 1
34187
+ }, chain);
34188
+ } catch {
34189
+ // The batch adapter already confirmed success. Receipt enrichment is
34190
+ // telemetry-only, so an additional RPC failure must not turn an accepted
34191
+ // money-moving operation into a retryable business failure.
34192
+ return transaction;
34193
+ }
34194
+ if (confirmed.status === 'reverted') {
34195
+ throw createTransactionRevertedError(chain.name, revertMessage, {
34196
+ batchId
34197
+ }, receipt.txHash, transaction.explorerUrl);
34198
+ }
34199
+ return {
34200
+ ...transaction,
34201
+ ...confirmed.gasUsed !== undefined && {
34202
+ gasUsed: confirmed.gasUsed
34203
+ },
34204
+ ...confirmed.effectiveGasPrice !== undefined && {
34205
+ effectiveGasPrice: confirmed.effectiveGasPrice
34206
+ }
34207
+ };
34208
+ }
34209
+ function throwBatchFailure(result, executeReceipt, chain, actionKey, revertMessage) {
34210
+ const cause = result.error;
34211
+ if (result.statusCode === 400) {
34212
+ throw new KitError({
34213
+ ...RpcError.ENDPOINT_ERROR,
34214
+ recoverability: 'RETRYABLE',
34215
+ message: `Batched earn ${actionKey} failed off-chain before inclusion (batch ${result.batchId}).`,
34216
+ cause: {
34217
+ trace: {
34218
+ batchId: result.batchId,
34219
+ statusCode: result.statusCode,
34220
+ cause
34221
+ }
34222
+ }
34223
+ });
34224
+ }
34225
+ const causeTrace = cause instanceof KitError && typeof cause.cause?.trace === 'object' && cause.cause.trace !== null ? cause.cause.trace : undefined;
34226
+ if (cause instanceof KitError && causeTrace?.['kind'] === 'failed_offchain') {
34227
+ throw cause;
34228
+ }
34229
+ 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 !== '';
34230
+ if (isConfirmedRevert) {
34231
+ throw createTransactionRevertedError(chain.name, revertMessage, {
34232
+ batchId: result.batchId,
34233
+ error: cause
34234
+ });
34235
+ }
34236
+ throw new KitError({
34237
+ ...NetworkError.TIMEOUT,
34238
+ recoverability: 'FATAL',
34239
+ message: `Batched earn ${actionKey} was submitted (batch ${result.batchId}) but its outcome could not be confirmed; check the transaction status before retrying.`,
34240
+ cause: {
34241
+ trace: {
34242
+ batchId: result.batchId,
34243
+ cause
34244
+ }
34245
+ }
34246
+ });
34247
+ }
34248
+ /**
34249
+ * Execute the `approve` and `execute` steps of a same-chain earn action as a
34250
+ * single atomic batch.
34251
+ *
34252
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw call
34253
+ * data via `getCallData()`, then submit both through the adapter's shared
34254
+ * `batchExecute`. `address` is forwarded as `opts.fromAddress` so
34255
+ * developer-controlled adapters batch on behalf of the right wallet;
34256
+ * `idempotencyKey` is forwarded for adapters that deduplicate ambiguous
34257
+ * submissions (the Circle developer-controlled adapter reuses the Earn
34258
+ * execution id); other adapters may ignore either option. Reused by both the
34259
+ * deposit and withdraw flows via the `actionKey` parameter.
34260
+ *
34261
+ * @param params - Adapter, chain, action key, signed payload, and approval inputs.
34262
+ * @returns The confirmed execute transaction hash, explorer URL, and receipt
34263
+ * gas data when the adapter can retrieve it.
34264
+ * @throws {@link KitError} when the source chain is not EVM.
34265
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not
34266
+ * supported by the prepared requests.
34267
+ * @throws {@link KitError} when the batch reverts on-chain (a confirmed
34268
+ * terminal revert), carrying `batchId`.
34269
+ * @throws {@link KitError} RETRYABLE when EIP-5792 reports an off-chain
34270
+ * failure carrying `batchId` and status code `400`; no call was included.
34271
+ * @throws {@link KitError} FATAL `NetworkError.TIMEOUT` when the batch was
34272
+ * submitted but its outcome could not be confirmed (poll timeout or any
34273
+ * other non-revert post-submission failure); carries `batchId` so the caller
34274
+ * can check transaction status before retrying.
34275
+ * @remarks
34276
+ * Once the batch has been submitted this function does not fall back to the
34277
+ * sequential path — the batch is already on its way, so a fallback would risk
34278
+ * double-spend. Post-submission failures surface through the adapter's batch
34279
+ * result: a confirmed on-chain revert (Circle: a `TRANSACTION_REVERTED` cause;
34280
+ * Viem: status code `500`/`600`) is thrown as a revert error, status code `400`
34281
+ * is reported as a retryable off-chain failure, and any other unconfirmed
34282
+ * outcome is thrown as a FATAL timeout error carrying `batchId`.
34283
+ *
34284
+ * @example
34285
+ * ```typescript
34286
+ * const { txHash, explorerUrl } = await executeBatchedEarnAction({
34287
+ * adapter,
34288
+ * chain,
34289
+ * address,
34290
+ * actionKey: 'earn.deposit',
34291
+ * executeParams,
34292
+ * tokenInputs,
34293
+ * signature,
34294
+ * approvalToken: usdcAddress,
34295
+ * delegate: adapterContractAddress,
34296
+ * requiredAllowance: 1_000_000n,
34297
+ * idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
34298
+ * revertMessage: 'Earn deposit reverted on-chain',
34299
+ * })
34300
+ * ```
34301
+ *
34302
+ * @internal
34303
+ */ async function executeBatchedEarnAction(params) {
34304
+ const { adapter, chain, address, actionKey, executeParams, tokenInputs, signature, approvalToken, delegate, requiredAllowance, idempotencyKey, revertMessage } = params;
34305
+ if (chain.type !== 'evm') {
34306
+ throw new KitError({
34307
+ ...InputError.INVALID_CHAIN,
34308
+ recoverability: 'FATAL',
34309
+ message: 'Batched earn execution is only supported on EVM chains.'
34310
+ });
34311
+ }
34312
+ const evmChain = chain;
34313
+ const batchAdapter = adapter;
34314
+ // Read the current allowance so the approval tops up only the missing amount.
34315
+ // When the existing allowance already covers the payload, skip the approve
34316
+ // call and batch only the execute — this mirrors the sequential
34317
+ // approveAllowanceIfNeeded guard and avoids an increaseAllowance underflow
34318
+ // (requiredAllowance - currentAllowance would be negative, which reverts as
34319
+ // an out-of-range uint256).
34320
+ const allowancePrepared = await adapter.prepareAction('token.allowance', {
34321
+ tokenAddress: approvalToken,
34322
+ delegate
34323
+ }, {
34324
+ chain,
34325
+ address
34326
+ });
34327
+ const currentAllowance = parseAllowanceResponse(await allowancePrepared.execute());
34328
+ const approvalNeeded = currentAllowance < requiredAllowance;
34329
+ const executePrepared = await adapter.prepareAction(actionKey, {
34330
+ executeParams,
34331
+ tokenInputs,
34332
+ signature
34333
+ }, {
34334
+ chain,
34335
+ address
34336
+ });
34337
+ const approvePrepared = approvalNeeded ? await prepareApprovalAction({
34338
+ adapter,
34339
+ chain,
34340
+ address,
34341
+ tokenAddress: approvalToken,
34342
+ delegate,
34343
+ currentAllowance,
34344
+ requiredAllowance
34345
+ }) : undefined;
34346
+ if (executePrepared.type !== 'evm' || !executePrepared.getCallData) {
34347
+ throw new KitError({
34348
+ ...InputError.UNSUPPORTED_ACTION,
34349
+ recoverability: 'FATAL',
34350
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
34351
+ });
34352
+ }
34353
+ if (approvePrepared !== undefined && (approvePrepared.type !== 'evm' || !approvePrepared.getCallData)) {
34354
+ throw new KitError({
34355
+ ...InputError.UNSUPPORTED_ACTION,
34356
+ recoverability: 'FATAL',
34357
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
34358
+ });
34359
+ }
34360
+ const executeCallData = executePrepared.getCallData();
34361
+ // Prepend the approve call only when an allowance top-up is required.
34362
+ const calls = approvePrepared?.type === 'evm' && approvePrepared.getCallData ? [
34363
+ approvePrepared.getCallData(),
34364
+ executeCallData
34365
+ ] : [
34366
+ executeCallData
34367
+ ];
34368
+ const authorization = buildBatchedEarnExecuteDescriptor({
34369
+ action: actionKey === 'earn.deposit' ? 'deposit' : 'withdraw',
34370
+ chain: evmChain.chain,
34371
+ executionParams: executeParams
34372
+ });
34373
+ const result = await batchAdapter.batchExecute(calls, evmChain, {
34374
+ fromAddress: address,
34375
+ idempotencyKey,
34376
+ atomicRequired: true,
34377
+ authorization
34378
+ });
34379
+ // Success fans one confirmed hash across every receipt; the execute call is
34380
+ // the last one (approve, if present, precedes it). On failure a confirming
34381
+ // adapter returns no receipts, so a missing/non-success last receipt — or a
34382
+ // populated `result.error` — means the batch failed after submission (point
34383
+ // of no return). We never fall back, which would double-spend.
34384
+ const receiptCountMatches = result.receipts.length === calls.length;
34385
+ const executeReceipt = receiptCountMatches ? result.receipts[calls.length - 1] : undefined;
34386
+ const succeeded = receiptCountMatches && (result.statusCode === undefined || result.statusCode === 200) && result.error === undefined && executeReceipt?.status === 'success' && executeReceipt.txHash !== '';
34387
+ if (succeeded) {
34388
+ return buildSuccessfulBatchResult(adapter, evmChain, executeReceipt, result.batchId, revertMessage);
34389
+ }
34390
+ // Distinguish an off-chain rejection, a confirmed on-chain revert, and an
34391
+ // unknown outcome across both adapter conventions that share this contract:
34392
+ // - Circle SCA: no receipts + `error`; its trace kind identifies an
34393
+ // off-chain rejection, confirmed revert, or unconfirmed outcome.
34394
+ // - Viem EIP-5792: statusCode 500/600 explicitly confirms an on-chain
34395
+ // full/partial revert.
34396
+ // - Legacy/string-status wallets: a real-hash error receipt with no cause
34397
+ // is the best available confirmed-revert signal.
34398
+ // statusCode 400 is terminal but off-chain: the wallet confirms no call was
34399
+ // included, so it must not be labeled as a revert or unknown outcome.
34400
+ // Anything else — a poll timeout or any other post-submission failure with no
34401
+ // confirmed-revert signal — means the batch was submitted but its fate is
34402
+ // unconfirmed. Surface that as a FATAL (non-auto-retry) error carrying
34403
+ // `batchId` so the caller checks status before retrying, rather than
34404
+ // mislabeling it a revert.
34405
+ return throwBatchFailure(result, executeReceipt, evmChain, actionKey, revertMessage);
34406
+ }
34407
+
32804
34408
  /**
32805
34409
  * Validate that a service-signed execution payload has not expired before
32806
34410
  * the SDK asks the wallet to broadcast a transaction.
@@ -34097,7 +35701,7 @@ const bridgeDepositPrepareReviewSchema = zod.z.object({
34097
35701
  }
34098
35702
 
34099
35703
  var name$1 = "@circle-fin/provider-earn-service";
34100
- var version$1 = "1.3.1";
35704
+ var version$1 = "1.4.0";
34101
35705
  var pkg$1 = {
34102
35706
  name: name$1,
34103
35707
  version: version$1};
@@ -35735,6 +37339,46 @@ function finishElapsedWait(lastStatus, lastError) {
35735
37339
  const approvalToken = resolveEarnApprovalToken(executionParams);
35736
37340
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
35737
37341
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
37342
+ const approvalNeeded = !options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n;
37343
+ // Batch-capable wallets bundle approve + deposit into one atomic
37344
+ // submission. Only attempt this when an approval is actually needed.
37345
+ if (approvalNeeded && approvalToken !== undefined && await shouldUseBatchedEarnAction({
37346
+ adapter,
37347
+ chain,
37348
+ address,
37349
+ batchTransactions: config?.batchTransactions
37350
+ })) {
37351
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
37352
+ try {
37353
+ const result = await executeBatchedEarnAction({
37354
+ adapter,
37355
+ chain,
37356
+ address,
37357
+ actionKey: 'earn.deposit',
37358
+ executeParams: executionParams,
37359
+ tokenInputs,
37360
+ signature,
37361
+ approvalToken,
37362
+ delegate: adapterContractAddress,
37363
+ requiredAllowance,
37364
+ idempotencyKey: execId,
37365
+ revertMessage: 'Earn deposit reverted on-chain'
37366
+ });
37367
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
37368
+ return result;
37369
+ } catch (error) {
37370
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
37371
+ throw error;
37372
+ }
37373
+ }, ({ txHash })=>txHash);
37374
+ return {
37375
+ kind: 'same-chain',
37376
+ txHash,
37377
+ explorerUrl,
37378
+ vaultAddress,
37379
+ amount: params.amount
37380
+ };
37381
+ }
35738
37382
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
35739
37383
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35740
37384
  try {
@@ -35767,6 +37411,8 @@ function finishElapsedWait(lastStatus, lastError) {
35767
37411
  tokenInputs,
35768
37412
  signature
35769
37413
  },
37414
+ action: 'deposit',
37415
+ executionParams,
35770
37416
  revertMessage: 'Earn deposit reverted on-chain'
35771
37417
  });
35772
37418
  reportTransactionSuccess(transactionReportContext, 'Deposit', result);
@@ -35898,6 +37544,44 @@ function finishElapsedWait(lastStatus, lastError) {
35898
37544
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
35899
37545
  const approvalToken = tokenInputs[0]?.token;
35900
37546
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
37547
+ // Batch-capable wallets bundle approve + withdraw into one atomic
37548
+ // submission. Only attempt this when an approval is actually needed.
37549
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n && await shouldUseBatchedEarnAction({
37550
+ adapter,
37551
+ chain,
37552
+ address,
37553
+ batchTransactions: config?.batchTransactions
37554
+ })) {
37555
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
37556
+ try {
37557
+ const result = await executeBatchedEarnAction({
37558
+ adapter,
37559
+ chain,
37560
+ address,
37561
+ actionKey: 'earn.withdraw',
37562
+ executeParams: executionParams,
37563
+ tokenInputs,
37564
+ signature,
37565
+ approvalToken,
37566
+ delegate: adapterContractAddress,
37567
+ requiredAllowance,
37568
+ idempotencyKey: execId,
37569
+ revertMessage: 'Earn withdraw reverted on-chain'
37570
+ });
37571
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
37572
+ return result;
37573
+ } catch (error) {
37574
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
37575
+ throw error;
37576
+ }
37577
+ }, ({ txHash })=>txHash);
37578
+ return {
37579
+ txHash,
37580
+ explorerUrl,
37581
+ vaultAddress,
37582
+ amount: params.amount
37583
+ };
37584
+ }
35901
37585
  if (!options.skipApprove && approvalToken !== undefined) {
35902
37586
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35903
37587
  try {
@@ -35930,6 +37614,8 @@ function finishElapsedWait(lastStatus, lastError) {
35930
37614
  tokenInputs,
35931
37615
  signature
35932
37616
  },
37617
+ action: 'withdraw',
37618
+ executionParams,
35933
37619
  revertMessage: 'Earn withdraw reverted on-chain'
35934
37620
  });
35935
37621
  reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
@@ -36009,6 +37695,8 @@ function finishElapsedWait(lastStatus, lastError) {
36009
37695
  tokenInputs: [],
36010
37696
  signature
36011
37697
  },
37698
+ action: 'claimRewards',
37699
+ executionParams,
36012
37700
  revertMessage: 'Earn claim rewards reverted on-chain'
36013
37701
  }), ({ txHash })=>txHash);
36014
37702
  return {
@@ -36692,11 +38380,16 @@ const sourceAdapterContextSchema = zod.z.object({
36692
38380
  *
36693
38381
  * Validate the optional Kit Key field using the standard `apiKeySchema`
36694
38382
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
36695
- * operates in permissionless mode.
38383
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
38384
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
38385
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
38386
+ * schema must accept them or a valid config object is rejected.
36696
38387
  *
36697
38388
  * @internal
36698
38389
  */ const earnConfigSchema = zod.z.object({
36699
- kitKey: apiKeySchema.optional()
38390
+ kitKey: apiKeySchema.optional(),
38391
+ baseUrl: zod.z.string().optional(),
38392
+ batchTransactions: zod.z.boolean().optional()
36700
38393
  }).strict();
36701
38394
  /**
36702
38395
  * Canonical decimal form: a leading digit with no leading zeros (a single
@@ -39873,7 +41566,7 @@ async function deposit$2(context, params) {
39873
41566
  }
39874
41567
 
39875
41568
  var name = "@circle-fin/unified-balance-kit";
39876
- var version = "1.3.1";
41569
+ var version = "1.4.0";
39877
41570
  var pkg = {
39878
41571
  name: name,
39879
41572
  version: version};
@@ -41214,7 +42907,8 @@ function throwNetworkMismatch(expected, actual) {
41214
42907
  };
41215
42908
  }
41216
42909
  /**
41217
- * Group intents by adapter and chain for signing (Solana one-per-intent, EVM batched by adapter).
42910
+ * Group intents for signing (Solana one-per-intent, EVM batched by adapter
42911
+ * with every source chain retained by Gateway domain).
41218
42912
  *
41219
42913
  * @param intents - Burn intents from estimate response.
41220
42914
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -42591,22 +44285,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
42591
44285
  *
42592
44286
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
42593
44287
  *
44288
+ * Sets flagged `contractSigner` carry `contractSigner: true`, which tells
44289
+ * Gateway to validate the signature with ERC-1271 (an offchain
44290
+ * `isValidSignature` simulation) instead of `ecrecover`. The flag is
44291
+ * omitted for EOA signers so their payloads stay byte-identical.
44292
+ *
42594
44293
  * @param signedSets - Signed intent sets (intents + signature per signer).
42595
44294
  * @returns Array of transfer payloads for POST /v1/transfer.
42596
44295
  */ function buildTransferRequestBody(signedSets) {
42597
44296
  return signedSets.map((set)=>{
42598
44297
  const firstIntent = set.intents[0];
44298
+ const contractSigner = set.contractSigner === true ? {
44299
+ contractSigner: true
44300
+ } : {};
42599
44301
  if (set.intents.length === 1 && firstIntent) {
42600
44302
  return {
42601
44303
  burnIntent: serializeBurnIntent(firstIntent),
42602
- signature: set.signature
44304
+ signature: set.signature,
44305
+ ...contractSigner
42603
44306
  };
42604
44307
  }
42605
44308
  return {
42606
44309
  burnIntentSet: {
42607
44310
  intents: set.intents.map(serializeBurnIntent)
42608
44311
  },
42609
- signature: set.signature
44312
+ signature: set.signature,
44313
+ ...contractSigner
42610
44314
  };
42611
44315
  });
42612
44316
  }
@@ -42969,11 +44673,16 @@ const BPS_DIVISOR = 100_000n;
42969
44673
  return required;
42970
44674
  }
42971
44675
 
44676
+ function requireEvmChainsByDomain(group) {
44677
+ if (group.chainsByDomain !== undefined) return group.chainsByDomain;
44678
+ throw createValidationFailedError$1('adapterGroup.chainsByDomain', group.chainsByDomain, 'must be provided for an EVM adapter group');
44679
+ }
42972
44680
  /**
42973
- * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
44681
+ * Sign each adapter group: Solana one intent per signature, and EVM either
44682
+ * batched for EOAs or split by source chain for ERC-1271 signers.
42974
44683
  *
42975
44684
  * @param adapterGroups - Groups from groupIntentsByAdapter.
42976
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
44685
+ * @returns Promise of signed sets for buildTransferRequestBody.
42977
44686
  *
42978
44687
  * @example
42979
44688
  * ```typescript
@@ -42986,9 +44695,10 @@ const BPS_DIVISOR = 100_000n;
42986
44695
  if (group.chain.type === 'solana') {
42987
44696
  return signSolanaIntentGroup(group);
42988
44697
  }
42989
- return [
42990
- await signEvmIntentGroup(group)
42991
- ];
44698
+ return await signEvmIntentGroup({
44699
+ ...group,
44700
+ chainsByDomain: requireEvmChainsByDomain(group)
44701
+ });
42992
44702
  }));
42993
44703
  return nested.flat();
42994
44704
  }
@@ -43638,7 +45348,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
43638
45348
  signedSetCount: signedSets.length,
43639
45349
  signatures: signedSets.map((s)=>({
43640
45350
  intentCount: s.intents.length,
43641
- signature: s.signature
45351
+ signature: s.signature,
45352
+ contractSigner: s.contractSigner === true
43642
45353
  }))
43643
45354
  }
43644
45355
  });
@@ -47264,6 +48975,33 @@ registerKit(`${pkg.name}/${pkg.version}`);
47264
48975
  }
47265
48976
  }
47266
48977
 
48978
+ const APP_KIT_CUSTOM_FEE_POLICY_KEYS = new Set([
48979
+ 'bridge',
48980
+ 'swap',
48981
+ 'unifiedBalance'
48982
+ ]);
48983
+ function assertAppKitCustomFeePolicy(policy) {
48984
+ if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) {
48985
+ throw createValidationFailedError$1('policy', policy, 'AppKit custom fee policy must be an object');
48986
+ }
48987
+ for (const key of Object.keys(policy)){
48988
+ if (!APP_KIT_CUSTOM_FEE_POLICY_KEYS.has(key)) {
48989
+ throw createValidationFailedError$1(`policy.${key}`, key, 'AppKit custom fee policy only supports bridge, swap, and unifiedBalance');
48990
+ }
48991
+ }
48992
+ const candidate = policy;
48993
+ if (candidate.bridge !== undefined) {
48994
+ assertCustomFeePolicy$2(candidate.bridge);
48995
+ }
48996
+ if (candidate.swap !== undefined) {
48997
+ assertCustomFeePolicy$1(candidate.swap);
48998
+ }
48999
+ }
49000
+ function assertAppKitCustomFeePolicyScope(operation) {
49001
+ if (typeof operation !== 'string' || !APP_KIT_CUSTOM_FEE_POLICY_KEYS.has(operation)) {
49002
+ throw createValidationFailedError$1('operation', operation, 'AppKit custom fee policy operation must be bridge, swap, or unifiedBalance');
49003
+ }
49004
+ }
47267
49005
  /**
47268
49006
  * A high-level SDK for stablecoin operations, including bridging, swapping, and earn.
47269
49007
  *
@@ -47386,13 +49124,10 @@ registerKit(`${pkg.name}/${pkg.version}`);
47386
49124
  * })
47387
49125
  * ```
47388
49126
  */ constructor(config = {}){
47389
- this.context = createContext({
47390
- ...config,
47391
- ...config.disableErrorReporting != null && {
47392
- disableErrorReporting: config.disableErrorReporting
47393
- }
47394
- });
47395
- this.unifiedBalance = new AppKitUnifiedBalance({
49127
+ if (config.customFeePolicy !== undefined) {
49128
+ assertAppKitCustomFeePolicy(config.customFeePolicy);
49129
+ }
49130
+ const unifiedBalance = new AppKitUnifiedBalance({
47396
49131
  ...config.unifiedBalance,
47397
49132
  ...config.disableErrorReporting != null && {
47398
49133
  disableErrorReporting: config.disableErrorReporting
@@ -47401,6 +49136,16 @@ registerKit(`${pkg.name}/${pkg.version}`);
47401
49136
  disableAnalytics: config.disableAnalytics
47402
49137
  }
47403
49138
  });
49139
+ if (config.customFeePolicy?.unifiedBalance != null) {
49140
+ unifiedBalance.setCustomFeePolicy(config.customFeePolicy.unifiedBalance);
49141
+ }
49142
+ this.context = createContext({
49143
+ ...config,
49144
+ ...config.disableErrorReporting != null && {
49145
+ disableErrorReporting: config.disableErrorReporting
49146
+ }
49147
+ });
49148
+ this.unifiedBalance = unifiedBalance;
47404
49149
  this.earn = {
47405
49150
  // A single implementation cannot satisfy the per-branch deposit
47406
49151
  // overloads, so assert the overloaded interface shape; the
@@ -47844,6 +49589,77 @@ registerKit(`${pkg.name}/${pkg.version}`);
47844
49589
  */ getSupportedChains(operationType) {
47845
49590
  return getSupportedChains$2(this.context, operationType, this.unifiedBalance);
47846
49591
  }
49592
+ /**
49593
+ * Set operation-scoped custom fee policies.
49594
+ *
49595
+ * Configure custom fees for only the operations that need them. Bridge and
49596
+ * swap policies are forwarded to the underlying kits when those operations
49597
+ * run. Unified balance policies are applied immediately to the namespaced
49598
+ * Unified Balance Kit.
49599
+ *
49600
+ * @param policy - Partial custom fee policy grouped by operation.
49601
+ * @returns Nothing.
49602
+ * @throws \{KitError\} If `policy` or a provided operation policy is invalid.
49603
+ *
49604
+ * @example
49605
+ * ```typescript
49606
+ * kit.setCustomFeePolicy({
49607
+ * bridge: {
49608
+ * computeFee: () => '1.00',
49609
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
49610
+ * },
49611
+ * })
49612
+ * ```
49613
+ */ setCustomFeePolicy(policy) {
49614
+ assertAppKitCustomFeePolicy(policy);
49615
+ if (policy.unifiedBalance != null) {
49616
+ this.unifiedBalance.setCustomFeePolicy(policy.unifiedBalance);
49617
+ }
49618
+ this.context.customFeePolicy = {
49619
+ ...this.context.customFeePolicy,
49620
+ ...policy
49621
+ };
49622
+ }
49623
+ /**
49624
+ * Remove an AppKit-level custom fee policy for one operation.
49625
+ *
49626
+ * Bridge and swap policies are removed from AppKit's persistent context so
49627
+ * future operations fall back to legacy fee hooks. Unified balance policies
49628
+ * are also removed from the namespaced Unified Balance Kit.
49629
+ *
49630
+ * @param operation - Operation whose custom fee policy should be removed.
49631
+ * @returns Nothing.
49632
+ * @throws \{KitError\} If `operation` is invalid.
49633
+ *
49634
+ * @example
49635
+ * ```typescript
49636
+ * kit.removeCustomFeePolicy('bridge')
49637
+ * ```
49638
+ */ removeCustomFeePolicy(operation) {
49639
+ assertAppKitCustomFeePolicyScope(operation);
49640
+ if (operation === 'unifiedBalance') {
49641
+ this.unifiedBalance.removeCustomFeePolicy();
49642
+ }
49643
+ if (this.context.customFeePolicy == null) {
49644
+ return;
49645
+ }
49646
+ const currentPolicy = this.context.customFeePolicy;
49647
+ const nextPolicy = {};
49648
+ if (operation !== 'bridge' && currentPolicy.bridge !== undefined) {
49649
+ nextPolicy.bridge = currentPolicy.bridge;
49650
+ }
49651
+ if (operation !== 'swap' && currentPolicy.swap !== undefined) {
49652
+ nextPolicy.swap = currentPolicy.swap;
49653
+ }
49654
+ if (operation !== 'unifiedBalance' && currentPolicy.unifiedBalance !== undefined) {
49655
+ nextPolicy.unifiedBalance = currentPolicy.unifiedBalance;
49656
+ }
49657
+ if (Object.keys(nextPolicy).length === 0) {
49658
+ delete this.context.customFeePolicy;
49659
+ return;
49660
+ }
49661
+ this.context.customFeePolicy = nextPolicy;
49662
+ }
47847
49663
  on(actionOrWildCard, handler) {
47848
49664
  const action = actionOrWildCard;
47849
49665
  const typedHandler = handler;
@@ -47900,6 +49716,7 @@ registerKit(`${pkg.name}/${pkg.version}`);
47900
49716
 
47901
49717
  exports.AppKit = AppKit;
47902
49718
  exports.BalanceError = BalanceError;
49719
+ exports.EARN_EXECUTE_REVIEW_KIND = EARN_EXECUTE_REVIEW_KIND;
47903
49720
  exports.EarnError = EarnError;
47904
49721
  exports.EarnKit = EarnKit;
47905
49722
  exports.InputError = InputError;
@@ -47940,6 +49757,7 @@ exports.getTokenDecimals = getTokenDecimals;
47940
49757
  exports.getVaultsParamsSchema = getVaultsParamsSchema;
47941
49758
  exports.getWithdrawalQuoteParamsSchema = getWithdrawalQuoteParamsSchema;
47942
49759
  exports.isBalanceError = isBalanceError;
49760
+ exports.isEarnExecuteReview = isEarnExecuteReview;
47943
49761
  exports.isFatalError = isFatalError;
47944
49762
  exports.isInputError = isInputError;
47945
49763
  exports.isKitError = isKitError;