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/3] 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/3] 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", + }, ], }, ], From 34ce20e73ba411ff6318c3790407b31fd2b00cdf Mon Sep 17 00:00:00 2001 From: Jayesh Bhole <54071350+jayeshbhole@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:27:53 +0530 Subject: [PATCH 3/3] docs(smart-recipes): cut 11 pages to 5 and match the shipped SDK The pages documented a pre-#33/#34 surface: bridgeAndSwap as a live recipe, swapMinOutput and the requiresSrcSwap/requiresDestSwap route flags, and a QuoteExpiredError requote path the server never emits (expiresAt is stamped and never checked). withdrawFromVault was missing entirely. Fold deposits, quotes, vault-discovery and tracking-status into recipes.mdx; api-reference and errors into reference.mdx. Drop integration-guide, which restated the other pages, and bridge-and-swap, whose method always rejects. Fix the error table: VAULT_CAP_EXCEEDED is 400 not 409, ASSET_MISMATCH 500 not 400, plus five missing codes including SLIPPAGE_TOO_LOW. --- .../onramp/smart-recipes/api-reference.mdx | 82 -------- .../onramp/smart-recipes/bridge-and-swap.mdx | 31 --- docs/pages/onramp/smart-recipes/deposits.mdx | 95 --------- docs/pages/onramp/smart-recipes/errors.mdx | 53 ----- .../onramp/smart-recipes/how-it-works.mdx | 64 +++--- docs/pages/onramp/smart-recipes/index.mdx | 60 ++---- .../smart-recipes/integration-guide.mdx | 162 -------------- .../pages/onramp/smart-recipes/quickstart.mdx | 65 +++--- docs/pages/onramp/smart-recipes/quotes.mdx | 66 ------ docs/pages/onramp/smart-recipes/recipes.mdx | 199 ++++++++++++++++++ docs/pages/onramp/smart-recipes/reference.mdx | 172 +++++++++++++++ .../onramp/smart-recipes/tracking-status.mdx | 77 ------- .../onramp/smart-recipes/vault-discovery.mdx | 84 -------- vocs.config.tsx | 36 +--- 14 files changed, 453 insertions(+), 793 deletions(-) delete mode 100644 docs/pages/onramp/smart-recipes/api-reference.mdx delete mode 100644 docs/pages/onramp/smart-recipes/bridge-and-swap.mdx delete mode 100644 docs/pages/onramp/smart-recipes/deposits.mdx delete mode 100644 docs/pages/onramp/smart-recipes/errors.mdx delete mode 100644 docs/pages/onramp/smart-recipes/integration-guide.mdx delete mode 100644 docs/pages/onramp/smart-recipes/quotes.mdx create mode 100644 docs/pages/onramp/smart-recipes/recipes.mdx create mode 100644 docs/pages/onramp/smart-recipes/reference.mdx delete mode 100644 docs/pages/onramp/smart-recipes/tracking-status.mdx delete mode 100644 docs/pages/onramp/smart-recipes/vault-discovery.mdx diff --git a/docs/pages/onramp/smart-recipes/api-reference.mdx b/docs/pages/onramp/smart-recipes/api-reference.mdx deleted file mode 100644 index 4174614..0000000 --- a/docs/pages/onramp/smart-recipes/api-reference.mdx +++ /dev/null @@ -1,82 +0,0 @@ -# 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/bridge-and-swap.mdx b/docs/pages/onramp/smart-recipes/bridge-and-swap.mdx deleted file mode 100644 index b14cf8a..0000000 --- a/docs/pages/onramp/smart-recipes/bridge-and-swap.mdx +++ /dev/null @@ -1,31 +0,0 @@ -# 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 deleted file mode 100644 index dcb0a52..0000000 --- a/docs/pages/onramp/smart-recipes/deposits.mdx +++ /dev/null @@ -1,95 +0,0 @@ -# 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 deleted file mode 100644 index 2827e20..0000000 --- a/docs/pages/onramp/smart-recipes/errors.mdx +++ /dev/null @@ -1,53 +0,0 @@ -# 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/how-it-works.mdx b/docs/pages/onramp/smart-recipes/how-it-works.mdx index 522cd07..bbabb69 100644 --- a/docs/pages/onramp/smart-recipes/how-it-works.mdx +++ b/docs/pages/onramp/smart-recipes/how-it-works.mdx @@ -1,41 +1,26 @@ # 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. +Where the funds are at each moment, and what happens when a step fails. Read this 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 +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 +**1. Quote.** The server takes your intent and does four things: verifies the target on-chain (`asset()` for an ERC-4626 vault, the reserve list for Aave), reads live vault state (`maxDeposit` headroom, deposit minimum, `previewDeposit` for expected shares), creates a [Smart Routing Address](/onramp/smart-routing-address) with the deposit actions stored at creation, and returns the quote. A wrong target fails here, typed, before any funds move. The stored actions cannot be changed afterwards. -The server receives your intent: owner, amount, token, chains, target vault. It then does four things. +**2. Fund.** The user signs one transfer of `amount` `token` to the SRA. That is the only signature in the flow. -- 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. +**3. Bridge.** The relayer bridges the funds to the SRA on the destination chain. Same-chain deposits skip this. -### 2. Fund +**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, so no dust is stranded by a locked-in figure. -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. +**5. Track.** Status comes from on-chain evidence at the SRA, never from an assertion the server cannot prove. ## Where the funds are @@ -47,30 +32,29 @@ The status comes from on-chain evidence at the SRA: deposits seen, bridges sent, | 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. +ZeroDev operates the relayer and never has custody. The SRA is a permissionless contract, and funds leave it in exactly two ways: 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. +| Failure | What happens | +|---|---| +| Wrong target vault | Caught by the on-chain probe at quote time. `VAULT_TYPE_MISMATCH` or `ASSET_MISMATCH` in milliseconds, no funds moved. | +| Vault fills up after the quote | The deposit call reverts. Funds stay in the SRA, recoverable by the `owner`. | +| User sends the wrong token | Tokens outside the route rest in the SRA. Same recovery path. | +| User never sends funds | The recipe reports `ABANDONED` after one hour. Nothing is lost, and late funds still execute. | -**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/). +No failure mode hands the funds to ZeroDev or to a third party. See [recovering a failed deposit](/onramp/smart-recipes/recipes#recovering-a-failed-deposit). -**The user sends the wrong token.** Tokens outside the route rest in the SRA. Same recovery path. +## Quote expiry -**The user never sends funds.** The recipe becomes `ABANDONED` after one hour. Nothing is lost. Late funds still execute. +`expiresAt` is stamped about 60 seconds out, and it is **advisory**. The server does not reject a late funding transaction, and the SRA stays valid: funds sent after `expiresAt` still execute. -No failure mode gives the funds to ZeroDev or to a third party. +What goes stale is the numbers. `estimatedFees`, `estimatedShares`, and `vaultApy` drift, and vault headroom can disappear, which turns into a revert at execution rather than an error at quote time. So re-quote to keep a UI honest, and re-quote whenever the user edits the form. A quote is free and read-only. ## 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. +You set `slippage` in bps at quote time, and SRA enforces the minimum-output floor on-chain at execution. A quoted estimate cannot be front-run below your floor. If the slippage you ask for cannot cover the route's own fees, the quote is rejected up front with `SLIPPAGE_TOO_LOW` rather than stranding the deposit at fill time. ## 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. +Routes, vault lists, calldata, and compliance screening change weekly, so 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, and every quote screens the owner against the OFAC SDN list. The SDK stays a thin, dependency-free HTTP client that you rarely need to upgrade. diff --git a/docs/pages/onramp/smart-recipes/index.mdx b/docs/pages/onramp/smart-recipes/index.mdx index 20b6410..4df1cd3 100644 --- a/docs/pages/onramp/smart-recipes/index.mdx +++ b/docs/pages/onramp/smart-recipes/index.mdx @@ -1,6 +1,6 @@ # Smart Recipes -**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. +**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, vault, or relayer code. ```ts const quote = await sr.morpho.deposit({ @@ -10,56 +10,34 @@ const quote = await sr.morpho.deposit({ srcChainId: 8453, // funds on Base into: vault, // vault on Arbitrum }); -// user signs one transfer → funds arrive in the vault +// user signs one transfer, funds arrive in the vault ``` -## The problem it removes +Without it you would build a bridge integration, per-protocol deposit calldata, a relayer to execute on the destination chain, cross-chain status tracking, and a recovery path for each failure. Smart Recipes replaces all of it: the server quotes the route, a [Smart Routing Address](/onramp/smart-routing-address) (SRA) executes it, your user signs a single transfer. -A cross-chain deposit normally requires all of this: +Three things worth knowing up front: -- 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 +- **Non-custodial.** Funds move through permissionless contracts. ZeroDev never holds them, and only the `owner` can recover a failed deposit. +- **No protocol maintenance.** Vault lists, routes, and calldata live server-side. A new vault needs no SDK update. +- **Any wallet stack.** Each quote carries both a plain transaction batch and an ERC-4337 user operation. The SDK has no signer and no runtime dependencies. -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. +## Recipes -## Why teams choose it +There are two, and one of them is currently off: -**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. +| Recipe | Call it with | Status | +|---|---|---| +| Deposit into a vault | `sr.aave.deposit`, `sr.morpho.deposit`, `sr.fluid.deposit`, `sr.yearn.deposit`, `sr.erc4626.deposit`, or `sr.depositIntoVault` | Available | +| Withdraw from a vault | `sr.withdrawFromVault` | Available, same-chain only | +| Bridge and swap | `sr.bridgeAndSwap` | Disabled, always rejects | -**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. +The five deposit facades are one recipe with the protocol pre-bound. `sr.depositIntoVault` is the same engine with `protocol` passed explicitly. Everything else in the SDK is discovery, status, and recovery around those calls. -**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. +Smart Recipes performs no swaps of its own. A deposit whose funding token differs from the vault asset still works: SRA converts on delivery as part of its own route. See [Recipes](/onramp/smart-recipes/recipes). -**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. - -**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. - -**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 | -|---|---| -| `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 ERC-4626 vault, by address. Your own vaults included. | - -New protocols land server-side. Your integration does not change. - -## Try it +## Next - [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 +- [Recipes](/onramp/smart-recipes/recipes): every parameter, deposits through recovery - [How it works](/onramp/smart-recipes/how-it-works): the flow, the custody model, failure behavior +- [Reference](/onramp/smart-recipes/reference): the `Quote` type, every method, every error code diff --git a/docs/pages/onramp/smart-recipes/integration-guide.mdx b/docs/pages/onramp/smart-recipes/integration-guide.mdx deleted file mode 100644 index 00ca203..0000000 --- a/docs/pages/onramp/smart-recipes/integration-guide.mdx +++ /dev/null @@ -1,162 +0,0 @@ -# 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 fcbcd5f..cf77c53 100644 --- a/docs/pages/onramp/smart-recipes/quickstart.mdx +++ b/docs/pages/onramp/smart-recipes/quickstart.mdx @@ -1,6 +1,6 @@ # Quickstart -## Installation +## Install :::code-group @@ -8,21 +8,21 @@ npm i @zerodev/smart-recipes ``` -```bash [yarn] -yarn add @zerodev/smart-recipes -``` - ```bash [pnpm] pnpm i @zerodev/smart-recipes ``` +```bash [yarn] +yarn add @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. +Node 18 or later. Ships ESM and CJS, uses the global `fetch`, and has no runtime dependencies. ## Create a client @@ -30,50 +30,49 @@ The SDK needs Node 18 or later. It uses the global `fetch`. You can inject a dif 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. +`projectId` is the only required option. There is no API key: the server authorizes requests by `projectId` plus a per-project allowlist of origins and IP addresses. -| Param | Type | Required | Notes | +| Option | Type | Default | 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. | +| `projectId` | `string` | required | Sent as `x-project-id` on every request | +| `serverUrl` | `string` | ZeroDev's hosted server | Set only for a self-hosted or staging server | +| `fetch` | `typeof fetch` | global `fetch` | Injectable, for tests or non-browser runtimes | +| `timeoutMs` | `number` | `30000` | Per-request abort timeout | +| `maxRetries` | `number` | `2` | Retries on transient failures, idempotent GETs only | -There is no API key. The server controls access with your `projectId` and a per-project allowlist of origins and IP addresses. +`createSmartRecipes` is synchronous. All async work happens when you call a method. -## Get a quote +## Quote a deposit -Find a vault. Then deposit into it with one call: +Find a vault, then deposit into it: ```ts -// Find USDC vaults on Arbitrum +// 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 +// The user's funds are on 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 + owner: "0xUSER", // funds, signs, and receives the shares + amount: "100", // display units; the server scales by token decimals + token: TOKENS.USDC, // symbol resolves to the canonical per-chain address srcChainId: 8453, // Base - into: vaults[0], // Vault object - destChainId derived from it + into: vaults[0], // a Vault object, so destChainId comes from it }); -console.log(quote.sra); // the address the user funds -console.log(quote.vaultApy); // APY snapshot -console.log(quote.estimatedShares); // expected vault shares +console.log(quote.sra); // the address the user funds +console.log(quote.estimatedShares); // expected vault shares +console.log(quote.vaultApy); // APY snapshot ``` -A quote prepares the transaction. It does not broadcast. The SDK has no signer. +A quote prepares the transaction and does not broadcast. The SDK holds no signer. ## Execute -Send the funding transaction with your own wallet: +Send the funding transaction with your own wallet. The two forms carry the same intent: ```ts // EOA: send the calls in order @@ -81,19 +80,21 @@ 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 +// ...or as one ERC-4337 user op 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. +The relayer then runs the vault-side approve and deposit on the destination chain. Your user does not sign those. + +Persist `quote.sra` before you execute. It is the handle for tracking and for recovery. ## Track ```ts const watcher = sr.watchStatus(quote.sra, { - onStatusChange: (s) => console.log(s.state), // PENDING → BRIDGING → EXECUTING → COMPLETED + 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. +Next: [Recipes](/onramp/smart-recipes/recipes) for every parameter, or [Reference](/onramp/smart-recipes/reference) for the `Quote` type and error codes. diff --git a/docs/pages/onramp/smart-recipes/quotes.mdx b/docs/pages/onramp/smart-recipes/quotes.mdx deleted file mode 100644 index e102c9e..0000000 --- a/docs/pages/onramp/smart-recipes/quotes.mdx +++ /dev/null @@ -1,66 +0,0 @@ -# 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/recipes.mdx b/docs/pages/onramp/smart-recipes/recipes.mdx new file mode 100644 index 0000000..bf097f7 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/recipes.mdx @@ -0,0 +1,199 @@ +# Recipes + +Everything you call, in the order you need it: deposit, discover, track, exit. + +## Deposits + +Every deposit routes through a Smart Routing Address and returns a [`Quote`](/onramp/smart-recipes/reference#quote), same-chain and cross-chain alike. The route is same-chain when `srcChainId === destChainId`. You do not select it. + +```ts +type DepositParams = { + owner: Address // funds, signs, receives shares, receives refunds + amount: number | string // display units; the server scales by decimals + token: TokenSymbol | Address // the funding token, symbol or address + srcChainId: number // the chain the user funds from + destChainId?: number // the execution chain; optional when `into` is a Vault + into?: string | Vault // vault id/address, or a Vault from listVaults() + slippage?: number // bps, 1 to 5000, default 100 (= 1%) +} +``` + +- `owner` is one role: funder, signer, share recipient, and refund recipient. There is no separate beneficiary. +- `amount` is display units, so `"100"` means 100 USDC. Use a string for values above 15 significant figures. +- `token` takes a symbol or a raw address. `TOKENS` covers `USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE`. Pass an address for a variant like USDC.e. +- `destChainId` can be omitted when `into` is a `Vault` object, which carries its own `chainId`. Passing both with different values is rejected as a caller bug. It is required for Aave, or when `into` is a string. +- `slippage` bounds SRA's route. A cross-chain quote whose slippage cannot cover the route fees is rejected with `SLIPPAGE_TOO_LOW`, and `details.minSlippageBps` tells you what to retry with. Same-chain quotes are not floor-gated. + +### Protocol facades + +Each facade binds its protocol so the server picks the right adapter. The adapter verifies the target on-chain before quoting, so pointing a 4626 facade at a Morpho-Blue market fails immediately with a typed error and no funds move. + +```ts +sr.aave.deposit(params) // `into` optional: 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) // any ERC-4626 vault by address, listed or not +``` + +Aave has one pool per chain, so `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. Pass `into` (an Aave listing from `listVaults({ protocol: 'aave' })`) to supply a different reserve than the funding token's own. + +For a vault with no facade, use the generic engine and name the protocol yourself: + +```ts +await sr.depositIntoVault({ + owner, + amount: "100", + token: TOKENS.USDC, + srcChainId: 42161, + destChainId: 42161, + into: "0xVAULT", + protocol: "erc4626", // 'aave' | 'morpho' | 'fluid' | 'yearn' | 'erc4626' +}); +``` + +An unknown protocol is rejected with `UNKNOWN_PROTOCOL`. Prefer a facade when one exists. + +### Funding token vs vault asset + +Smart Recipes performs no swaps. A deposit whose funding token differs from the vault asset still works, because SRA's own cross-token route converts on delivery. `quote.route.bridgeTokenDest` tells you what actually lands on the destination chain: the route token, or the vault asset when SRA converts. There is nothing for you to branch on. + +## Discovery + +`listVaults` returns live APY and TVL, and each result can route a deposit with no further lookups. + +```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 floor + minApy: 2, // optional, percent + page: 0, // zero-based; walk until nextPage is null +}); +``` + +:::info +Omitting `minTvl` applies a default floor of $100,000. Pass `minTvl: 0` to include smaller vaults. +::: + +`Vault` is a union discriminated on `category`, so `maturity` is reachable only after narrowing: + +```ts +type VaultCommon = { + id: string // what `into` keys on + address: Address // the vault contract + chainId: number + protocol: string + 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 }) +``` + +`getVault(vaultId, chainId?)` returns the same shape plus `apyBreakdown`, `apy7day`, `apy30day`, and `description`. Pass `chainId` when you know it to use the direct lookup instead of a list scan. The result is still valid as `into`. + +Build selectors from the server registry with `sr.getChains()` and `sr.getTokens({ chainId })`. + +### preflight + +`preflight` reads vault state without creating a quote. Its one unique signal is `depositsDisabled`: the vault's on-chain `maxDeposit` is 0, so it accepts nothing. Vault listings cannot see this, only the on-chain read can. + +```ts +const check = await sr.preflight({ + owner: "0xUSER", + vaultId: vault.address, + destChainId: vault.chainId, + amount: "100", +}); +// check.depositsDisabled the vault accepts no deposits; pick another +// check.maxDeposit remaining headroom, null when the kind has no per-owner cap +// check.route { bridgeTokenType, bridgeTokenDest } or null +``` + +## Tracking + +Status is derived from on-chain evidence at the SRA: deposits seen, bridges sent, executions settled. The server never asserts a state it cannot prove, so the status stays correct when funds arrive late or a step is retried. + +``` +PENDING -> BRIDGING -> EXECUTING -> COMPLETED + -> FAILED +PENDING (no funds within 1h) -> ABANDONED +``` + +`ABANDONED` is not a lock. Funds arriving later still execute, and a fresh `watchStatus` picks the recipe back up from live evidence. + +```ts +const watcher = sr.watchStatus(quote.sra, { + interval: 4000, // ms, default 4000 + timeout: 600_000, // total watch bound, default 10 min; 0 = poll forever + maxRetries: 5, // consecutive poll failures tolerated, default 5 + onStatusChange: (s) => setPhase(s.state), + onError: (e) => setError(e), +}); + +await watcher.done; // resolves on a terminal state, rejects on persistent failure +watcher.stop(); // or call watcher() to unsubscribe +``` + +Polling backs off on failure and stops on `COMPLETED`, `FAILED`, or `ABANDONED`. A persistent failure both calls `onError` and rejects `done`, so the error is always observable. + +Use `sr.getStatus(sra)` for a single read of the same data (`state`, `deposits`, `failureReason`), or `sr.getDepositStatus({ sra, owner, destChainId, vaultId })` to also get `vaultBalance`. + +## Withdraw + +`withdrawFromVault` builds the owner-signed calls that exit a position. It is same-chain and immediate: no SRA, no bridge, and no quote to expire. + +```ts +const exit = await sr.withdrawFromVault({ + owner, + vaultId: vault.id, + chainId: vault.chainId, + max: true, // or amount: "50"; pass neither for a preview +}); + +for (const call of exit.calls) { + await wallet.sendTransaction({ ...call, value: BigInt(call.value), chainId: exit.chainId }); +} +``` + +- Pass `amount` **or** `max`, not both. Pass neither for a preview: same on-chain reads, reports `available` with no calls, which is how a UI shows a position before the user picks an amount. +- `exitAll: true` means the calls name no amount and use the protocol's own full-exit form, so interest accruing before the signature cannot leave dust behind. `false` means a protocol cap (health factor, liquidity, vault limit) held the exit below the position, and the calls carry an exact amount. +- `available` is the ceiling the amount was checked against, which is not always the position size. On Aave a supply backing a borrow reports only what can leave with the health factor intact. + +## Recovering a failed deposit + +There is no on-chain refund fallback. If the destination action reverts, or the user sends a token outside the route, the funds rest in the SRA. Only the `owner` can move them. + +```ts +const { data } = await sr.getWithdrawCalls({ + sra, + tokens: [{ chainId: 42161, token: usdcAddress }], +}); +for (const { chainId, calls } of data) { + for (const call of calls) { + await wallet.sendTransaction({ ...call, value: BigInt(call.value), chainId }); + } +} +``` + +You can also send users to the [SRA portal](https://smart-routing-address.zerodev.app/), which offers the same recovery with no code on your side. diff --git a/docs/pages/onramp/smart-recipes/reference.mdx b/docs/pages/onramp/smart-recipes/reference.mdx new file mode 100644 index 0000000..4b0a8c3 --- /dev/null +++ b/docs/pages/onramp/smart-recipes/reference.mdx @@ -0,0 +1,172 @@ +# Reference + +Every method, the `Quote` type, and every error code. All methods are async and all rejections are typed `SmartRecipeError`s. + +## Quote + +Every recipe returns the same `Quote`. A quote prepares the transaction and does not broadcast. + +```ts +type Quote = { + quoteId: string + expiresAt: string // ISO-8601, ~60s out, advisory (see How It Works) + sra: Address | null // the address to fund; always set for deposits + transaction: { chainId: number; calls: OnChainCall[] } // the whole src batch, in order + userOp: { callData: Hex; calls: OnChainCall[]; chainId: number } + estimatedFees: { // route-token base units, NOT USD + totalFeeAmount: string | null // null when chains mix denominations; use perChain + totalFeeToken: Address | null // null whenever totalFeeAmount is + perChain: { chainId: number; feeAmount: string | null; feeToken: Address | null }[] + } + estimatedReceiveAmount: string // base units on the dest chain + estimatedShares?: string // vault recipes only + vaultApy?: number // percent (2.57 = 2.57%) + route?: { + bridgeTokenType: string | null // the token the SRA is funded with + bridgeTokenSrc?: Address + bridgeTokenDest?: Address // route token, or the vault asset when SRA converts + sameChain: boolean // no bridge leg (still an SRA deposit) + } +} + +type OnChainCall = { to: Address; data: Hex; value: string } // value is a decimal string +``` + +Every amount on the wire is a base-10 string, because JSON has no bigint. Parse it yourself: `BigInt(quote.estimatedReceiveAmount)`. + +### Executing a quote + +`transaction` and `userOp` carry the same intent: send `amount` of `token` to the SRA. `transaction` is the **complete** src-chain batch, in order. There is no `destTransaction`, because the relayer runs the dest-chain actions stored in the SRA. That is the point of the product: the owner signs once, on one chain. + +```ts +// EOA +for (const call of quote.transaction.calls) { + await wallet.sendTransaction({ ...call, value: BigInt(call.value) }); +} + +// ERC-4337: the server encoded a Kernel v3 / ERC-7579 executeBatch(calls) +await kernelClient.sendUserOp({ callData: quote.userOp.callData }); +``` + +Your kernel client supplies the sender, nonce, gas, and signature. `userOp.calls` ships alongside `callData`, so a non-Kernel account (Safe, Biconomy) can re-encode the same batch in its own format. + +### Fees + +Fee amounts are base units of the route token, not USD. The server has no USD oracle, and base units only add within one denomination, so both `totalFeeAmount` and `totalFeeToken` are `null` when the chains disagree. They are always null together: a non-null amount is complete for its named token. Render the `perChain` rows in the null case, and note that a single chain can be `null` too for the same reason. + +Sponsored fees are excluded from every sum, since they are notional and never charged. Sponsorship follows your project's gas policy through `projectId`. There is no per-quote flag. + +## Methods + +### Deposits + +All return a [`Quote`](#quote) and share `owner`, `amount`, `token`, `srcChainId`, `destChainId?`, `slippage?`. See [Recipes](/onramp/smart-recipes/recipes#deposits). + +| Method | Extra params | Notes | +|---|---|---| +| `sr.aave.deposit(p)` | `destChainId` required, `into` optional | Omit `into` for the funding token's own 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 behind all five facades | + +### Withdraw + +| Method | Params | Returns | +|---|---|---| +| `sr.withdrawFromVault(p)` | `owner`, `vaultId`, `chainId`, and `amount` **or** `max` (neither = preview) | Owner-signed exit `calls`, plus `available`, `exitAll`, `amount`, `asset` | +| `sr.getWithdrawCalls(p)` | `sra`, `tokens: [{ chainId, token }]` | Per-chain recovery calls for funds stuck in an SRA | + +### Discovery + +| Method | Params | Returns | +|---|---|---| +| `sr.listVaults(p?)` | `asset?`, `chains?`, `protocol?`, `minTvl?`, `minApy?`, `page?` | `{ vaults: Vault[]; nextPage: number \| null }` | +| `sr.getVault(vaultId, chainId?)` | Pass `chainId` for a direct lookup | `VaultDetails` | +| `sr.getChains()` | none | `ChainInfo[]` | +| `sr.getTokens(p?)` | `chainId?` | `TokenInfo[]` | +| `sr.preflight(p)` | `owner`, `vaultId`, `destChainId`, `amount`, `srcChainId?`, `srcToken?` | `maxDeposit`, `depositsDisabled`, `route`, `vaultEntry` | + +Omitting `minTvl` applies a $100,000 default floor. 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?` (4000), `timeout?` (10 min, `0` = forever), `maxRetries?` (5), `onStatusChange`, `onError?` | `Watcher`: callable unsubscribe, with `.stop()` and `.done` | +| `sr.getDepositStatus(p)` | `sra`, `owner`, `destChainId`, `vaultId` | `phase`, `deposits`, `vaultBalance` | +| `sr.getSraInfo(p)` | `sra` | Stored routing config: owner, actions, src tokens, slippage | +| `sr.getSraFeeEstimates(p)` | `sra` | Per-chain fee estimates with `isSponsored` flags | + +```ts +type RecipeState = "PENDING" | "BRIDGING" | "EXECUTING" | "COMPLETED" | "FAILED" | "ABANDONED"; +``` + +### Disabled + +`sr.bridgeAndSwap(p)` always rejects with `FEATURE_DISABLED`, locally and without a request. Smart Recipes performs no swaps: routing and token conversion belong to SRA. Use a deposit recipe, or route the conversion through SRA directly. + +## Errors + +Each rejection carries a `code`, a `message`, and a `requestId` (from the server's `x-request-id` header). Quote the `requestId` in a bug report. Every code has a bound subclass, so you can branch with `instanceof`: + +```ts +import { SmartRecipeError, VaultCapExceededError } from "@zerodev/smart-recipes"; + +try { + await sr.morpho.deposit({ /* ... */ }); +} catch (e) { + if (e instanceof VaultCapExceededError) suggestSmallerAmount(); + else if (e instanceof SmartRecipeError) showError(e.code, e.message); +} +``` + +| 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 | `VAULT_NOT_ALLOWLISTED` | The vault is not allowlisted on that chain | +| 400 | `CHAIN_NOT_SUPPORTED` | The chain is not configured on the server | +| 400 | `VAULT_CAP_EXCEEDED` | The amount is over the vault's remaining capacity; retry smaller | +| 400 | `VAULT_DEPOSITS_DISABLED` | The vault's on-chain `maxDeposit` is 0; pick another vault | +| 403 | `SANCTIONED_ADDRESS` | The owner is on the OFAC SDN list | +| 403 | `VAULT_BLOCKED` | The vault is on the server blocklist | +| 403 | `ACCESS_DENIED` | The origin or IP is not on the project's allowlist | +| 403 | `FEATURE_DISABLED` | The requested flow is switched off (see `bridgeAndSwap`) | +| 404 | `SRA_NOT_FOUND` | No SRA at that address | +| 409 | `IDEMPOTENCY_KEY_CONFLICT` | An `Idempotency-Key` was reused with a different body | +| 413 | `PAYLOAD_TOO_LARGE` | The request body is over the size cap | +| 422 | `INSUFFICIENT_AMOUNT` | The amount is below the vault or bridge minimum after fees | +| 422 | `SLIPPAGE_TOO_LOW` | Slippage cannot cover route fees; `details.minSlippageBps` is the retry value | +| 422 | `SWAP_ROUTE_NOT_FOUND` | No route was found for the pair | +| 429 | `RATE_LIMITED` | Too many requests; idempotent GETs retry with backoff | +| 500 | `ASSET_MISMATCH` | The vault asset is not the expected token | +| 500 | `INTERNAL_ERROR` | An unexpected server error | +| 502 | `SRA_UNAVAILABLE` / `QUOTER_UNAVAILABLE` / `RPC_UNAVAILABLE` | An upstream service failed | +| 503 | `SERVICE_UNAVAILABLE` | Access control or another dependency is temporarily down | + +`WATCH_TIMEOUT` (`WatchTimeoutError`) is client-only. `watchStatus` raises it when a recipe stays non-terminal past `timeout`. The recipe may still complete, so raise `timeout` or set it to `0` to poll indefinitely. + +### Retries + +The SDK retries idempotent GETs on 429, 502, 503, and network failures, with backoff, up to `maxRetries`. Quote-building POSTs are **never** retried: each one can create a new SRA server-side, so a transient failure must not duplicate it. + +## Exports + +```ts +import { + createSmartRecipes, + DEFAULT_SERVER_URL, // ZeroDev's hosted server, used when serverUrl is omitted + TOKENS, // USDC | USDT | DAI | WETH | WBTC | EURC | NATIVE + SmartRecipeError, // base error, plus one subclass per code above + VaultCapExceededError, + SlippageTooLowError, + WatchTimeoutError, +} from "@zerodev/smart-recipes"; +``` + +Types: `Quote`, `Vault`, `VaultDetails`, `DepositParams`, `VaultWithdrawParams`, `VaultWithdrawResult`, `RecipeStatus`, `RecipeState`, `Watcher`, and the params and result types of every method above. diff --git a/docs/pages/onramp/smart-recipes/tracking-status.mdx b/docs/pages/onramp/smart-recipes/tracking-status.mdx deleted file mode 100644 index e2cc876..0000000 --- a/docs/pages/onramp/smart-recipes/tracking-status.mdx +++ /dev/null @@ -1,77 +0,0 @@ -# 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 deleted file mode 100644 index cfb7ef9..0000000 --- a/docs/pages/onramp/smart-recipes/vault-discovery.mdx +++ /dev/null @@ -1,84 +0,0 @@ -# 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 e823e48..c3688c7 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -304,40 +304,16 @@ export default defineConfig({ 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", - }, - { - text: "Vault Discovery", - link: "/onramp/smart-recipes/vault-discovery", + text: "Recipes", + link: "/onramp/smart-recipes/recipes", }, { - 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", + text: "How It Works", + link: "/onramp/smart-recipes/how-it-works", }, { - text: "API Reference", - link: "/onramp/smart-recipes/api-reference", + text: "Reference", + link: "/onramp/smart-recipes/reference", }, ], },