# @circle-fin/earn-kit

## 1.7.0

### Minor Changes

- Add Arc mainnet (`Blockchain.Arc`, chainId 5042) as an EarnKit-selectable chain alongside Arc Testnet. `EarnChain.Arc` is now exposed for earn operations, and the Earn Service provider maps `Blockchain.Arc` to the `ARC` Earn API chain and reports it in `supportedChains`. App Kit re-exports this through its `earn` namespace, so `EarnChain.Arc` is selectable via App Kit as well.

## 1.6.1

### Patch Changes

- Fix `getSupportedChains({ sourceFeeSupported: true })` over-reporting source-fee
  bridging support. Previously, source-fee eligibility was inferred from a contract
  address shared with the fast-deposit forwarding path, causing fast-deposit-only chains
  to appear supported and fail at the Quote API instead of being rejected client-side.
  Source-fee support is now gated on an explicit allowlist of eligible source chains.

## 1.6.0

### Minor Changes

- Accept Circle API keys as the credential for service-backed swap and earn
  operations via the new `config.apiKey` field. Previously only `kitKey` values
  were accepted, so a Circle API key was rejected before it could be used.

  `config.kitKey` is deprecated but still works, so no change is required. Move to
  `config.apiKey` when convenient; when both are set, `apiKey` wins. `apiKey` is
  also stripped from results and error traces, matching `kitKey`.

## 1.5.1

### Patch Changes

- Read-only calls (Earn allowance checks, Gateway delegate and withdrawal-state
  lookups) no longer pass through `prepareAction`. If you wrap `prepareAction` to
  verify transactions before signing, these reads no longer appear there — only
  operations that may be signed or broadcast do.

## 1.5.0

### Minor Changes

- Same-chain Earn deposits, withdrawals, and reward claims can now surface a
  verified `earn.execute` review to an adapter `onBeforeAuthorize` hook before
  authorization. Use `isEarnExecuteReview` to inspect the decoded Earn operation
  and exact call data; returning `reject` prevents signing.
- Batch same-chain Earn deposits and withdrawals through an adapter's shared
  `supportsAtomicBatch` and `batchExecute` capabilities. Approval and execution
  calls are submitted atomically when supported, with a configuration option to
  force the existing sequential flow. Legacy Viem batches now request atomic
  EIP-5792 execution by default.

## 1.4.0

### Minor Changes

- EarnKit now sends structured telemetry for public-operation errors and for
  completed vault lookups, vault discovery, deposits, withdrawals, and reward
  claims. No code changes are required. Telemetry is enabled by default and
  contains no wallet addresses or amounts. To opt out when constructing
  `EarnKit`:

  ```ts
  const kit = new EarnKit({
    disableAnalytics: true,
    disableErrorReporting: true,
  });
  ```

  AppKit now forwards its existing `disableErrorReporting` option to its EarnKit
  operations.

### Patch Changes

- Make the SDK safe to bundle and run in browsers/client-side apps.

  - Browser requests omit Node-only headers that would cause CORS failures, including EarnKit’s SDK-version header.
  - Solana operations in App Kit, Adapter Solana Kit, and Gateway work in a browser without requiring a consumer-provided `Buffer` polyfill.
  - Supplying a `kitKey` or Circle Wallets `apiKey` in a browser now fails early. Keep those secrets on the server and forward a prepared transaction or other safe result to the client.

## 1.3.0

### Minor Changes

- Surface bridge quote metadata and quote-expired errors for cross-chain Earn deposits.

  Cross-chain deposit results now include optional `quoteIssuedAt` and `quoteExpiry` metadata so applications can display quote validity and refresh before expiration. Expired quotes are surfaced as fatal `EarnError.BRIDGE_QUOTE_EXPIRED` errors, signaling that callers should start a new prepare/deposit flow instead of retrying stale prepared data.

