@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.mjs CHANGED
@@ -38,46 +38,9 @@ import { PublicKey } from '@solana/web3.js';
38
38
  import 'bn.js';
39
39
  import '@coral-xyz/anchor';
40
40
  import '@noble/curves/ed25519';
41
+ import { decodeFunctionData } from 'viem';
41
42
  import { keccak256 } from '@ethersproject/keccak256';
42
43
 
43
- /**
44
- * Creates a AppKit context.
45
- *
46
- * This function constructs a context object, initializes the actions registry
47
- * used for event handlers, and merges in any custom implementations provided
48
- * via params.
49
- *
50
- * @param params - Optional custom implementations to override defaults
51
- * @returns A AppKitContext
52
- *
53
- * @example
54
- * ```typescript
55
- * // Create context with all defaults
56
- * const defaultContext = createContext()
57
- *
58
- * // Create context with custom fee calculation
59
- * const customContext = createContext({
60
- * getFee: async (type, params) => {
61
- * if (type === 'bridge') {
62
- * // Custom bridge fee logic
63
- * return await calculateBridgeFee(params)
64
- * }
65
- * // Use default for other types
66
- * return defaultFeeCalculation(type, params)
67
- * }
68
- * })
69
- * ```
70
- */ const createContext = (params = {})=>{
71
- return {
72
- ...params,
73
- actions: {
74
- bridge: {},
75
- earn: {},
76
- ...params.actions
77
- }
78
- };
79
- };
80
-
81
44
  // Import global type declarations
82
45
  /**
83
46
  * Check whether the current runtime is Node.js.
@@ -4123,6 +4086,8 @@ function getOptionalString(value) {
4123
4086
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
4124
4087
  Blockchain["XDC"] = "XDC";
4125
4088
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
4089
+ Blockchain["X_Layer"] = "X_Layer";
4090
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
4126
4091
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
4127
4092
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
4128
4093
  })(Blockchain || (Blockchain = {}));
@@ -4176,6 +4141,7 @@ var BridgeChain;
4176
4141
  BridgeChain["Unichain"] = "Unichain";
4177
4142
  BridgeChain["World_Chain"] = "World_Chain";
4178
4143
  BridgeChain["XDC"] = "XDC";
4144
+ BridgeChain["X_Layer"] = "X_Layer";
4179
4145
  // Testnet chains with CCTPv2 support
4180
4146
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
4181
4147
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -4201,6 +4167,7 @@ var BridgeChain;
4201
4167
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
4202
4168
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
4203
4169
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
4170
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
4204
4171
  })(BridgeChain || (BridgeChain = {}));
4205
4172
  var UnifiedBalanceChain;
4206
4173
  (function(UnifiedBalanceChain) {
@@ -6759,7 +6726,8 @@ var EarnChain;
6759
6726
  isTestnet: true,
6760
6727
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
6761
6728
  rpcEndpoints: [
6762
- 'https://rpc-amoy.polygon.technology'
6729
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
6730
+ 'https://polygon-amoy.drpc.org'
6763
6731
  ],
6764
6732
  eurcAddress: null,
6765
6733
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -7624,6 +7592,104 @@ var EarnChain;
7624
7592
  }
7625
7593
  });
7626
7594
 
7595
+ /**
7596
+ * X Layer Mainnet chain definition
7597
+ * @remarks
7598
+ * This represents the official production network for the X Layer blockchain.
7599
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
7600
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
7601
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
7602
+ */ const XLayer = defineChain({
7603
+ type: 'evm',
7604
+ chain: Blockchain.X_Layer,
7605
+ name: 'X Layer',
7606
+ title: 'X Layer Mainnet',
7607
+ nativeCurrency: {
7608
+ name: 'OKB',
7609
+ symbol: 'OKB',
7610
+ decimals: 18
7611
+ },
7612
+ chainId: 196,
7613
+ isTestnet: false,
7614
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
7615
+ rpcEndpoints: [
7616
+ 'https://xlayerrpc.okx.com'
7617
+ ],
7618
+ eurcAddress: null,
7619
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7620
+ usdtAddress: null,
7621
+ cctp: {
7622
+ domain: 37,
7623
+ contracts: {
7624
+ v2: {
7625
+ type: 'split',
7626
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
7627
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
7628
+ confirmations: 65,
7629
+ fastConfirmations: 1
7630
+ }
7631
+ },
7632
+ forwarderSupported: {
7633
+ source: false,
7634
+ destination: false
7635
+ }
7636
+ },
7637
+ kitContracts: {
7638
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
7639
+ }
7640
+ });
7641
+
7642
+ /**
7643
+ * X Layer Testnet chain definition
7644
+ * @remarks
7645
+ * This represents the official test network for the X Layer blockchain.
7646
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
7647
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
7648
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
7649
+ */ const XLayerTestnet = defineChain({
7650
+ type: 'evm',
7651
+ chain: Blockchain.X_Layer_Testnet,
7652
+ name: 'X Layer Testnet',
7653
+ title: 'X Layer Testnet',
7654
+ nativeCurrency: {
7655
+ name: 'OKB',
7656
+ symbol: 'OKB',
7657
+ decimals: 18
7658
+ },
7659
+ chainId: 1952,
7660
+ isTestnet: true,
7661
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
7662
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
7663
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
7664
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
7665
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
7666
+ rpcEndpoints: [
7667
+ 'https://testrpc.xlayer.tech'
7668
+ ],
7669
+ eurcAddress: null,
7670
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7671
+ usdtAddress: null,
7672
+ cctp: {
7673
+ domain: 37,
7674
+ contracts: {
7675
+ v2: {
7676
+ type: 'split',
7677
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
7678
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
7679
+ confirmations: 65,
7680
+ fastConfirmations: 1
7681
+ }
7682
+ },
7683
+ forwarderSupported: {
7684
+ source: false,
7685
+ destination: false
7686
+ }
7687
+ },
7688
+ kitContracts: {
7689
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
7690
+ }
7691
+ });
7692
+
7627
7693
  /**
7628
7694
  * ZKSync Era Mainnet chain definition
7629
7695
  * @remarks
@@ -7743,6 +7809,8 @@ var Chains = /*#__PURE__*/Object.freeze({
7743
7809
  WorldChainSepolia: WorldChainSepolia,
7744
7810
  XDC: XDC,
7745
7811
  XDCApothem: XDCApothem,
7812
+ XLayer: XLayer,
7813
+ XLayerTestnet: XLayerTestnet,
7746
7814
  ZKSyncEra: ZKSyncEra,
7747
7815
  ZKSyncEraSepolia: ZKSyncEraSepolia
7748
7816
  });
@@ -10106,6 +10174,7 @@ function parseOrThrow(value, schema, context) {
10106
10174
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
10107
10175
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
10108
10176
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
10177
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
10109
10178
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
10110
10179
  // =========================================================================
10111
10180
  // Testnets (alphabetically sorted)
@@ -10140,6 +10209,7 @@ function parseOrThrow(value, schema, context) {
10140
10209
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
10141
10210
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
10142
10211
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
10212
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
10143
10213
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
10144
10214
  }
10145
10215
  };
@@ -11089,6 +11159,52 @@ function parseOrThrow(value, schema, context) {
11089
11159
  return explorerUrl;
11090
11160
  }
11091
11161
 
11162
+ /**
11163
+ * Assert that a value has type `never` (exhaustive switch helper).
11164
+ *
11165
+ * @remarks
11166
+ * Use in the `default` branch of a switch over a discriminated union.
11167
+ * If all union members are handled, the default is unreachable and TypeScript
11168
+ * narrows the parameter to `never`. If a member is missed, the compiler errors.
11169
+ *
11170
+ * @param _x - The value (typed as `never` when switch is exhaustive).
11171
+ * @returns Never returns; always throws.
11172
+ * @throws Error when the switch is not exhaustive.
11173
+ *
11174
+ * @example
11175
+ * ```typescript
11176
+ * type Foo = { type: 'a'; x: number } | { type: 'b'; y: string }
11177
+ *
11178
+ * function handle(foo: Foo): string {
11179
+ * switch (foo.type) {
11180
+ * case 'a': return String(foo.x)
11181
+ * case 'b': return foo.y
11182
+ * default: return assertNever(foo)
11183
+ * }
11184
+ * }
11185
+ * ```
11186
+ */ function assertNever$2(x) {
11187
+ // Plain `String(x)` collapses non-primitive union members (objects, arrays)
11188
+ // to `'[object Object]'`, which is useless when triaging which discriminant
11189
+ // was missed. Attempt `JSON.stringify` first so the thrown message preserves
11190
+ // the offending shape. Fall back to a minimal `typeof`-based label if
11191
+ // serialization fails (`BigInt` member, circular references, host objects).
11192
+ //
11193
+ // `x` is statically typed as `never` (the whole point of this helper), but
11194
+ // at runtime callers may still pass an unexpected value when the switch is
11195
+ // not actually exhaustive — that's exactly the bug we want to surface. Cast
11196
+ // through `unknown` so the runtime defence is not stripped by the compiler.
11197
+ const value = x;
11198
+ let stringified;
11199
+ try {
11200
+ const json = JSON.stringify(value);
11201
+ stringified = typeof json === 'string' ? json : `<${typeof value}>`;
11202
+ } catch {
11203
+ stringified = `<unstringifiable ${typeof value}>`;
11204
+ }
11205
+ throw new Error(`Unhandled switch case: ${stringified}`);
11206
+ }
11207
+
11092
11208
  /**
11093
11209
  * CCTP forwarding magic bytes prefix.
11094
11210
  *
@@ -11749,7 +11865,7 @@ function resolveOptions(options) {
11749
11865
  }
11750
11866
 
11751
11867
  var name$4 = "@circle-fin/bridge-kit";
11752
- var version$5 = "1.12.2";
11868
+ var version$5 = "1.13.0";
11753
11869
  var pkg$5 = {
11754
11870
  name: name$4,
11755
11871
  version: version$5};
@@ -11786,13 +11902,21 @@ const assertCustomFeePolicySymbol$2 = Symbol('assertCustomFeePolicy');
11786
11902
  computeFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
11787
11903
  calculateFee: z.function().returns(z.string().or(z.promise(z.string()))).optional(),
11788
11904
  resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
11789
- }).strict().refine((data)=>{
11905
+ }).strict().superRefine((data, ctx)=>{
11790
11906
  const hasComputeFee = data.computeFee !== undefined;
11791
11907
  const hasCalculateFee = data.calculateFee !== undefined;
11792
- // XOR: exactly one must be provided
11793
- return hasComputeFee !== hasCalculateFee;
11794
- }, {
11795
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
11908
+ if (hasComputeFee && hasCalculateFee) {
11909
+ ctx.addIssue({
11910
+ code: z.ZodIssueCode.custom,
11911
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
11912
+ });
11913
+ }
11914
+ if (!hasComputeFee && !hasCalculateFee) {
11915
+ ctx.addIssue({
11916
+ code: z.ZodIssueCode.custom,
11917
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
11918
+ });
11919
+ }
11796
11920
  });
11797
11921
  /**
11798
11922
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
@@ -14592,14 +14716,32 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
14592
14716
  ;
14593
14717
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
14594
14718
  ;
14595
- // Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
14596
- // chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
14597
- // Kept separate from the fee-estimate averages above.
14598
- const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
14719
+ // Gas FLOORS, not ceilings kept separate from the fee-estimate averages
14720
+ // above. `executePreparedChainRequest` submits
14721
+ // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
14722
+ // is covered by its own estimate, and a chain whose estimator under-reports
14723
+ // (Cronos: returns 30_600 where the EIP-7623 calldata floor is 45_000) is
14724
+ // covered by the floor.
14725
+ //
14726
+ // Two distinct chain surcharges drive these numbers, both measured live:
14727
+ // Sei — ~+51_500 per NEWLY CREATED storage slot (73_595 vs vanilla 22_100);
14728
+ // no flat per-tx surcharge (31_535, identical to Base).
14729
+ // Edge — ~+53_200 flat on EVERY tx (84_751 vs Base 31_535); storage priced
14730
+ // normally. Edge therefore fails warm as well as cold.
14731
+ // A floor must clear the worst COLD cost, since a slot that exists at estimate
14732
+ // time can be consumed before inclusion and cost a full step more on execution.
14733
+ // Each floor is therefore derived from the worst observed estimate *after* the
14734
+ // 1.25x buffer, plus headroom — sizing it below the buffered value would leave
14735
+ // the estimate governing and defeat the point of the floor.
14736
+ //
14737
+ // The `*_GAS_LIMIT_EVM` names are kept despite these being floors: they are
14738
+ // exported, so renaming to `*_GAS_FLOOR_EVM` would be a breaking change for
14739
+ // consumers. Read "LIMIT" here as "the limit we submit", never as a ceiling.
14740
+ const APPROVE_GAS_LIMIT_EVM = 150_000n // buffered worst cold 149_355 (Edge Testnet 119_484 x 1.25) + drift headroom
14599
14741
  ;
14600
- const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
14742
+ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 500_000n // buffered worst 474_078 (Sei 379_263 x 1.25) + ~26k headroom
14601
14743
  ;
14602
- const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
14744
+ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears Cronos' calldata floor ~10x
14603
14745
  ;
14604
14746
  /**
14605
14747
  * The minimum finality threshold for CCTPv2 transfers.
@@ -16064,6 +16206,63 @@ function hasPendingState(analysis, result) {
16064
16206
  return waitForPendingTransaction(pendingStep, adapter, chain);
16065
16207
  }
16066
16208
 
16209
+ /**
16210
+ * Multiplier applied to a successful gas estimate before it is submitted.
16211
+ *
16212
+ * Estimates are exact, not padded: Sei returns 109_739 for an approve that
16213
+ * consumes 107_717 (1.9% headroom). Chains that price storage in large steps
16214
+ * can exceed the estimate if state changes between estimation and inclusion,
16215
+ * so the estimate is padded before use.
16216
+ *
16217
+ * @remarks
16218
+ * This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
16219
+ * scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
16220
+ * covers a slot that exists at estimation time and is consumed before
16221
+ * inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
16222
+ * the estimate covers it. For burn the buffer does cover a step (25% of
16223
+ * ~300_000 exceeds 51_500).
16224
+ */ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
16225
+ /**
16226
+ * Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
16227
+ *
16228
+ * Estimates first so chains whose real cost exceeds the floor are covered by
16229
+ * their own measurement, and falls back to the floor whenever estimation is
16230
+ * unavailable or under-reports. Estimation failure is never fatal here: before
16231
+ * floors existed these requests were submitted with a pinned limit and no
16232
+ * estimate at all, so degrading to the floor is never worse than the previous
16233
+ * behaviour.
16234
+ *
16235
+ * @param request - The prepared EVM request to size a gas limit for
16236
+ * @param gasFloor - The minimum gas limit to submit, in gas units
16237
+ * @returns The gas limit to submit, in gas units
16238
+ * @throws Never — estimation failures degrade to `gasFloor`
16239
+ *
16240
+ * @example
16241
+ * ```typescript
16242
+ * const gasLimit = await resolveGasLimit(request, 150_000)
16243
+ * ```
16244
+ */ const resolveGasLimit = async (request, gasFloor)=>{
16245
+ try {
16246
+ // Deliberately called without a `fallback`: both the viem and ethers
16247
+ // adapters *return* the supplied fallback object when estimation reverts
16248
+ // rather than throwing, which would set the estimate to the floor and then
16249
+ // multiply it by the buffer below. Omitting it routes reverts through the
16250
+ // catch, so a failed estimate degrades to exactly the floor.
16251
+ const estimate = await request.estimate();
16252
+ // The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
16253
+ // typed `bigint`, but adapters are a public extension point and may be
16254
+ // implemented in plain JS, so a non-bigint `gas` would throw here
16255
+ // ("Cannot mix BigInt and other types"). Guarding it keeps the documented
16256
+ // contract — estimation never aborts a step, it degrades to the floor.
16257
+ const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
16258
+ // Convert before comparing: Math.max throws on BigInt operands, and gas
16259
+ // units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
16260
+ return Math.max(Number(buffered), gasFloor);
16261
+ } catch {
16262
+ // Estimation is best-effort; the floor is the known-safe value.
16263
+ return gasFloor;
16264
+ }
16265
+ };
16067
16266
  /**
16068
16267
  * Executes a prepared chain request and returns the result as a bridge step.
16069
16268
  *
@@ -16077,8 +16276,8 @@ function hasPendingState(analysis, result) {
16077
16276
  * - `adapter`: The adapter that will execute the transaction
16078
16277
  * - `confirmations`: The number of confirmations to wait for (defaults to 1)
16079
16278
  * - `timeout`: The timeout for the request in milliseconds
16080
- * - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
16081
- * bypassing `eth_estimateGas`; ignored for non-EVM requests
16279
+ * - `gasFloor`: Optional minimum gas limit (number); the request is submitted
16280
+ * with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
16082
16281
  * @returns The bridge step with the transaction details and explorer URL
16083
16282
  * @throws If the transaction execution fails
16084
16283
  *
@@ -16093,7 +16292,7 @@ function hasPendingState(analysis, result) {
16093
16292
  * })
16094
16293
  * console.log('Transaction hash:', step.txHash)
16095
16294
  * ```
16096
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
16295
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
16097
16296
  const step = {
16098
16297
  name,
16099
16298
  state: 'pending'
@@ -16106,8 +16305,8 @@ function hasPendingState(analysis, result) {
16106
16305
  step.state = 'noop';
16107
16306
  return step;
16108
16307
  }
16109
- const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
16110
- gasLimit
16308
+ const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
16309
+ gasLimit: await resolveGasLimit(request, gasFloor)
16111
16310
  }) : await request.execute();
16112
16311
  step.txHash = txHash;
16113
16312
  const retryOptions = {
@@ -16181,7 +16380,7 @@ function hasPendingState(analysis, result) {
16181
16380
  adapter: params.source.adapter,
16182
16381
  chain: params.source.chain,
16183
16382
  request: await provider.approve(params.source, approvalAmount),
16184
- gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
16383
+ gasFloor: Number(APPROVE_GAS_LIMIT_EVM)
16185
16384
  });
16186
16385
  }
16187
16386
 
@@ -16209,7 +16408,7 @@ function hasPendingState(analysis, result) {
16209
16408
  adapter: params.source.adapter,
16210
16409
  chain: params.source.chain,
16211
16410
  request: await provider.burn(params),
16212
- gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
16411
+ gasFloor: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
16213
16412
  });
16214
16413
  }
16215
16414
 
@@ -16303,10 +16502,9 @@ function hasPendingState(analysis, result) {
16303
16502
  request: mintRequest,
16304
16503
  // Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
16305
16504
  // eth_estimateGas does not account for, returning a below-floor value
16306
- // without reverting. Pinning to a value above the observed execution max
16307
- // (310_839) bypasses re-estimation and guarantees we clear both the floor
16308
- // and the actual execution cost.
16309
- gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
16505
+ // without reverting. The floor covers those; chains that cost more than the
16506
+ // floor are covered by their own estimate.
16507
+ gasFloor: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
16310
16508
  });
16311
16509
  // Add forwarded: false for non-relayer mints
16312
16510
  return {
@@ -16829,7 +17027,7 @@ const mockAttestationMessage = {
16829
17027
  return step;
16830
17028
  }
16831
17029
 
16832
- var version$4 = "1.10.1";
17030
+ var version$4 = "1.10.2";
16833
17031
  var pkg$4 = {
16834
17032
  version: version$4};
16835
17033
 
@@ -19375,75 +19573,8 @@ function assertCCTPV2Config(config) {
19375
19573
  // Auto-register this kit for user agent tracking
19376
19574
  registerKit(`${pkg$5.name}/${pkg$5.version}`);
19377
19575
 
19378
- /**
19379
- * Create a BridgeKit instance with optional developer fee configuration.
19380
- *
19381
- * This utility creates a BridgeKit instance that optionally includes developer
19382
- * fee configuration based on the provided AppKit context. If the context
19383
- * provides both `getFee` and `getFeeRecipient` methods, they will be configured
19384
- * as developer fees in the BridgeKit instance using the `setCustomFeePolicy` method.
19385
- *
19386
- * The fee integration transforms string-based fees from the context into the
19387
- * format expected by BridgeKit, enabling seamless fee calculation across both kits.
19388
- *
19389
- * @param context - The AppKit context containing optional fee methods
19390
- * @returns A configured BridgeKit instance with or without developer fees
19391
- *
19392
- * @example
19393
- * ```typescript
19394
- * import { createBridgeKit } from '@circle-fin/app-kit/utils'
19395
- * import { createContext } from '@circle-fin/app-kit/context'
19396
- *
19397
- * // Create context with fee methods
19398
- * const context = createContext({
19399
- * getFee: async (type, params) => '1000000', // 1 USDC in micro-units
19400
- * getFeeRecipient: async (type, info) => '0x742d35Cc4634C0532925a3b8D1d7'
19401
- * })
19402
- *
19403
- * // Create BridgeKit with developer fees
19404
- * const bridgeKit = createBridgeKit(context)
19405
- * ```
19406
- *
19407
- * @example
19408
- * ```typescript
19409
- * import { createBridgeKit } from '@circle-fin/app-kit/utils'
19410
- * import { createContext } from '@circle-fin/app-kit/context'
19411
- *
19412
- * // Create context without fee methods
19413
- * const context = createContext()
19414
- *
19415
- * // Create standard BridgeKit instance
19416
- * const bridgeKit = createBridgeKit(context)
19417
- * ```
19418
- */ const createBridgeKit = (context)=>{
19419
- const getFee = context.getFee?.bind(context);
19420
- const getFeeRecipient = context.getFeeRecipient?.bind(context);
19421
- const hasBoth = typeof getFee === 'function' && typeof getFeeRecipient === 'function';
19422
- const kit = new BridgeKit({
19423
- ...context.disableErrorReporting != null && {
19424
- disableErrorReporting: context.disableErrorReporting
19425
- },
19426
- ...context.headers != null && {
19427
- headers: context.headers
19428
- }
19429
- });
19430
- if (hasBoth) {
19431
- kit.setCustomFeePolicy({
19432
- calculateFee: async (params)=>{
19433
- const feeStr = await getFee('bridge', params);
19434
- return feeStr;
19435
- },
19436
- resolveFeeRecipientAddress: async (chain, params)=>await getFeeRecipient('bridge', {
19437
- chain,
19438
- params: params || {}
19439
- })
19440
- });
19441
- }
19442
- return kit;
19443
- };
19444
-
19445
19576
  var name$3 = "@circle-fin/swap-kit";
