From 1741ada6a79467ae84000d945004482a9baee944 Mon Sep 17 00:00:00 2001 From: Jayesh Bhole <54071350+jayeshbhole@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:51:12 +0530 Subject: [PATCH 1/2] docs: add Smart Recipes section Eight pages under Onramp: introduction, quickstart, deposits, vault discovery, bridge and swap, quotes, tracking status, errors. Sidebar group added next to Smart Routing Address. --- .../onramp/smart-recipes/bridge-and-swap.mdx | 31 ++++++ docs/pages/onramp/smart-recipes/deposits.mdx | 95 ++++++++++++++++++ docs/pages/onramp/smart-recipes/errors.mdx | 53 ++++++++++ docs/pages/onramp/smart-recipes/index.mdx | 42 ++++++++ .../pages/onramp/smart-recipes/quickstart.mdx | 99 +++++++++++++++++++ docs/pages/onramp/smart-recipes/quotes.mdx | 66 +++++++++++++ .../onramp/smart-recipes/tracking-status.mdx | 77 +++++++++++++++ .../onramp/smart-recipes/vault-discovery.mdx | 84 ++++++++++++++++ vocs.config.tsx | 38 +++++++ 9 files changed, 585 insertions(+) create mode 100644 docs/pages/onramp/smart-recipes/bridge-and-swap.mdx create mode 100644 docs/pages/onramp/smart-recipes/deposits.mdx create mode 100644 docs/pages/onramp/smart-recipes/errors.mdx create mode 100644 docs/pages/onramp/smart-recipes/index.mdx create mode 100644 docs/pages/onramp/smart-recipes/quickstart.mdx create mode 100644 docs/pages/onramp/smart-recipes/quotes.mdx create mode 100644 docs/pages/onramp/smart-recipes/tracking-status.mdx create mode 100644 docs/pages/onramp/smart-recipes/vault-discovery.mdx diff --git a/docs/pages/onramp/smart-recipes/bridge-and-swap.mdx b/docs/pages/onramp/smart-recipes/bridge-and-swap.mdx new file mode 100644 index 0000000..b14cf8a --- /dev/null +++ b/docs/pages/onramp/smart-recipes/bridge-and-swap.mdx @@ -0,0 +1,31 @@ +# Bridge & Swap + +Bridge funds to a different chain and swap them into a target token, with one signature. The flow uses the same SRA as a deposit. The destination action is a swap. The `owner` receives the output. + +```ts +const quote = await sr.bridgeAndSwap({ + owner: "0xUSER", + amount: "100", + token: TOKENS.USDC, // source funding token + srcChainId: 8453, // Base + destChainId: 42161, // Arbitrum + toToken: "0xTARGET", // the token to receive on the destination chain + slippage: 100, // bps, default 100 (= 1%) +}); +``` + +Execute and track the quote in the same way as a deposit: + +```ts +for (const call of quote.transaction.calls) { + await wallet.sendTransaction({ ...call, value: BigInt(call.value) }); +} + +await sr.watchStatus(quote.sra, { onStatusChange: (s) => console.log(s.state) }).done; +``` + +The quote does not include the vault fields (`vaultApy`, `estimatedShares`). It includes `swapMinOutput`. This value is an estimate. The server derives the enforced minimum output from your `slippage` value. + +:::info +The server can disable swap routes. A disabled route rejects with `FEATURE_DISABLED` (403). Same-token routes are not affected. This includes same-chain deposits and plain cross-chain bridges. See [Errors](/onramp/smart-recipes/errors). +::: diff --git a/docs/pages/onramp/smart-recipes/deposits.mdx b/docs/pages/onramp/smart-recipes/deposits.mdx new file mode 100644 index 0000000..2ebb5b2 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/deposits.mdx @@ -0,0 +1,95 @@ +# Deposits + +Each deposit routes through a Smart Routing Address and returns a [Quote](/onramp/smart-recipes/quotes). This applies to same-chain and cross-chain deposits. The route is same-chain when `srcChainId === destChainId`. You do not select the route yourself. + +## Protocol facades + +Each facade binds its protocol. The server maps the protocol to an adapter. The adapter builds the deposit calls and verifies the target on-chain before it quotes. Example: you pass a Morpho-Blue market where a 4626 vault is expected. The call fails immediately with a typed error. No funds move. + +```ts +sr.aave.deposit(params) // no `into` — the pool follows from token + destChainId +sr.morpho.deposit(params) // `into` required +sr.fluid.deposit(params) // `into` required +sr.yearn.deposit(params) // `into` required +sr.erc4626.deposit(params) // unbranded ERC-4626 vault by id/address +``` + +## Parameters + +Each deposit takes this core shape. Facades narrow it. Aave drops `into`. Morpho, Fluid, and Yearn require it. + +```ts +type DepositParams = { + owner: Address // funds + signs + receives shares + gets refunds (one role) + amount: number | string // display units; the server scales by decimals (string math, no float) + token: TokenSymbol | Address // symbol → canonical per-chain address; address to disambiguate (USDC.e) + srcChainId: number // the chain the user funds from + destChainId?: number // the execution chain; equal to srcChainId ⇒ same-chain (no bridge). + // optional when `into` is a Vault (derived from vault.chainId); + // required for Aave or a string id/address `into` + into?: string | Vault // multi-vault only: vault id/address, or a Vault from listVaults() + slippage?: number // bps, default 100 (= 1%) +} +``` + +- `owner` is one role: funder, signer, shares receiver, and refund recipient. There is no separate `beneficiary`. +- `amount` is in display units. `"100"` means 100 USDC. Use a `string` for very large values or values with more than 15 significant figures. +- `token` accepts a symbol or an address. `TOKENS` contains `USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE`. A symbol resolves to the canonical per-chain address. Pass a raw address for a variant (USDC.e) or an arbitrary token. +- `slippage` is an integer in bps. `50` means 0.5%. The server enforces the real floor. The quote's `swapMinOutput` is an estimate. + +### Chain discovery into deposits with `into` + +When `into` is a `Vault` object from [`listVaults()`](/onramp/smart-recipes/vault-discovery), you can omit `destChainId`. The vault contains its own `chainId`: + +```ts +const { vaults } = await sr.listVaults({ asset: TOKENS.USDC, chains: [42161] }); + +await sr.morpho.deposit({ + owner, + amount: "100", + token: TOKENS.USDC, + srcChainId: 8453, + into: vaults[0], // destChainId derived = vault.chainId +}); +``` + +## Aave + +Aave has one pool for each chain. The `token` and `destChainId` identify the target: + +```ts +await sr.aave.deposit({ + owner, + amount: "100", + token: TOKENS.USDC, + srcChainId: 8453, // Base + destChainId: 42161, // Arbitrum + slippage: 50, // 0.5% +}); +``` + +The `owner` receives the canonical aToken position. + +`into` is optional for Aave. Omit it to supply the reserve of the funding token. Pass an Aave listing from `listVaults({ protocol: 'aave' })` to select a different reserve. The funding token then routes into that reserve. + +## Generic deposits with `depositIntoVault` + +Use `depositIntoVault` for a vault that has no facade. This includes ERC-4626 vaults that ZeroDev does not list. No facade binds the protocol here, so you must pass it: + +```ts +await sr.depositIntoVault({ + owner, + amount: "100", + token: TOKENS.USDC, + srcChainId: 42161, + destChainId: 42161, + into: "0xVAULT", + protocol: "erc4626", // required — 'aave' | 'morpho' | 'fluid' | 'yearn' | 'erc4626' +}); +``` + +The server rejects an unknown protocol with `UNKNOWN_PROTOCOL`. Use a facade when one exists. + +## Failure behavior + +A destination action can revert after the bridge. In that case the funds stay in the SRA. ZeroDev does not hold the funds. Only the `owner` can recover them, with `getWithdrawCalls`. See [Tracking Status](/onramp/smart-recipes/tracking-status#recovering-funds). diff --git a/docs/pages/onramp/smart-recipes/errors.mdx b/docs/pages/onramp/smart-recipes/errors.mdx new file mode 100644 index 0000000..2827e20 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/errors.mdx @@ -0,0 +1,53 @@ +# Errors + +Each rejection is a `SmartRecipeError`. It contains a `code`, a `message`, and a `requestId` (from the server's `x-request-id` header). Include the `requestId` in a bug report. Each code has a bound subclass, so you can branch with `instanceof`: + +```ts +import { SmartRecipeError, QuoteExpiredError } from "@zerodev/smart-recipes"; + +try { + await sr.aave.deposit({ /* … */ }); +} catch (e) { + if (e instanceof QuoteExpiredError) { + // request a new quote + } else if (e instanceof SmartRecipeError) { + console.error(`[${e.code}] ${e.message} (request ${e.requestId})`); + } +} +``` + +## Error codes + +| HTTP | Code | When | +|---|---|---| +| 400 | `INVALID_REQUEST` | A parameter is missing or malformed | +| 400 | `UNSUPPORTED_TOKEN` | The token does not resolve | +| 400 | `UNKNOWN_PROTOCOL` | The `protocol` has no registered adapter | +| 400 | `VAULT_TYPE_MISMATCH` | The target is not the expected vault kind (on-chain probe) | +| 400 | `ASSET_MISMATCH` | The vault asset is not the expected token | +| 400 | `CHAIN_NOT_SUPPORTED` | The chain is not configured on the server | +| 400 | `VAULT_DEPOSITS_DISABLED` | The vault's on-chain `maxDeposit` is 0. It accepts no deposits. | +| 403 | `SANCTIONED_ADDRESS` | The owner is on the OFAC SDN list | +| 403 | `VAULT_BLOCKED` | The vault is on the server blocklist | +| 403 | `FEATURE_DISABLED` | Swap-leg routes are disabled on the server | +| 403 | `ACCESS_DENIED` | The origin or IP is not on the project's allowlist | +| 409 | `VAULT_CAP_EXCEEDED` | The amount is over the vault's remaining capacity | +| 410 | `QUOTE_EXPIRED` | The quote is past its TTL. Request a new quote. | +| 413 | `PAYLOAD_TOO_LARGE` | The request body is over the size cap | +| 422 | `INSUFFICIENT_AMOUNT` | The amount is below the vault minimum after fees | +| 422 | `SWAP_ROUTE_NOT_FOUND` | No swap route was found | +| 429 | `RATE_LIMITED` | Too many requests. Idempotent GETs retry with backoff. | +| 500 | `INTERNAL_ERROR` | An unexpected server error | +| 502 | `SRA_UNAVAILABLE` / `QUOTER_UNAVAILABLE` / `RPC_UNAVAILABLE` | An upstream service failed | + +## Codes that need special handling + +`VAULT_CAP_EXCEEDED` and `VAULT_DEPOSITS_DISABLED` are different conditions. For `VAULT_CAP_EXCEEDED`, try a smaller amount. For `VAULT_DEPOSITS_DISABLED`, the vault accepts no deposits at all. Select a different vault. [`preflight`](/onramp/smart-recipes/vault-discovery#preflight) shows both conditions before you quote. + +`FEATURE_DISABLED` means the route needs a swap leg, and the server has swaps disabled. Same-token routes are not affected. This includes same-chain deposits and plain cross-chain bridges. A retry with different parameters does not help. + +`QUOTE_EXPIRED` occurs because a quote lives for about 60 seconds. Request a new quote. + +## Retry behavior + +The SDK does not retry quote-building POST requests. Each POST can create a new SRA on the server. The SDK retries idempotent GET requests on 429, 502, 503, and network failures, with backoff, up to the client's `maxRetries`. diff --git a/docs/pages/onramp/smart-recipes/index.mdx b/docs/pages/onramp/smart-recipes/index.mdx new file mode 100644 index 0000000..a17ba45 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/index.mdx @@ -0,0 +1,42 @@ +# Smart Recipes + +Smart Recipes turns a DeFi intent into one SDK call. Examples of intents: "deposit funds into Aave", "deposit funds into a Morpho vault", "bridge and swap to this token". The funds can come from a different chain. + +Each call returns a **Quote**. The Quote contains a [Smart Routing Address](/onramp/smart-routing-address) (SRA) and a source-chain transaction that is ready to sign. It also contains an equivalent ERC-4337 user operation. The user sends funds to the SRA. The ZeroDev relayer bridges the funds to the destination chain and runs the prepared calls there. + +Smart Recipes runs on the latest [SRA v1](/onramp/smart-routing-address/alpha). Each recipe executes through a v1 routing address. You do not create or manage the address yourself. + +You do not write bridge, swap, vault, or relayer code. The server does this work. + +## How it works + +``` +1. Quote sr.morpho.deposit({...}) → Quote { sra, transaction, userOp, ... } +2. Fund the user signs quote.transaction (or quote.userOp) — one transfer to the SRA +3. Settle the ZeroDev relayer bridges and runs the deposit on the destination chain +4. Track sr.watchStatus(quote.sra) → PENDING → BRIDGING → EXECUTING → COMPLETED +``` + +The SDK is a thin HTTP client with no runtime dependencies. It contains no signer and no bundler. You sign and send the prepared transaction with the wallet stack your app already has. The server creates the SRA, builds the calldata, finds the routes, lists the vaults, and calculates the fees. New vaults and protocols do not need an SDK update. + +## What you can build + +- Cross-chain yield deposits. A user holds USDC on Base and deposits into a Morpho vault on Arbitrum with one signature. +- One-click earn flows. Find vaults with `listVaults` and pass a result directly into a deposit. +- Chain-abstracted swaps. Bridge and swap to a target token on a different chain. + +## Protocols + +Each protocol has its own namespace: + +| Facade | Notes | +|---|---| +| `sr.aave.deposit` | The token and chain identify the pool. No vault is necessary. | +| `sr.morpho.deposit` | A vault is required. | +| `sr.fluid.deposit` | A vault is required. | +| `sr.yearn.deposit` | A vault is required. | +| `sr.erc4626.deposit` | Any unbranded ERC-4626 vault, by address. | + +Two generic engine methods are also available: `sr.depositIntoVault` (any protocol, passed explicitly) and `sr.bridgeAndSwap`. + +See the [Quickstart](/onramp/smart-recipes/quickstart) to start. diff --git a/docs/pages/onramp/smart-recipes/quickstart.mdx b/docs/pages/onramp/smart-recipes/quickstart.mdx new file mode 100644 index 0000000..f3ad8f9 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/quickstart.mdx @@ -0,0 +1,99 @@ +# Quickstart + +## Installation + +:::code-group + +```bash [npm] +npm i @zerodev/smart-recipes +``` + +```bash [yarn] +yarn add @zerodev/smart-recipes +``` + +```bash [pnpm] +pnpm i @zerodev/smart-recipes +``` + +```bash [bun] +bun add @zerodev/smart-recipes +``` + +::: + +The SDK needs Node 18 or later. It uses the global `fetch`. You can inject a different fetch. The package ships as ESM and CJS. + +## Create a client + +```ts +import { createSmartRecipes, TOKENS } from "@zerodev/smart-recipes"; + +const sr = createSmartRecipes({ + serverUrl: "https://recipes.example.com", + projectId: "", +}); +``` + +`createSmartRecipes` is synchronous. All async work occurs when you call a method. + +| Param | Type | Required | Notes | +|---|---|---|---| +| `serverUrl` | `string` | yes | The Smart Recipes server base URL | +| `projectId` | `string` | yes | Your ZeroDev project ID. Sent on every request. | +| `fetch` | `typeof fetch` | no | An injectable fetch, for tests or non-browser environments | +| `timeoutMs` | `number` | no | The abort timeout for each request. Default 30000. | +| `maxRetries` | `number` | no | Retries on transient failures. Applies to idempotent GETs only. Default 2. | + +There is no API key. The server controls access with your `projectId` and a per-project allowlist of origins and IP addresses. + +## Get a quote + +Find a vault. Then deposit into it with one call: + +```ts +// Find USDC vaults on Arbitrum +const { vaults } = await sr.listVaults({ asset: TOKENS.USDC, chains: [42161] }); + +// Quote a deposit: the user funds from Base, the vault is on Arbitrum +const quote = await sr.morpho.deposit({ + owner: "0xUSER", // funds + signs + receives shares + amount: "100", // display units — the server scales by token decimals + token: TOKENS.USDC, // symbol → canonical per-chain address + srcChainId: 8453, // Base + into: vaults[0], // Vault object — destChainId derived from it +}); + +console.log(quote.sra); // the address the user funds +console.log(quote.vaultApy); // APY snapshot +console.log(quote.estimatedShares); // expected vault shares +``` + +A quote prepares the transaction. It does not broadcast. The SDK has no signer. + +## Execute + +Send the funding transaction with your own wallet: + +```ts +// EOA: send the calls in order +for (const call of quote.transaction.calls) { + await wallet.sendTransaction({ ...call, value: BigInt(call.value) }); +} + +// …or as one ERC-4337 user op with a kernel client +await kernelClient.sendUserOp({ callData: quote.userOp.callData }); +``` + +The two forms have the same intent: send `amount` of `token` to the SRA. The relayer runs the vault-side approve and deposit calls on the destination chain. Your user does not sign those calls. + +## Track + +```ts +const watcher = sr.watchStatus(quote.sra, { + onStatusChange: (s) => console.log(s.state), // PENDING → BRIDGING → EXECUTING → COMPLETED +}); +await watcher.done; +``` + +See [Tracking Status](/onramp/smart-recipes/tracking-status) for the full lifecycle. See [Deposits](/onramp/smart-recipes/deposits) for all protocols and parameters. diff --git a/docs/pages/onramp/smart-recipes/quotes.mdx b/docs/pages/onramp/smart-recipes/quotes.mdx new file mode 100644 index 0000000..acf15e2 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/quotes.mdx @@ -0,0 +1,66 @@ +# Quotes & Execution + +Each recipe returns a **Quote**. A quote prepares the transaction. It does not broadcast. You send it with your own wallet. + +## The `Quote` type + +```ts +type Quote = { + quoteId: string + expiresAt: string // ISO-8601, typically now + 60s + sra: Address | null // the address to fund; always set for deposits + transaction: { chainId: number; calls: OnChainCall[] } // EOA sends in order + userOp: { callData: Hex; calls: OnChainCall[]; chainId: number } // kernel client fills the rest + srcTransactions?: { chainId: number; calls: OnChainCall[] }[] // present when a src swap is needed + estimatedFees: { // amounts in route-token base units, NOT USD + totalFeeAmount: string // summed in totalFeeToken units; partial when denominations mix + totalFeeToken: Address | null // null = mixed/none — render perChain instead + perChain: { chainId: number; feeAmount: string; feeToken: Address | null }[] + } + estimatedReceiveAmount: string // base units on destChain + estimatedShares?: string // vault recipes only + vaultApy?: number // percent (2.57 = 2.57%) + swapMinOutput?: string // estimate — the server enforces the real floor + route?: { // the resolved flow, for UI display + bridgeTokenType: string | null // the token the SRA is funded with + requiresSrcSwap: boolean // owner-signed swap before funding + requiresDestSwap: boolean // server-synthesized swap before the deposit + sameChain: boolean // no bridge leg (still an SRA deposit) + } + recipeVersion: number +} + +type OnChainCall = { to: Address; data: Hex; value: string } // value is a decimal string +``` + +All wire amounts are base-10 strings, because JSON has no bigint. Parse them yourself: `BigInt(quote.estimatedReceiveAmount)`. + +## Execute a quote + +`transaction` and `userOp` have the same intent: send `amount` of `token` to the SRA. When the funding token is different from the route token, the calls start with owner-signed approve and swap calls. + +For an EOA, send `transaction.calls` in sequence: + +```ts +for (const call of quote.transaction.calls) { + await wallet.sendTransaction({ ...call, value: BigInt(call.value) }); +} +``` + +For ERC-4337, send `userOp.callData` as one user operation. The server encodes a Kernel v3 / ERC-7579 `executeBatch(calls)`. Your kernel client supplies the sender, nonce, gas, and signature: + +```ts +await kernelClient.sendUserOp({ callData: quote.userOp.callData }); +``` + +The relayer runs the vault-side approve and deposit on the destination chain. Your user signs only the funding transaction. + +## Expiry + +A quote usually expires after 60 seconds. Send the funds promptly. A stale quote rejects with `QuoteExpiredError`. Request a new quote in that case. + +## Fees + +The `estimatedFees` amounts are in base units of the route token, not USD. When the fee tokens differ across chains, `totalFeeToken` is `null`. Show the `perChain` entries in that case. + +Sponsored fee entries are not included in the sums. Sponsorship follows the gas policy of your project, through your `projectId`. There is no per-quote flag. diff --git a/docs/pages/onramp/smart-recipes/tracking-status.mdx b/docs/pages/onramp/smart-recipes/tracking-status.mdx new file mode 100644 index 0000000..f7d5572 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/tracking-status.mdx @@ -0,0 +1,77 @@ +# Tracking Status + +The server derives the recipe status from on-chain evidence at the SRA: deposits seen, bridges sent, executions settled. The server does not assert a status. The status stays correct when funds arrive late or when a retry occurs. + +## Lifecycle + +``` +PENDING → BRIDGING → EXECUTING → COMPLETED + → FAILED +PENDING (no funds within 1h) → ABANDONED +``` + +```ts +type RecipeState = + | "PENDING" // quote issued, SRA not yet funded + | "BRIDGING" // funds received, bridge in flight (cross-chain only) + | "EXECUTING" // destination action in progress + | "COMPLETED" + | "FAILED" // carries failureReason + | "ABANDONED"; // the SRA received no funds within 1 hour after the quote +``` + +An `ABANDONED` recipe is not locked. Funds that arrive late still execute. A new `watchStatus` call picks the recipe up again from live evidence. + +## `watchStatus` + +The SDK polls the server, with backoff. Polling stops on a terminal state (`COMPLETED` / `FAILED` / `ABANDONED`). + +```ts +const watcher = sr.watchStatus(quote.sra, { + interval: 4000, // ms; default 4000 + timeout: 600_000, // total watch bound, default 10 min; 0 = poll indefinitely + maxRetries: 5, // consecutive poll failures tolerated; default 5 + onStatusChange: (s) => console.log(s.state), + onError: (e) => console.error(e), // persistent poll failure or timeout +}); + +await watcher.done; // resolves on terminal state / unsubscribe, rejects on persistent failure +watcher.stop(); // or call watcher() — unsubscribe +``` + +## `getStatus` + +Use `getStatus` for one read of the same data: + +```ts +const status = await sr.getStatus(sra); +// status.state — RecipeState +// status.deposits — per-deposit evidence: deposit tx, bridge tx, execution tx +// status.failureReason — the first deposit error when state === 'FAILED' +``` + +## Recovering funds + +There is no on-chain refund fallback. When the destination action reverts, the funds stay in the SRA. The SRA is non-custodial. Only the `owner` can recover the funds. `getWithdrawCalls` returns the recovery calls for the owner to sign: + +```ts +const { data, receiver } = await sr.getWithdrawCalls({ + sra, + tokens: [{ chainId: 42161, token: "0xTOKEN" }], +}); + +for (const { chainId, calls } of data) { + for (const call of calls) { + await wallet.sendTransaction({ ...call, chainId, value: BigInt(call.value) }); + } +} +``` + +## Other SRA reads + +```ts +sr.getSraInfo({ sra }) // the stored routing config: owner, actions, src tokens, slippage +sr.getSraFeeEstimates({ sra }) // per-chain bridge fee estimates (incl. sponsored entries) +sr.getDepositStatus({ sra, owner, destChainId, vaultId }) + // deposit phase (pending/bridging/deposited/failed) + vault balance +``` diff --git a/docs/pages/onramp/smart-recipes/vault-discovery.mdx b/docs/pages/onramp/smart-recipes/vault-discovery.mdx new file mode 100644 index 0000000..388b1fa --- /dev/null +++ b/docs/pages/onramp/smart-recipes/vault-discovery.mdx @@ -0,0 +1,84 @@ +# Vault Discovery + +The server finds vaults for you. You can pass a result directly into a deposit. You do not enter the address, chain, or asset again. + +## `listVaults` + +```ts +const { vaults, nextPage } = await sr.listVaults({ + asset: TOKENS.USDC, // optional — filter by asset + chains: [42161], // optional — filter by chain + protocol: "morpho", // optional — filter by protocol + minTvl: 1_000_000, // optional — USD TVL floor + minApy: 2, // optional — percent + page: 0, // zero-based; walk until nextPage is null +}); +``` + +:::info +If you omit `minTvl`, the listing applies a default minimum of $100,000. Pass `minTvl: 0` to include vaults with a lower TVL. +::: + +## The `Vault` type + +The type is a union that discriminates on `category`. A vault contains the fields that you select a vault by, and the fields that a deposit routes with: + +```ts +type VaultCommon = { + id: string // server vault id — what `into` keys on + address: Address // the vault contract (deposit target) + chainId: number + protocol: string // 'aave' | 'morpho' | 'fluid' | 'yearn' | ... + asset: { symbol: string; address: Address; decimals: number } + apy: number | null // percent (2.57 = 2.57%) + tvlUsd: number | null + name?: string +} + +type Vault = + | (VaultCommon & { category: "lend" }) + | (VaultCommon & { category: "liquid-staking" }) + | (VaultCommon & { category: "fixed-yield"; maturity: string }) +``` + +`maturity` exists only on fixed-yield vaults. The compiler blocks access to it until you narrow on `category`. + +You can pass a `Vault` directly as the `into` of a deposit. The deposit gets `destChainId` from the vault. + +## `getVault` + +`getVault` returns the detail view: a `Vault` plus data that the list omits. You can also pass the result as `into`. + +```ts +const details = await sr.getVault(vaultId, chainId); +// details.apyBreakdown { base?, reward?, total? } +// details.apy7day / details.apy30day +// details.description +``` + +Pass `chainId` when you know it. The call then uses the direct single-vault endpoint and does not scan a list. + +## `preflight` + +`preflight` reports the vault state without a quote. It includes `depositsDisabled`: the vault's on-chain `maxDeposit` is 0, so the vault accepts no deposits. Vault listings cannot see this state. Only the on-chain read shows it. + +```ts +const check = await sr.preflight({ + owner: "0xUSER", + vaultId: vault.address, + destChainId: vault.chainId, + amount: "100", +}); +if (check.depositsDisabled) { + // the vault accepts no deposits — select a different vault +} +// check.maxDeposit — remaining cap headroom (null when the vault kind has no per-owner cap) +// check.route — the bridge token, and whether a src or dest swap is required +``` + +## Supported chains and tokens + +```ts +const chains = await sr.getChains(); // ChainInfo[] +const tokens = await sr.getTokens({ chainId: 8453 }); // TokenInfo[] +``` diff --git a/vocs.config.tsx b/vocs.config.tsx index 0cba285..005b3cd 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -291,6 +291,44 @@ export default defineConfig({ }, ], }, + { + text: "Smart Recipes", + collapsed: false, + items: [ + { + text: "Introduction", + link: "/onramp/smart-recipes", + }, + { + text: "Quickstart", + link: "/onramp/smart-recipes/quickstart", + }, + { + text: "Deposits", + link: "/onramp/smart-recipes/deposits", + }, + { + text: "Vault Discovery", + link: "/onramp/smart-recipes/vault-discovery", + }, + { + text: "Bridge & Swap", + link: "/onramp/smart-recipes/bridge-and-swap", + }, + { + text: "Quotes & Execution", + link: "/onramp/smart-recipes/quotes", + }, + { + text: "Tracking Status", + link: "/onramp/smart-recipes/tracking-status", + }, + { + text: "Errors", + link: "/onramp/smart-recipes/errors", + }, + ], + }, ], "/smart-accounts": [ { From bcfc1a2fa50e515084ecbce5280c03e052aec059 Mon Sep 17 00:00:00 2001 From: Jayesh Bhole <54071350+jayeshbhole@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:14:52 +0530 Subject: [PATCH 2/2] docs: smart recipes selling rework, three new pages Landing-style intro, How It Works, Integration Guide, API Reference. Simplified Technical English throughout, short sentences, no em dashes. --- .../onramp/smart-recipes/api-reference.mdx | 82 +++++++++ docs/pages/onramp/smart-recipes/deposits.mdx | 4 +- .../onramp/smart-recipes/how-it-works.mdx | 76 ++++++++ docs/pages/onramp/smart-recipes/index.mdx | 65 ++++--- .../smart-recipes/integration-guide.mdx | 162 ++++++++++++++++++ .../pages/onramp/smart-recipes/quickstart.mdx | 4 +- docs/pages/onramp/smart-recipes/quotes.mdx | 4 +- .../onramp/smart-recipes/tracking-status.mdx | 8 +- .../onramp/smart-recipes/vault-discovery.mdx | 18 +- vocs.config.tsx | 12 ++ 10 files changed, 395 insertions(+), 40 deletions(-) create mode 100644 docs/pages/onramp/smart-recipes/api-reference.mdx create mode 100644 docs/pages/onramp/smart-recipes/how-it-works.mdx create mode 100644 docs/pages/onramp/smart-recipes/integration-guide.mdx diff --git a/docs/pages/onramp/smart-recipes/api-reference.mdx b/docs/pages/onramp/smart-recipes/api-reference.mdx new file mode 100644 index 0000000..4174614 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/api-reference.mdx @@ -0,0 +1,82 @@ +# API Reference + +This page lists every method on the client from `createSmartRecipes(config)`. All methods are async. All rejections are typed [`SmartRecipeError`](/onramp/smart-recipes/errors)s. + +## Deposits + +All deposit methods return a [`Quote`](/onramp/smart-recipes/quotes). They share the core parameters: `owner`, `amount`, `token`, `srcChainId`, `destChainId?`, `slippage?`. See [Deposits](/onramp/smart-recipes/deposits) for the shared vocabulary. + +| Method | Extra params | Notes | +|---|---|---| +| `sr.aave.deposit(p)` | `destChainId` required, `into` optional | Omit `into` to supply the funding token's reserve. Pass an Aave listing to select a different reserve. | +| `sr.morpho.deposit(p)` | `into` required | Vault id, address, or a `Vault` object. | +| `sr.fluid.deposit(p)` | `into` required | | +| `sr.yearn.deposit(p)` | `into` required | | +| `sr.erc4626.deposit(p)` | `into` required | Any ERC-4626 vault, listed or not. | +| `sr.depositIntoVault(p)` | `into` + `protocol` required | The generic engine. `protocol`: `'aave' \| 'morpho' \| 'fluid' \| 'yearn' \| 'erc4626'`. | + +## Swaps + +| Method | Params | Returns | +|---|---|---| +| `sr.bridgeAndSwap(p)` | `owner`, `amount`, `token`, `toToken`, `srcChainId`, `destChainId`, `slippage?` | `Quote` (no vault fields) | + +## Discovery + +| Method | Params | Returns | +|---|---|---| +| `sr.listVaults(p?)` | `asset?`, `chains?`, `protocol?`, `minTvl?`, `minApy?`, `page?` | `{ vaults: Vault[]; nextPage: number \| null }` | +| `sr.getVault(vaultId, chainId?)` | Pass `chainId` when known, for a direct lookup | `VaultDetails` | +| `sr.getChains()` | (none) | `ChainInfo[]` | +| `sr.getTokens(p?)` | `chainId?` | `TokenInfo[]` | +| `sr.listOpportunities(p?)` | Alias of `listVaults` (back-compat) | `VaultPage` | + +If you omit `minTvl` on `listVaults`, a $100,000 default minimum applies. Pass `minTvl: 0` to include smaller vaults. + +## Status + +| Method | Params | Returns | +|---|---|---| +| `sr.getStatus(sra)` | The SRA address | `RecipeStatus`: `state`, `deposits`, `failureReason?` | +| `sr.watchStatus(sra, opts)` | `interval?` (default 4000 ms), `timeout?` (default 10 min, `0` = forever), `maxRetries?` (default 5), `onStatusChange`, `onError?` | `Watcher`: callable unsubscribe, with `.stop()` and `.done` | + +`watchStatus` polls with backoff. It stops on `COMPLETED`, `FAILED`, or `ABANDONED`. A persistent poll failure rejects `watcher.done` and calls `onError`. + +## SRA lifecycle + +| Method | Params | Returns | +|---|---|---| +| `sr.preflight(p)` | `owner`, `vaultId`, `destChainId`, `amount`, `srcChainId?`, `srcToken?` | Vault entry, `maxDeposit`, `depositsDisabled`, route | +| `sr.getSraInfo(p)` | `sra` | The stored routing config: owner, actions, src tokens, slippage | +| `sr.getSraFeeEstimates(p)` | `sra` | Per-chain fee estimates, with `isSponsored` flags | +| `sr.getDepositStatus(p)` | `sra`, `owner`, `destChainId`, `vaultId` (the vault address) | `phase`, `deposits`, `vaultBalance` | +| `sr.getWithdrawCalls(p)` | `sra`, `tokens: [{ chainId, token }]` | Owner-signed recovery calls, per chain | + +## Client config + +```ts +createSmartRecipes({ + serverUrl: string, // required + projectId: string, // required - sent on every request + fetch?: typeof fetch, // injectable, for tests / non-browser runtimes + timeoutMs?: number, // per-request abort timeout, default 30000 + maxRetries?: number, // GET retries on 429/502/503/network, default 2 +}) +``` + +The factory is synchronous. POST requests (quotes) are never retried. Each attempt can create a new SRA. + +## Exports + +```ts +import { + createSmartRecipes, + TOKENS, // USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE + SmartRecipeError, // base error, plus one subclass per code + QuoteExpiredError, + VaultCapExceededError, + // ... see Errors for the full list +} from "@zerodev/smart-recipes"; +``` + +Types: `Quote`, `Vault`, `VaultDetails`, `DepositParams`, `BridgeAndSwapParams`, `RecipeStatus`, `RecipeState`, `Watcher`, and the params and result types of every method above. diff --git a/docs/pages/onramp/smart-recipes/deposits.mdx b/docs/pages/onramp/smart-recipes/deposits.mdx index 2ebb5b2..dcb0a52 100644 --- a/docs/pages/onramp/smart-recipes/deposits.mdx +++ b/docs/pages/onramp/smart-recipes/deposits.mdx @@ -7,7 +7,7 @@ Each deposit routes through a Smart Routing Address and returns a [Quote](/onram Each facade binds its protocol. The server maps the protocol to an adapter. The adapter builds the deposit calls and verifies the target on-chain before it quotes. Example: you pass a Morpho-Blue market where a 4626 vault is expected. The call fails immediately with a typed error. No funds move. ```ts -sr.aave.deposit(params) // no `into` — the pool follows from token + destChainId +sr.aave.deposit(params) // no `into` - the pool follows from token + destChainId sr.morpho.deposit(params) // `into` required sr.fluid.deposit(params) // `into` required sr.yearn.deposit(params) // `into` required @@ -84,7 +84,7 @@ await sr.depositIntoVault({ srcChainId: 42161, destChainId: 42161, into: "0xVAULT", - protocol: "erc4626", // required — 'aave' | 'morpho' | 'fluid' | 'yearn' | 'erc4626' + protocol: "erc4626", // required - 'aave' | 'morpho' | 'fluid' | 'yearn' | 'erc4626' }); ``` diff --git a/docs/pages/onramp/smart-recipes/how-it-works.mdx b/docs/pages/onramp/smart-recipes/how-it-works.mdx new file mode 100644 index 0000000..522cd07 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/how-it-works.mdx @@ -0,0 +1,76 @@ +# How It Works + +This page explains the flow from signature to vault shares. It shows where the funds are at each moment. It also shows what happens on failure. Read it to evaluate the trust model. + +## The flow + +``` +your app sr.morpho.deposit(...) ── 1. quote +user signs one transfer to the SRA ── 2. fund +ZeroDev relayer bridges to the destination ── 3. bridge (cross-chain only) +ZeroDev relayer runs the stored deposit calls ── 4. execute +your app sr.watchStatus(sra) ── 5. track +``` + +### 1. Quote + +The server receives your intent: owner, amount, token, chains, target vault. It then does four things. + +- It verifies the target on-chain. For an ERC-4626 vault it reads `asset()`. For Aave it checks the reserve list. A wrong target fails here, with a typed error. No funds have moved. +- It reads the vault's live state: `maxDeposit` headroom, the deposit minimum, and `previewDeposit` for expected shares. +- It creates a [Smart Routing Address](/onramp/smart-routing-address) (SRA v1). The deposit actions are stored at creation. Nobody can change them afterwards. +- It returns the Quote: the SRA, a ready-to-sign transaction, a user operation, fees, and expected output. + +### 2. Fund + +The user signs one transfer of `amount` `token` to the SRA. That is the only signature in the flow. A source swap adds approve and swap calls to the same batch. + +### 3. Bridge + +The relayer bridges the funds to the SRA on the destination chain. A same-chain deposit skips this step. + +### 4. Execute + +The relayer runs the actions stored in the SRA. Approve the vault. Deposit. Credit the shares to the `owner`. The calls take the full arrived amount. No dust is stranded by a locked-in amount. + +### 5. Track + +The status comes from on-chain evidence at the SRA: deposits seen, bridges sent, executions settled. The server never asserts a state it cannot prove. `watchStatus` polls for you and stops on a terminal state. + +## Where the funds are + +| Moment | Funds are | +|---|---| +| Before funding | In the user's wallet | +| After funding, before bridge | In the user's SRA | +| In the bridge | In the bridge protocol, addressed to the SRA | +| After bridge, before execution | In the user's SRA on the destination chain | +| After execution | Vault shares, credited to the `owner` | + +ZeroDev operates the relayer. ZeroDev never has custody. The SRA is a permissionless contract. Funds leave it in two ways only: the stored actions, or an owner withdrawal. + +## When something fails + +**The target vault is wrong.** The on-chain probe catches it at quote time. You get `VAULT_TYPE_MISMATCH` or `ASSET_MISMATCH` in milliseconds. No funds moved. + +**The vault fills up after the quote.** The deposit call reverts. The funds stay in the SRA. The `owner` recovers them with [`getWithdrawCalls`](/onramp/smart-recipes/tracking-status#recovering-funds), or through the [SRA portal](https://smart-routing-address.zerodev.app/). + +**The user sends the wrong token.** Tokens outside the route rest in the SRA. Same recovery path. + +**The user never sends funds.** The recipe becomes `ABANDONED` after one hour. Nothing is lost. Late funds still execute. + +No failure mode gives the funds to ZeroDev or to a third party. + +## Slippage + +You set `slippage` in bps at quote time. The SRA enforces the minimum-output floor on-chain, at execution. A quoted estimate cannot be front-run below your floor. + +## Why the server builds the route + +Routes, vault lists, calldata, and compliance screening change weekly. They live server-side. The result: + +- A new vault or protocol reaches your users with no SDK update. +- A bad vault can be blocklisted globally, at once. +- Each quote screens the owner against the OFAC SDN list, refreshed daily. + +The SDK stays a thin, dependency-free HTTP client. You almost never need to upgrade it. diff --git a/docs/pages/onramp/smart-recipes/index.mdx b/docs/pages/onramp/smart-recipes/index.mdx index a17ba45..20b6410 100644 --- a/docs/pages/onramp/smart-recipes/index.mdx +++ b/docs/pages/onramp/smart-recipes/index.mdx @@ -1,33 +1,52 @@ # Smart Recipes -Smart Recipes turns a DeFi intent into one SDK call. Examples of intents: "deposit funds into Aave", "deposit funds into a Morpho vault", "bridge and swap to this token". The funds can come from a different chain. +**Cross-chain DeFi deposits in one call.** Your user holds USDC on Base. Your app deposits it into a vault on Arbitrum. One quote, one signature. You write no bridge, swap, vault, or relayer code. -Each call returns a **Quote**. The Quote contains a [Smart Routing Address](/onramp/smart-routing-address) (SRA) and a source-chain transaction that is ready to sign. It also contains an equivalent ERC-4337 user operation. The user sends funds to the SRA. The ZeroDev relayer bridges the funds to the destination chain and runs the prepared calls there. +```ts +const quote = await sr.morpho.deposit({ + owner: user, + amount: "100", + token: TOKENS.USDC, + srcChainId: 8453, // funds on Base + into: vault, // vault on Arbitrum +}); +// user signs one transfer → funds arrive in the vault +``` -Smart Recipes runs on the latest [SRA v1](/onramp/smart-routing-address/alpha). Each recipe executes through a v1 routing address. You do not create or manage the address yourself. +## The problem it removes -You do not write bridge, swap, vault, or relayer code. The server does this work. +A cross-chain deposit normally requires all of this: -## How it works +- a bridge integration, with its failure modes +- a swap integration on one or both chains +- deposit calldata and ABIs for each protocol +- a relayer to execute on the destination chain +- status tracking across two chains +- a recovery path for failed steps -``` -1. Quote sr.morpho.deposit({...}) → Quote { sra, transaction, userOp, ... } -2. Fund the user signs quote.transaction (or quote.userOp) — one transfer to the SRA -3. Settle the ZeroDev relayer bridges and runs the deposit on the destination chain -4. Track sr.watchStatus(quote.sra) → PENDING → BRIDGING → EXECUTING → COMPLETED -``` +Smart Recipes replaces all of it with one SDK call. The server quotes the route. The latest [Smart Routing Address](/onramp/smart-routing-address) (SRA v1) executes it. Your user signs a single transfer. -The SDK is a thin HTTP client with no runtime dependencies. It contains no signer and no bundler. You sign and send the prepared transaction with the wallet stack your app already has. The server creates the SRA, builds the calldata, finds the routes, lists the vaults, and calculates the fees. New vaults and protocols do not need an SDK update. +## Why teams choose it -## What you can build +**One signature for the user.** The user sends one transfer to a deposit address. The bridge, the swap, and the vault deposit happen behind it. No chain switching. No multi-step approval flows. + +**Zero protocol maintenance.** Vault lists, routes, calldata, and fees live on the server. A new vault or protocol needs no SDK update and no redeploy. + +**Non-custodial by construction.** Funds move through permissionless smart contracts. ZeroDev never holds them. Failed funds rest in the user's own routing address. Only the `owner` can recover them. -- Cross-chain yield deposits. A user holds USDC on Base and deposits into a Morpho vault on Arbitrum with one signature. -- One-click earn flows. Find vaults with `listVaults` and pass a result directly into a deposit. -- Chain-abstracted swaps. Bridge and swap to a target token on a different chain. +**Fails fast, not expensively.** The server verifies each deposit target on-chain before it quotes. A wrong vault address returns a typed error in milliseconds. You do not learn about it from a failed bridge. -## Protocols +**Works with any wallet stack.** Each quote carries a plain transaction batch and an ERC-4337 user operation. The SDK has no signer and no runtime dependencies. -Each protocol has its own namespace: +**Gas sponsorship built in.** Your project's gas policy applies to each deposit automatically. Sponsor your users' fees from the dashboard. No code change is necessary. + +## What you can build + +- **Earn features.** Show vaults with live APY and TVL. Deposit from the chain the user's funds are on. +- **Cross-chain onboarding.** Accept deposits from any chain, or from a CEX. +- **Chain-abstracted swaps.** Bridge and swap to a target token in one signature. + +## Supported protocols | Facade | Notes | |---|---| @@ -35,8 +54,12 @@ Each protocol has its own namespace: | `sr.morpho.deposit` | A vault is required. | | `sr.fluid.deposit` | A vault is required. | | `sr.yearn.deposit` | A vault is required. | -| `sr.erc4626.deposit` | Any unbranded ERC-4626 vault, by address. | +| `sr.erc4626.deposit` | Any ERC-4626 vault, by address. Your own vaults included. | + +New protocols land server-side. Your integration does not change. -Two generic engine methods are also available: `sr.depositIntoVault` (any protocol, passed explicitly) and `sr.bridgeAndSwap`. +## Try it -See the [Quickstart](/onramp/smart-recipes/quickstart) to start. +- [Quickstart](/onramp/smart-recipes/quickstart): a first deposit in about 20 lines +- [Live demo](https://github.com/zerodevapp/smart-recipes): vault picker, quote, execute, track +- [How it works](/onramp/smart-recipes/how-it-works): the flow, the custody model, failure behavior diff --git a/docs/pages/onramp/smart-recipes/integration-guide.mdx b/docs/pages/onramp/smart-recipes/integration-guide.mdx new file mode 100644 index 0000000..00ca203 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/integration-guide.mdx @@ -0,0 +1,162 @@ +# Integration Guide + +Build a complete earn feature: vault list, cross-chain deposit, progress tracking, and fund recovery. Most teams ship it in a day. + +The [demo app](https://github.com/zerodevapp/smart-recipes/tree/master/examples/demo) implements this exact flow in React. Use it as a reference. + +## 1. Set up the client + +Create one client for your whole app. There is no API key and no signer. + +```ts +// client.ts +import { createSmartRecipes } from "@zerodev/smart-recipes"; + +export const sr = createSmartRecipes({ + serverUrl: import.meta.env.VITE_SERVER_URL, + projectId: import.meta.env.VITE_ZD_PROJECT_ID, +}); +``` + +## 2. Show vaults + +`listVaults` returns live APY and TVL. Each result can route a deposit directly. A table row becomes a deposit with no extra lookups. + +```ts +const { vaults } = await sr.listVaults({ + chains: [8453, 42161], + minTvl: 1_000_000, +}); + +// render: vault.name, vault.protocol, vault.apy, vault.tvlUsd, vault.asset.symbol +``` + +Build your token selector from the server's registry: + +```ts +const tokens = await sr.getTokens({ chainId: 8453 }); +``` + +## 3. Quote + +The user picked a vault, a source chain, and an amount. Quote it: + +```ts +const quote = await sr.morpho.deposit({ + owner: userAddress, + amount: "100", + token: TOKENS.USDC, + srcChainId: 8453, + into: vault, // the Vault object from listVaults + slippage: 100, // 1% +}); +``` + +Show the quote before the user commits: + +- `quote.estimatedShares`: what they receive +- `quote.vaultApy`: the APY snapshot +- `quote.estimatedFees`: route fees, in token base units +- `quote.expiresAt`: quotes live about 60 seconds + +A quote is free and read-only. Re-quote each time the user edits the form. + +## 4. Execute + +The quote carries two equivalent forms. Use the one that matches your wallet stack. + +For an EOA (wagmi, viem, ethers): + +```ts +for (const call of quote.transaction.calls) { + await walletClient.sendTransaction({ + to: call.to, + data: call.data, + value: BigInt(call.value), + chainId: quote.transaction.chainId, + }); +} +``` + +For a smart account (ERC-4337, EIP-7702), send one batched user op: + +```ts +await kernelClient.sendUserOp({ callData: quote.userOp.callData }); +``` + +The user signs this one step. The relayer runs the bridge and the vault deposit. No further signatures are needed. + +## 5. Track + +Drive your progress UI from `watchStatus`: + +```ts +const watcher = sr.watchStatus(quote.sra, { + onStatusChange: (s) => { + // PENDING → BRIDGING → EXECUTING → COMPLETED + setPhase(s.state); + }, + onError: (e) => setError(e), +}); +await watcher.done; +``` + +`getDepositStatus` adds the vault balance: + +```ts +const ds = await sr.getDepositStatus({ + sra: quote.sra, + owner: userAddress, + destChainId: vault.chainId, + vaultId: vault.address, +}); +// ds.phase: 'pending' | 'bridging' | 'deposited' | 'failed' +// ds.vaultBalance: the user's balance in the vault +``` + +## 6. Handle failure and recovery + +Persist `quote.sra` before execution. Use local storage or your backend. It is the recovery handle. A reload, a revert, or a wrong token leaves funds in the SRA. Only the `owner` can drain it. + +```ts +const { data } = await sr.getWithdrawCalls({ + sra, + tokens: [{ chainId: 42161, token: usdcAddress }], +}); +for (const { chainId, calls } of data) { + for (const call of calls) { + await walletClient.sendTransaction({ ...call, value: BigInt(call.value), chainId }); + } +} +``` + +You can also send users to the [SRA portal](https://smart-routing-address.zerodev.app/). It provides the same recovery with no code on your side. + +## 7. Handle errors + +Every rejection carries a typed code. Three cover most of your UI: + +```ts +import { SmartRecipeError, QuoteExpiredError, VaultCapExceededError } from "@zerodev/smart-recipes"; + +try { + await getQuote(); +} catch (e) { + if (e instanceof QuoteExpiredError) requote(); + else if (e instanceof VaultCapExceededError) suggestSmallerAmount(); + else if (e instanceof SmartRecipeError) showError(e.code, e.message); +} +``` + +See [Errors](/onramp/smart-recipes/errors) for the full code table. + +## Checklist + +- [ ] Client created once, with `serverUrl` and `projectId` +- [ ] Vault list from `listVaults`, token selector from `getTokens` +- [ ] Quote display: shares, APY, fees, expiry countdown +- [ ] Execute path for your wallet stack +- [ ] Progress UI from `watchStatus` +- [ ] `quote.sra` persisted before execution +- [ ] Recovery via `getWithdrawCalls` +- [ ] Error handling for `QUOTE_EXPIRED` and `VAULT_CAP_EXCEEDED` diff --git a/docs/pages/onramp/smart-recipes/quickstart.mdx b/docs/pages/onramp/smart-recipes/quickstart.mdx index f3ad8f9..fcbcd5f 100644 --- a/docs/pages/onramp/smart-recipes/quickstart.mdx +++ b/docs/pages/onramp/smart-recipes/quickstart.mdx @@ -58,10 +58,10 @@ const { vaults } = await sr.listVaults({ asset: TOKENS.USDC, chains: [42161] }); // Quote a deposit: the user funds from Base, the vault is on Arbitrum const quote = await sr.morpho.deposit({ owner: "0xUSER", // funds + signs + receives shares - amount: "100", // display units — the server scales by token decimals + amount: "100", // display units - the server scales by token decimals token: TOKENS.USDC, // symbol → canonical per-chain address srcChainId: 8453, // Base - into: vaults[0], // Vault object — destChainId derived from it + into: vaults[0], // Vault object - destChainId derived from it }); console.log(quote.sra); // the address the user funds diff --git a/docs/pages/onramp/smart-recipes/quotes.mdx b/docs/pages/onramp/smart-recipes/quotes.mdx index acf15e2..e102c9e 100644 --- a/docs/pages/onramp/smart-recipes/quotes.mdx +++ b/docs/pages/onramp/smart-recipes/quotes.mdx @@ -14,13 +14,13 @@ type Quote = { srcTransactions?: { chainId: number; calls: OnChainCall[] }[] // present when a src swap is needed estimatedFees: { // amounts in route-token base units, NOT USD totalFeeAmount: string // summed in totalFeeToken units; partial when denominations mix - totalFeeToken: Address | null // null = mixed/none — render perChain instead + totalFeeToken: Address | null // null = mixed/none - render perChain instead perChain: { chainId: number; feeAmount: string; feeToken: Address | null }[] } estimatedReceiveAmount: string // base units on destChain estimatedShares?: string // vault recipes only vaultApy?: number // percent (2.57 = 2.57%) - swapMinOutput?: string // estimate — the server enforces the real floor + swapMinOutput?: string // estimate - the server enforces the real floor route?: { // the resolved flow, for UI display bridgeTokenType: string | null // the token the SRA is funded with requiresSrcSwap: boolean // owner-signed swap before funding diff --git a/docs/pages/onramp/smart-recipes/tracking-status.mdx b/docs/pages/onramp/smart-recipes/tracking-status.mdx index f7d5572..e2cc876 100644 --- a/docs/pages/onramp/smart-recipes/tracking-status.mdx +++ b/docs/pages/onramp/smart-recipes/tracking-status.mdx @@ -36,7 +36,7 @@ const watcher = sr.watchStatus(quote.sra, { }); await watcher.done; // resolves on terminal state / unsubscribe, rejects on persistent failure -watcher.stop(); // or call watcher() — unsubscribe +watcher.stop(); // or call watcher() - unsubscribe ``` ## `getStatus` @@ -45,9 +45,9 @@ Use `getStatus` for one read of the same data: ```ts const status = await sr.getStatus(sra); -// status.state — RecipeState -// status.deposits — per-deposit evidence: deposit tx, bridge tx, execution tx -// status.failureReason — the first deposit error when state === 'FAILED' +// status.state - RecipeState +// status.deposits - per-deposit evidence: deposit tx, bridge tx, execution tx +// status.failureReason - the first deposit error when state === 'FAILED' ``` ## Recovering funds diff --git a/docs/pages/onramp/smart-recipes/vault-discovery.mdx b/docs/pages/onramp/smart-recipes/vault-discovery.mdx index 388b1fa..cfb7ef9 100644 --- a/docs/pages/onramp/smart-recipes/vault-discovery.mdx +++ b/docs/pages/onramp/smart-recipes/vault-discovery.mdx @@ -6,11 +6,11 @@ The server finds vaults for you. You can pass a result directly into a deposit. ```ts const { vaults, nextPage } = await sr.listVaults({ - asset: TOKENS.USDC, // optional — filter by asset - chains: [42161], // optional — filter by chain - protocol: "morpho", // optional — filter by protocol - minTvl: 1_000_000, // optional — USD TVL floor - minApy: 2, // optional — percent + asset: TOKENS.USDC, // optional - filter by asset + chains: [42161], // optional - filter by chain + protocol: "morpho", // optional - filter by protocol + minTvl: 1_000_000, // optional - USD TVL floor + minApy: 2, // optional - percent page: 0, // zero-based; walk until nextPage is null }); ``` @@ -25,7 +25,7 @@ The type is a union that discriminates on `category`. A vault contains the field ```ts type VaultCommon = { - id: string // server vault id — what `into` keys on + id: string // server vault id - what `into` keys on address: Address // the vault contract (deposit target) chainId: number protocol: string // 'aave' | 'morpho' | 'fluid' | 'yearn' | ... @@ -70,10 +70,10 @@ const check = await sr.preflight({ amount: "100", }); if (check.depositsDisabled) { - // the vault accepts no deposits — select a different vault + // the vault accepts no deposits - select a different vault } -// check.maxDeposit — remaining cap headroom (null when the vault kind has no per-owner cap) -// check.route — the bridge token, and whether a src or dest swap is required +// check.maxDeposit - remaining cap headroom (null when the vault kind has no per-owner cap) +// check.route - the bridge token, and whether a src or dest swap is required ``` ## Supported chains and tokens diff --git a/vocs.config.tsx b/vocs.config.tsx index 005b3cd..e823e48 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -303,6 +303,14 @@ export default defineConfig({ text: "Quickstart", link: "/onramp/smart-recipes/quickstart", }, + { + text: "How It Works", + link: "/onramp/smart-recipes/how-it-works", + }, + { + text: "Integration Guide", + link: "/onramp/smart-recipes/integration-guide", + }, { text: "Deposits", link: "/onramp/smart-recipes/deposits", @@ -327,6 +335,10 @@ export default defineConfig({ text: "Errors", link: "/onramp/smart-recipes/errors", }, + { + text: "API Reference", + link: "/onramp/smart-recipes/api-reference", + }, ], }, ],