- Add nested `EarnOpportunity` vault facets to the earn-service response.

  `getVaults`/`exploreVaults` now surface a protocol-neutral, nested shape alongside the existing flat fields:

  - `manager` — curator identity (`null` when the product has no manager)
  - `apyProfile` — current APY plus trailing `d7`/`d30`/`d90` and reward share
  - `fee` — performance/management split
  - `liquidityProfile` — `totalDeposits`, `available`, `totalSupply`, `status`
  - `riskSignals` — `circleSentinel` plus warnings
  - `productType` — opportunity discriminator (`'vault'` today)
  - `asOf` — freshness provenance
  - `allocationPct` — per-collateral allocation share

  `VaultInfo` is now a discriminated union over `productType` (`VaultOpportunity`), aliased for source compatibility; `EarnVaultInfo` is derived with a distributive `Omit` so each variant keeps its own fields.

  The change is additive and non-breaking:

  - The new facets — including `productType` — are optional in the published types, so existing code that reads or constructs `VaultInfo` keeps compiling. A future major release makes them required once every backend emits them.
  - The deprecated flat aliases (`vaultAddress`, `currentApy`, `nativeApy`, `vaultFee`, `totalDeposits`, `liquidity`, `status`, `circleGuarded`, `warnings`, `earnKitWarnings`) are still read and validated.
  - The vault list parses tolerantly: opportunities with an unknown `productType` are dropped rather than failing the whole response, so future provider types do not break this SDK version.

### Patch Changes

- Deposit and withdrawal quotes now report the gas-fee estimate provided by the Earn Service instead of estimating it in the SDK. Previously the SDK estimated gas locally by simulating each action against current on-chain state, so the deposit or withdrawal entry could come back as `fees: null` with a revert error whenever a token approval had not yet been made — the Earn Service estimates gas for those actions, so quotes now return a usable gas fee in that case. Claim-rewards quotes do not carry a gas estimate (the service does not provide one), so their `gasFees` is now always empty. The response shape is unchanged (`EarnGasFeeEstimate[]`).

## 1.2.2

### Patch Changes

- Externalize the Earn Service provider from the EarnKit package bundle so custom provider instances use the same provider types as consumers.
- Fix withdrawal fee metadata being dropped, and correctly surface errors for pending profit-and-loss (PnL) calculations.
- Fix a missing runtime dependency (`pino`) that could cause `Could not find package`
  errors under strict module resolvers (Deno, Supabase Edge Runtime, pnpm).

## 1.2.1

### Patch Changes

- Add support for depositing tokens beyond USDC into Earn. EarnKit now accepts EURC and cirBTC deposits (both same-chain and cross-chain) and validates each deposit against the correct token's address, decimals, amount precision, and fees. Cross-chain deposits are accepted only for tokens that are configured for bridging.

  Surface clearer errors for an invalid deposit amount and for an unrecoverable bridge correlation mismatch.

## 1.2.0

### Minor Changes

- Vault results from `getVaults`, `exploreVaults`, and `exploreVaultsIterator`
  now include a `circleGuarded` boolean that marks vaults on Circle's curated
  Circle-guarded list.

  ```typescript
  const result = await kit.getVaults({
    vaults: [{ chain: "Ethereum", vaultAddress: "0xbeef..." }],
  });
  console.log(result.vaults[0].circleGuarded);
  ```

  This release requires an Earn Service version that returns the
  `circleGuarded` field.

- EarnKit can now discover vaults with the new `exploreVaultsIterator` and
  `exploreVaults` operations.

  `exploreVaultsIterator` lazily iterates every vault available on a chain,
  fetching pages on demand so there is no page arithmetic to manage:

  ```typescript
  for await (const vault of kit.exploreVaultsIterator({
    chain: "Arc_Testnet",
    sortBy: "apy",
  })) {
    console.log(`${vault.name}: ${(vault.currentApy * 100).toFixed(2)}% APY`);
  }
  ```

  `exploreVaults` fetches a single page with pagination metadata for paged
  UIs. Both operations accept optional `protocol`, `asset`, `minApy`, and
  `minTvl` filters (protocol and asset are matched case-insensitively) and a
  `sortBy` of `apy`, `tvl`, or `name`.

  Also available through `@circle-fin/app-kit` as `kit.earn.exploreVaults()` /
  `kit.earn.exploreVaultsIterator()` and the functional `earnExploreVaults` /
  `earnExploreVaultsIterator` exports.

  No changes are required for custom `EarningProvider` implementations: the
  new `exploreVaults` method is optional on the interface. Providers that do
  not implement it cause a clear "no provider supports exploreVaults" error
  when vault discovery is called; implement the method to opt in.

