@circle-fin/app-kit 1.6.0 → 1.7.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
@@ -2301,7 +2301,8 @@ const DEFAULT_RETRYABLE_ERROR_CODES = [
2301
2301
  *
2302
2302
  * @remarks
2303
2303
  * Check order for KitError instances:
2304
- * 1. If `recoverability === 'RETRYABLE'`, return `true` immediately (priority check).
2304
+ * 1. If `recoverability === 'RETRYABLE'` or `recoverability === 'RESUMABLE'`,
2305
+ * return `true` immediately (priority check).
2305
2306
  * 2. Otherwise, check if `error.code` is in `DEFAULT_RETRYABLE_ERROR_CODES` (fallback check).
2306
2307
  * 3. Non-KitError instances always return `false`.
2307
2308
  *
@@ -2312,6 +2313,12 @@ const DEFAULT_RETRYABLE_ERROR_CODES = [
2312
2313
  * subsequent attempts, such as network timeouts or temporary service
2313
2314
  * unavailability. These errors are safe to retry after a delay.
2314
2315
  *
2316
+ * RESUMABLE errors indicate a multi-phase operation that completed some phases
2317
+ * before failing (for example, a token approval landed but the execution
2318
+ * transaction failed). They are also retryable — re-running the operation is
2319
+ * safe — but callers that have a kit-level `retry()` should prefer it so that
2320
+ * already-completed phases are skipped.
2321
+ *
2315
2322
  * @param error - Unknown error to check
2316
2323
  * @returns True if error is retryable
2317
2324
  *
@@ -2357,16 +2364,29 @@ const DEFAULT_RETRYABLE_ERROR_CODES = [
2357
2364
  * })
2358
2365
  * isRetryableError(error3) // false
2359
2366
  *
2367
+ * // KitError with RESUMABLE recoverability (partially-completed operation)
2368
+ * const error4 = new KitError({
2369
+ * code: 8101,
2370
+ * name: 'EARN_EXECUTION_FAILED',
2371
+ * type: 'SERVICE',
2372
+ * recoverability: 'RESUMABLE',
2373
+ * message: 'Execution failed after approval',
2374
+ * })
2375
+ * isRetryableError(error4) // true
2376
+ *
2360
2377
  * // Non-KitError
2361
- * const error4 = new Error('Standard error')
2362
- * isRetryableError(error4) // false
2378
+ * const error5 = new Error('Standard error')
2379
+ * isRetryableError(error5) // false
2363
2380
  * ```
2364
2381
  */
2365
2382
  function isRetryableError$1(error) {
2366
2383
  // Use proper type guard to check if it's a KitError
2367
2384
  if (isKitError(error)) {
2368
- // Priority check: explicit recoverability
2369
- if (error.recoverability === 'RETRYABLE') {
2385
+ // Priority check: explicit recoverability. RESUMABLE errors are a subset of
2386
+ // retryable errors re-running the operation is safe, but a kit-level
2387
+ // retry() can resume from the failed phase instead.
2388
+ if (error.recoverability === 'RETRYABLE' ||
2389
+ error.recoverability === 'RESUMABLE') {
2370
2390
  return true;
2371
2391
  }
2372
2392
  // Fallback check: error code against default retryable codes
@@ -11039,7 +11059,7 @@ function emitResultStepErrorTelemetry(failedStep, stepEventMap, fallbackEventTyp
11039
11059
  }
11040
11060
 
11041
11061
  var name$3 = "@circle-fin/bridge-kit";
11042
- var version$4 = "1.10.1";
11062
+ var version$4 = "1.10.2";
11043
11063
  var pkg$4 = {
11044
11064
  name: name$3,
11045
11065
  version: version$4};
@@ -11530,60 +11550,6 @@ const validateBalanceForTransaction = async (params) => {
11530
11550
  }
11531
11551
  };
11532
11552
 
11533
- /**
11534
- * Validate that the adapter has sufficient native token balance for transaction fees.
11535
- *
11536
- * This function checks if the adapter's current native token balance (ETH, SOL, etc.)
11537
- * is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
11538
- * if the balance is zero, indicating the wallet cannot pay for transaction fees.
11539
- *
11540
- * @param params - The validation parameters containing adapter and operation context.
11541
- * @returns A promise that resolves to void if validation passes.
11542
- * @throws {KitError} When the adapter's native balance is zero (code: 9002).
11543
- *
11544
- * @example
11545
- * ```typescript
11546
- * import { validateNativeBalanceForTransaction } from '@core/adapter'
11547
- * import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
11548
- * import { isKitError, ERROR_TYPES } from '@core/errors'
11549
- *
11550
- * const adapter = createViemAdapterFromPrivateKey({
11551
- * privateKey: '0x...',
11552
- * chain: 'Ethereum',
11553
- * })
11554
- *
11555
- * try {
11556
- * await validateNativeBalanceForTransaction({
11557
- * adapter,
11558
- * operationContext: { chain: 'Ethereum' },
11559
- * })
11560
- * console.log('Native balance validation passed')
11561
- * } catch (error) {
11562
- * if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
11563
- * console.error('Insufficient gas funds:', error.message)
11564
- * }
11565
- * }
11566
- * ```
11567
- */
11568
- const validateNativeBalanceForTransaction = async (params) => {
11569
- const { adapter, operationContext } = params;
11570
- const balancePrepared = await adapter.prepareAction('native.balanceOf', {
11571
- walletAddress: operationContext.address,
11572
- }, operationContext);
11573
- const balance = await balancePrepared.execute();
11574
- if (BigInt(balance) === 0n) {
11575
- const { chain } = operationContext;
11576
- const chainName = extractChainInfo(chain).name;
11577
- const nativeSymbol = typeof chain === 'object' && chain !== null && 'nativeCurrency' in chain
11578
- ? chain.nativeCurrency.symbol
11579
- : undefined;
11580
- throw createInsufficientGasError(chainName, nativeSymbol, {
11581
- balance: '0',
11582
- walletAddress: operationContext.address,
11583
- });
11584
- }
11585
- };
11586
-
11587
11553
  /**
11588
11554
  * Permit signature standards for gasless token approvals.
11589
11555
  *
@@ -16015,7 +15981,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain, statusCo
16015
15981
  return step;
16016
15982
  }
16017
15983
 
16018
- var version$3 = "1.8.2";
15984
+ var version$3 = "1.8.3";
16019
15985
  var pkg$3 = {
16020
15986
  version: version$3};
16021
15987
 
@@ -16833,7 +16799,7 @@ class CCTPV2BridgingProvider extends BridgingProvider {
16833
16799
  async bridge(params) {
16834
16800
  // CCTP-specific bridge params validation (includes base validation)
16835
16801
  assertCCTPv2BridgeParams(params);
16836
- const { source, destination, amount, token } = params;
16802
+ const { source, amount, token } = params;
16837
16803
  // Extract operation context from source wallet context for balance validation
16838
16804
  const sourceOperationContext = this.extractOperationContext(source);
16839
16805
  // Validate USDC balance for transaction on source chain
@@ -16844,24 +16810,6 @@ class CCTPV2BridgingProvider extends BridgingProvider {
16844
16810
  tokenAddress: source.chain.usdcAddress,
16845
16811
  operationContext: sourceOperationContext,
16846
16812
  });
16847
- // Validate native balance > 0 for gas fees on source chain
16848
- await validateNativeBalanceForTransaction({
16849
- adapter: source.adapter,
16850
- operationContext: sourceOperationContext,
16851
- });
16852
- // Only validate destination native balance if there's an adapter
16853
- // and forwarder is not being used (forwarder handles mint, no gas needed)
16854
- if ('adapter' in destination &&
16855
- destination.adapter &&
16856
- !destination.useForwarder) {
16857
- // Extract operation context from destination wallet context
16858
- const destinationOperationContext = this.extractOperationContext(destination);
16859
- // Validate native balance > 0 for gas fees on destination chain
16860
- await validateNativeBalanceForTransaction({
16861
- adapter: destination.adapter,
16862
- operationContext: destinationOperationContext,
16863
- });
16864
- }
16865
16813
  return bridge$1(params, this);
16866
16814
  }
16867
16815
  /**
@@ -18414,7 +18362,7 @@ const createBridgeKit = (context) => {
18414
18362
  };
18415
18363
 
18416
18364
  var name$2 = "@circle-fin/swap-kit";
18417
- var version$2 = "1.2.2";
18365
+ var version$2 = "1.2.3";
18418
18366
  var pkg$2 = {
18419
18367
  name: name$2,
18420
18368
  version: version$2};
@@ -29577,7 +29525,7 @@ const createSwapKit = (context) => {
29577
29525
  };
29578
29526
 
29579
29527
  var name$1 = "@circle-fin/earn-kit";
29580
- var version$1 = "1.0.0";
29528
+ var version$1 = "1.1.0";
29581
29529
  var pkg$1 = {
29582
29530
  name: name$1,
29583
29531
  version: version$1};
@@ -29987,6 +29935,253 @@ function validateExecutionDeadline(executionParams) {
29987
29935
  }
29988
29936
  }
29989
29937
 
29938
+ /** Base discriminators shared by every earn action payload. */
29939
+ const EARN_ACTION_BASE = {
29940
+ protocol: 'earn',
29941
+ service: 'earn-service',
29942
+ };
29943
+ /**
29944
+ * Dispatch a step event for an earn operation through the kit's action
29945
+ * dispatcher.
29946
+ *
29947
+ * Builds the action payload for the given action/operation/step combination
29948
+ * and forwards it to any registered listeners (including wildcard `'*'`
29949
+ * listeners). The call is a no-op when no dispatcher has been registered, so
29950
+ * callers can invoke it unconditionally.
29951
+ *
29952
+ * The `approve` action only fires for `deposit` and `withdraw` operations and
29953
+ * only carries `approve` steps; the `deposit`, `withdraw`, and `claimRewards`
29954
+ * actions carry only `fetchParams` and `execute` steps. Mismatched
29955
+ * combinations are ignored defensively.
29956
+ *
29957
+ * @param dispatcher - The action dispatcher, or `undefined` if none registered.
29958
+ * @param action - The action name to dispatch under.
29959
+ * @param operation - The earn operation the step belongs to.
29960
+ * @param step - The step to deliver as the event payload.
29961
+ *
29962
+ * @example
29963
+ * ```typescript
29964
+ * dispatchEarnEvent(dispatcher, 'deposit', 'deposit', {
29965
+ * name: 'execute',
29966
+ * state: 'success',
29967
+ * txHash: '0xabc...',
29968
+ * })
29969
+ * ```
29970
+ */
29971
+ function dispatchEarnEvent(dispatcher, action, operation, step) {
29972
+ if (dispatcher === undefined) {
29973
+ return;
29974
+ }
29975
+ // Listener errors must never abort the financial operation flow. A throwing
29976
+ // listener on a 'success' event after a transaction has landed on-chain
29977
+ // would otherwise propagate to the caller as a RESUMABLE error and trigger
29978
+ // a retry that re-submits the same transaction (double-spend risk).
29979
+ try {
29980
+ if (action === 'approve') {
29981
+ // The approve action only applies to deposit/withdraw approval steps.
29982
+ if (step.name !== 'approve' || operation === 'claimRewards') {
29983
+ return;
29984
+ }
29985
+ dispatcher.dispatch('approve', {
29986
+ ...EARN_ACTION_BASE,
29987
+ operation,
29988
+ method: 'approve',
29989
+ values: step,
29990
+ });
29991
+ return;
29992
+ }
29993
+ // deposit / withdraw / claimRewards actions carry fetchParams + execute steps.
29994
+ if (step.name === 'approve') {
29995
+ return;
29996
+ }
29997
+ const payload = { ...EARN_ACTION_BASE, method: step.name, values: step };
29998
+ if (action === 'deposit') {
29999
+ dispatcher.dispatch('deposit', { ...payload, operation: 'deposit' });
30000
+ return;
30001
+ }
30002
+ if (action === 'withdraw') {
30003
+ dispatcher.dispatch('withdraw', { ...payload, operation: 'withdraw' });
30004
+ return;
30005
+ }
30006
+ if (action === 'claimRewards') {
30007
+ dispatcher.dispatch('claimRewards', {
30008
+ ...payload,
30009
+ operation: 'claimRewards',
30010
+ });
30011
+ return;
30012
+ }
30013
+ }
30014
+ catch {
30015
+ // Swallow listener errors. Observability of the failure is the listener's
30016
+ // responsibility; the operation flow continues unaffected.
30017
+ return;
30018
+ }
30019
+ // Reached only when `action` is not a known earn action. Thrown outside the
30020
+ // try/catch so a missing case surfaces as a developer error instead of being
30021
+ // silently swallowed alongside listener failures.
30022
+ const exhaustive = action;
30023
+ throw new Error(`Unhandled earn action: ${String(exhaustive)}`);
30024
+ }
30025
+
30026
+ const EARN_OPERATIONS = new Set([
30027
+ 'deposit',
30028
+ 'withdraw',
30029
+ 'claimRewards',
30030
+ ]);
30031
+ /**
30032
+ * Operations whose service params carry a top-level `amount` input field
30033
+ * (`deposit` and `withdraw`); `claimRewards` params have no `amount`. This
30034
+ * refers to the request input — distinct from the reward `amount` that appears
30035
+ * inside a claimRewards *result*.
30036
+ */
30037
+ const OPERATIONS_WITH_AMOUNT = new Set([
30038
+ 'deposit',
30039
+ 'withdraw',
30040
+ ]);
30041
+ /**
30042
+ * Validate that `params` carries the minimum shape `retry()` needs to resume
30043
+ * the given operation: an adapter context (`from`) and a `vaultAddress`, plus
30044
+ * an `amount` for `deposit`/`withdraw`. This is a structural sanity check, not
30045
+ * a full schema validation — operations re-validate their inputs when re-run —
30046
+ * but it lets a mismatched or forged trace fail the {@link isEarnErrorTrace}
30047
+ * guard cleanly instead of crashing deep inside the resume path.
30048
+ */
30049
+ function hasEarnServiceParamsShape(operation, params) {
30050
+ if (params['from'] === null || typeof params['from'] !== 'object') {
30051
+ return false;
30052
+ }
30053
+ if (typeof params['vaultAddress'] !== 'string') {
30054
+ return false;
30055
+ }
30056
+ if (OPERATIONS_WITH_AMOUNT.has(operation) &&
30057
+ typeof params['amount'] !== 'string') {
30058
+ return false;
30059
+ }
30060
+ return true;
30061
+ }
30062
+ /**
30063
+ * Wrap an error thrown during a multi-phase earn operation with step-tracking
30064
+ * and resume context.
30065
+ *
30066
+ * Produces a new {@link KitError} that preserves the original error's code,
30067
+ * name, type, and message, merges any existing `cause.trace`, and adds an
30068
+ * {@link EarnErrorTrace} (`provider`, `operation`, `steps`, `params`). When a
30069
+ * prior phase had already committed work (a successful step exists) and the
30070
+ * underlying error is retryable, the recoverability is upgraded to
30071
+ * `RESUMABLE` so callers know `retry()` can resume from the failed phase. A
30072
+ * fatal underlying error stays fatal — it cannot be resumed.
30073
+ *
30074
+ * Non-{@link KitError} inputs (which should not occur on provider code paths)
30075
+ * are normalized to a retryable {@link ServiceError.INTERNAL_ERROR}.
30076
+ *
30077
+ * @param error - The error caught from a phase of the operation.
30078
+ * @param context - The operation context used to build the trace.
30079
+ * @returns A new {@link KitError} carrying the {@link EarnErrorTrace}.
30080
+ *
30081
+ * @example
30082
+ * ```typescript
30083
+ * try {
30084
+ * await executeEarnAction(...)
30085
+ * } catch (error) {
30086
+ * throw augmentEarnError(error, {
30087
+ * providerName: 'EarnService',
30088
+ * operation: 'deposit',
30089
+ * steps,
30090
+ * params,
30091
+ * })
30092
+ * }
30093
+ * ```
30094
+ */
30095
+ function augmentEarnError(error, context) {
30096
+ const base = isKitError(error)
30097
+ ? error
30098
+ : new KitError({
30099
+ ...ServiceError.INTERNAL_ERROR,
30100
+ recoverability: 'RETRYABLE',
30101
+ message: getErrorMessage(error),
30102
+ });
30103
+ // Only upgrade to RESUMABLE when a prior *on-chain* phase succeeded. The
30104
+ // RESUMABLE label promises retry() can skip already-committed work — true
30105
+ // for a landed `approve` or `execute`, but a successful `fetchParams` saves
30106
+ // nothing (retry() re-fetches it anyway, since execution params/deadlines
30107
+ // expire), so it stays a plain RETRYABLE.
30108
+ const priorOnChainPhaseSucceeded = context.steps.some((step) => step.state === 'success' &&
30109
+ (step.name === 'approve' || step.name === 'execute'));
30110
+ const recoverability = priorOnChainPhaseSucceeded && base.recoverability === 'RETRYABLE'
30111
+ ? 'RESUMABLE'
30112
+ : base.recoverability;
30113
+ const existingTrace = base.cause?.trace !== undefined &&
30114
+ base.cause.trace !== null &&
30115
+ typeof base.cause.trace === 'object'
30116
+ ? base.cause.trace
30117
+ : {};
30118
+ // Snapshot the steps so the trace `retry()` reads can't be mutated by a
30119
+ // future code path that retains the live `EarnRunContext.steps` array.
30120
+ const trace = {
30121
+ ...existingTrace,
30122
+ provider: context.providerName,
30123
+ operation: context.operation,
30124
+ steps: [...context.steps],
30125
+ };
30126
+ // Store the original service params non-enumerably. `retry()` reads them by
30127
+ // direct property access (`trace.params`), but JSON.stringify, console.log,
30128
+ // and telemetry serializers skip non-enumerable properties — so a
30129
+ // permissioned call's `config.kitKey` (a `KIT_KEY:<id>:<secret>` value) and
30130
+ // the in-memory adapter never leak through a logged error. defineProperty
30131
+ // also redefines any enumerable `params` an upstream trace may have carried.
30132
+ Object.defineProperty(trace, 'params', {
30133
+ value: context.params,
30134
+ enumerable: false,
30135
+ writable: true,
30136
+ configurable: true,
30137
+ });
30138
+ return new KitError({
30139
+ code: base.code,
30140
+ name: base.name,
30141
+ type: base.type,
30142
+ recoverability,
30143
+ message: base.message,
30144
+ cause: { trace },
30145
+ });
30146
+ }
30147
+ /**
30148
+ * Runtime type guard for {@link EarnErrorTrace}.
30149
+ *
30150
+ * Verifies the value carries the fields `retry()` needs to resume an earn
30151
+ * operation: a provider name, a known operation, a steps array, and a `params`
30152
+ * object whose shape matches that operation (an adapter context and vault
30153
+ * address, plus an amount for `deposit`/`withdraw`). Used to validate
30154
+ * `KitError.cause.trace` before resuming so a mismatched or forged trace is
30155
+ * rejected cleanly rather than crashing inside the resume path.
30156
+ *
30157
+ * @param value - The candidate value (typically `error.cause?.trace`).
30158
+ * @returns `true` if `value` is a usable {@link EarnErrorTrace}.
30159
+ *
30160
+ * @example
30161
+ * ```typescript
30162
+ * const trace = error.cause?.trace
30163
+ * if (isEarnErrorTrace(trace)) {
30164
+ * console.log(trace.operation, trace.steps.length)
30165
+ * }
30166
+ * ```
30167
+ */
30168
+ function isEarnErrorTrace(value) {
30169
+ if (value === null || typeof value !== 'object') {
30170
+ return false;
30171
+ }
30172
+ const candidate = value;
30173
+ const operation = candidate['operation'];
30174
+ if (typeof candidate['provider'] !== 'string' ||
30175
+ typeof operation !== 'string' ||
30176
+ !EARN_OPERATIONS.has(operation) ||
30177
+ !Array.isArray(candidate['steps']) ||
30178
+ candidate['params'] === null ||
30179
+ typeof candidate['params'] !== 'object') {
30180
+ return false;
30181
+ }
30182
+ return hasEarnServiceParamsShape(operation, candidate['params']);
30183
+ }
30184
+
29990
30185
  // ---------------------------------------------------------------------------
29991
30186
  // Shared primitives
29992
30187
  // ---------------------------------------------------------------------------
@@ -30974,12 +31169,63 @@ async function fetchClaimRewardsQuote(params) {
30974
31169
  }
30975
31170
  }
30976
31171
 
31172
+ /**
31173
+ * Build the `pending` {@link EarnStep} for a phase about to start,
31174
+ * discriminating on `stepName` so the result is a typed union member rather
31175
+ * than an unsafe `as EarnStep` cast.
31176
+ *
31177
+ * @param stepName - The name of the phase about to start.
31178
+ * @returns The typed pending step for the phase.
31179
+ */
31180
+ function buildPendingStep(stepName) {
31181
+ if (stepName === 'fetchParams') {
31182
+ return { name: 'fetchParams', state: 'pending' };
31183
+ }
31184
+ return { name: stepName, state: 'pending' };
31185
+ }
31186
+ /**
31187
+ * Build the `success` {@link EarnStep} for a completed phase, discriminating
31188
+ * on `stepName` so the `fetchParams` variant (which has no `txHash` in its
31189
+ * type) cannot accidentally carry a transaction hash.
31190
+ *
31191
+ * @param stepName - The name of the completed phase.
31192
+ * @param txHash - The on-chain transaction hash, if any.
31193
+ * @returns The typed success step for the phase.
31194
+ */
31195
+ function buildSuccessStep(stepName, txHash) {
31196
+ if (stepName === 'fetchParams') {
31197
+ return { name: 'fetchParams', state: 'success' };
31198
+ }
31199
+ if (txHash === undefined) {
31200
+ return { name: stepName, state: 'success' };
31201
+ }
31202
+ return { name: stepName, state: 'success', txHash };
31203
+ }
31204
+ /**
31205
+ * Build the `error` {@link EarnStep} for a failed phase, discriminating on
31206
+ * `stepName` so the result is a typed union member rather than an unsafe
31207
+ * `as EarnStep` cast.
31208
+ *
31209
+ * @param stepName - The name of the failed phase.
31210
+ * @param errorMessage - The human-readable failure message.
31211
+ * @param error - The raw error that caused the phase to fail.
31212
+ * @returns The typed error step for the phase.
31213
+ */
31214
+ function buildErrorStep(stepName, errorMessage, error) {
31215
+ if (stepName === 'fetchParams') {
31216
+ return { name: 'fetchParams', state: 'error', errorMessage, error };
31217
+ }
31218
+ return { name: stepName, state: 'error', errorMessage, error };
31219
+ }
30977
31220
  /**
30978
31221
  * Earn Service provider for the Circle earn service API.
30979
31222
  *
30980
31223
  * Implement the {@link EarningProvider} interface for yield-bearing vault
30981
31224
  * operations including vault discovery, position queries, deposits,
30982
- * withdrawals, and reward claiming.
31225
+ * withdrawals, and reward claiming. Multi-phase operations (deposit,
31226
+ * withdraw, claimRewards) emit step-level events through the kit's action
31227
+ * dispatcher and attach step progress to thrown errors so callers can resume
31228
+ * via {@link EarnServiceProvider.retry}.
30983
31229
  *
30984
31230
  * @example
30985
31231
  * ```typescript
