You are integrating the **Ryze protocol** (an intent-based AMM on Base) into this repository so it can swap ERC-20 tokens. Implement it in this repository's existing language, framework, HTTP client, and coding conventions — study the repo first and match its style (module layout, config handling, error types, logging, test framework). ## How Ryze swaps work (authoritative — trust this over anything you infer) Ryze is intent-based: the user never sends a swap transaction. The flow you must implement: 1. **Quote** the swap from the Ryze Router service. 2. Ensure the user has **approved** the `MultiHopRouter` contract for `tokenIn` (standard ERC-20 `approve` — the only on-chain transaction in the whole flow). 3. Read the user's **swap nonce** from the `MultiHopRouter` contract. 4. Have the user **sign an EIP-712 `SwapIntent`** (typed data, no gas). 5. **POST** the signed intent to the Ryze execution relayer. 6. **Poll** the relayer until the intent is `confirmed` or `failed`. The relayer batches intents, attaches all required oracle data (Pyth + signed CEX prices) to the execution transaction, executes on-chain, and pays gas. Therefore: **do not** fetch or attach any price/oracle data, **do not** call any `execute*` function on the contract, and **do not** build raw swap calldata. If you find yourself doing any of those, you are off the intended path. ## Configuration Read these from environment/config (never hardcode; defaults shown are Base Sepolia testnet, mainnet values in parentheses): | Env var | Default (Base Sepolia, chainId 84532) | Base mainnet (chainId 8453) | |---|---|---| | `RYZE_CHAIN_ID` | `84532` | `8453` | | `RYZE_RELAYER_URL` | `https://sepolia.relayer.ryze.pro/api/v1` | `https://mainnet.relayer.ryze.pro/api/v1` | | `RYZE_ROUTER_URL` | `https://sepolia.router.ryze.pro` | `https://mainnet.router.ryze.pro` | | `RYZE_MULTIHOP_ROUTER` | `0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A` | `0x8e20A1534a204DE569E9d62D410c19795b81DF70` | | `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC | Reference tokens (native ETH is not supported — use WETH): - Sepolia: USDC `0x6aEB5326b5DcA2163e0995824DcB816194b157a6`, WETH `0x6Dd0C6a2e058bc3F7D6AaCe23965180BcBbb2635`, cbBTC `0xa7BaE7da3E4950700BE9f107EB186e89a474d180` - Mainnet: USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`, WETH `0x4200000000000000000000000000000000000006`, cbBTC `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` ## Step 1 — Quote `POST {RYZE_ROUTER_URL}/quote` with JSON: ```json { "tokenIn": "
", "tokenOut": "
", "amountIn": "", "slippageTolerance": 50, "maxHops": 3, "userAddress": "" } ``` - `slippageTolerance` is basis points (50 = 0.5%); make it a parameter of your API. - `maxHops` must be 1–3. - Response key fields: `output.amount` (expected out, base units), `steps[]` (each with `pool`, `tokenIn`, `tokenOut`), `priceImpact`, `executionPrice`, `prices[]`. Derive: - `path = steps.map(s => ({ pool: s.pool, tokenIn: s.tokenIn, tokenOut: s.tokenOut }))` - `minAmountOut` — see the deployed behavior below; use big-integer math, never floats. One deployed behavior (verified live) MUST be handled here: 1. **Flat intent fee.** `MultiHopRouter.intentFee()` (view → `uint256`) is a flat USD fee in WAD (currently 1e16 = $0.01), converted to tokenIn units at oracle price (`feeTok = ceilDiv(intentFee × 10^decimals(tokenIn), blendedPrice(tokenIn))`, prices from the quote's `prices[]`) and deducted `feeTok × path.length` from `amountIn` before the swap. Quotes exclude it, so derate: `minAmountOut = floor(output.amount × (amountIn − feeTok×hops) / amountIn × (10000 − slippageBps) / 10000)`. Warn (or refuse) when `amountIn` is worth < ~$10 — the fee alone then exceeds typical slippage and the intent reverts `InvalidSlippage()`/`SlippageExceeded()`. ## Step 2 — Approval Check `ERC20(tokenIn).allowance(user, RYZE_MULTIHOP_ROUTER)`; if below `amountIn`, the user must send `approve(RYZE_MULTIHOP_ROUTER, amount)`. Expose this as a separate step in your API (allowance check + approval tx builder), since in most products the user's wallet sends it. ## Step 3 — Nonce Nonces are per-user and **per-intent-type** on the contract. For swaps, `eth_call`: ```solidity function swapNonces(address user) external view returns (uint256); ``` on `RYZE_MULTIHOP_ROUTER`. The submitted nonce must EXACTLY equal this value. Read it fresh right before signing, and never allow two in-flight (unconfirmed) swap intents for the same user — serialize them. ## Step 4 — EIP-712 signature Domain: ```json { "name": "MultiHopRouter", "version": "1", "chainId": , "verifyingContract": "" } ``` Types (exact field order): ```text SwapIntent(address user,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,Hop[] path,address recipient,uint256 deadline,uint256 nonce) Hop(address pool,address tokenIn,address tokenOut) ``` Message values: `user` = signer address, `recipient` = who receives `tokenOut` (usually the user), `deadline` = unix seconds ≈ `now + 1800`, `nonce` from step 3, the rest from the quote. Sign with the user's key (`signTypedData` / equivalent) and submit the 65-byte signature as one hex string (`r‖s‖v`, `v` = 27/28; low-`s` required — standard signers comply automatically). ERC-1271 smart-contract wallets (e.g. Safe) submit their contract signature bytes instead. ## Step 5 — Submit `POST {RYZE_RELAYER_URL}/intents/submit`: ```json { "type": "swap", "user": "0x…", "tokenIn": "0x…", "tokenOut": "0x…", "amountIn": "1000000", "minAmountOut": "412000000000000000", "path": [ { "pool": "0x…", "tokenIn": "0x…", "tokenOut": "0x…" } ], "recipient": "0x…", "deadline": 1789000000, "nonce": 0, "signature": "0x…" } ``` All amounts are base-10 **strings**. `deadline`/`nonce` are JSON numbers. - Success → HTTP **202**: `{ "success": true, "intentId": "0x…", "status": "pending", … }`. 202 means *accepted for execution*, not executed. `intentId` is the EIP-712 digest; resubmitting the identical signed intent is idempotent. - HTTP **400** → `{ "success": false, "status": "error", "message": "…" }` — surface `message` to the caller (covers bad signature, nonce mismatch with expected-value hint, expired deadline, discontinuous path). - HTTP **429** → rate limited (~100 req/min/IP); retry with backoff. ## Step 6 — Poll status `GET {RYZE_RELAYER_URL}/intents/{intentId}` every ~2s until `intent.status` is `confirmed` or `failed` (typical total: 3–12s; timeout after ~90s but return the `intentId` so callers can resume polling). Response includes `txHash`, `blockNumber`, `gasUsed`, and on failure a decoded revert reason in `error`. A `failed` intent had no on-chain effect. Treat transient HTTP 500s from this endpoint as retryable. ## What to build 1. A **Ryze client module** in this repo's conventions exposing at minimum: - `getSwapQuote(tokenIn, tokenOut, amountIn, slippageBps, user)` → quote + derived `path`/`minAmountOut` - `checkAllowance(user, token, amount)` / approval-tx helper - `buildSwapIntent(quote, user, recipient, deadlineSecs)` → unsigned EIP-712 payload (domain + types + message) ready for any signer - `submitSwapIntent(intent, signature)` → `intentId` - `waitForIntent(intentId, {pollMs, timeoutMs})` → final status - a one-call convenience `swap(...)` composing all of the above when a signer is available 2. **Config** via the env vars above, wired into this repo's config system. 3. **Error handling**: typed/structured errors for quote failures, allowance-missing, nonce conflicts, 4xx validation messages, timeouts. 4. **Tests** in this repo's framework: mock the Router and Relayer HTTP responses (use the exact JSON shapes above), assert the EIP-712 payload (domain, type string, field order), the big-integer slippage math, the signature hex encoding, and the polling state machine. ## Acceptance checklist - [ ] No oracle/price code, no direct contract execution calls, no raw swap calldata. - [ ] All amounts flow through big-integer types; JSON emits them as strings. - [ ] Nonce read fresh per signing; concurrent same-user swaps are serialized or rejected. - [ ] `minAmountOut > 0` always; slippage math is integer-exact and derates for `intentFee()` (× hops). - [ ] EIP-712 domain/type strings byte-match the spec above. - [ ] 202/400/429 and `pending/confirmed/failed` all handled and tested. - [ ] Everything configurable via env; defaults point at Base Sepolia.