You are integrating the **Ryze protocol** (an intent-based AMM on Base) into this repository with ALL four operations: token swap (exact-in), single-asset liquidity join, proportional liquidity join, and proportional liquidity exit. Implement it in this repository's existing language, framework, HTTP client, and conventions — study the repo first and match its style. ## How Ryze works (authoritative — trust this over anything you infer) Ryze is intent-based. Users never send pool transactions. For every operation the flow is: **quote → (ERC-20 approve `MultiHopRouter` if needed) → read per-type nonce → sign EIP-712 intent → POST to relayer → poll status.** The relayer batches intents, attaches all required oracle data (Pyth price payloads + ECDSA-signed CEX prices) to the execution transaction, executes on-chain, and pays gas. Hard rules: 1. **Never** fetch, sign, or attach oracle/price data — the relayer does it. 2. **Never** call `executeSwapIntents`/`executeJoinIntents`/etc. on the contract. 3. The only user transaction is ERC-20 `approve(RYZE_MULTIHOP_ROUTER, amount)` for input tokens (NOT needed for exits — LP shares are burned, not pulled). 4. All `uint256` values travel as base-10 strings in JSON; use big-integer math everywhere. 5. Every `min…Out` must be > 0 and derived from a quote with integer slippage math: `min = floor(expected × (10000 − slippageBps) / 10000)`. ## Configuration (env vars — never hardcode) | 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_API_URL` | `https://sepolia.api.ryze.pro/api` | `https://mainnet.api.ryze.pro/api` | | `RYZE_MULTIHOP_ROUTER` | `0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A` | `0x8e20A1534a204DE569E9d62D410c19795b81DF70` | | `RYZE_POOL_QUERIES` | `0x1a7B7D9071c30935448b6cC3d114c0c794d0a3C0` | `0xD4b5DD638d09aB7b6Eb4Ec3490F02A96d0DD4100` | | `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC | Discovery endpoints: `GET {RYZE_API_URL}/assets` (tokens + decimals), `GET {RYZE_API_URL}/pools/list` (pools with ordered `tokens[{address,weight,decimals}]`, `totalSupplyLP`). Native ETH is not supported — use WETH. Reference addresses — Sepolia: USDC `0x6aEB5326b5DcA2163e0995824DcB816194b157a6`, WETH `0x6Dd0C6a2e058bc3F7D6AaCe23965180BcBbb2635`, cbBTC `0xa7BaE7da3E4950700BE9f107EB186e89a474d180`, WETH-USDC pool `0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723`, cbBTC-USDC pool `0xaC522dD3B13c8b2F4b765d100b1Ea13493Cf55bA`. Mainnet: USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`, WETH `0x4200000000000000000000000000000000000006`, cbBTC `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf`, WETH-USDC pool `0x22f902cEfcF8b0bEc6489Cb8ac11FdDa9B2aF125`, cbBTC-USDC pool `0x40F3DAaE59BfE03f9Fb019Bb089Bb0C381DE27Cf`. ## Quotes - **Swap**: `POST {RYZE_ROUTER_URL}/quote` — `{ "tokenIn", "tokenOut", "amountIn", "slippageTolerance": , "maxHops": 1..3, "userAddress" }` → `output.amount`, `steps[{pool,tokenIn,tokenOut,…}]`, `priceImpact`, `prices[]`. Intent `path = steps.map(s => ({pool, tokenIn, tokenOut}))`. - **Joins (both kinds)**: `POST {RYZE_ROUTER_URL}/join-quote` — `{ "poolAddress", "tokensIn": [{token, amount}, …] }`. One token → `joinType:"single"` (fees apply); all pool tokens → `joinType:"proportional"` (fee-free). → `sharesOut` (18-decimal string), `tvl`, `shareOfPool`, `fees{…}`, `prices[]`. - **Exit**: no HTTP endpoint — `eth_call` `WeightedPoolQueries.queryProportionalExit(pool, sharesIn)`. ⚠ The deployed contract returns a **struct-wrapped array**: declare the ABI output as `tuple(uint256[] amountsOut)`, NOT bare `uint256[]` (decoding as `uint256[]` throws). Amounts are ordered like the pool's asset list; also available: `queryProportionalExitForUser(pool, user)`. ## Execution-reality rules (verified against the live deployment — build ALL of these in) 1. **Flat intent fee.** `MultiHopRouter.intentFee()` (view → `uint256`) is a flat **USD fee in WAD** (currently 1e16 = $0.01) per execution, converted to token units at oracle price: `feeTok(t) = ceilDiv(intentFee × 10^decimals(t), blendedPrice(t))` using the quote's `prices[]`. Deducted: swap — `feeTok(tokenIn) × path.length` off `amountIn`; single join — `feeTok(tokenIn)` off `amountIn`; proportional join — `feeTok(tokensIn[0])` off `amountsIn[0]`; exit — `feeTok(poolAsset0)` off the outputs (contract enforces `out[0] ≥ min[0] + fee`). Quotes exclude this fee → derate every min by the fee ratio before slippage (exit: subtract `feeTok(asset0)` from `minAmountsOut[0]`, floor 1), and warn when amounts are worth < ~$10 (the fee alone then exceeds typical slippage and intents revert `InvalidSlippage()`). 2. **LP shares are auto-staked in the pool's gauge vault.** When `MultiHopRouter.poolToGaugeVault(pool)` is non-zero (true for the listed pools), join shares go to the LPGaugeVault: `pool.balanceOf(user)` stays 0. LP position = `pool.balanceOf(user) + gauge.getEligibleBalance(user)`; exits burn from the gauge balance (no approval). ## EIP-712 signing One domain for all types: ```json { "name": "MultiHopRouter", "version": "1", "chainId": , "verifyingContract": "" } ``` Exact type definitions (field order matters): ```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) JoinIntent(address user,address pool,address tokenIn,uint256 amountIn,uint256 minSharesOut,address recipient,uint256 deadline,uint256 nonce) JoinProportionalIntent(address user,address pool,address[] tokensIn,uint256[] amountsIn,uint256 minSharesOut,address recipient,uint256 deadline,uint256 nonce) ExitProportionalIntent(address user,address pool,uint256 sharesIn,uint256[] minAmountsOut,address recipient,uint256 deadline,uint256 nonce) ``` `deadline` = unix seconds ≈ `now + 1800`. Submit the 65-byte signature as one hex string: `"signature": "0x…"` (`r‖s‖v`, `v` = 27/28; low-`s` enforced). ERC-1271 smart-contract wallets (e.g. Safe) submit their contract signature bytes instead. ## Nonces — per-user AND per-type, on `RYZE_MULTIHOP_ROUTER` ```solidity function swapNonces(address) view returns (uint256); function joinNonces(address) view returns (uint256); function joinProportionalNonces(address) view returns (uint256); function exitProportionalNonces(address) view returns (uint256); ``` The submitted `nonce` must EXACTLY equal the on-chain value for that type. Read fresh right before signing, and serialize intents per (user, type): never two unconfirmed intents of the same type for the same user. ## Relayer submit bodies — `POST {RYZE_RELAYER_URL}/intents/submit` Common fields all types: `user`, `recipient`, `deadline` (number), `nonce` (number), `signature` (hex string, 65-byte `r‖s‖v` or ERC-1271 contract signature). Type-specific: ```jsonc // swap { "type": "swap", "tokenIn": "0x…", "tokenOut": "0x…", "amountIn": "…", "minAmountOut": "…", "path": [{ "pool": "0x…", "tokenIn": "0x…", "tokenOut": "0x…" }], … } // single-asset join — otherPoolToken (the pool's other asset) is REQUIRED in the body // but NOT part of the signed message { "type": "join", "pool": "0x…", "tokenIn": "0x…", "amountIn": "…", "minSharesOut": "…", "otherPoolToken": "0x…", … } // proportional join — signed field is named minSharesOut but the BODY field is // minPoolTokensOut (same value). tokensIn/amountsIn: aligned, ≥2, no duplicates, all >0. { "type": "joinProportional", "pool": "0x…", "tokensIn": ["0x…","0x…"], "amountsIn": ["…","…"], "minPoolTokensOut": "…", … } // exit — poolTokens (pool's ordered asset list, same order/length as minAmountsOut) is // REQUIRED in the body but NOT part of the signed message. No approval needed for exits. { "type": "exitProportional", "pool": "0x…", "sharesIn": "…", "minAmountsOut": ["…","…"], "poolTokens": ["0x…","0x…"], … } ``` Responses: - HTTP **202** `{ "success": true, "intentId": "0x…", "status": "pending", … }` — accepted, not yet executed. `intentId` = EIP-712 digest; identical resubmission is idempotent. - HTTP **400** `{ "success": false, "status": "error", "message": "…" }` — bad signature, nonce mismatch (message hints the expected value), expired deadline, path/array errors. - HTTP **429** — rate limit (~100 req/min/IP); back off with jitter. ## Status polling — `GET {RYZE_RELAYER_URL}/intents/{intentId}` `{ "success": true, "intent": { "intentId", "status": "pending|confirmed|failed", "txHash", "batchId", "blockNumber", "gasUsed", "error" } }` Poll ~2s; typical completion 3–12s; timeout ~90s but always return `intentId` so callers can resume. `failed` → decoded revert in `error`, no on-chain effect. Transient 500s are retryable. Health check for circuit breaking: `GET {RYZE_RELAYER_URL}/health` (503 when degraded). ## What to build Design it in layers, in this repo's idiom: 1. **`RyzeConfig`** — env-driven config (URLs, chainId, addresses) with Sepolia defaults. 2. **Transport layer** — thin HTTP clients for Router + Relayer (timeouts, 429 backoff, structured error extraction from `message`) and a minimal contract-read helper for the four nonce functions + `queryProportionalExit` + `allowance`/`balanceOf`. 3. **Quote layer** — `getSwapQuote`, `getJoinQuote` (single + proportional), `getExitQuote`, each returning expected output AND the derived intent inputs (`path`, `min…Out`, `otherPoolToken`, `poolTokens`). 4. **Intent layer** — `buildXxxIntent(...)` for all four types returning the unsigned EIP-712 payload `{domain, types, primaryType, message}` so ANY signer (backend key, browser wallet) can sign it; the raw hex signature is submitted as-is. 5. **Execution layer** — `submitIntent(body)` → `intentId`; `waitForIntent(intentId, {pollMs, timeoutMs})` → terminal status; per-(user,type) in-flight serialization. 6. **Facade** — `RyzeClient` with `swap()`, `joinSingle()`, `joinProportional()`, `exit()` composing quote → allowance guard → nonce → sign → submit → wait, plus the granular methods exposed for UIs that split signing from submission. 7. **Tests** — mock all HTTP + eth_call responses with the exact JSON shapes above. Must cover: EIP-712 domain/type-string byte-equality for all four types, array encoding (proportional join, exit), the `minSharesOut`/`minPoolTokensOut` naming mapping, `otherPoolToken`/`poolTokens` present in body but absent from signed message, integer slippage math edge cases, signature hex encoding, 400 nonce-mismatch surfacing, 429 backoff, polling state machine incl. timeout and `failed` with revert reason. ## Acceptance checklist - [ ] Four operations working through quote → approve → nonce → sign → submit → poll. - [ ] Zero price/oracle code; zero direct `execute*` contract calls; zero raw calldata. - [ ] All execution-reality rules implemented: intent-fee derating on every min and gauge-aware LP position reads. - [ ] Per-type nonces used correctly and read fresh; per-(user,type) serialization. - [ ] Big-integer amounts as strings end to end; all mins > 0. - [ ] The three field traps handled: `otherPoolToken` (join), `minPoolTokensOut` (proportional join body) vs `minSharesOut` (signed), `poolTokens` (exit). - [ ] Exits skip approval; joins validate approval for every input token. - [ ] All URLs/addresses/chainId from env with Base Sepolia defaults. - [ ] Test suite passes; add a short README section documenting usage of the facade.