@@ -31010,6 +31256,8 @@ class EarnServiceProvider {
31010
31256
  name = 'EarnService';
31011
31257
  /** {@inheritdoc} */
31012
31258
  supportedChains = Object.keys(CHAIN_TO_API).map((chain) => resolveChainIdentifier(chain));
31259
+ /** {@inheritdoc} */
31260
+ actionDispatcher = undefined;
31013
31261
  defaultConfig;
31014
31262
  /**
31015
31263
  * Create a new EarnServiceProvider.
@@ -31020,12 +31268,49 @@ class EarnServiceProvider {
31020
31268
  constructor(config) {
31021
31269
  this.defaultConfig = config;
31022
31270
  }
31271
+ /** {@inheritdoc} */
31272
+ registerDispatcher(dispatcher) {
31273
+ this.actionDispatcher = dispatcher;
31274
+ }
31023
31275
  resolveConfig(config) {
31024
31276
  const resolvedConfig = this.defaultConfig === undefined
31025
31277
  ? config
31026
31278
  : { ...this.defaultConfig, ...config };
31027
31279
  return resolvedConfig;
31028
31280
  }
31281
+ /**
31282
+ * Run one phase of a multi-phase earn operation: dispatch a `pending` event,
31283
+ * execute it, then dispatch a `success` event (recording the resulting step)
31284
+ * or — on failure — dispatch an `error` event, record the failed step, and
31285
+ * re-throw the original error for the caller to wrap with full step context.
31286
+ *
31287
+ * @typeParam R - The phase's result type.
31288
+ * @param ctx - The operation context (dispatcher, operation, steps, params).
31289
+ * @param action - The action name to dispatch the events under.
31290
+ * @param stepName - The phase name.
31291
+ * @param run - The phase work to execute.
31292
+ * @param txHashOf - Extracts the on-chain transaction hash from the result, if any.
31293
+ * @returns The phase result.
31294
+ */
31295
+ async runPhase(ctx, action, stepName, run, txHashOf) {
31296
+ dispatchEarnEvent(ctx.dispatcher, action, ctx.operation, buildPendingStep(stepName));
31297
+ let result;
31298
+ try {
31299
+ result = await run();
31300
+ }
31301
+ catch (error) {
31302
+ const failedStep = buildErrorStep(stepName, getErrorMessage(error), error);
31303
+ ctx.steps.push(failedStep);
31304
+ dispatchEarnEvent(ctx.dispatcher, action, ctx.operation, failedStep);
31305
+ throw error;
31306
+ }
31307
+ // Discriminate on stepName so the `fetchParams` variant (which has no
31308
+ // `txHash` field in its type) cannot accidentally pick up a tx hash.
31309
+ const successStep = buildSuccessStep(stepName, txHashOf(result));
31310
+ ctx.steps.push(successStep);
31311
+ dispatchEarnEvent(ctx.dispatcher, action, ctx.operation, successStep);
31312
+ return result;
31313
+ }
31029
31314
  /** {@inheritdoc} */
31030
31315
  async getVaults(params) {
31031
31316
  const config = this.resolveConfig(params.config);
@@ -31047,146 +31332,246 @@ class EarnServiceProvider {
31047
31332
  }
31048
31333
  /** {@inheritdoc} */
31049
31334
  async deposit(params) {
31050
- const config = this.resolveConfig(params.config);
31051
- const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
31052
- const adapterContractAddress = requireAdapterContract(chain);
31053
- const rawUsdcAddress = chain.usdcAddress;
31054
- if (rawUsdcAddress === null) {
31055
- throw createUnsupportedTokenError('USDC', chain.name);
31056
- }
31057
- const usdcAddress = assertHexAddress('chain.usdcAddress', rawUsdcAddress, `USDC address for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
31058
- const { executionParams, signature } = await fetchDeposit({
31059
- vaultAddress: params.vaultAddress,
31060
- amount: params.amount,
31061
- address,
31062
- chain: apiChain,
31063
- config,
31064
- });
31065
- validateExecutionDeadline(executionParams);
31066
- const { adapter } = params.from;
31067
- const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
31068
- const approvalToken = tokenInputs[0]?.token;
31069
- if (approvalToken !== undefined) {
31070
- await approveMaxIfNeeded({
31335
+ return this.runDepositFlow(params, { skipApprove: false });
31336
+ }
31337
+ async runDepositFlow(params, options) {
31338
+ const ctx = {
31339
+ dispatcher: this.actionDispatcher,
31340
+ operation: 'deposit',
31341
+ steps: [],
31342
+ params,
31343
+ };
31344
+ try {
31345
+ const config = this.resolveConfig(params.config);
31346
+ const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
31347
+ const adapterContractAddress = requireAdapterContract(chain);
31348
+ const rawUsdcAddress = chain.usdcAddress;
31349
+ if (rawUsdcAddress === null) {
31350
+ throw createUnsupportedTokenError('USDC', chain.name);
31351
+ }
31352
+ const usdcAddress = assertHexAddress('chain.usdcAddress', rawUsdcAddress, `USDC address for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
31353
+ const { adapter } = params.from;
31354
+ const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async () => fetchDeposit({
31355
+ vaultAddress: params.vaultAddress,
31356
+ amount: params.amount,
31357
+ address,
31358
+ chain: apiChain,
31359
+ config,
31360
+ }), () => undefined);
31361
+ validateExecutionDeadline(executionParams);
31362
+ const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
31363
+ const approvalToken = tokenInputs[0]?.token;
31364
+ if (!options.skipApprove && approvalToken !== undefined) {
31365
+ await this.runPhase(ctx, 'approve', 'approve', async () => approveMaxIfNeeded({
31366
+ adapter,
31367
+ chain,
31368
+ tokenAddress: approvalToken,
31369
+ delegate: adapterContractAddress,
31370
+ address,
31371
+ revertMessage: 'USDC approval reverted on-chain',
31372
+ }), (txHash) => txHash);
31373
+ }
31374
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async () => executeEarnAction({
31071
31375
  adapter,
31072
31376
  chain,
31073
- tokenAddress: approvalToken,
31074
- delegate: adapterContractAddress,
31075
31377
  address,
31076
- revertMessage: 'USDC approval reverted on-chain',
31378
+ actionKey: 'earn.deposit',
31379
+ actionParams: {
31380
+ executeParams: executionParams,
31381
+ tokenInputs,
31382
+ signature,
31383
+ },
31384
+ revertMessage: 'Earn deposit reverted on-chain',
31385
+ }), ({ txHash }) => txHash);
31386
+ return {
31387
+ txHash,
31388
+ explorerUrl,
31389
+ vaultAddress: params.vaultAddress,
31390
+ amount: params.amount,
31391
+ };
31392
+ }
31393
+ catch (error) {
31394
+ throw augmentEarnError(error, {
31395
+ providerName: this.name,
31396
+ operation: ctx.operation,
31397
+ steps: ctx.steps,
31398
+ params: ctx.params,
31077
31399
  });
31078
31400
  }
31079
- const { txHash, explorerUrl } = await executeEarnAction({
31080
- adapter,
31081
- chain,
31082
- address,
31083
- actionKey: 'earn.deposit',
31084
- actionParams: {
31085
- executeParams: executionParams,
31086
- tokenInputs,
31087
- signature,
31088
- },
31089
- revertMessage: 'Earn deposit reverted on-chain',
31090
- });
31091
- return {
31092
- txHash,
31093
- explorerUrl,
31094
- vaultAddress: params.vaultAddress,
31095
- amount: params.amount,
31096
- };
31097
31401
  }