19446
- var version$3 = "1.5.0";
19577
+ var version$3 = "1.5.1";
19447
19578
  var pkg$3 = {
19448
19579
  name: name$3,
19449
19580
  version: version$3};
@@ -20859,6 +20990,138 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
20859
20990
  return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
20860
20991
  };
20861
20992
 
20993
+ /**
20994
+ * IAdapter contract ABI.
20995
+ *
20996
+ * Shared ABI for the on-chain Adapter contract used by multiple kits
20997
+ * (swap, earn) for executing signed instruction sets. The `execute()`
20998
+ * function accepts EIP-712 signed execution parameters, token inputs,
20999
+ * and a signature, then executes the corresponding on-chain
21000
+ * instructions.
21001
+ */ const adapterContractAbi = [
21002
+ {
21003
+ type: 'function',
21004
+ name: 'execute',
21005
+ inputs: [
21006
+ {
21007
+ name: 'params',
21008
+ type: 'tuple',
21009
+ internalType: 'struct IAdapter.ExecutionParams',
21010
+ components: [
21011
+ {
21012
+ name: 'instructions',
21013
+ type: 'tuple[]',
21014
+ internalType: 'struct IAdapter.Instruction[]',
21015
+ components: [
21016
+ {
21017
+ name: 'target',
21018
+ type: 'address',
21019
+ internalType: 'address'
21020
+ },
21021
+ {
21022
+ name: 'data',
21023
+ type: 'bytes',
21024
+ internalType: 'bytes'
21025
+ },
21026
+ {
21027
+ name: 'value',
21028
+ type: 'uint256',
21029
+ internalType: 'uint256'
21030
+ },
21031
+ {
21032
+ name: 'tokenIn',
21033
+ type: 'address',
21034
+ internalType: 'address'
21035
+ },
21036
+ {
21037
+ name: 'amountToApprove',
21038
+ type: 'uint256',
21039
+ internalType: 'uint256'
21040
+ },
21041
+ {
21042
+ name: 'tokenOut',
21043
+ type: 'address',
21044
+ internalType: 'address'
21045
+ },
21046
+ {
21047
+ name: 'minTokenOut',
21048
+ type: 'uint256',
21049
+ internalType: 'uint256'
21050
+ }
21051
+ ]
21052
+ },
21053
+ {
21054
+ name: 'tokens',
21055
+ type: 'tuple[]',
21056
+ internalType: 'struct IAdapter.TokenRecipient[]',
21057
+ components: [
21058
+ {
21059
+ name: 'token',
21060
+ type: 'address',
21061
+ internalType: 'address'
21062
+ },
21063
+ {
21064
+ name: 'beneficiary',
21065
+ type: 'address',
21066
+ internalType: 'address'
21067
+ }
21068
+ ]
21069
+ },
21070
+ {
21071
+ name: 'execId',
21072
+ type: 'uint256',
21073
+ internalType: 'uint256'
21074
+ },
21075
+ {
21076
+ name: 'deadline',
21077
+ type: 'uint256',
21078
+ internalType: 'uint256'
21079
+ },
21080
+ {
21081
+ name: 'metadata',
21082
+ type: 'bytes',
21083
+ internalType: 'bytes'
21084
+ }
21085
+ ]
21086
+ },
21087
+ {
21088
+ name: 'tokenInputs',
21089
+ type: 'tuple[]',
21090
+ internalType: 'struct IAdapter.TokenInput[]',
21091
+ components: [
21092
+ {
21093
+ name: 'permitType',
21094
+ type: 'uint8',
21095
+ internalType: 'enum IAdapter.PermitType'
21096
+ },
21097
+ {
21098
+ name: 'token',
21099
+ type: 'address',
21100
+ internalType: 'address'
21101
+ },
21102
+ {
21103
+ name: 'amount',
21104
+ type: 'uint256',
21105
+ internalType: 'uint256'
21106
+ },
21107
+ {
21108
+ name: 'permitCalldata',
21109
+ type: 'bytes',
21110
+ internalType: 'bytes'
21111
+ }
21112
+ ]
21113
+ },
21114
+ {
21115
+ name: 'signature',
21116
+ type: 'bytes',
21117
+ internalType: 'bytes'
21118
+ }
21119
+ ],
21120
+ outputs: [],
21121
+ stateMutability: 'payable'
21122
+ }
21123
+ ];
21124
+
20862
21125
  /**
20863
21126
  * USDC ABI
20864
21127
  *
@@ -22094,6 +22357,179 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
22094
22357
  }
22095
22358
  ];
22096
22359
 
22360
+ /**
22361
+ * Minimal ERC-4626 tokenized-vault ABI.
22362
+ *
22363
+ * Covers only the mutating methods EarnKit bundles as inner instructions inside
22364
+ * an Adapter `execute()` call: `deposit`, `withdraw`, and `redeem`. It exists so
22365
+ * clients can decode the inner instruction calldata into a human-readable
22366
+ * summary of what a signer is authorizing (asset amount, receiver, owner)
22367
+ * rather than showing opaque bytes. The 4-byte selectors match the calldata the
22368
+ * earn service signs (`deposit(uint256,address)` = `0x6e553f65`,
22369
+ * `withdraw(uint256,address,address)` = `0xb460af94`,
22370
+ * `redeem(uint256,address,address)` = `0xba087652`).
22371
+ */ const erc4626VaultAbi = [
22372
+ {
22373
+ type: 'function',
22374
+ name: 'deposit',
22375
+ stateMutability: 'nonpayable',
22376
+ inputs: [
22377
+ {
22378
+ name: 'assets',
22379
+ type: 'uint256',
22380
+ internalType: 'uint256'
22381
+ },
22382
+ {
22383
+ name: 'receiver',
22384
+ type: 'address',
22385
+ internalType: 'address'
22386
+ }
22387
+ ],
22388
+ outputs: [
22389
+ {
22390
+ name: 'shares',
22391
+ type: 'uint256',
22392
+ internalType: 'uint256'
22393
+ }
22394
+ ]
22395
+ },
22396
+ {
22397
+ type: 'function',
22398
+ name: 'withdraw',
22399
+ stateMutability: 'nonpayable',
22400
+ inputs: [
22401
+ {
22402
+ name: 'assets',
22403
+ type: 'uint256',
22404
+ internalType: 'uint256'
22405
+ },
22406
+ {
22407
+ name: 'receiver',
22408
+ type: 'address',
22409
+ internalType: 'address'
22410
+ },
22411
+ {
22412
+ name: 'owner',
22413
+ type: 'address',
22414
+ internalType: 'address'
22415
+ }
22416
+ ],
22417
+ outputs: [
22418
+ {
22419
+ name: 'shares',
22420
+ type: 'uint256',
22421
+ internalType: 'uint256'
22422
+ }
22423
+ ]
22424
+ },
22425
+ {
22426
+ type: 'function',
22427
+ name: 'redeem',
22428
+ stateMutability: 'nonpayable',
22429
+ inputs: [
22430
+ {
22431
+ name: 'shares',
22432
+ type: 'uint256',
22433
+ internalType: 'uint256'
22434
+ },
22435
+ {
22436
+ name: 'receiver',
22437
+ type: 'address',
22438
+ internalType: 'address'
22439
+ },
22440
+ {
22441
+ name: 'owner',
22442
+ type: 'address',
22443
+ internalType: 'address'
22444
+ }
22445
+ ],
22446
+ outputs: [
22447
+ {
22448
+ name: 'assets',
22449
+ type: 'uint256',
22450
+ internalType: 'uint256'
22451
+ }
22452
+ ]
22453
+ }
22454
+ ];
22455
+
22456
+ /**
22457
+ * Minimal FeeTaker ABI.
22458
+ *
22459
+ * The earn service appends a `takeFeeERC20` instruction to withdraw bundles
22460
+ * when Circle charges a withdrawal fee. This ABI decodes that inner instruction
22461
+ * so the fee (token, beneficiary, amount) is visible in the signing summary
22462
+ * instead of appearing as opaque calldata alongside the redeem/withdraw call.
22463
+ */ const feeTakerAbi = [
22464
+ {
22465
+ type: 'function',
22466
+ name: 'takeFeeERC20',
22467
+ stateMutability: 'nonpayable',
22468
+ inputs: [
22469
+ {
22470
+ name: 'token',
22471
+ type: 'address',
22472
+ internalType: 'address'
22473
+ },
22474
+ {
22475
+ name: 'beneficiary',
22476
+ type: 'address',
22477
+ internalType: 'address'
22478
+ },
22479
+ {
22480
+ name: 'fee',
22481
+ type: 'uint256',
22482
+ internalType: 'uint256'
22483
+ },
22484
+ {
22485
+ name: 'kitType',
22486
+ type: 'bytes8',
22487
+ internalType: 'bytes8'
22488
+ }
22489
+ ],
22490
+ outputs: []
22491
+ }
22492
+ ];
22493
+
22494
+ /**
22495
+ * Minimal Merkl Distributor ABI.
22496
+ *
22497
+ * EarnKit claim-rewards bundles a single `claim` instruction targeting the
22498
+ * Merkl Distributor, batching one entry per reward token. This ABI decodes that
22499
+ * inner instruction so the claimed tokens and amounts are visible in the signing
22500
+ * summary. `claim` uses dynamic array arguments, which is why a real ABI decoder
22501
+ * (rather than fixed-word slicing) is required for the reward instruction.
22502
+ */ const merklDistributorAbi = [
22503
+ {
22504
+ type: 'function',
22505
+ name: 'claim',
22506
+ stateMutability: 'nonpayable',
22507
+ inputs: [
22508
+ {
22509
+ name: 'users',
22510
+ type: 'address[]',
22511
+ internalType: 'address[]'
22512
+ },
22513
+ {
22514
+ name: 'tokens',
22515
+ type: 'address[]',
22516
+ internalType: 'address[]'
22517
+ },
22518
+ {
22519
+ name: 'amounts',
22520
+ type: 'uint256[]',
22521
+ internalType: 'uint256[]'
22522
+ },
22523
+ {
22524
+ name: 'proofs',
22525
+ type: 'bytes32[][]',
22526
+ internalType: 'bytes32[][]'
22527
+ }
22528
+ ],
22529
+ outputs: []
22530
+ }
22531
+ ];
22532
+
22097
22533
  /**
22098
22534
  * Zod schema for validating EVM adapter capabilities.
22099
22535
  *
@@ -23602,72 +24038,55 @@ function evmSigningData(burnIntent) {
23602
24038
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
23603
24039
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
23604
24040
  * signatures, so for Gateway's purposes a 7702-delegated address is
23605
- * an EOA, not an SCA.
24041
+ * an EOA, not a contract signer.
23606
24042
  *
23607
24043
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
23608
24044
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
23609
24045
  /**
23610
- * Assert that `address` on `chain` can sign Gateway burn intents.
24046
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
24047
+ * rather than as an EOA.
23611
24048
  *
23612
- * Gateway verifies burn-intent signatures with plain `ecrecover` (see
23613
- * `evm-gateway-contracts/src/lib/EIP712Domain.sol`). Smart-contract
23614
- * accounts (SCAs) produce signatures over wrapped hashes (ERC-1271 /
23615
- * ERC-6492 / ERC-6900 replay-safe hashes) that Gateway cannot verify.
23616
- * Additionally, the Circle Wallets backend rejects SCA typed-data signing
23617
- * against Gateway's chainId-less domain with an opaque
23618
- * `invalid integer value <nil>/<nil> for type uint256` error.
24049
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
24050
+ * check for EOAs, and — for requests that carry `contractSigner: true`
24051
+ * an offchain `isValidSignature` simulation against the signing contract
24052
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
24053
+ * declare it. This detects the contract case from on-chain bytecode.
23619
24054
  *
23620
- * EIP-7702-delegated EOAs are exempt: they expose non-empty bytecode
23621
- * (`0xef0100<delegate>`) but the underlying secp256k1 key still produces
23622
- * `ecrecover`-verifiable signatures, so Gateway accepts them.
24055
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
24056
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
24057
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
24058
+ * stays correct for them.
23623
24059
  *
23624
- * When the signer is a true SCA, raises an `INPUT_UNSUPPORTED_ACTION`
23625
- * error directing the caller to register an EOA delegate against the
23626
- * SCA and then submit the spend with the delegate EOA as the signer
23627
- * and the SCA as the source account. See the unified-balance / Gateway
23628
- * docs for the exact API.
23629
- *
23630
- * If bytecode cannot be read (RPC failure, etc.) the pre-check is
23631
- * skipped and downstream signing surfaces its own error — a warning is
23632
- * logged so the skip is diagnosable.
24060
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
24061
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
24062
+ * genuine contract signer misreported this way is rejected by Gateway with
24063
+ * an invalid-signature error rather than silently mis-attested.
23633
24064
  *
23634
24065
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
23635
- * @param address - Signer address to validate.
24066
+ * @param address - Signer address to classify.
23636
24067
  * @param chain - EVM chain where the signer lives.
23637
- * @throws {KitError} INPUT_UNSUPPORTED_ACTION when `address` is an SCA.
24068
+ * @returns `true` when the signer is a contract account and the transfer
24069
+ * request must set `contractSigner: true`; `false` otherwise.
23638
24070
  *
23639
24071
  * @example
23640
24072
  * ```typescript
23641
- * import { assertSignerIsEoa } from '@core/adapter-evm'
24073
+ * import { isContractSigner } from '@core/adapter-evm'
23642
24074
  * import { Ethereum } from '@core/chains'
23643
24075
  *
23644
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
24076
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
23645
24077
  * ```
23646
- */ async function assertSignerIsEoa(adapter, address, chain) {
24078
+ */ async function isContractSigner(adapter, address, chain) {
23647
24079
  let code;
23648
24080
  try {
23649
24081
  code = await adapter.readBytecode(address, chain);
23650
24082
  } catch (err) {
23651
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
23652
- return;
24083
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
24084
+ return false;
23653
24085
  }
23654
24086
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
23655
- return;
24087
+ return false;
23656
24088
  }
23657
- throw new KitError({
23658
- ...InputError.UNSUPPORTED_ACTION,
23659
- recoverability: 'FATAL',
23660
- 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.`,
23661
- cause: {
23662
- trace: {
23663
- operation: 'signEvmIntentGroup.assertSignerIsEoa',
23664
- address,
23665
- chain: chain.name,
23666
- bytecodeBytes: (code.length - 2) / 2,
23667
- bytecodePrefix: code.slice(0, 12)
23668
- }
23669
- }
23670
- });
24089
+ return true;
23671
24090
  }
23672
24091
 
23673
24092
  /**
@@ -23694,78 +24113,177 @@ function evmSigningData(burnIntent) {
23694
24113
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
23695
24114
  }
23696
24115
 
24116
+ function resolveIntentChain(group, intent) {
24117
+ const sourceDomain = intent.spec.sourceDomain;
24118
+ const chain = group.chainsByDomain.get(sourceDomain);
24119
+ if (chain !== undefined) return chain;
24120
+ throw createValidationFailedError$1('intent.spec.sourceDomain', sourceDomain, `No source chain found for Gateway domain ${String(sourceDomain)}`);
24121
+ }
24122
+ function normalizeSignatureResult(result) {
24123
+ if (typeof result === 'string') {
24124
+ return {
24125
+ signature: result,
24126
+ contractSigner: false
24127
+ };
24128
+ }
24129
+ if (typeof result === 'object' && result !== null && 'signature' in result && typeof result.signature === 'string') {
24130
+ return {
24131
+ signature: result.signature,
24132
+ contractSigner: 'contractSigner' in result && result.contractSigner === true
24133
+ };
24134
+ }
24135
+ throw createValidationFailedError$1('signature', result, 'must be a signature string or an object containing a signature string');
24136
+ }
24137
+ function validateGroupIntents(intents) {
24138
+ evmSigningData(intents);
24139
+ }
24140
+ function collectChainsByDomain(group) {
24141
+ const chainsByDomain = new Map();
24142
+ for (const intent of group.intents){
24143
+ chainsByDomain.set(intent.spec.sourceDomain, resolveIntentChain(group, intent));
24144
+ }
24145
+ return chainsByDomain;
24146
+ }
24147
+ async function classifySignerTypes(group, chainsByDomain) {
24148
+ const { adapter, address } = group;
24149
+ // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
24150
+ // each consumer package bundles its own copy of the base class and the
24151
+ // `instanceof` identity check fails across package boundaries.
24152
+ // Empty strings are rejected to avoid calling eth_getCode('') on the RPC.
24153
+ const hasResolvedSigner = typeof address === 'string' && address.length > 0;
24154
+ const signerTypes = await Promise.all([
24155
+ ...chainsByDomain
24156
+ ].map(async ([sourceDomain, sourceChain])=>{
24157
+ const contractSigner = hasResolvedSigner && sourceChain.type === 'evm' && isEvmAdapterLike(adapter) ? await isContractSigner(adapter, address, sourceChain) : false;
24158
+ return [
24159
+ sourceDomain,
24160
+ contractSigner
24161
+ ];
24162
+ }));
24163
+ return new Map(signerTypes);
24164
+ }
24165
+ function createSigningUnits(group, signerTypeByDomain) {
24166
+ const contractUnitsByDomain = new Map();
24167
+ let eoaUnit;
24168
+ for (const [index, intent] of group.intents.entries()){
24169
+ const sourceDomain = intent.spec.sourceDomain;
24170
+ const contractSigner = signerTypeByDomain.get(sourceDomain) ?? false;
24171
+ if (contractSigner) {
24172
+ const existingUnit = contractUnitsByDomain.get(sourceDomain);
24173
+ if (existingUnit === undefined) {
24174
+ contractUnitsByDomain.set(sourceDomain, {
24175
+ intents: [
24176
+ intent
24177
+ ],
24178
+ chain: resolveIntentChain(group, intent),
24179
+ contractSigner: true,
24180
+ firstIntentIndex: index
24181
+ });
24182
+ } else {
24183
+ existingUnit.intents.push(intent);
24184
+ }
24185
+ } else {
24186
+ eoaUnit ??= {
24187
+ intents: [],
24188
+ chain: resolveIntentChain(group, intent),
24189
+ contractSigner: false,
24190
+ firstIntentIndex: index
24191
+ };
24192
+ eoaUnit.intents.push(intent);
24193
+ }
24194
+ }
24195
+ const signingUnits = [
24196
+ ...contractUnitsByDomain.values()
24197
+ ];
24198
+ if (eoaUnit !== undefined) signingUnits.push(eoaUnit);
24199
+ signingUnits.sort((a, b)=>a.firstIntentIndex - b.firstIntentIndex);
24200
+ return signingUnits;
24201
+ }
24202
+ async function signUnit(group, unit) {
24203
+ const { adapter, address } = group;
24204
+ const firstIntent = unit.intents[0];
24205
+ const typedData = unit.intents.length === 1 && firstIntent !== undefined ? evmSigningData(firstIntent) : evmSigningData(unit.intents);
24206
+ const operationContext = address === undefined ? {
24207
+ chain: unit.chain
24208
+ } : {
24209
+ chain: unit.chain,
24210
+ address
24211
+ };
24212
+ const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
24213
+ typedData,
24214
+ chain: unit.chain
24215
+ }, operationContext);
24216
+ const result = normalizeSignatureResult(await signRequest.execute());
24217
+ return {
24218
+ intents: unit.intents,
24219
+ signature: result.signature,
24220
+ contractSigner: result.contractSigner || unit.contractSigner
24221
+ };
24222
+ }
24223
+ async function signUnits(group, signingUnits) {
24224
+ const signedSets = [];
24225
+ // Keep wallet prompts deterministic. Multiple adapter groups can still sign
24226
+ // in parallel, but one signer is asked for its chain-bound signatures in
24227
+ // source-intent order.
24228
+ for (const unit of signingUnits){
24229
+ signedSets.push(await signUnit(group, unit));
24230
+ }
24231
+ return signedSets;
24232
+ }
23697
24233
  /**
23698
- * Sign an EVM adapter group: batches all intents and produces a single
23699
- * EIP-712 ECDSA signature.
24234
+ * Sign an EVM adapter group.
23700
24235
  *
23701
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
23702
- * For multi-intent groups, `primaryType` is `'BurnIntentSet'`.
24236
+ * EOA intents remain batched into one EIP-712 `BurnIntentSet`. ERC-1271
24237
+ * intents are grouped and signed per source chain because smart accounts
24238
+ * commonly include `chainId` in their replay-safe signature hash.
24239
+ * All returned entries can still be submitted together in one atomic Gateway
24240
+ * transfer request.
23703
24241
  *
23704
- * Before signing, asserts that the signer address is an EOA. Gateway
23705
- * verifies burn-intent signatures with plain `ecrecover` (no ERC-1271
23706
- * fallback), so signatures produced by smart-contract accounts (SCAs)
23707
- * cannot be verified. When an SCA is detected, a clear error is raised
23708
- * directing the caller to the delegate workflow (DEVX-2774).
24242
+ * Before signing, classifies the signer as an EOA or a contract account.
24243
+ * Gateway validates EOA signatures with `ecrecover` and contract-account
24244
+ * signatures with ERC-1271, but it does not infer which one applies — the
24245
+ * transfer request has to declare it. The returned `contractSigner` flag
24246
+ * carries that decision through to `buildTransferRequestBody`.
23709
24247
  *
23710
24248
  * @param group - The adapter group containing the adapter, chain, and
23711
24249
  * burn intents to sign.
23712
- * @returns A signed set with the intents and the ECDSA signature.
24250
+ * @returns Signed entries with their intents, signatures, and Gateway signer
24251
+ * validation mode.
24252
+ * @throws KitError when an intent has no source-chain mapping or a signing
24253
+ * action returns an invalid signature shape.
23713
24254
  *
23714
24255
  * @example
23715
24256
  * ```typescript
23716
24257
  * import { signEvmIntentGroup } from '@core/adapter-evm'
23717
24258
  *
23718
- * const signedSet = await signEvmIntentGroup({
24259
+ * const signedSets = await signEvmIntentGroup({
23719
24260
  * adapter: evmAdapter,
23720
24261
  * chain: ethereumChain,
23721
24262
  * intents: [burnIntent1, burnIntent2],
24263
+ * chainsByDomain: new Map([
24264
+ * [0, ethereumChain],
24265
+ * [6, baseChain],
24266
+ * ]),
23722
24267
  * address: '0x...',
23723
24268
  * })
23724
- * console.log(signedSet.signature)
24269
+ * console.log(signedSets)
23725
24270
  * ```
23726
24271
  */ async function signEvmIntentGroup(group) {
23727
- const { adapter, intents: groupIntents, chain, address } = group;
23728
- const operationContext = address === undefined ? {
23729
- chain
23730
- } : {
23731
- chain,
23732
- address
23733
- };
23734
- // Gateway verifies burn-intent signatures with plain ecrecover. An SCA
23735
- // signer silently produces a signature over a wrapped hash that Gateway
23736
- // cannot verify, and Circle Wallets' KMS rejects the typed data up front
23737
- // with an opaque `<nil>/<nil>` error. Short-circuit with a clear message
23738
- // when we can detect bytecode at the signer address. See DEVX-2774.
23739
- //
23740
- // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
23741
- // each consumer package bundles its own copy of the base class and the
23742
- // `instanceof` identity check fails across package boundaries.
23743
- //
23744
- // Empty string is defended against because assertSignerIsEoa would
23745
- // otherwise call eth_getCode('') on the RPC.
23746
- const hasResolvedSigner = typeof address === 'string' && address.length > 0;
23747
- if (hasResolvedSigner && chain.type === 'evm' && isEvmAdapterLike(adapter)) {
23748
- await assertSignerIsEoa(adapter, address, chain);
23749
- }
23750
- const firstIntent = groupIntents[0];
23751
- const typedData = groupIntents.length === 1 && firstIntent ? evmSigningData(firstIntent) : evmSigningData(groupIntents);
23752
- const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
23753
- typedData,
23754
- chain
23755
- }, operationContext);
23756
- const sig = await signRequest.execute();
23757
- return {
23758
- intents: groupIntents,
23759
- signature: sig
23760
- };
24272
+ // Validate the collection before doing bytecode reads or asking a wallet
24273
+ // to sign. evmSigningData owns the canonical BurnIntent validation.
24274
+ validateGroupIntents(group.intents);
24275
+ const chainsByDomain = collectChainsByDomain(group);
24276
+ const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
24277
+ const signingUnits = createSigningUnits(group, signerTypeByDomain);
24278
+ return await signUnits(group, signingUnits);
23761
24279
  }
23762
24280
 
23763
24281
  /**
23764
24282
  * Add an EVM intent into the batched EVM group map.
23765
24283
  *
23766
24284
  * On EVM, all intents for the same adapter are batched into a single
23767
- * group so that they can be signed in one EIP-712 `BurnIntentSet`
23768
- * operation.
24285
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
24286
+ * while signing ERC-1271 intents separately on their source chains.
23769
24287
  *
23770
24288
  * @param intent - The burn intent to group.
23771
24289
  * @param alloc - The allocation that resolved to this intent.
@@ -23782,6 +24300,7 @@ function evmSigningData(burnIntent) {
23782
24300
  const existing = evmGroups.get(alloc.adapter);
23783
24301
  if (existing) {
23784
24302
  existing.intents.push(intent);
24303
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
23785
24304
  } else {
23786
24305
  evmGroups.set(alloc.adapter, {
23787
24306
  adapter: alloc.adapter,
@@ -23789,6 +24308,12 @@ function evmSigningData(burnIntent) {
23789
24308
  intents: [
23790
24309
  intent
23791
24310
  ],
24311
+ chainsByDomain: new Map([
24312
+ [
24313
+ alloc.chain.gateway.domain,
24314
+ alloc.chain
24315
+ ]
24316
+ ]),
23792
24317
  address: alloc.sourceSigner
23793
24318
  });
23794
24319
  }
@@ -31994,6 +32519,113 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
31994
32519
  // Auto-register this kit for user agent tracking
31995
32520
  registerKit(`${pkg$3.name}/${pkg$3.version}`);
31996
32521
 
32522
+ /**
32523
+ * Creates a AppKit context.
32524
+ *
32525
+ * This function constructs a context object, initializes the actions registry
32526
+ * used for event handlers, and merges in any custom implementations provided
32527
+ * via params.
32528
+ *
32529
+ * @param params - Optional custom implementations to override defaults
32530
+ * @returns A AppKitContext
32531
+ *
32532
+ * @example
32533
+ * ```typescript
32534
+ * // Create context with all defaults
32535
+ * const defaultContext = createContext()
32536
+ *
32537
+ * // Create context with custom fee calculation
32538
+ * const customContext = createContext({
32539
+ * getFee: async (type, params) => {
32540
+ * if (type === 'bridge') {
32541
+ * // Custom bridge fee logic
32542
+ * return await calculateBridgeFee(params)
32543
+ * }
32544
+ * // Use default for other types
32545
+ * return defaultFeeCalculation(type, params)
32546
+ * }
32547
+ * })
32548
+ * ```
32549
+ */ const createContext = (params = {})=>{
32550
+ return {
32551
+ ...params,
32552
+ actions: {
32553
+ bridge: {},
32554
+ earn: {},
32555
+ ...params.actions
32556
+ }
32557
+ };
32558
+ };
32559
+
32560
+ /**
32561
+ * Create a BridgeKit instance with optional developer fee configuration.
32562
+ *
32563
+ * This utility creates a BridgeKit instance that optionally includes developer
32564
+ * fee configuration based on the provided AppKit context. If the context
32565
+ * provides both `getFee` and `getFeeRecipient` methods, they will be configured
32566
+ * as developer fees in the BridgeKit instance using the `setCustomFeePolicy` method.
32567
+ *
32568
+ * The fee integration transforms string-based fees from the context into the
32569
+ * format expected by BridgeKit, enabling seamless fee calculation across both kits.
32570
+ *
32571
+ * @param context - The AppKit context containing optional fee methods
32572
+ * @returns A configured BridgeKit instance with or without developer fees
32573
+ *
32574
+ * @example
32575
+ * ```typescript
32576
+ * import { createBridgeKit } from '@circle-fin/app-kit/utils'
32577
+ * import { createContext } from '@circle-fin/app-kit/context'
32578
+ *
32579
+ * // Create context with fee methods
32580
+ * const context = createContext({
32581
+ * getFee: async (type, params) => '1000000', // 1 USDC in micro-units
32582
+ * getFeeRecipient: async (type, info) => '0x742d35Cc4634C0532925a3b8D1d7'
32583
+ * })
32584
+ *
32585
+ * // Create BridgeKit with developer fees
32586
+ * const bridgeKit = createBridgeKit(context)
32587
+ * ```
32588
+ *
32589
+ * @example
32590
+ * ```typescript
32591
+ * import { createBridgeKit } from '@circle-fin/app-kit/utils'
32592
+ * import { createContext } from '@circle-fin/app-kit/context'
32593
+ *
32594
+ * // Create context without fee methods
32595
+ * const context = createContext()
32596
+ *
32597
+ * // Create standard BridgeKit instance
32598
+ * const bridgeKit = createBridgeKit(context)
32599
+ * ```
32600
+ */ const createBridgeKit = (context)=>{
32601
+ const getFee = context.getFee?.bind(context);
32602
+ const getFeeRecipient = context.getFeeRecipient?.bind(context);
32603
+ const hasBoth = typeof getFee === 'function' && typeof getFeeRecipient === 'function';
32604
+ const kit = new BridgeKit({
32605
+ ...context.disableErrorReporting != null && {
32606
+ disableErrorReporting: context.disableErrorReporting
32607
+ },
32608
+ ...context.headers != null && {
32609
+ headers: context.headers
32610
+ }
32611
+ });
32612
+ if (context.customFeePolicy?.bridge != null) {
32613
+ kit.setCustomFeePolicy(context.customFeePolicy.bridge);
32614
+ } else if (hasBoth) {
32615
+ kit.setCustomFeePolicy({
32616
+ calculateFee: async (params)=>{
32617
+ const feeStr = await getFee('bridge', params);
32618
+ return feeStr;
32619
+ },
32620
+ resolveFeeRecipientAddress: async (chain, params)=>await getFeeRecipient('bridge', {
32621
+ chain,
32622
+ params: params || {}
32623
+ })
32624
+ });
32625
+ }
32626
+ return kit;
32627
+ };
32628
+
31997
32629
  /**
31998
32630
  * Create a SwapKit instance with optional developer fee configuration.
31999
32631
  *
@@ -32032,7 +32664,9 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32032
32664
  disableAnalytics: context.disableAnalytics
32033
32665
  }
32034
32666
  });
32035
- if (hasBoth) {
32667
+ if (context.customFeePolicy?.swap != null) {
32668
+ kit.setCustomFeePolicy(context.customFeePolicy.swap);
32669
+ } else if (hasBoth) {
32036
32670
  kit.setCustomFeePolicy({
32037
32671
  computeFee: async (params)=>{
32038
32672
  // Adapt provider-level params (with tokenIn/tokenOut)
@@ -32091,7 +32725,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
32091
32725
  };
32092
32726
 
32093
32727
  var name$2 = "@circle-fin/earn-kit";
32094
- var version$2 = "1.4.0";
32728
+ var version$2 = "1.5.0";
32095
32729
  var pkg$2 = {
32096
32730
  name: name$2,
32097
32731
  version: version$2};
@@ -32734,6 +33368,683 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
32734
33368
  return approvedToken;
32735
33369
  }
32736
33370
 
33371
+ /**
33372
+ * Combined ABI of every inner instruction EarnKit can bundle inside an Adapter
33373
+ * `execute()` call. `decodeFunctionData` matches an instruction's calldata to
33374
+ * one of these functions by its 4-byte selector.
33375
+ */ const earnInstructionAbi = [
33376
+ ...erc4626VaultAbi,
33377
+ ...feeTakerAbi,
33378
+ ...merklDistributorAbi
33379
+ ];
33380
+ /**
33381
+ * Extract and shallow-validate the `instructions` array from loosely-typed
33382
+ * signed execution params.
33383
+ *
33384
+ * The earn service schema validates `tokenIn`/`amountToApprove` and passes the
33385
+ * remaining instruction fields through untyped, so the params arrive as a plain
33386
+ * record; each accessed field is narrowed at runtime.
33387
+ */ function requireInstructions(executionParams) {
33388
+ const instructions = executionParams['instructions'];
33389
+ if (!Array.isArray(instructions)) {
33390
+ throw decodeMismatchError('execution params are missing an instructions array', {
33391
+ instructions
33392
+ });
33393
+ }
33394
+ return instructions.map((instruction, index)=>{
33395
+ if (typeof instruction !== 'object' || instruction === null) {
33396
+ throw decodeMismatchError(`instructions[${index.toString()}] is not an object`, {
33397
+ index
33398
+ });
33399
+ }
33400
+ return instruction;
33401
+ });
33402
+ }
33403
+ /**
33404
+ * Build a fail-closed {@link KitError} for an earn decode or review failure.
33405
+ *
33406
+ * Marked non-recoverable: a mismatch between what would be shown and what would
33407
+ * be signed is never safe to retry, so the operation fails fast rather than
33408
+ * presenting misleading decoded data. `messagePrefix` names the failing stage
33409
+ * (calldata decode vs. review construction); callers bind it once and pass the
33410
+ * specific failure as `message`.
33411
+ */ function failClosedEarnError(messagePrefix, message, trace) {
33412
+ return new KitError({
33413
+ ...EarnError.INTERNAL_ERROR,
33414
+ recoverability: 'FATAL',
33415
+ message: `${messagePrefix}: ${message}`,
33416
+ cause: {
33417
+ trace
33418
+ }
33419
+ });
33420
+ }
33421
+ /**
33422
+ * Build a {@link KitError} for a decode or consistency failure.
33423
+ *
33424
+ * Thin wrapper over {@link failClosedEarnError} bound to the decode-stage
33425
+ * message prefix.
33426
+ */ function decodeMismatchError(message, trace) {
33427
+ return failClosedEarnError('Unable to decode earn transaction', message, trace);
33428
+ }
33429
+ /**
33430
+ * Narrow an untyped value to a 0x-prefixed hex string, or fail fast.
33431
+ */ function requireHex(value, path) {
33432
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value)) {
33433
+ return value;
33434
+ }
33435
+ throw decodeMismatchError(`${path} is not a hex string`, {
33436
+ path,
33437
+ value
33438
+ });
33439
+ }
33440
+ /**
33441
+ * Narrow an untyped value to a 20-byte EVM address, or fail fast.
33442
+ */ function requireAddress(value, path) {
33443
+ if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) {
33444
+ return value;
33445
+ }
33446
+ throw decodeMismatchError(`${path} is not an address`, {
33447
+ path,
33448
+ value
33449
+ });
33450
+ }
33451
+ /**
33452
+ * Narrow an untyped `uint256`-like value (decimal string, bigint, or integer)
33453
+ * to a bigint, or fail fast.
33454
+ */ function requireUint(value, path) {
33455
+ if (typeof value === 'bigint') {
33456
+ return value;
33457
+ }
33458
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
33459
+ return BigInt(value);
33460
+ }
33461
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
33462
+ return BigInt(value);
33463
+ }
33464
+ throw decodeMismatchError(`${path} is not a uint256 value`, {
33465
+ path,
33466
+ value
33467
+ });
33468
+ }
33469
+ /**
33470
+ * Decode inner instruction calldata against the earn instruction ABI, mapping a
33471
+ * viem decode failure (unknown selector, malformed args) to a fail-fast error.
33472
+ */ function decodeEarnInstructionData(data, index) {
33473
+ try {
33474
+ return decodeFunctionData({
33475
+ abi: earnInstructionAbi,
33476
+ data
33477
+ });
33478
+ } catch (error) {
33479
+ throw decodeMismatchError(`instructions[${index.toString()}] calldata is not a recognized earn instruction`, {
33480
+ index,
33481
+ selector: data.slice(0, 10),
33482
+ error: String(error)
33483
+ });
33484
+ }
33485
+ }
33486
+ /**
33487
+ * Decode Adapter `execute()` calldata, mapping a viem decode failure to a
33488
+ * fail-fast error.
33489
+ */ function decodeExecuteCalldata(calldata) {
33490
+ try {
33491
+ return decodeFunctionData({
33492
+ abi: adapterContractAbi,
33493
+ data: calldata
33494
+ });
33495
+ } catch (error) {
33496
+ throw decodeMismatchError('encoded calldata is not a valid Adapter execute() call', {
33497
+ error: String(error)
33498
+ });
33499
+ }
33500
+ }
33501
+ /**
33502
+ * Decode one inner instruction's calldata into a typed {@link
33503
+ * DecodedEarnInstruction}.
33504
+ */ function decodeInstruction(instruction, index) {
33505
+ const target = requireAddress(instruction['target'], `instructions[${index.toString()}].target`);
33506
+ const data = requireHex(instruction['data'], `instructions[${index.toString()}].data`);
33507
+ const decoded = decodeEarnInstructionData(data, index);
33508
+ switch(decoded.functionName){
33509
+ case 'deposit':
33510
+ {
33511
+ const [assets, receiver] = decoded.args;
33512
+ return {
33513
+ method: 'deposit',
33514
+ vault: target,
33515
+ assets: assets.toString(),
33516
+ receiver
33517
+ };
33518
+ }
33519
+ case 'withdraw':
33520
+ {
33521
+ const [assets, receiver, owner] = decoded.args;
33522
+ return {
33523
+ method: 'withdraw',
33524
+ vault: target,
33525
+ assets: assets.toString(),
33526
+ receiver,
33527
+ owner
33528
+ };
33529
+ }
33530
+ case 'redeem':
33531
+ {
33532
+ const [shares, receiver, owner] = decoded.args;
33533
+ return {
33534
+ method: 'redeem',
33535
+ vault: target,
33536
+ shares: shares.toString(),
33537
+ receiver,
33538
+ owner
33539
+ };
33540
+ }
33541
+ case 'takeFeeERC20':
33542
+ {
33543
+ const [token, beneficiary, fee, kitType] = decoded.args;
33544
+ return {
33545
+ method: 'takeFeeERC20',
33546
+ feeTaker: target,
33547
+ token,
33548
+ beneficiary,
33549
+ fee: fee.toString(),
33550
+ kitType
33551
+ };
33552
+ }
33553
+ case 'claim':
33554
+ {
33555
+ const users = decoded.args[0];
33556
+ const tokens = decoded.args[1];
33557
+ const amounts = decoded.args[2];
33558
+ // Merkl claim(users, tokens, amounts, proofs) carries parallel arrays,
33559
+ // one entry per reward. Reject any length skew rather than padding with
33560
+ // zero amounts or dropping trailing entries, so the preview can never
33561
+ // misstate what is claimed or for whom.
33562
+ //
33563
+ // Note: Merkl `amounts` are the *cumulative lifetime* total claimable per
33564
+ // (user, token); the Distributor transfers only `amount - alreadyClaimed`.
33565
+ // This decode faithfully surfaces the signed cumulative value, which is
33566
+ // what `DecodedRewardClaim.amount` documents. See that type's doc.
33567
+ if (new Set([
33568
+ users.length,
33569
+ tokens.length,
33570
+ amounts.length
33571
+ ]).size !== 1) {
33572
+ throw decodeMismatchError(`instructions[${index.toString()}] claim has mismatched recipient/token/amount lengths`, {
33573
+ index,
33574
+ users: users.length,
33575
+ tokens: tokens.length,
33576
+ amounts: amounts.length
33577
+ });
33578
+ }
33579
+ const rewards = tokens.map((token, rewardIndex)=>({
33580
+ recipient: requireAddress(users[rewardIndex], `instructions[${index.toString()}].claim.users[${rewardIndex.toString()}]`),
33581
+ address: token,
33582
+ amount: requireUint(amounts[rewardIndex], `instructions[${index.toString()}].claim.amounts[${rewardIndex.toString()}]`).toString()
33583
+ }));
33584
+ return {
33585
+ method: 'claim',
33586
+ distributor: target,
33587
+ rewards
33588
+ };
33589
+ }
33590
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
33591
+ return assertNever$2(decoded);
33592
+ }
33593
+ }
33594
+ /**
33595
+ * Lift the primary values a wallet prompt cares about out of the decoded
33596
+ * instructions into a flat summary.
33597
+ */ function buildSummary(instructions, envelope) {
33598
+ const summary = {};
33599
+ instructions.forEach((instruction, index)=>{
33600
+ switch(instruction.method){
33601
+ case 'deposit':
33602
+ case 'withdraw':
33603
+ case 'redeem':
33604
+ {
33605
+ // The summary lifts a single primary token movement to the top level.
33606
+ // An earn bundle carries exactly one deposit/withdraw/redeem today;
33607
+ // fail fast rather than silently overwriting an earlier one, which
33608
+ // would drop it from the wallet-facing preview.
33609
+ if (summary.token !== undefined) {
33610
+ throw decodeMismatchError('multiple deposit/withdraw/redeem instructions cannot be summarized into a single preview', {
33611
+ index
33612
+ });
33613
+ }
33614
+ // Pair the amount with the token it is actually denominated in so the
33615
+ // preview never folds two units into one entry:
33616
+ // - deposit: `assets` of the underlying asset pulled in (`tokenIn`)
33617
+ // - redeem: `shares` of the vault-share token burned (`tokenIn`)
33618
+ // - withdraw: `assets` of the underlying asset paid out (`tokenOut`).
33619
+ // `withdraw(assets)` counts the underlying received, not the shares
33620
+ // burned to produce it, so `tokenIn` (the share token) would misstate
33621
+ // the unit; the underlying is the instruction's `tokenOut`.
33622
+ const amount = instruction.method === 'redeem' ? instruction.shares : instruction.assets;
33623
+ const tokenField = instruction.method === 'withdraw' ? 'tokenOut' : 'tokenIn';
33624
+ summary.vault = instruction.vault;
33625
+ summary.receiver = instruction.receiver;
33626
+ summary.token = {
33627
+ address: requireAddress(envelope[index]?.[tokenField], `instructions[${index.toString()}].${tokenField}`),
33628
+ amount
33629
+ };
33630
+ break;
33631
+ }
33632
+ case 'takeFeeERC20':
33633
+ {
33634
+ // As with the vault case, a second fee would silently overwrite the
33635
+ // first and understate what is charged; fail fast instead.
33636
+ if (summary.fee !== undefined) {
33637
+ throw decodeMismatchError('multiple fee instructions cannot be summarized into a single preview', {
33638
+ index
33639
+ });
33640
+ }
33641
+ summary.fee = {
33642
+ address: instruction.token,
33643
+ amount: instruction.fee
33644
+ };
33645
+ break;
33646
+ }
33647
+ case 'claim':
33648
+ {
33649
+ // A second claim would silently drop the first from the preview
33650
+ // (rewards are already batched inside one Merkl claim); fail fast.
33651
+ if (summary.rewards !== undefined) {
33652
+ throw decodeMismatchError('multiple claim instructions cannot be summarized into a single preview', {
33653
+ index
33654
+ });
33655
+ }
33656
+ summary.rewards = instruction.rewards;
33657
+ break;
33658
+ }
33659
+ /* v8 ignore next 2 -- exhaustive switch; default is unreachable */ default:
33660
+ assertNever$2(instruction);
33661
+ }
33662
+ });
33663
+ return summary;
33664
+ }
33665
+ /**
33666
+ * Vault/claim instruction methods each declared action may decode to. The
33667
+ * mapping is many-to-one: a full withdrawal decodes to `redeem`, and any action
33668
+ * may carry an auxiliary `takeFeeERC20` alongside its primary instruction.
33669
+ */ const ACTION_ALLOWED_METHODS = {
33670
+ deposit: new Set([
33671
+ 'deposit'
33672
+ ]),
33673
+ withdraw: new Set([
33674
+ 'withdraw',
33675
+ 'redeem'
33676
+ ]),
33677
+ claimRewards: new Set([
33678
+ 'claim'
33679
+ ])
33680
+ };
33681
+ /**
33682
+ * Fail fast when the caller-declared `action` disagrees with the decoded
33683
+ * instructions, so the preview's headline can never mislabel what is signed
33684
+ * (e.g. a `deposit`-labeled call handed withdraw params). `takeFeeERC20` is an
33685
+ * auxiliary Circle-fee instruction and is allowed alongside any action.
33686
+ */ function assertActionMatchesInstructions(action, instructions) {
33687
+ const allowed = ACTION_ALLOWED_METHODS[action];
33688
+ instructions.forEach((instruction, index)=>{
33689
+ if (instruction.method === 'takeFeeERC20') {
33690
+ return;
33691
+ }
33692
+ if (!allowed.has(instruction.method)) {
33693
+ throw decodeMismatchError(`decoded instruction method '${instruction.method}' does not match the '${action}' action`, {
33694
+ action,
33695
+ method: instruction.method,
33696
+ index
33697
+ });
33698
+ }
33699
+ });
33700
+ }
33701
+ /**
33702
+ * Decode a same-chain earn `execute()` bundle into a human-readable summary.
33703
+ *
33704
+ * Decodes every inner instruction in the service-signed `executionParams` — the
33705
+ * same object the SDK ABI-encodes into the transaction — so the returned decode
33706
+ * is a faithful, drift-free view of what the signer is authorizing: input token
33707
+ * and amount, target vault, receiver, any Circle fee, and claimed rewards. Fails
33708
+ * fast with a non-recoverable {@link KitError} if any instruction cannot be
33709
+ * decoded, rather than returning misleading data.
33710
+ *
33711
+ * @param input - Action, chain, adapter, and the signed execution params.
33712
+ * @returns The decoded transaction summary.
33713
+ * @throws {@link KitError} If an instruction's calldata cannot be decoded.
33714
+ *
33715
+ * @example
33716
+ * ```typescript
33717
+ * const decoded = decodeEarnExecute({
33718
+ * action: 'deposit',
33719
+ * chain: 'Arc_Testnet',
33720
+ * adapter: '0x7fb8c7260b63934d8da38af902f87ae6e284a845',
33721
+ * executionParams,
33722
+ * })
33723
+ * // decoded.summary -> { token: { address, amount }, vault, receiver }
33724
+ * ```
33725
+ *
33726
+ * @internal
33727
+ */ function decodeEarnExecute(input) {
33728
+ const { action, chain, adapter, executionParams } = input;
33729
+ const envelope = requireInstructions(executionParams);
33730
+ const instructions = envelope.map((instruction, index)=>decodeInstruction(instruction, index));
33731
+ assertActionMatchesInstructions(action, instructions);
33732
+ return {
33733
+ action,
33734
+ chain,
33735
+ adapter,
33736
+ instructions,
33737
+ summary: buildSummary(instructions, envelope)
33738
+ };
33739
+ }
33740
+ /**
33741
+ * Assert that ABI-encoded Adapter `execute()` calldata encodes the same
33742
+ * instruction set as the service-signed execution params.
33743
+ *
33744
+ * Fail-fast preview check: the SDK encodes `execute(executeParams, ...)` locally,
33745
+ * so decoding those final bytes and comparing every field of each instruction
33746
+ * against the signed params proves the previewed instruction set matches what
33747
+ * will be signed. It compares `instructions[]` only — the outer `tokens`,
33748
+ * `execId`, `deadline`, and `metadata` are not re-compared here. The
33749
+ * authoritative integrity guarantee for the full signed struct is the on-chain
33750
+ * EIP-712 signature verification, which reverts if any signed field is altered.
33751
+ *
33752
+ * @param calldata - Encoded `execute()` calldata about to be signed.
33753
+ * @param executionParams - Service-signed execution params.
33754
+ * @throws {@link KitError} If the calldata is not an `execute()` call or any
33755
+ * instruction field differs from the signed params.
33756
+ *
33757
+ * @example
33758
+ * ```typescript
33759
+ * assertEarnCalldataMatchesExecuteParams(
33760
+ * prepared.getCallData().data,
33761
+ * executionParams,
33762
+ * )
33763
+ * ```
33764
+ *
33765
+ * @internal
33766
+ */ function assertEarnCalldataMatchesExecuteParams(calldata, executionParams) {
33767
+ // adapterContractAbi declares only `execute`, so a successful decode is always
33768
+ // the execute() call; a non-execute selector throws inside
33769
+ // decodeExecuteCalldata above.
33770
+ const decoded = decodeExecuteCalldata(calldata);
33771
+ const encoded = decoded.args[0].instructions;
33772
+ const signed = requireInstructions(executionParams);
33773
+ // Compare each encoded instruction against its signed counterpart. Iterating
33774
+ // the encoded instructions and indexing the signed set keeps both mismatch
33775
+ // branches reachable: a signed set that is too short trips the guard below,
33776
+ // and one that is too long trips the post-loop check.
33777
+ encoded.forEach((instruction, index)=>{
33778
+ const signedInstruction = signed[index];
33779
+ if (signedInstruction === undefined) {
33780
+ throw decodeMismatchError(`signed params are missing instruction ${index.toString()}`, {
33781
+ index,
33782
+ encoded: encoded.length,
33783
+ signed: signed.length
33784
+ });
33785
+ }
33786
+ const path = `instructions[${index.toString()}]`;
33787
+ assertHexEqual(instruction.target, signedInstruction['target'], `${path}.target`);
33788
+ assertHexEqual(instruction.data, signedInstruction['data'], `${path}.data`);
33789
+ assertUintEqual(instruction.value, signedInstruction['value'], `${path}.value`);
33790
+ assertHexEqual(instruction.tokenIn, signedInstruction['tokenIn'], `${path}.tokenIn`);
33791
+ assertUintEqual(instruction.amountToApprove, signedInstruction['amountToApprove'], `${path}.amountToApprove`);
33792
+ assertHexEqual(instruction.tokenOut, signedInstruction['tokenOut'], `${path}.tokenOut`);
33793
+ assertUintEqual(instruction.minTokenOut, signedInstruction['minTokenOut'], `${path}.minTokenOut`);
33794
+ });
33795
+ if (signed.length > encoded.length) {
33796
+ throw decodeMismatchError('signed params contain more instructions than the encoded calldata', {
33797
+ encoded: encoded.length,
33798
+ signed: signed.length
33799
+ });
33800
+ }
33801
+ }
33802
+ /**
33803
+ * Assert two hex values are equal, case-insensitively (addresses and calldata).
33804
+ */ function assertHexEqual(encoded, signed, path) {
33805
+ const signedHex = requireHex(signed, path);
33806
+ if (encoded.toLowerCase() !== signedHex.toLowerCase()) {
33807
+ throw decodeMismatchError(`${path} differs from signed params`, {
33808
+ path,
33809
+ encoded,
33810
+ signed: signedHex
33811
+ });
33812
+ }
33813
+ }
33814
+ /**
33815
+ * Assert an encoded bigint equals a signed `uint256`-like value.
33816
+ */ function assertUintEqual(encoded, signed, path) {
33817
+ const signedUint = requireUint(signed, path);
33818
+ if (encoded !== signedUint) {
33819
+ throw decodeMismatchError(`${path} differs from signed params`, {
33820
+ path,
33821
+ encoded: encoded.toString(),
33822
+ signed: signedUint.toString()
33823
+ });
33824
+ }
33825
+ }
33826
+
33827
+ /**
33828
+ * Namespaced discriminator for the EarnKit authorization review.
33829
+ *
33830
+ * Applications match on this in an adapter `onBeforeAuthorize` hook to decide
33831
+ * whether the request carries EarnKit semantic data. Prefer the
33832
+ * {@link isEarnExecuteReview} type guard over comparing this string directly.
33833
+ *
33834
+ * @example
33835
+ * ```typescript
33836
+ * if (review?.kind === EARN_EXECUTE_REVIEW_KIND) { … }
33837
+ * ```
33838
+ */ const EARN_EXECUTE_REVIEW_KIND = 'earn.execute';
33839
+
33840
+ /**
33841
+ * Build a fail-closed {@link KitError} for a review-construction failure.
33842
+ *
33843
+ * Marked non-recoverable: a review that cannot prove the calldata matches the
33844
+ * signed operation must abort authorization, never retry with misleading data.
33845
+ */ function reviewError(message, trace) {
33846
+ return failClosedEarnError('Unable to build earn authorization review', message, trace);
33847
+ }
33848
+ /**
33849
+ * Narrow the canonical authorization payload to the single Adapter `execute()`
33850
+ * call a same-chain earn operation authorizes.
33851
+ *
33852
+ * Same-chain deposit, withdraw, and claim-rewards each authorize exactly one
33853
+ * `evm-calls` payload carrying one call. Anything else (typed data, a batch,
33854
+ * an empty call list) means this descriptor was attached to the wrong
33855
+ * authorization unit, so fail closed rather than decode misleading data.
33856
+ */ function assertSingleEvmCallPayload(payload) {
33857
+ if (payload.type !== 'evm-calls') {
33858
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
33859
+ type: payload.type
33860
+ });
33861
+ }
33862
+ const [call, ...rest] = payload.calls;
33863
+ if (call === undefined) {
33864
+ throw reviewError('the evm-calls payload contains no calls to review', {
33865
+ callCount: payload.calls.length
33866
+ });
33867
+ }
33868
+ if (rest.length > 0) {
33869
+ throw reviewError('a same-chain earn operation authorizes exactly one Adapter execute() call', {
33870
+ callCount: payload.calls.length
33871
+ });
33872
+ }
33873
+ return call;
33874
+ }
33875
+ /**
33876
+ * Select the final earn `execute()` call from an atomic Earn batch.
33877
+ *
33878
+ * Same-chain batched deposit/withdraw authorizes either `[execute]` when the
33879
+ * current allowance is sufficient, or `[approve, execute]` when a top-up is
33880
+ * required. Any other shape means the descriptor was attached to an
33881
+ * unexpected authorization unit, so fail closed.
33882
+ */ function assertBatchedEarnExecuteCall(payload) {
33883
+ if (payload.type !== 'evm-calls') {
33884
+ throw reviewError(`expected an 'evm-calls' payload but received '${payload.type}'`, {
33885
+ type: payload.type
33886
+ });
33887
+ }
33888
+ if (payload.calls.length !== 1 && payload.calls.length !== 2) {
33889
+ throw reviewError('a batched earn operation authorizes [execute] or [approve, execute]', {
33890
+ callCount: payload.calls.length
33891
+ });
33892
+ }
33893
+ const executeCall = payload.calls.at(-1);
33894
+ if (executeCall === undefined) {
33895
+ throw reviewError('the earn batch contains no execute call to review', {
33896
+ callCount: payload.calls.length
33897
+ });
33898
+ }
33899
+ return executeCall;
33900
+ }
33901
+ /**
33902
+ * Map a canonical {@link EvmCall} to the {@link EarnEncodedTransaction} preview
33903
+ * shape, failing closed when the earn `execute()` call carries no calldata.
33904
+ */ function toEarnEncodedTransaction(call) {
33905
+ if (call.data === undefined) {
33906
+ throw reviewError('the earn execute() call is missing calldata', {
33907
+ to: call.to
33908
+ });
33909
+ }
33910
+ return {
33911
+ to: call.to,
33912
+ data: call.data,
33913
+ ...call.value !== undefined && {
33914
+ value: call.value
33915
+ }
33916
+ };
33917
+ }
33918
+ /**
33919
+ * Create an Earn authorization descriptor using the supplied canonical-payload
33920
+ * call selector.
33921
+ *
33922
+ * @param input - The action, chain, and service-signed execution params.
33923
+ * @param selectCall - Fail-closed selector for the execute call under review.
33924
+ * @returns A lazy descriptor that decodes and verifies the selected call.
33925
+ *
33926
+ * @internal
33927
+ */ function createEarnExecuteDescriptor(input, selectCall) {
33928
+ const { action, chain, executionParams } = input;
33929
+ const createReview = (payload)=>{
33930
+ const call = selectCall(payload);
33931
+ const encoded = toEarnEncodedTransaction(call);
33932
+ const decoded = decodeEarnExecute({
33933
+ action,
33934
+ chain,
33935
+ adapter: encoded.to,
33936
+ executionParams
33937
+ });
33938
+ // Prove the calldata about to be signed encodes the same instruction set as
33939
+ // the service-signed params. Throwing here aborts before the wallet prompt.
33940
+ assertEarnCalldataMatchesExecuteParams(encoded.data, executionParams);
33941
+ const review = {
33942
+ kind: EARN_EXECUTE_REVIEW_KIND,
33943
+ data: {
33944
+ encoded,
33945
+ decoded
33946
+ }
33947
+ };
33948
+ return review;
33949
+ };
33950
+ return {
33951
+ createReview
33952
+ };
33953
+ }
33954
+ /**
33955
+ * Build the lazy `earn.execute` authorization descriptor for a same-chain earn
33956
+ * action.
33957
+ *
33958
+ * The returned descriptor carries only a `createReview` factory — no intent
33959
+ * override, because the adapter's action system supplies the intent from the
33960
+ * action key. The factory is evaluated at most once, and only when the
33961
+ * application configured an adapter `onBeforeAuthorize` hook. When it runs it:
33962
+ *
33963
+ * 1. narrows the canonical payload to its single Adapter `execute()` call;
33964
+ * 2. lifts that call into an {@link EarnEncodedTransaction};
33965
+ * 3. decodes it into a `DecodedEarnTx`; and
33966
+ * 4. asserts the decoded calldata matches the service-signed params, throwing
33967
+ * (aborting authorization before the wallet or signer) on any mismatch.
33968
+ *
33969
+ * @param input - The action, chain, and service-signed execution params.
33970
+ * @returns An authorization descriptor to pass as the fourth `prepareAction`
33971
+ * argument for the final earn action only (never the allowance approval).
33972
+ * @throws {@link KitError} From the review factory when the payload is not a
33973
+ * single earn `execute()` call or the calldata diverges from the signed
33974
+ * params. The throw surfaces through the adapter gate before authorization.
33975
+ *
33976
+ * @example
33977
+ * ```typescript
33978
+ * const descriptor = buildEarnExecuteDescriptor({
33979
+ * action: 'deposit',
33980
+ * chain: 'Arc_Testnet',
33981
+ * executionParams,
33982
+ * })
33983
+ * await adapter.prepareAction('earn.deposit', actionParams, ctx, {
33984
+ * authorization: descriptor,
33985
+ * })
33986
+ * ```
33987
+ *
33988
+ * @internal
33989
+ */ function buildEarnExecuteDescriptor(input) {
33990
+ return createEarnExecuteDescriptor(input, assertSingleEvmCallPayload);
33991
+ }
33992
+ /**
33993
+ * Build a lazy `earn.execute` authorization descriptor for an atomic Earn
33994
+ * batch containing either `[execute]` or `[approve, execute]`.
33995
+ *
33996
+ * The review always decodes and verifies the final call against the
33997
+ * service-signed execution params. Unexpected payload types and call counts
33998
+ * fail closed before wallet authorization.
33999
+ *
34000
+ * @param input - The action, chain, and service-signed execution params.
34001
+ * @returns A descriptor suitable for `batchExecute` authorization options.
34002
+ * @throws {@link KitError} From the lazy review factory when the batch shape or
34003
+ * final execute calldata cannot be verified.
34004
+ *
34005
+ * @internal
34006
+ */ function buildBatchedEarnExecuteDescriptor(input) {
34007
+ return createEarnExecuteDescriptor(input, assertBatchedEarnExecuteCall);
34008
+ }
34009
+ /**
34010
+ * Type guard: narrow an adapter authorization review to an EarnKit
34011
+ * `earn.execute` review.
34012
+ *
34013
+ * Use this inside an adapter `onBeforeAuthorize` hook to detect whether the
34014
+ * request carries EarnKit semantic data before reading it, instead of
34015
+ * comparing `review.kind` by hand. Robust against plain-JavaScript callers:
34016
+ * accepts `unknown` and checks the shape at runtime.
34017
+ *
34018
+ * @param review - The `review` field from an `AuthorizationRequest`, or any
34019
+ * value.
34020
+ * @returns `true` when `review` is an `earn.execute` review with the expected
34021
+ * `{ encoded, decoded }` data shape.
34022
+ *
34023
+ * @example
34024
+ * ```typescript
34025
+ * onBeforeAuthorize: async ({ review }) => {
34026
+ * if (isEarnExecuteReview(review)) {
34027
+ * await showEarnConfirmation(review.data.decoded.summary)
34028
+ * }
34029
+ * return 'approve'
34030
+ * }
34031
+ * ```
34032
+ */ function isEarnExecuteReview(review) {
34033
+ if (typeof review !== 'object' || review === null) {
34034
+ return false;
34035
+ }
34036
+ const candidate = review;
34037
+ if (candidate.kind !== EARN_EXECUTE_REVIEW_KIND) {
34038
+ return false;
34039
+ }
34040
+ const data = candidate.data;
34041
+ if (typeof data !== 'object' || data === null) {
34042
+ return false;
34043
+ }
34044
+ const { encoded, decoded } = data;
34045
+ return typeof encoded === 'object' && encoded !== null && typeof decoded === 'object' && decoded !== null;
34046
+ }
34047
+
32737
34048
  /**
32738
34049
  * Prepare an earn adapter action, execute it, wait for confirmation, and
32739
34050
  * throw a structured revert error if the receipt status is `'reverted'`.
@@ -32760,16 +34071,37 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
32760
34071
  * address,
32761
34072
  * actionKey: 'earn.deposit',
32762
34073
  * actionParams: { executeParams, tokenInputs, signature },
34074
+ * action: 'deposit',
34075
+ * executionParams,
32763
34076
  * revertMessage: 'Earn deposit reverted on-chain',
32764
34077
  * })
32765
34078
  * ```
32766
34079
  *
32767
34080
  * @internal
32768
34081
  */ async function executeEarnAction(params) {
32769
- const { adapter, chain, address, actionKey, actionParams, revertMessage } = params;
34082
+ const { adapter, chain, address, actionKey, actionParams, action, executionParams, revertMessage } = params;
34083
+ // Attach the lazy `earn.execute` review to the final earn action only (never
34084
+ // the allowance approval, which runs on a separate path). The adapter's
34085
+ // action system supplies the intent from `actionKey`, so the descriptor
34086
+ // carries only the review factory. The factory is evaluated at most once,
34087
+ // and only when the application configured an `onBeforeAuthorize` hook.
34088
+ const authorization = buildEarnExecuteDescriptor({
34089
+ action,
34090
+ // The provider validates the chain is Earn-supported in
34091
+ // `resolveAdapterContext` before reaching execute, so the concrete chain
34092
+ // identifier is a valid `EarnChainIdentifier`. It is carried through to the
34093
+ // decoded preview's display `chain` field only.
34094
+ chain: chain.chain,
34095
+ executionParams
34096
+ });
34097
+ // The abstract `Adapter.prepareAction` is 3-arg; the fourth authorization
34098
+ // argument lives on the `withLegacyCompat` wrapper that produced the concrete
34099
+ // adapter passed here. Narrow the single seam that threads the descriptor.
32770
34100
  const prepared = await adapter.prepareAction(actionKey, actionParams, {
32771
34101
  chain,
32772
34102
  address
34103
+ }, {
34104
+ authorization
32773
34105
  });
32774
34106
  const gasLimitOverride = await estimateBufferedGasLimit(prepared);
32775
34107
  const txHash = prepared.type === 'evm' && gasLimitOverride !== undefined ? await prepared.execute({
@@ -32794,6 +34126,278 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
32794
34126
  };
32795
34127
  }
32796
34128
 
34129
+ /**
34130
+ * Decide whether a same-chain earn action should be submitted as a single
34131
+ * atomic batch.
34132
+ *
34133
+ * Returns `true` only when the consumer has not opted out
34134
+ * (`batchTransactions !== false`), the source chain is EVM, the adapter
34135
+ * structurally exposes the shared batch methods, and the wallet reports atomic
34136
+ * batch support. `address` is forwarded as `fromAddress` so developer-controlled
34137
+ * adapters can probe the specific wallet. Any thrown capability probe is
34138
+ * treated as "no support".
34139
+ *
34140
+ * @param params - Adapter, chain, address, and the resolved `batchTransactions` flag.
34141
+ * @returns `true` when batched execution should be attempted.
34142
+ *
34143
+ * @example
34144
+ * ```typescript
34145
+ * if (await shouldUseBatchedEarnAction({ adapter, chain, address, batchTransactions })) {
34146
+ * // take the batched approve + execute path
34147
+ * }
34148
+ * ```
34149
+ *
34150
+ * @internal
34151
+ */ async function shouldUseBatchedEarnAction(params) {
34152
+ const { adapter, chain, address, batchTransactions } = params;
34153
+ if (batchTransactions === false) {
34154
+ return false;
34155
+ }
34156
+ if (chain.type !== 'evm') {
34157
+ return false;
34158
+ }
34159
+ const candidate = adapter;
34160
+ if (typeof candidate.supportsAtomicBatch !== 'function' || typeof candidate.batchExecute !== 'function') {
34161
+ return false;
34162
+ }
34163
+ try {
34164
+ return await candidate.supportsAtomicBatch(chain, {
34165
+ fromAddress: address
34166
+ });
34167
+ } catch {
34168
+ return false;
34169
+ }
34170
+ }
34171
+ async function buildSuccessfulBatchResult(adapter, chain, receipt, batchId, revertMessage) {
34172
+ const transaction = {
34173
+ txHash: receipt.txHash,
34174
+ explorerUrl: buildExplorerUrl(chain, receipt.txHash)
34175
+ };
34176
+ let confirmed;
34177
+ try {
34178
+ confirmed = await adapter.waitForTransaction(receipt.txHash, {
34179
+ confirmations: 1
34180
+ }, chain);
34181
+ } catch {
34182
+ // The batch adapter already confirmed success. Receipt enrichment is
34183
+ // telemetry-only, so an additional RPC failure must not turn an accepted
34184
+ // money-moving operation into a retryable business failure.
34185
+ return transaction;
34186
+ }
34187
+ if (confirmed.status === 'reverted') {
34188
+ throw createTransactionRevertedError(chain.name, revertMessage, {
34189
+ batchId
34190
+ }, receipt.txHash, transaction.explorerUrl);
34191
+ }
34192
+ return {
34193
+ ...transaction,
34194
+ ...confirmed.gasUsed !== undefined && {
34195
+ gasUsed: confirmed.gasUsed
34196
+ },
34197
+ ...confirmed.effectiveGasPrice !== undefined && {
34198
+ effectiveGasPrice: confirmed.effectiveGasPrice
34199
+ }
34200
+ };
34201
+ }
34202
+ function throwBatchFailure(result, executeReceipt, chain, actionKey, revertMessage) {
34203
+ const cause = result.error;
34204
+ if (result.statusCode === 400) {
34205
+ throw new KitError({
34206
+ ...RpcError.ENDPOINT_ERROR,
34207
+ recoverability: 'RETRYABLE',
34208
+ message: `Batched earn ${actionKey} failed off-chain before inclusion (batch ${result.batchId}).`,
34209
+ cause: {
34210
+ trace: {
34211
+ batchId: result.batchId,
34212
+ statusCode: result.statusCode,
34213
+ cause
34214
+ }
34215
+ }
34216
+ });
34217
+ }
34218
+ const causeTrace = cause instanceof KitError && typeof cause.cause?.trace === 'object' && cause.cause.trace !== null ? cause.cause.trace : undefined;
34219
+ if (cause instanceof KitError && causeTrace?.['kind'] === 'failed_offchain') {
34220
+ throw cause;
34221
+ }
34222
+ 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 !== '';
34223
+ if (isConfirmedRevert) {
34224
+ throw createTransactionRevertedError(chain.name, revertMessage, {
34225
+ batchId: result.batchId,
34226
+ error: cause
34227
+ });
34228
+ }
34229
+ throw new KitError({
34230
+ ...NetworkError.TIMEOUT,
34231
+ recoverability: 'FATAL',
34232
+ message: `Batched earn ${actionKey} was submitted (batch ${result.batchId}) but its outcome could not be confirmed; check the transaction status before retrying.`,
34233
+ cause: {
34234
+ trace: {
34235
+ batchId: result.batchId,
34236
+ cause
34237
+ }
34238
+ }
34239
+ });
34240
+ }
34241
+ /**
34242
+ * Execute the `approve` and `execute` steps of a same-chain earn action as a
34243
+ * single atomic batch.
34244
+ *
34245
+ * Prepare both `PreparedChainRequest` objects upfront, extract their raw call
34246
+ * data via `getCallData()`, then submit both through the adapter's shared
34247
+ * `batchExecute`. `address` is forwarded as `opts.fromAddress` so
34248
+ * developer-controlled adapters batch on behalf of the right wallet;
34249
+ * `idempotencyKey` is forwarded for adapters that deduplicate ambiguous
34250
+ * submissions (the Circle developer-controlled adapter reuses the Earn
34251
+ * execution id); other adapters may ignore either option. Reused by both the
34252
+ * deposit and withdraw flows via the `actionKey` parameter.
34253
+ *
34254
+ * @param params - Adapter, chain, action key, signed payload, and approval inputs.
34255
+ * @returns The confirmed execute transaction hash, explorer URL, and receipt
34256
+ * gas data when the adapter can retrieve it.
34257
+ * @throws {@link KitError} when the source chain is not EVM.
34258
+ * @throws {@link KitError} when calldata extraction (`getCallData`) is not
34259
+ * supported by the prepared requests.
34260
+ * @throws {@link KitError} when the batch reverts on-chain (a confirmed
34261
+ * terminal revert), carrying `batchId`.
34262
+ * @throws {@link KitError} RETRYABLE when EIP-5792 reports an off-chain
34263
+ * failure carrying `batchId` and status code `400`; no call was included.
34264
+ * @throws {@link KitError} FATAL `NetworkError.TIMEOUT` when the batch was
34265
+ * submitted but its outcome could not be confirmed (poll timeout or any
34266
+ * other non-revert post-submission failure); carries `batchId` so the caller
34267
+ * can check transaction status before retrying.
34268
+ * @remarks
34269
+ * Once the batch has been submitted this function does not fall back to the
34270
+ * sequential path — the batch is already on its way, so a fallback would risk
34271
+ * double-spend. Post-submission failures surface through the adapter's batch
34272
+ * result: a confirmed on-chain revert (Circle: a `TRANSACTION_REVERTED` cause;
34273
+ * Viem: status code `500`/`600`) is thrown as a revert error, status code `400`
34274
+ * is reported as a retryable off-chain failure, and any other unconfirmed
34275
+ * outcome is thrown as a FATAL timeout error carrying `batchId`.
34276
+ *
34277
+ * @example
34278
+ * ```typescript
34279
+ * const { txHash, explorerUrl } = await executeBatchedEarnAction({
34280
+ * adapter,
34281
+ * chain,
34282
+ * address,
34283
+ * actionKey: 'earn.deposit',
34284
+ * executeParams,
34285
+ * tokenInputs,
34286
+ * signature,
34287
+ * approvalToken: usdcAddress,
34288
+ * delegate: adapterContractAddress,
34289
+ * requiredAllowance: 1_000_000n,
34290
+ * idempotencyKey: '550e8400-e29b-41d4-a716-446655440000',
34291
+ * revertMessage: 'Earn deposit reverted on-chain',
34292
+ * })
34293
+ * ```
34294
+ *
34295
+ * @internal
34296
+ */ async function executeBatchedEarnAction(params) {
34297
+ const { adapter, chain, address, actionKey, executeParams, tokenInputs, signature, approvalToken, delegate, requiredAllowance, idempotencyKey, revertMessage } = params;
34298
+ if (chain.type !== 'evm') {
34299
+ throw new KitError({
34300
+ ...InputError.INVALID_CHAIN,
34301
+ recoverability: 'FATAL',
34302
+ message: 'Batched earn execution is only supported on EVM chains.'
34303
+ });
34304
+ }
34305
+ const evmChain = chain;
34306
+ const batchAdapter = adapter;
34307
+ // Read the current allowance so the approval tops up only the missing amount.
34308
+ // When the existing allowance already covers the payload, skip the approve
34309
+ // call and batch only the execute — this mirrors the sequential
34310
+ // approveAllowanceIfNeeded guard and avoids an increaseAllowance underflow
34311
+ // (requiredAllowance - currentAllowance would be negative, which reverts as
34312
+ // an out-of-range uint256).
34313
+ const allowancePrepared = await adapter.prepareAction('token.allowance', {
34314
+ tokenAddress: approvalToken,
34315
+ delegate
34316
+ }, {
34317
+ chain,
34318
+ address
34319
+ });
34320
+ const currentAllowance = parseAllowanceResponse(await allowancePrepared.execute());
34321
+ const approvalNeeded = currentAllowance < requiredAllowance;
34322
+ const executePrepared = await adapter.prepareAction(actionKey, {
34323
+ executeParams,
34324
+ tokenInputs,
34325
+ signature
34326
+ }, {
34327
+ chain,
34328
+ address
34329
+ });
34330
+ const approvePrepared = approvalNeeded ? await prepareApprovalAction({
34331
+ adapter,
34332
+ chain,
34333
+ address,
34334
+ tokenAddress: approvalToken,
34335
+ delegate,
34336
+ currentAllowance,
34337
+ requiredAllowance
34338
+ }) : undefined;
34339
+ if (executePrepared.type !== 'evm' || !executePrepared.getCallData) {
34340
+ throw new KitError({
34341
+ ...InputError.UNSUPPORTED_ACTION,
34342
+ recoverability: 'FATAL',
34343
+ message: 'Batched earn execution requires EVM prepared requests with getCallData() support.'
34344
+ });
34345
+ }
34346
+ if (approvePrepared !== undefined && (approvePrepared.type !== 'evm' || !approvePrepared.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
+ const executeCallData = executePrepared.getCallData();
34354
+ // Prepend the approve call only when an allowance top-up is required.
34355
+ const calls = approvePrepared?.type === 'evm' && approvePrepared.getCallData ? [
34356
+ approvePrepared.getCallData(),
34357
+ executeCallData
34358
+ ] : [
34359
+ executeCallData
34360
+ ];
34361
+ const authorization = buildBatchedEarnExecuteDescriptor({
34362
+ action: actionKey === 'earn.deposit' ? 'deposit' : 'withdraw',
34363
+ chain: evmChain.chain,
34364
+ executionParams: executeParams
34365
+ });
34366
+ const result = await batchAdapter.batchExecute(calls, evmChain, {
34367
+ fromAddress: address,
34368
+ idempotencyKey,
34369
+ atomicRequired: true,
34370
+ authorization
34371
+ });
34372
+ // Success fans one confirmed hash across every receipt; the execute call is
34373
+ // the last one (approve, if present, precedes it). On failure a confirming
34374
+ // adapter returns no receipts, so a missing/non-success last receipt — or a
34375
+ // populated `result.error` — means the batch failed after submission (point
34376
+ // of no return). We never fall back, which would double-spend.
34377
+ const receiptCountMatches = result.receipts.length === calls.length;
34378
+ const executeReceipt = receiptCountMatches ? result.receipts[calls.length - 1] : undefined;
34379
+ const succeeded = receiptCountMatches && (result.statusCode === undefined || result.statusCode === 200) && result.error === undefined && executeReceipt?.status === 'success' && executeReceipt.txHash !== '';
34380
+ if (succeeded) {
34381
+ return buildSuccessfulBatchResult(adapter, evmChain, executeReceipt, result.batchId, revertMessage);
34382
+ }
34383
+ // Distinguish an off-chain rejection, a confirmed on-chain revert, and an
34384
+ // unknown outcome across both adapter conventions that share this contract:
34385
+ // - Circle SCA: no receipts + `error`; its trace kind identifies an
34386
+ // off-chain rejection, confirmed revert, or unconfirmed outcome.
34387
+ // - Viem EIP-5792: statusCode 500/600 explicitly confirms an on-chain
34388
+ // full/partial revert.
34389
+ // - Legacy/string-status wallets: a real-hash error receipt with no cause
34390
+ // is the best available confirmed-revert signal.
34391
+ // statusCode 400 is terminal but off-chain: the wallet confirms no call was
34392
+ // included, so it must not be labeled as a revert or unknown outcome.
34393
+ // Anything else — a poll timeout or any other post-submission failure with no
34394
+ // confirmed-revert signal — means the batch was submitted but its fate is
34395
+ // unconfirmed. Surface that as a FATAL (non-auto-retry) error carrying
34396
+ // `batchId` so the caller checks status before retrying, rather than
34397
+ // mislabeling it a revert.
34398
+ return throwBatchFailure(result, executeReceipt, evmChain, actionKey, revertMessage);
34399
+ }
34400
+
32797
34401
  /**
32798
34402
  * Validate that a service-signed execution payload has not expired before
32799
34403
  * the SDK asks the wallet to broadcast a transaction.
@@ -34090,7 +35694,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
34090
35694
  }
34091
35695
 
34092
35696
  var name$1 = "@circle-fin/provider-earn-service";
34093
- var version$1 = "1.3.1";
35697
+ var version$1 = "1.4.0";
34094
35698
  var pkg$1 = {
34095
35699
  name: name$1,
34096
35700
  version: version$1};
@@ -35728,6 +37332,46 @@ function finishElapsedWait(lastStatus, lastError) {
35728
37332
  const approvalToken = resolveEarnApprovalToken(executionParams);
35729
37333
  const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
35730
37334
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
37335
+ const approvalNeeded = !options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n;
37336
+ // Batch-capable wallets bundle approve + deposit into one atomic
37337
+ // submission. Only attempt this when an approval is actually needed.
37338
+ if (approvalNeeded && approvalToken !== undefined && await shouldUseBatchedEarnAction({
37339
+ adapter,
37340
+ chain,
37341
+ address,
37342
+ batchTransactions: config?.batchTransactions
37343
+ })) {
37344
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
37345
+ try {
37346
+ const result = await executeBatchedEarnAction({
37347
+ adapter,
37348
+ chain,
37349
+ address,
37350
+ actionKey: 'earn.deposit',
37351
+ executeParams: executionParams,
37352
+ tokenInputs,
37353
+ signature,
37354
+ approvalToken,
37355
+ delegate: adapterContractAddress,
37356
+ requiredAllowance,
37357
+ idempotencyKey: execId,
37358
+ revertMessage: 'Earn deposit reverted on-chain'
37359
+ });
37360
+ reportTransactionSuccess(transactionReportContext, 'Deposit', result);
37361
+ return result;
37362
+ } catch (error) {
37363
+ reportTransactionFailure(transactionReportContext, 'Deposit', error);
37364
+ throw error;
37365
+ }
37366
+ }, ({ txHash })=>txHash);
37367
+ return {
37368
+ kind: 'same-chain',
37369
+ txHash,
37370
+ explorerUrl,
37371
+ vaultAddress,
37372
+ amount: params.amount
37373
+ };
37374
+ }
35731
37375
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
35732
37376
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35733
37377
  try {
@@ -35760,6 +37404,8 @@ function finishElapsedWait(lastStatus, lastError) {
35760
37404
  tokenInputs,
35761
37405
  signature
35762
37406
  },
37407
+ action: 'deposit',
37408
+ executionParams,
35763
37409
  revertMessage: 'Earn deposit reverted on-chain'
35764
37410
  });
35765
37411
  reportTransactionSuccess(transactionReportContext, 'Deposit', result);
@@ -35891,6 +37537,44 @@ function finishElapsedWait(lastStatus, lastError) {
35891
37537
  const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
35892
37538
  const approvalToken = tokenInputs[0]?.token;
35893
37539
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
37540
+ // Batch-capable wallets bundle approve + withdraw into one atomic
37541
+ // submission. Only attempt this when an approval is actually needed.
37542
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n && await shouldUseBatchedEarnAction({
37543
+ adapter,
37544
+ chain,
37545
+ address,
37546
+ batchTransactions: config?.batchTransactions
37547
+ })) {
37548
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
37549
+ try {
37550
+ const result = await executeBatchedEarnAction({
37551
+ adapter,
37552
+ chain,
37553
+ address,
37554
+ actionKey: 'earn.withdraw',
37555
+ executeParams: executionParams,
37556
+ tokenInputs,
37557
+ signature,
37558
+ approvalToken,
37559
+ delegate: adapterContractAddress,
37560
+ requiredAllowance,
37561
+ idempotencyKey: execId,
37562
+ revertMessage: 'Earn withdraw reverted on-chain'
37563
+ });
37564
+ reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
37565
+ return result;
37566
+ } catch (error) {
37567
+ reportTransactionFailure(transactionReportContext, 'Withdraw', error);
37568
+ throw error;
37569
+ }
37570
+ }, ({ txHash })=>txHash);
37571
+ return {
37572
+ txHash,
37573
+ explorerUrl,
37574
+ vaultAddress,
37575
+ amount: params.amount
37576
+ };
37577
+ }
35894
37578
  if (!options.skipApprove && approvalToken !== undefined) {
35895
37579
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
35896
37580
  try {
@@ -35923,6 +37607,8 @@ function finishElapsedWait(lastStatus, lastError) {
35923
37607
  tokenInputs,
35924
37608
  signature
35925
37609
  },
37610
+ action: 'withdraw',
37611
+ executionParams,
35926
37612
  revertMessage: 'Earn withdraw reverted on-chain'
35927
37613
  });
35928
37614
  reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
@@ -36002,6 +37688,8 @@ function finishElapsedWait(lastStatus, lastError) {
36002
37688
  tokenInputs: [],
36003
37689
  signature
36004
37690
  },
37691
+ action: 'claimRewards',
37692
+ executionParams,
36005
37693
  revertMessage: 'Earn claim rewards reverted on-chain'
36006
37694
  }), ({ txHash })=>txHash);
36007
37695
  return {
@@ -36685,11 +38373,16 @@ const sourceAdapterContextSchema = z.object({
36685
38373
  *
36686
38374
  * Validate the optional Kit Key field using the standard `apiKeySchema`
36687
38375
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
36688
- * operates in permissionless mode.
38376
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
38377
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
38378
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
38379
+ * schema must accept them or a valid config object is rejected.
36689
38380
  *
36690
38381
  * @internal
36691
38382
  */ const earnConfigSchema = z.object({
36692
- kitKey: apiKeySchema.optional()
38383
+ kitKey: apiKeySchema.optional(),
38384
+ baseUrl: z.string().optional(),
38385
+ batchTransactions: z.boolean().optional()
36693
38386
  }).strict();
36694
38387
  /**
36695
38388
  * Canonical decimal form: a leading digit with no leading zeros (a single
@@ -39866,7 +41559,7 @@ async function deposit$2(context, params) {
39866
41559
  }
39867
41560
 
39868
41561
  var name = "@circle-fin/unified-balance-kit";
39869
- var version = "1.3.1";
41562
+ var version = "1.4.0";
39870
41563
  var pkg = {
39871
41564
  name: name,
39872
41565
  version: version};
@@ -41207,7 +42900,8 @@ function throwNetworkMismatch(expected, actual) {
41207
42900
  };
41208
42901
  }
41209
42902
  /**
41210
- * Group intents by adapter and chain for signing (Solana one-per-intent, EVM batched by adapter).
42903
+ * Group intents for signing (Solana one-per-intent, EVM batched by adapter
42904
+ * with every source chain retained by Gateway domain).
41211
42905
  *
41212
42906
  * @param intents - Burn intents from estimate response.
41213
42907
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -42584,22 +44278,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
42584
44278
  *
42585
44279
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
42586
44280
  *
44281
+ * Sets flagged `contractSigner` carry `contractSigner: true`, which tells
44282
+ * Gateway to validate the signature with ERC-1271 (an offchain
44283
+ * `isValidSignature` simulation) instead of `ecrecover`. The flag is
44284
+ * omitted for EOA signers so their payloads stay byte-identical.
44285
+ *
42587
44286
  * @param signedSets - Signed intent sets (intents + signature per signer).
42588
44287
  * @returns Array of transfer payloads for POST /v1/transfer.
42589
44288
  */ function buildTransferRequestBody(signedSets) {
42590
44289
  return signedSets.map((set)=>{
42591
44290
  const firstIntent = set.intents[0];
44291
+ const contractSigner = set.contractSigner === true ? {
44292
+ contractSigner: true
44293
+ } : {};
42592
44294
  if (set.intents.length === 1 && firstIntent) {
42593
44295
  return {
42594
44296
  burnIntent: serializeBurnIntent(firstIntent),
42595
- signature: set.signature
44297
+ signature: set.signature,
44298
+ ...contractSigner
42596
44299
  };
42597
44300
  }
42598
44301
  return {
42599
44302
  burnIntentSet: {
42600
44303
  intents: set.intents.map(serializeBurnIntent)
42601
44304
  },
42602
- signature: set.signature
44305
+ signature: set.signature,
44306
+ ...contractSigner
42603
44307
  };
42604
44308
  });
42605
44309
  }
@@ -42962,11 +44666,16 @@ const BPS_DIVISOR = 100_000n;
42962
44666
  return required;
42963
44667
  }
42964
44668
 
44669
+ function requireEvmChainsByDomain(group) {
44670
+ if (group.chainsByDomain !== undefined) return group.chainsByDomain;
44671
+ throw createValidationFailedError$1('adapterGroup.chainsByDomain', group.chainsByDomain, 'must be provided for an EVM adapter group');
44672
+ }
42965
44673
  /**
42966
- * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
44674
+ * Sign each adapter group: Solana one intent per signature, and EVM either
44675
+ * batched for EOAs or split by source chain for ERC-1271 signers.
42967
44676
  *
42968
44677
  * @param adapterGroups - Groups from groupIntentsByAdapter.
42969
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
44678
+ * @returns Promise of signed sets for buildTransferRequestBody.
42970
44679
  *
42971
44680
  * @example
42972
44681
  * ```typescript
@@ -42979,9 +44688,10 @@ const BPS_DIVISOR = 100_000n;
42979
44688
  if (group.chain.type === 'solana') {
42980
44689
  return signSolanaIntentGroup(group);
42981
44690
  }
42982
- return [
42983
- await signEvmIntentGroup(group)
42984
- ];
44691
+ return await signEvmIntentGroup({
44692
+ ...group,
44693
+ chainsByDomain: requireEvmChainsByDomain(group)
44694
+ });
42985
44695
  }));
42986
44696
  return nested.flat();
42987
44697
  }
@@ -43631,7 +45341,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
43631
45341
  signedSetCount: signedSets.length,
43632
45342
  signatures: signedSets.map((s)=>({
43633
45343
  intentCount: s.intents.length,
43634
- signature: s.signature
45344
+ signature: s.signature,
45345
+ contractSigner: s.contractSigner === true
43635
45346
  }))
43636
45347
  }
43637
45348
  });
@@ -47257,6 +48968,33 @@ registerKit(`${pkg.name}/${pkg.version}`);
47257
48968
  }
47258
48969
  }
47259
48970
 
48971
+ const APP_KIT_CUSTOM_FEE_POLICY_KEYS = new Set([
48972
+ 'bridge',
48973
+ 'swap',
48974
+ 'unifiedBalance'
48975
+ ]);
48976
+ function assertAppKitCustomFeePolicy(policy) {
48977
+ if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) {
48978
+ throw createValidationFailedError$1('policy', policy, 'AppKit custom fee policy must be an object');
48979
+ }
48980
+ for (const key of Object.keys(policy)){
48981
+ if (!APP_KIT_CUSTOM_FEE_POLICY_KEYS.has(key)) {
48982
+ throw createValidationFailedError$1(`policy.${key}`, key, 'AppKit custom fee policy only supports bridge, swap, and unifiedBalance');
48983
+ }
48984
+ }
48985
+ const candidate = policy;
48986
+ if (candidate.bridge !== undefined) {
48987
+ assertCustomFeePolicy$2(candidate.bridge);
48988
+ }
48989
+ if (candidate.swap !== undefined) {
48990
+ assertCustomFeePolicy$1(candidate.swap);
48991
+ }
48992
+ }
48993
+ function assertAppKitCustomFeePolicyScope(operation) {
48994
+ if (typeof operation !== 'string' || !APP_KIT_CUSTOM_FEE_POLICY_KEYS.has(operation)) {
48995
+ throw createValidationFailedError$1('operation', operation, 'AppKit custom fee policy operation must be bridge, swap, or unifiedBalance');
48996
+ }
48997
+ }
47260
48998
  /**
47261
48999
  * A high-level SDK for stablecoin operations, including bridging, swapping, and earn.
47262
49000
  *
@@ -47379,13 +49117,10 @@ registerKit(`${pkg.name}/${pkg.version}`);
47379
49117
  * })
47380
49118
  * ```
47381
49119
  */ constructor(config = {}){
47382
- this.context = createContext({
47383
- ...config,
47384
- ...config.disableErrorReporting != null && {
47385
- disableErrorReporting: config.disableErrorReporting
47386
- }
47387
- });
47388
- this.unifiedBalance = new AppKitUnifiedBalance({
49120
+ if (config.customFeePolicy !== undefined) {
49121
+ assertAppKitCustomFeePolicy(config.customFeePolicy);
49122
+ }
49123
+ const unifiedBalance = new AppKitUnifiedBalance({
47389
49124
  ...config.unifiedBalance,
47390
49125
  ...config.disableErrorReporting != null && {
47391
49126
  disableErrorReporting: config.disableErrorReporting
@@ -47394,6 +49129,16 @@ registerKit(`${pkg.name}/${pkg.version}`);
47394
49129
  disableAnalytics: config.disableAnalytics
47395
49130
  }
47396
49131
  });
49132
+ if (config.customFeePolicy?.unifiedBalance != null) {
49133
+ unifiedBalance.setCustomFeePolicy(config.customFeePolicy.unifiedBalance);
49134
+ }
49135
+ this.context = createContext({
49136
+ ...config,
49137
+ ...config.disableErrorReporting != null && {
49138
+ disableErrorReporting: config.disableErrorReporting
49139
+ }
49140
+ });
49141
+ this.unifiedBalance = unifiedBalance;
47397
49142
  this.earn = {
47398
49143
  // A single implementation cannot satisfy the per-branch deposit
47399
49144
  // overloads, so assert the overloaded interface shape; the
@@ -47837,6 +49582,77 @@ registerKit(`${pkg.name}/${pkg.version}`);
47837
49582
  */ getSupportedChains(operationType) {
47838
49583
  return getSupportedChains$2(this.context, operationType, this.unifiedBalance);
47839
49584
  }
49585
+ /**
49586
+ * Set operation-scoped custom fee policies.
49587
+ *
49588
+ * Configure custom fees for only the operations that need them. Bridge and
49589
+ * swap policies are forwarded to the underlying kits when those operations
49590
+ * run. Unified balance policies are applied immediately to the namespaced
49591
+ * Unified Balance Kit.
49592
+ *
49593
+ * @param policy - Partial custom fee policy grouped by operation.
49594
+ * @returns Nothing.
49595
+ * @throws \{KitError\} If `policy` or a provided operation policy is invalid.
49596
+ *
49597
+ * @example
49598
+ * ```typescript
49599
+ * kit.setCustomFeePolicy({
49600
+ * bridge: {
49601
+ * computeFee: () => '1.00',
49602
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
49603
+ * },
49604
+ * })
49605
+ * ```
49606
+ */ setCustomFeePolicy(policy) {
49607
+ assertAppKitCustomFeePolicy(policy);
49608
+ if (policy.unifiedBalance != null) {
49609
+ this.unifiedBalance.setCustomFeePolicy(policy.unifiedBalance);
49610
+ }
49611
+ this.context.customFeePolicy = {
49612
+ ...this.context.customFeePolicy,
49613
+ ...policy
49614
+ };
49615
+ }
49616
+ /**
49617
+ * Remove an AppKit-level custom fee policy for one operation.
49618
+ *
49619
+ * Bridge and swap policies are removed from AppKit's persistent context so
49620
+ * future operations fall back to legacy fee hooks. Unified balance policies
49621
+ * are also removed from the namespaced Unified Balance Kit.
49622
+ *
49623
+ * @param operation - Operation whose custom fee policy should be removed.
49624
+ * @returns Nothing.
49625
+ * @throws \{KitError\} If `operation` is invalid.
49626
+ *
49627
+ * @example
49628
+ * ```typescript
49629
+ * kit.removeCustomFeePolicy('bridge')
49630
+ * ```
49631
+ */ removeCustomFeePolicy(operation) {
49632
+ assertAppKitCustomFeePolicyScope(operation);
49633
+ if (operation === 'unifiedBalance') {
49634
+ this.unifiedBalance.removeCustomFeePolicy();
49635
+ }
49636
+ if (this.context.customFeePolicy == null) {
49637
+ return;
49638
+ }
49639
+ const currentPolicy = this.context.customFeePolicy;
49640
+ const nextPolicy = {};
49641
+ if (operation !== 'bridge' && currentPolicy.bridge !== undefined) {
49642
+ nextPolicy.bridge = currentPolicy.bridge;
49643
+ }
49644
+ if (operation !== 'swap' && currentPolicy.swap !== undefined) {
49645
+ nextPolicy.swap = currentPolicy.swap;
49646
+ }
49647
+ if (operation !== 'unifiedBalance' && currentPolicy.unifiedBalance !== undefined) {
49648
+ nextPolicy.unifiedBalance = currentPolicy.unifiedBalance;
49649
+ }
49650
+ if (Object.keys(nextPolicy).length === 0) {
49651
+ delete this.context.customFeePolicy;
49652
+ return;
49653
+ }
49654
+ this.context.customFeePolicy = nextPolicy;
49655
+ }
47840
49656
  on(actionOrWildCard, handler) {
47841
49657
  const action = actionOrWildCard;
47842
49658
  const typedHandler = handler;
@@ -47891,5 +49707,5 @@ registerKit(`${pkg.name}/${pkg.version}`);
47891
49707
  }
47892
49708
  }
47893
49709
 
47894
- export { AppKit, BalanceError, Blockchain, BridgeChain, EarnChain, EarnError, EarnKit, InputError, KitError, NetworkError, OnchainError, RateLimitError, RpcError, ServiceError, SwapChain, TOKEN_ALIASES, TransferSpeed, UnifiedBalanceChain, anyDepositParamsSchema, claimRewardsParamsSchema, createEarnKitContext, depositParamsSchema$1 as depositParamsSchema, claimRewards$1 as earnClaimRewards, deposit$3 as earnDeposit, exploreVaults$1 as earnExploreVaults, exploreVaultsIterator$1 as earnExploreVaultsIterator, getClaimRewardsQuote$1 as earnGetClaimRewardsQuote, getCrossChainDepositStatus$1 as earnGetCrossChainDepositStatus, getCrossChainDepositStatusOutcome as earnGetCrossChainDepositStatusOutcome, getDepositQuote$1 as earnGetDepositQuote, getPosition$1 as earnGetPosition, getSupportedChains$3 as earnGetSupportedChains, getVaults$1 as earnGetVaults, getWithdrawalQuote$1 as earnGetWithdrawalQuote, waitForCrossChainDeposit$1 as earnWaitForCrossChainDeposit, withdraw$1 as earnWithdraw, exploreVaultsIteratorParamsSchema, exploreVaultsParamsSchema, getClaimRewardsQuoteParamsSchema, getCrossChainDepositStatusParamsSchema, getDepositQuoteParamsSchema, getErrorCode, getErrorMessage, getPositionParamsSchema, getTokenDecimals, getVaultsParamsSchema, getWithdrawalQuoteParamsSchema, isBalanceError, isFatalError, isInputError, isKitError, isNetworkError, isOnchainError, isRateLimitError, isRetryableError$1 as isRetryableError, isRpcError, isServiceError, isTerminalCrossChainDepositStatus as isTerminalEarnCrossChainDepositStatus, isTokenAddress, isTokenAlias, isUserCancellationError, setExternalPrefix, validateToken, waitForCrossChainDepositParamsSchema, withdrawParamsSchema };
49710
+ export { AppKit, BalanceError, Blockchain, BridgeChain, EARN_EXECUTE_REVIEW_KIND, EarnChain, EarnError, EarnKit, InputError, KitError, NetworkError, OnchainError, RateLimitError, RpcError, ServiceError, SwapChain, TOKEN_ALIASES, TransferSpeed, UnifiedBalanceChain, anyDepositParamsSchema, claimRewardsParamsSchema, createEarnKitContext, depositParamsSchema$1 as depositParamsSchema, claimRewards$1 as earnClaimRewards, deposit$3 as earnDeposit, exploreVaults$1 as earnExploreVaults, exploreVaultsIterator$1 as earnExploreVaultsIterator, getClaimRewardsQuote$1 as earnGetClaimRewardsQuote, getCrossChainDepositStatus$1 as earnGetCrossChainDepositStatus, getCrossChainDepositStatusOutcome as earnGetCrossChainDepositStatusOutcome, getDepositQuote$1 as earnGetDepositQuote, getPosition$1 as earnGetPosition, getSupportedChains$3 as earnGetSupportedChains, getVaults$1 as earnGetVaults, getWithdrawalQuote$1 as earnGetWithdrawalQuote, waitForCrossChainDeposit$1 as earnWaitForCrossChainDeposit, withdraw$1 as earnWithdraw, exploreVaultsIteratorParamsSchema, exploreVaultsParamsSchema, getClaimRewardsQuoteParamsSchema, getCrossChainDepositStatusParamsSchema, getDepositQuoteParamsSchema, getErrorCode, getErrorMessage, getPositionParamsSchema, getTokenDecimals, getVaultsParamsSchema, getWithdrawalQuoteParamsSchema, isBalanceError, isEarnExecuteReview, isFatalError, isInputError, isKitError, isNetworkError, isOnchainError, isRateLimitError, isRetryableError$1 as isRetryableError, isRpcError, isServiceError, isTerminalCrossChainDepositStatus as isTerminalEarnCrossChainDepositStatus, isTokenAddress, isTokenAlias, isUserCancellationError, setExternalPrefix, validateToken, waitForCrossChainDepositParamsSchema, withdrawParamsSchema };
47895
49711
  //# sourceMappingURL=index.mjs.map