- Earn quotes now include gas fee estimates.

  `getDepositQuote`, `getWithdrawalQuote`, and `getClaimRewardsQuote` now return a
  `gasFees` array estimating the native-token gas cost of each transaction in the
  flow (approval, deposit, withdraw, claim), so you can show users the expected
  cost before they commit. When an estimate cannot be produced, the entry's `fees`
  is `null` and `error` carries a short message describing why, while the rest of
  the quote still resolves.

  Also available through `@circle-fin/app-kit`'s earn quote operations.

- Expose deposit and withdrawal quote `gasFees` gas amounts as decimal strings at the provider-to-kit boundary. Previously kit-level quote results carried `gas` and `gasPrice` as raw `bigint` (from the provider `EstimatedGas`), which made `JSON.stringify(quote)` throw `TypeError: Do not know how to serialize a BigInt` at every consumer that logs or persists a quote. Gas amounts are now formatted as decimal strings, matching how all other amounts on kit results are exposed, keeping quote results fully JSON-serializable.

  Note: this changes the representation of `EarnDepositQuoteInfo.gasFees[].fees.gas` and `.gasPrice` from `bigint` to `string` (via a new `EarnEstimatedGas` type). Callers that read those two fields as `bigint` must read them as decimal strings; `fee` was already a string and every other quote amount is already a string.

- Surface a typed, retryable `EarnError.PAUSED` when EarnKit is temporarily
  paused off-chain (the earn service returns backend code `380414` /
  `EARNKIT_OFFCHAIN_PAUSED` / HTTP `409`).

  | Code   | Name          | Type      | Recoverability |
  | ------ | ------------- | --------- | -------------- |
  | `8104` | `EARN_PAUSED` | `SERVICE` | `RETRYABLE`    |

  - `@circle-fin/earn-kit` / `@circle-fin/app-kit`: the publicly re-exported
    `EarnError` registry now includes `EARN_PAUSED`. Consumers that branch on
    `EarnError` (or on `recoverability`) gain this value and can detect the
    pause and retry later.
  - `@circle-fin/provider-earn-service`: `parseEarnApiError` now maps `380414`
    to `EarnError.PAUSED`, so `EarnServiceProvider` deposit/withdraw/claim calls
    throw a retryable `EARN_PAUSED` instead of falling through to the previous
    fatal `VALIDATION_FAILED`.

- **Behavioral change:** the re-exported `isRetryableError` no longer
  treats nonce errors (`4003` / `RPC_NONCE_ERROR`) as retryable.

  A nonce error is genuinely ambiguous — the transaction may already have
  been accepted (the nonce moved on), a concurrent transaction from the
  same key may have consumed the slot, or a local nonce tracker may have
  drifted. Generic automatic retry risks a double-spend or a
  nonce-conflict flip-flop, so `4003` was removed from
  `DEFAULT_RETRYABLE_ERROR_CODES` and callers must now decide explicitly
  whether to refresh the nonce and resubmit.

  For `@circle-fin/app-kit` specifically, the documented resume guard
  `if (isRetryableError(failedStep.error)) await kit.retryBridge(...)` now
  skips bridges that failed on a nonce error; handle code `4003` yourself
  if you want to refresh the nonce and retry.

- Add APIs to track a cross-chain Earn deposit after it is submitted.

  A cross-chain `deposit()` returns an `execId` while the bridge (source burn ->
  CCTP attestation -> destination mint) is still settling. Two new kit methods let
  you follow it:

  - `kit.getCrossChainDepositStatus({ execId })` - read the deposit's current
    bridge status (source, CCTP, and destination hops) once.
  - `kit.waitForCrossChainDeposit({ execId, pollIntervalMs?, maxWaitMs?, signal? })`
    - poll until the deposit settles, fails, or the wait budget elapses (default
      20 min). On timeout it resolves with the last status instead of throwing, so
      you can see where the bridge stalled. Pass an `AbortSignal` to cancel a long
      wait. While polling it emits events you can subscribe to with
      `kit.on('crossChainDepositStatus', ...)`.

  New exported types: `EarnCrossChainDepositStatus`,
  `EarnCrossChainDepositWaitResult`, `EarnBridgeHopStatus`,
  `EarnBridgeCctpStatus`, plus the param types and schemas for both methods. All
  are re-exported from `@circle-fin/app-kit` with earn-prefixed names.