31098
31402
  /** {@inheritdoc} */
31099
31403
  async withdraw(params) {
31100
- const config = this.resolveConfig(params.config);
31101
- const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
31102
- const adapterContractAddress = requireAdapterContract(chain);
31103
- const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
31104
- const { executionParams, signature } = await fetchWithdraw({
31105
- vaultAddress,
31106
- amount: params.amount,
31107
- address,
31108
- chain: apiChain,
31109
- config,
31110
- });
31111
- validateExecutionDeadline(executionParams);
31112
- const { adapter } = params.from;
31113
- const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
31114
- const approvalToken = tokenInputs[0]?.token;
31115
- if (approvalToken !== undefined) {
31116
- await approveMaxIfNeeded({
31404
+ return this.runWithdrawFlow(params, { skipApprove: false });
31405
+ }
31406
+ async runWithdrawFlow(params, options) {
31407
+ const ctx = {
31408
+ dispatcher: this.actionDispatcher,
31409
+ operation: 'withdraw',
31410
+ steps: [],
31411
+ params,
31412
+ };
31413
+ try {
31414
+ const config = this.resolveConfig(params.config);
31415
+ const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
31416
+ const adapterContractAddress = requireAdapterContract(chain);
31417
+ const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
31418
+ const { adapter } = params.from;
31419
+ const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async () => fetchWithdraw({
31420
+ vaultAddress,
31421
+ amount: params.amount,
31422
+ address,
31423
+ chain: apiChain,
31424
+ config,
31425
+ }), () => undefined);
31426
+ validateExecutionDeadline(executionParams);
31427
+ const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
31428
+ const approvalToken = tokenInputs[0]?.token;
31429
+ if (!options.skipApprove && approvalToken !== undefined) {
31430
+ await this.runPhase(ctx, 'approve', 'approve', async () => approveMaxIfNeeded({
31431
+ adapter,
31432
+ chain,
31433
+ tokenAddress: approvalToken,
31434
+ delegate: adapterContractAddress,
31435
+ address,
31436
+ revertMessage: 'Vault share token approval reverted on-chain',
31437
+ }), (txHash) => txHash);
31438
+ }
31439
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async () => executeEarnAction({
31117
31440
  adapter,
31118
31441
  chain,
31119
- tokenAddress: approvalToken,
31120
- delegate: adapterContractAddress,
31121
31442
  address,
31122
- revertMessage: 'Vault share token approval reverted on-chain',
31443
+ actionKey: 'earn.withdraw',
31444
+ actionParams: {
31445
+ executeParams: executionParams,
31446
+ tokenInputs,
31447
+ signature,
31448
+ },
31449
+ revertMessage: 'Earn withdraw reverted on-chain',
31450
+ }), ({ txHash }) => txHash);
31451
+ return {
31452
+ txHash,
31453
+ explorerUrl,
31454
+ vaultAddress,
31455
+ amount: params.amount,
31456
+ };
31457
+ }
31458
+ catch (error) {
31459
+ throw augmentEarnError(error, {
31460
+ providerName: this.name,
31461
+ operation: ctx.operation,
31462
+ steps: ctx.steps,
31463
+ params: ctx.params,
31123
31464
  });
31124
31465
  }
31125
- const { txHash, explorerUrl } = await executeEarnAction({
31126
- adapter,
31127
- chain,
31128
- address,
31129
- actionKey: 'earn.withdraw',
31130
- actionParams: {
31131
- executeParams: executionParams,
31132
- tokenInputs,
31133
- signature,
31134
- },
31135
- revertMessage: 'Earn withdraw reverted on-chain',
31136
- });
31137
- return {
31138
- txHash,
31139
- explorerUrl,
31140
- vaultAddress,
31141
- amount: params.amount,
31142
- };
31143
31466
  }
31144
31467
  /** {@inheritdoc} */
31145
31468
  async claimRewards(params) {
31146
- const config = this.resolveConfig(params.config);
31147
- const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
31148
- // Claim rewards has no approval step, but still requires adapter support.
31149
- requireAdapterContract(chain);
31150
- const { rewards, executionParams, signature } = await fetchClaimRewards({
31151
- address,
31152
- chain: apiChain,
31153
- vaultAddress: params.vaultAddress,
31154
- config,
31155
- });
31156
- const nothingToClaim = rewards.length === 0;
31157
- if (nothingToClaim) {
31158
- return { status: 'no_rewards', rewards: [] };
31159
- }
31160
- const missingExecutionParams = executionParams === undefined;
31161
- const missingSignature = signature === undefined;
31162
- if (missingExecutionParams || missingSignature) {
31163
- throw new KitError({
31164
- ...EarnError.INTERNAL_ERROR,
31165
- recoverability: 'RETRYABLE',
31166
- message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
31167
- cause: {
31168
- trace: {
31169
- rewardsCount: rewards.length,
31170
- missingExecutionParams,
31171
- missingSignature,
31469
+ return this.runClaimRewardsFlow(params);
31470
+ }
31471
+ async runClaimRewardsFlow(params) {
31472
+ const ctx = {
31473
+ dispatcher: this.actionDispatcher,
31474
+ operation: 'claimRewards',
31475
+ steps: [],
31476
+ params,
31477
+ };
31478
+ try {
31479
+ const config = this.resolveConfig(params.config);
31480
+ const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
31481
+ // Claim rewards has no approval step, but still requires adapter support.
31482
+ requireAdapterContract(chain);
31483
+ const { rewards, executionParams, signature } = await this.runPhase(ctx, 'claimRewards', 'fetchParams', async () => fetchClaimRewards({
31484
+ address,
31485
+ chain: apiChain,
31486
+ vaultAddress: params.vaultAddress,
31487
+ config,
31488
+ }), () => undefined);
31489
+ const nothingToClaim = rewards.length === 0;
31490
+ if (nothingToClaim) {
31491
+ return { status: 'no_rewards', rewards: [] };
31492
+ }
31493
+ const missingExecutionParams = executionParams === undefined;
31494
+ const missingSignature = signature === undefined;
31495
+ if (missingExecutionParams || missingSignature) {
31496
+ throw new KitError({
31497
+ ...EarnError.INTERNAL_ERROR,
31498
+ recoverability: 'RETRYABLE',
31499
+ message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
31500
+ cause: {
31501
+ trace: {
31502
+ rewardsCount: rewards.length,
31503
+ missingExecutionParams,
31504
+ missingSignature,
31505
+ },
31172
31506
  },
31507
+ });
31508
+ }
31509
+ validateExecutionDeadline(executionParams);
31510
+ const { txHash, explorerUrl } = await this.runPhase(ctx, 'claimRewards', 'execute', async () => executeEarnAction({
31511
+ adapter: params.from.adapter,
31512
+ chain,
31513
+ address,
31514
+ actionKey: 'earn.claimRewards',
31515
+ actionParams: {
31516
+ executeParams: executionParams,
31517
+ tokenInputs: [],
31518
+ signature,
31173
31519
  },
31520
+ revertMessage: 'Earn claim rewards reverted on-chain',
31521
+ }), ({ txHash }) => txHash);
31522
+ return { status: 'claimed', rewards, txHash, explorerUrl };
31523
+ }
31524
+ catch (error) {
31525
+ throw augmentEarnError(error, {
31526
+ providerName: this.name,
31527
+ operation: ctx.operation,
31528
+ steps: ctx.steps,
31529
+ params: ctx.params,
31174
31530
  });
31175
31531
  }
31176
- validateExecutionDeadline(executionParams);
31177
- const { txHash, explorerUrl } = await executeEarnAction({
31178
- adapter: params.from.adapter,
31179
- chain,
31180
- address,
31181
- actionKey: 'earn.claimRewards',
31182
- actionParams: {
31183
- executeParams: executionParams,
31184
- tokenInputs: [],
31185
- signature,
31186
- },
31187
- revertMessage: 'Earn claim rewards reverted on-chain',
31188
- });
31189
- return { status: 'claimed', rewards, txHash, explorerUrl };
31532
+ }
31533
+ /** {@inheritdoc} */
31534
+ supportsRetry(error) {
31535
+ return (isKitError(error) &&
31536
+ isRetryableError$1(error) &&
31537
+ isEarnErrorTrace(error.cause?.trace) &&
31538
+ error.cause.trace.provider === this.name);
31539
+ }
31540
+ /** {@inheritdoc} */
31541
+ async retry(error) {
31542
+ // Validation order mirrors EarnKit.retry().
31543
+ if (!isKitError(error)) {
31544
+ throw createValidationFailedError$1('error', error, 'retry() requires a KitError thrown by a previous earn operation');
31545
+ }
31546
+ if (!isRetryableError$1(error)) {
31547
+ throw createValidationFailedError$1('error.recoverability', error.recoverability, 'retry() requires a retryable or resumable error — check isRetryableError(error) first');
31548
+ }
31549
+ const trace = error.cause?.trace;
31550
+ if (!isEarnErrorTrace(trace)) {
31551
+ throw createValidationFailedError$1('error.cause.trace', trace, 'retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
31552
+ }
31553
+ if (trace.provider !== this.name) {
31554
+ throw createValidationFailedError$1('error.cause.trace.provider', trace.provider, `Cannot retry: error was produced by provider "${trace.provider}", not "${this.name}"`);
31555
+ }
31556
+ const approveCompleted = trace.steps.some((step) => step.name === 'approve' && step.state === 'success');
31557
+ // `trace` is a discriminated union on `operation`, so each branch narrows
31558
+ // `trace.params` to the matching service-params type — no cast needed.
31559
+ switch (trace.operation) {
31560
+ case 'deposit':
31561
+ return this.runDepositFlow(trace.params, {
31562
+ skipApprove: approveCompleted,
31563
+ });
31564
+ case 'withdraw':
31565
+ return this.runWithdrawFlow(trace.params, {
31566
+ skipApprove: approveCompleted,
31567
+ });
31568
+ case 'claimRewards':
31569
+ return this.runClaimRewardsFlow(trace.params);
31570
+ default: {
31571
+ const exhaustive = trace;
31572
+ throw createValidationFailedError$1('error.cause.trace.operation', exhaustive, 'retry() does not support this earn operation');
31573
+ }
31574
+ }
31190
31575
  }
31191
31576
  /** {@inheritdoc} */
31192
31577
  async getDepositQuote(params) {
@@ -31270,54 +31655,6 @@ function createEarnKitContext(config = {}) {
31270
31655
  return context;
31271
31656
  }
31272
31657
 
31273
- /**
31274
- * Symbol used to track that assertEarnParams has validated an object.
31275
- * @internal
31276
- */
31277
- const ASSERT_EARN_PARAMS_SYMBOL = Symbol('assertEarnParams');
31278
- /**
31279
- * Assert that the provided value conforms to the given earn params schema.
31280
- *
31281
- * Validate earn parameters using the provided Zod schema and track
31282
- * validation state to avoid duplicate checks. Throw a structured
31283
- * error with detailed validation messages if any parameter is invalid.
31284
- *
31285
- * @typeParam T - The expected type after validation
31286
- * @param params - The earn parameters to validate
31287
- * @param schema - The Zod schema to validate against
31288
- * @throws {@link KitError} If the parameters fail validation
31289
- *
31290
- * @example
31291
- * ```typescript
31292
- * import { assertEarnParams, depositParamsSchema } from '@circle-fin/earn-kit'
31293
- *
31294
- * assertEarnParams(params, depositParamsSchema)
31295
- * ```
31296
- */
31297
- function assertEarnParams(params, schema) {
31298
- validateWithStateTracking(params, schema, 'earn parameters', ASSERT_EARN_PARAMS_SYMBOL);
31299
- }
31300
-
31301
- function findProvider(context, chain, operation = 'earn') {
31302
- let fallback;
31303
- for (const provider of context.providers) {
31304
- fallback ??= provider;
31305
- if (provider.supportedChains.length === 0)
31306
- continue;
31307
- if (chain === undefined ||
31308
- provider.supportedChains.some((c) => c.chain === chain.chain)) {
31309
- return provider;
31310
- }
31311
- }
31312
- if (fallback === undefined) {
31313
- throw createValidationFailedError$1('context.providers', [], 'No earn providers configured');
31314
- }
31315
- if (chain !== undefined) {
31316
- throw createUnsupportedEarnRouteError(operation, chain.name);
31317
- }
31318
- return fallback;
31319
- }
31320
-
31321
31658
  /**
31322
31659
  * Format a provider amount object as a human-readable decimal string.
31323
31660
  *
@@ -31460,6 +31797,54 @@ function formatClaimRewardsResult(result) {
31460
31797
  };
31461
31798
  }
31462
31799
 
31800
+ /**
31801
+ * Symbol used to track that assertEarnParams has validated an object.
31802
+ * @internal
31803
+ */
31804
+ const ASSERT_EARN_PARAMS_SYMBOL = Symbol('assertEarnParams');
31805
+ /**
31806
+ * Assert that the provided value conforms to the given earn params schema.
31807
+ *
31808
+ * Validate earn parameters using the provided Zod schema and track
31809
+ * validation state to avoid duplicate checks. Throw a structured
31810
+ * error with detailed validation messages if any parameter is invalid.
31811
+ *
31812
+ * @typeParam T - The expected type after validation
31813
+ * @param params - The earn parameters to validate
31814
+ * @param schema - The Zod schema to validate against
31815
+ * @throws {@link KitError} If the parameters fail validation
31816
+ *
31817
+ * @example
31818
+ * ```typescript
31819
+ * import { assertEarnParams, depositParamsSchema } from '@circle-fin/earn-kit'
31820
+ *
31821
+ * assertEarnParams(params, depositParamsSchema)
31822
+ * ```
31823
+ */
31824
+ function assertEarnParams(params, schema) {
31825
+ validateWithStateTracking(params, schema, 'earn parameters', ASSERT_EARN_PARAMS_SYMBOL);
31826
+ }
31827
+
31828
+ function findProvider(context, chain, operation = 'earn') {
31829
+ let fallback;
31830
+ for (const provider of context.providers) {
31831
+ fallback ??= provider;
31832
+ if (provider.supportedChains.length === 0)
31833
+ continue;
31834
+ if (chain === undefined ||
31835
+ provider.supportedChains.some((c) => c.chain === chain.chain)) {
31836
+ return provider;
31837
+ }
31838
+ }
31839
+ if (fallback === undefined) {
31840
+ throw createValidationFailedError$1('context.providers', [], 'No earn providers configured');
31841
+ }
31842
+ if (chain !== undefined) {
31843
+ throw createUnsupportedEarnRouteError(operation, chain.name);
31844
+ }
31845
+ return fallback;
31846
+ }
31847
+
31463
31848
  /**
31464
31849
  * Schema for the adapter context within earn operations.
31465
31850
  *
@@ -32109,6 +32494,19 @@ async function getClaimRewardsQuote$1(context, params) {
32109
32494
  return formatClaimRewardsQuoteInfo(result);
32110
32495
  }
32111
32496
 
32497
+ function formatRetryResult(operation, result) {
32498
+ switch (operation) {
32499
+ case 'deposit':
32500
+ case 'withdraw':
32501
+ return result;
32502
+ case 'claimRewards':
32503
+ return formatClaimRewardsResult(result);
32504
+ default: {
32505
+ const exhaustive = operation;
32506
+ throw createValidationFailedError$1('error.cause.trace.operation', exhaustive, 'EarnKit.retry() does not support this earn operation');
32507
+ }
32508
+ }
32509
+ }
32112
32510
  /**
32113
32511
  * A high-level class-based interface for DeFi lending vault operations.
32114
32512
  *
@@ -32166,6 +32564,12 @@ async function getClaimRewardsQuote$1(context, params) {
32166
32564
  */
32167
32565
  class EarnKit {
32168
32566
  context;
32567
+ /**
32568
+ * Event dispatcher for step-level events emitted during multi-phase earn
32569
+ * operations. Prefer {@link EarnKit.on} / {@link EarnKit.off} over using
32570
+ * this directly.
32571
+ */
32572
+ actionDispatcher;
32169
32573
  /**
32170
32574
  * Create a new EarnKit instance.
32171
32575
  *
@@ -32185,6 +32589,89 @@ class EarnKit {
32185
32589
  */
32186
32590
  constructor(config = {}) {
32187
32591
  this.context = createEarnKitContext(config);
32592
+ this.actionDispatcher = new Actionable();
32593
+ for (const provider of this.context.providers) {
32594
+ provider.registerDispatcher(this.actionDispatcher);
32595
+ }
32596
+ }
32597
+ on(action, handler) {
32598
+ if (action === '*') {
32599
+ this.actionDispatcher.on('*', handler);
32600
+ }
32601
+ else {
32602
+ this.actionDispatcher.on(action, handler);
32603
+ }
32604
+ }
32605
+ off(action, handler) {
32606
+ if (action === '*') {
32607
+ this.actionDispatcher.off('*', handler);
32608
+ }
32609
+ else {
32610
+ this.actionDispatcher.off(action, handler);
32611
+ }
32612
+ }
32613
+ /**
32614
+ * Resume a multi-phase earn operation that previously failed.
32615
+ *
32616
+ * Pass the {@link KitError} caught from a `deposit`, `withdraw`, or
32617
+ * `claimRewards` call. The error carries the original operation, inputs,
32618
+ * and step progress, so the operation can be re-run while skipping phases
32619
+ * that already completed (for example a successful token approval). Use
32620
+ * `isRetryableError(error)` to check whether retrying is worthwhile before
32621
+ * calling this.
32622
+ *
32623
+ * @remarks
32624
+ * Resuming re-fetches execution params and re-submits the `execute`
32625
+ * transaction; the earn service deduplicates execution server-side, which
32626
+ * makes this safe in the common case. But if a prior attempt broadcast the
32627
+ * `execute` transaction and then failed before its receipt was observed,
32628
+ * that transaction may still be in flight when `retry()` re-broadcasts.
32629
+ * Treat `retry()` as best-effort recovery, not an atomic operation.
32630
+ *
32631
+ * @param error - The error caught from a previous multi-phase earn operation.
32632
+ * @returns A promise resolving to the result of the resumed operation.
32633
+ * @throws {@link KitError} If `error` is not a retryable {@link KitError}
32634
+ * carrying earn retry context, names an unknown provider, or the resumed
32635
+ * operation itself fails.
32636
+ *
32637
+ * @example
32638
+ * ```typescript
32639
+ * import { isRetryableError } from '@circle-fin/earn-kit'
32640
+ *
32641
+ * try {
32642
+ * await kit.deposit(params)
32643
+ * } catch (error) {
32644
+ * if (isRetryableError(error)) {
32645
+ * const result = await kit.retry(error)
32646
+ * console.log('recovered, tx:', 'txHash' in result ? result.txHash : undefined)
32647
+ * }
32648
+ * }
32649
+ * ```
32650
+ */
32651
+ async retry(error) {
32652
+ if (!isKitError(error)) {
32653
+ throw createValidationFailedError$1('error', error, 'EarnKit.retry() requires a KitError thrown by a previous earn operation');
32654
+ }
32655
+ if (!isRetryableError$1(error)) {
32656
+ throw createValidationFailedError$1('error.recoverability', error.recoverability, 'EarnKit.retry() requires a retryable or resumable error — check isRetryableError(error) first');
32657
+ }
32658
+ const trace = error.cause?.trace;
32659
+ if (!isEarnErrorTrace(trace)) {
32660
+ throw createValidationFailedError$1('error.cause.trace', trace, 'EarnKit.retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
32661
+ }
32662
+ const provider = this.context.providers.find((candidate) => candidate.name === trace.provider);
32663
+ if (provider === undefined) {
32664
+ throw createValidationFailedError$1('error.cause.trace.provider', trace.provider, `No earn provider named "${trace.provider}" is registered with this kit`);
32665
+ }
32666
+ const result = await provider.retry(error);
32667
+ // `provider.retry` returns a flat result union with no compile-time link to
32668
+ // `trace.operation`, so narrow the operation here to select the matching
32669
+ // overload. The result cast in each branch is sound: the provider always
32670
+ // returns the result type corresponding to the resumed operation.
32671
+ if (trace.operation === 'claimRewards') {
32672
+ return formatRetryResult(trace.operation, result);
32673
+ }
32674
+ return formatRetryResult(trace.operation, result);
32188
32675
  }
32189
32676
  /**
32190
32677
  * Return the chains supported by configured earn providers.
@@ -33527,7 +34014,7 @@ async function getClaimRewardsQuote(context, params) {
33527
34014
  }
33528
34015
 
33529
34016
  var name = "@circle-fin/unified-balance-kit";
33530
- var version = "1.1.2";
34017
+ var version = "1.1.3";
33531
34018
  var pkg = {
33532
34019
  name: name,
33533
34020
  version: version};
@@ -34000,10 +34487,6 @@ async function deposit$1(params) {
34000
34487
  const signerAddress = await resolveSignerAddress(params.from, chain);
34001
34488
  const resolvedContext = { ...operationContext, address: signerAddress };
34002
34489
  await validateDepositBalance(params.from.adapter, chain, valueBigInt, resolvedContext, params.token);
34003
- await validateNativeBalanceForTransaction({
34004
- adapter: params.from.adapter,
34005
- operationContext: { ...resolvedContext, chain },
34006
- });
34007
34490
  const tokenAddress = getTokenAddress(chain, params.token);
34008
34491
  const strategy = params.allowanceStrategy ?? 'authorize';
34009
34492
  let txHash;
@@ -34048,10 +34531,6 @@ async function depositFor$1(params) {
34048
34531
  const signerAddress = await resolveSignerAddress(params.from, chain);
34049
34532
  const resolvedContext = { ...operationContext, address: signerAddress };
34050
34533
  await validateDepositBalance(params.from.adapter, chain, valueBigInt, resolvedContext, params.token);
34051
- await validateNativeBalanceForTransaction({
34052
- adapter: params.from.adapter,
34053
- operationContext: { ...resolvedContext, chain },
34054
- });
34055
34534
  const tokenAddress = getTokenAddress(chain, params.token);
34056
34535
  let txHash;
34057
34536
  if (chain.type === 'solana') {
@@ -37335,10 +37814,6 @@ async function updateDelegate(params, action, state) {
37335
37814
  const tokenAddress = getTokenAddress(chain, params.token);
37336
37815
  assertValidAddress(delegateAddress, chain, 'delegate');
37337
37816
  assertNotSelfDelegation(chain, signerAddress, delegateAddress, action);
37338
- await validateNativeBalanceForTransaction({
37339
- adapter,
37340
- operationContext: { ...operationContext, chain, address: signerAddress },
37341
- });
37342
37817
  const request = await adapter.prepareAction(action, { token: tokenAddress, delegate: delegateAddress, chain }, operationContext);
37343
37818
  const txHash = await executeAndWait(adapter, request, chain);
37344
37819
  return {
@@ -37541,10 +38016,6 @@ async function initiateRemoveFund$1(params) {
37541
38016
  const signerAddress = await resolveSignerAddress(from, chain);
37542
38017
  const tokenAddress = getTokenAddress(chain, params.token);
37543
38018
  const value = parseAmountSafe(amount);
37544
- await validateNativeBalanceForTransaction({
37545
- adapter,
37546
- operationContext: { ...operationContext, chain, address: signerAddress },
37547
- });
37548
38019
  const request = await adapter.prepareAction('gateway.v1.initiateWithdrawal', { token: tokenAddress, value, chain }, operationContext);
37549
38020
  let txHash;
37550
38021
  try {
@@ -37603,10 +38074,6 @@ async function removeFund$1(params) {
37603
38074
  const operationContext = extractOperationContext(from);
37604
38075
  const signerAddress = await resolveSignerAddress(from, chain);
37605
38076
  const tokenAddress = getTokenAddress(chain, params.token);
37606
- await validateNativeBalanceForTransaction({
37607
- adapter,
37608
- operationContext: { ...operationContext, chain, address: signerAddress },
37609
- });
37610
38077
  // Read the pending balance before withdrawing — the contract resets it to 0
37611
38078
  // after withdraw() executes, so this is the only way to capture the amount.
37612
38079
  const withdrawingBalanceReq = await adapter.prepareAction('gateway.v1.withdrawingBalance', { token: tokenAddress, depositor: signerAddress, chain }, operationContext);