You are integrating the **Ryze protocol** (an intent-based AMM on Base) into this repository so users can add liquidity to a Ryze pool with a **single token** and receive LP shares. Implement it in this repository's existing language, framework, HTTP client, and conventions — study the repo first and match its style. ## How Ryze single-asset joins work (authoritative) Ryze is intent-based: the user deposits one of the pool's tokens; the pool internally rebalances (so swap fees apply to part of the deposit) and mints LP shares (the pool contract itself is the ERC-20 LP token). The user never sends the join transaction: 1. **Quote** the join from the Ryze Router service. 2. Ensure the user has **approved** the `MultiHopRouter` contract for `tokenIn`. 3. Read the user's **join nonce** from the `MultiHopRouter` contract. 4. Have the user **sign an EIP-712 `JoinIntent`** (typed data, no gas). 5. **POST** the signed intent to the Ryze execution relayer. 6. **Poll** the relayer until `confirmed` or `failed`. The relayer batches intents, attaches all oracle data (Pyth + signed CEX prices), executes on-chain, and pays gas. **Do not** fetch/attach prices, call `execute*` contract functions, or build raw calldata. ## Configuration Read from environment/config (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` | | `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC | Reference pools (discover dynamically via `GET {RYZE_API_URL}/pools/list`, which returns each pool's `tokens[{address,weight,decimals}]`): - Sepolia: WETH-USDC `0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723`, cbBTC-USDC `0xaC522dD3B13c8b2F4b765d100b1Ea13493Cf55bA` - Mainnet: WETH-USDC `0x22f902cEfcF8b0bEc6489Cb8ac11FdDa9B2aF125`, cbBTC-USDC `0x40F3DAaE59BfE03f9Fb019Bb089Bb0C381DE27Cf` ## Step 1 — Quote `POST {RYZE_ROUTER_URL}/join-quote`: ```json { "poolAddress": "0x…", "tokensIn": [ { "token": "0x…", "amount": "1000000" } ] } ``` Exactly one entry in `tokensIn` → the response comes back with `joinType: "single"`. Key response fields: `sharesOut` (expected LP shares, 18 decimals, string), `tvl`, `shareOfPool`, `fees{swapFee,takerFee,slippageFee,wbfFee,wbrReward,…}`, `prices[]`. Errors: 400 invalid params, 404 unknown pool. Deriving `minSharesOut` MUST account for the **flat intent fee** (deployed behavior, verified live): `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 from `amountIn` before the join. Quotes exclude it, so: `minSharesOut = floor(sharesOut × (amountIn − feeTok) / amountIn × (10000 − slippageBps) / 10000)` with big-integer math (never floats); must be > 0. Warn (or refuse) when `amountIn` is worth < ~$10 — the fee alone then exceeds typical slippage and the intent reverts `InvalidSlippage()`. Also determine `otherPoolToken`: the pool's *other* asset (for two-token pools, the one the user is not depositing) from the pool's `tokens[]` list. It is required by the relayer (used off-chain for oracle coverage) but is **NOT part of the signed message**. ## Step 2 — Approval Check `ERC20(tokenIn).allowance(user, RYZE_MULTIHOP_ROUTER)` and have the user `approve(RYZE_MULTIHOP_ROUTER, amountIn)` if insufficient. This is the only on-chain transaction the user sends. ## Step 3 — Nonce Joins have their own nonce mapping. `eth_call` on `RYZE_MULTIHOP_ROUTER`: ```solidity function joinNonces(address user) external view returns (uint256); ``` Submitted nonce must EXACTLY equal this value. Read fresh before signing; never allow two in-flight join intents for the same user. ## Step 4 — EIP-712 signature Domain: ```json { "name": "MultiHopRouter", "version": "1", "chainId": , "verifyingContract": "" } ``` Type (exact field order — note `otherPoolToken` is absent): ```text JoinIntent(address user,address pool,address tokenIn,uint256 amountIn,uint256 minSharesOut,address recipient,uint256 deadline,uint256 nonce) ``` `recipient` receives the LP shares (usually the user); `deadline` = unix seconds ≈ `now + 1800`. Sign with the user's key and submit the 65-byte signature as one hex string (`r‖s‖v`, `v` = 27/28; low-`s` required). 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": "join", "user": "0x…", "pool": "0x…", "tokenIn": "0x…", "amountIn": "1000000", "minSharesOut": "998877665544332211", "otherPoolToken": "0x…", "recipient": "0x…", "deadline": 1789000000, "nonce": 0, "signature": "0x…" } ``` Amounts are base-10 strings. Responses: HTTP **202** `{ "success": true, "intentId": "0x…", "status": "pending" }` (accepted, not yet executed; idempotent on resubmit); HTTP **400** with `message` (bad signature / nonce mismatch with expected-value hint / expired deadline / missing `otherPoolToken`); HTTP **429** rate limit (~100 req/min/IP) — back off. ## Step 6 — Poll status `GET {RYZE_RELAYER_URL}/intents/{intentId}` every ~2s until `confirmed` or `failed` (typical 3–12s; timeout ~90s but return `intentId` for resumable polling). `failed` includes a decoded revert reason in `error` and has no on-chain effect. ⚠ **LP shares are auto-staked** (deployed behavior, verified live): when `MultiHopRouter.poolToGaugeVault(pool)` (view → `address`) is non-zero — true for the listed pools — join shares are staked into the LPGaugeVault for the recipient, so `ERC20(pool).balanceOf(recipient)` stays 0. The LP position is `pool.balanceOf(recipient) + gauge.getEligibleBalance(recipient)`. ## What to build 1. A **Ryze join module** in this repo's conventions exposing at minimum: - `getJoinQuote(pool, tokenIn, amountIn, slippageBps)` → quote + `minSharesOut` + `otherPoolToken` - allowance check / approval-tx helper - `buildJoinIntent(...)` → unsigned EIP-712 payload (domain + types + message) - `submitJoinIntent(intent, signature)` → `intentId` - `waitForIntent(intentId, {pollMs, timeoutMs})` - a one-call convenience `joinSingle(...)` composing the flow when a signer is available 2. **Config** via the env vars above. 3. **Typed errors** for quote failures, missing allowance, nonce conflict, validation 400s, timeout. 4. **Tests**: mock Router/Relayer HTTP with the exact JSON shapes above; assert the EIP-712 type string and field order (especially that `otherPoolToken` is in the HTTP body but NOT the signed message), integer slippage math, signature hex encoding, polling state machine. ## Acceptance checklist - [ ] No oracle/price code, no direct contract execution, no raw calldata. - [ ] `otherPoolToken` present in submit body, absent from EIP-712 message. - [ ] Amounts as big-integer-backed strings end to end; `minSharesOut > 0` and derated for `intentFee()`. - [ ] LP position read as wallet + gauge (`poolToGaugeVault`/`getEligibleBalance`). - [ ] `joinNonces` (not `swapNonces`) used; fresh read per signing; in-flight serialization. - [ ] 202/400/429 and `pending/confirmed/failed` handled and tested. - [ ] Everything configurable via env; defaults point at Base Sepolia.