- Surface the source-collected bridge fees in cross-chain Earn deposit quotes, and let callers pick the CCTP transfer speed.

  New:

  - `getDepositQuote` now accepts a cross-chain variant: pass a destination `chain` and `address` that differ from `from.chain` (same `SameChain` / `CrossChain` discriminator pattern as `deposit()`). Same-chain quotes are unchanged.
  - Cross-chain quotes return the source-collected bridge fees in `EarnDepositQuoteInfo.fees` as one entry per fee item. Each entry carries its own `type` (e.g. `'FORWARD'`, `'PRE_FINALITY'`) and `status` (e.g. `'estimated'` for a pre-sign estimate). Same-chain quotes return `fees: []`.
  - Cross-chain `deposit()` and `getDepositQuote()` accept an optional `transferSpeed` (`'FAST'` | `'SLOW'`), forwarded to the bridge prepare request so the quote prices — and the deposit burns — the chosen CCTP finality path.
  - New param types: `SameChainGetDepositQuoteParams` / `CrossChainGetDepositQuoteParams` (the existing `GetDepositQuoteParams` becomes their union). Provider gains `SameChainGetDepositQuoteServiceParams` / `CrossChainGetDepositQuoteServiceParams`.
  - The provider sends `sourceChain` / `sourceAddress` / `transferSpeed` to `POST /v1/earnKit/deposit/quote` and parses the per-fee `fees[]` response (each with `type` + `status`).
  - `AssetAmount` gains optional `type` and `status` tags (used on fee entries).
  - The cross-chain pre-sign value guard is now fee-aware: the prepared bundle's `feeQuote` (`feeToken` + `items[]`) is parsed, the fee token is pinned to the source token contract, and the signed authorization `value` is checked against `principal + sum(feeQuote.items[].amount)` rather than the principal alone, so fee-bearing deposits are accepted while any other value tampering is still rejected before signing.

  Notes:

  - The cross-chain fees are pre-sign **estimates** sourced from the bridge prepare path; the value shown at quote time may differ from the amount locked at deposit/sign. This is signalled per fee via `status: 'estimated'`.
  - Existing same-chain `getDepositQuote` callers are unaffected — the response shape is unchanged for same-chain quotes.

- Add cross-chain Earn deposits through the bridge prepare, sign, and submit flow.

  Cross-chain deposits use the same nested `from`/`to` input shape as the other kits: `from: { adapter, chain }` is the source signer and `to: { chain, recipientAddress }` is the Earn destination. `from.chain` and `to.chain` are constrained at compile time to the supported route (Ethereum Sepolia, Arbitrum Sepolia, or Base Sepolia as sources; Arc Testnet as the destination), the source adapter must support `signTypedData`, and amounts accept at most 6 decimal places (USDC precision). Cross-chain results report the bridge `execId`, an API-defined bridge lifecycle `status`, `sourceChain`/`destinationChain`, and the prepared bundle expiry.

  Every published name keeps its existing shape; cross-chain support lands under new names:

  - `EarnDepositOutcome` is the new union of `EarnSameChainDepositResult | EarnCrossChainDepositResult`. Narrow with `result.kind === 'cross-chain'` — `kind` is always set at runtime, but optional on the same-chain member so the pre-cross-chain result shape remains assignable.
  - `AnyDepositParams` (kit) and `AnyDepositServiceParams` (provider) are the new param unions of the same-chain and cross-chain variants; `anyDepositParamsSchema` is the matching validation schema.
  - `deposit()` gains overloads: same-chain params resolve to `EarnSameChainDepositResult`, cross-chain params to `EarnCrossChainDepositResult`, so existing same-chain call sites keep their narrow result type.
  - Bridge phases emit on a new `crossChainDeposit` event (`method: 'prepare' | 'sign' | 'submit'`, `values: EarnBridgeDepositStep`). The existing `deposit` event is unchanged and never fires bridge phases.
  - `EarningProvider` gains an **optional** `supportsCrossChainDeposit(source, destination)` method (mirroring swap-kit's route-aware `supportsRoute`). The kit only routes cross-chain deposits to providers that implement it and return `true`; existing provider implementations keep compiling and never receive cross-chain params.

  Deprecations (existing aliases keep working):

  - `EarnDepositResult` — use `EarnSameChainDepositResult`, or `EarnDepositOutcome` for cross-chain-aware code. This alias may repoint to `EarnDepositOutcome` in the next major.
  - `DepositParams` / `DepositServiceParams` — use `SameChainDepositParams` / `SameChainDepositServiceParams`, or the `Any*` unions for cross-chain-aware code.
  - `depositParamsSchema` — use `anyDepositParamsSchema`, which also accepts cross-chain deposit params.

  Type changes (diagnostic types only):

  - `EarnErrorTrace`: the deposit variant's `params` is now `AnyDepositServiceParams`, and `steps` may include `EarnBridgeDepositStep` entries for cross-chain deposits.
  - `EarningProvider.retry()` and `EarnKit.retry()` may now resolve to `EarnCrossChainDepositResult` when resuming a cross-chain deposit.
  - `EarnActionName` gains `'crossChainDeposit'`.

### Patch Changes

- EarnKit now validates amount precision and format locally to match the Earn
  Service, so malformed amounts fail fast with a clear SDK error instead of a
  server round-trip.

  - Same-chain deposit/withdraw/quote amounts are now capped at 6 decimal places
    (USDC/EURC), matching the server and the existing cross-chain amount schema.
    Previously up to 18 decimal places passed local validation and only the
    server rejected 7+ decimal-place inputs.
  - Non-canonical numeric forms are now rejected locally on both same-chain and
    cross-chain amounts: a leading decimal point ('.5') and leading zeros
    ('00.5', '07'). These passed the generic decimal-string validator but were
    rejected by the server.

  Well-formed amounts are unaffected.

- Validate the `config` argument passed to `createEarnKitContext` (and therefore `new EarnKit`) at runtime. A null, non-object, or array config — or a non-array `providers` — now throws a structured `INPUT_VALIDATION_FAILED` KitError instead of a raw `TypeError` or being silently accepted, matching the validation behavior of every other public surface.
- Harden EarnKit deposit input validation: validate `from.address` overrides on the same-chain and cross-chain source adapter contexts as real EVM addresses (rejecting malformed values like `0x123` and empty strings that would silently fall back to the adapter wallet); make `sameChainDepositParamsSchema` and `crossChainDepositParamsSchema` `.strict()` so stray or typo'd top-level keys produce a clear error instead of being silently stripped; reject the zero (burn) address as a cross-chain `to.recipientAddress`; and reject EVM addresses (vault and recipient) carrying an invalid EIP-55 mixed-case checksum via a new keccak256-based checksum helper, while still accepting all-lowercase and all-uppercase forms that claim no checksum.
- Correct EarnKit documentation. The INPUT error-range comment in `earnErrorCodes.ts` now reads `1100-1105` to include the already-present `1105` `UNSUPPORTED_BRIDGE_ROUTE` code, and a stale example vault address in JSDoc/example blocks is replaced with the live EarnKit USDC vault `0xAabbeF1D3971c710276ed41eC791BbE14CdB8E88`. Documentation/comment-only; no runtime behavior changes.

## 1.1.0

### Minor Changes

- EarnKit now emits step-level events for multi-phase operations (`deposit`, `withdraw`, `claimRewards`) via `kit.on()` / `kit.off()`, and exposes `kit.retry(error)` to resume an operation that failed after a prior phase (such as a token approval) already succeeded. Failed earn operations now throw a `KitError` carrying step progress and resume context, and `isRetryableError` recognizes `RESUMABLE` errors as retryable.

  Custom `EarningProvider` implementations must now provide `actionDispatcher`, `registerDispatcher(dispatcher)`, `supportsRetry(error)`, and `retry(error)`. A no-op `registerDispatcher` and a `supportsRetry` that returns `false` are sufficient for providers that do not participate in events or resume.

## 1.0.1

### Patch Changes

- Add a "coming soon" disclosure to the Earn Kit product entry in the README, and replace placeholder vault addresses with the mock testnet vault address (`0xAabbeF1D3971c710276ed41eC791BbE14CdB8E88`) in all examples.

## 1.0.0

### Major Changes

- Initial EarnKit release for Arc Testnet earn flows. Supports vault discovery, position fetching, deposit/withdrawal quotes, reward claims, and transaction execution.
