/** * rLending module — trust-based distributed mortgage lending. * * Extracted from rFlows. Uses the shared FlowsDoc (mortgagePositions + * reinvestmentPositions fields) so the rPool dashboard in rFlows keeps * working without a data migration. */ import { Hono } from "hono"; import { renderShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyToken, extractToken } from "../../server/auth"; import { renderLanding } from "./landing"; import type { SyncServer } from "../../server/local-first/sync-server"; import { flowsDocId, type FlowsDoc } from "../rflows/schemas"; import type { MortgagePosition, ReinvestmentPosition } from "../rflows/lib/types"; import { ensureFlowsDoc } from "../rflows/mod"; let _syncServer: SyncServer | null = null; // ── Aave v3 Pool on Base (for live lending rate lookup) ── const AAVE_V3_POOL_BASE = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5"; const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; const BASE_RPC = "https://mainnet.base.org"; // ── Routes ── const routes = new Hono(); // GET /api/mortgage/rates — Live Aave v3 USDC supply APY on Base routes.get("/api/mortgage/rates", async (c) => { try { // getReserveData(address) selector = 0x35ea6a75 const calldata = "0x35ea6a75000000000000000000000000" + USDC_BASE.slice(2).toLowerCase(); const res = await fetch(BASE_RPC, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to: AAVE_V3_POOL_BASE, data: calldata }, "latest"], }), }); const json = await res.json() as any; if (json.error) throw new Error(json.error.message); // currentLiquidityRate is the 3rd word in the result tuple (27 decimals / ray) const resultHex = json.result as string; const liquidityRateHex = "0x" + resultHex.slice(2 + 64 * 2, 2 + 64 * 3); const liquidityRate = Number(BigInt(liquidityRateHex)) / 1e27; const SECONDS_PER_YEAR = 31536000; const apy = (Math.pow(1 + liquidityRate / SECONDS_PER_YEAR, SECONDS_PER_YEAR) - 1) * 100; return c.json({ rates: [ { protocol: "Aave v3", chain: "Base", asset: "USDC", apy: Math.round(apy * 100) / 100, updatedAt: Date.now() }, ], }); } catch (err) { console.error("[rlending] Rate fetch failed:", err); return c.json({ rates: [ { protocol: "Aave v3", chain: "Base", asset: "USDC", apy: null, updatedAt: Date.now(), error: "Unavailable" }, ], }); } }); // GET /api/mortgage/positions — List mortgage positions for a space routes.get("/api/mortgage/positions", async (c) => { const space = c.req.query("space") || "demo"; const doc = ensureFlowsDoc(space); return c.json(Object.values(doc.mortgagePositions || {})); }); // POST /api/mortgage/positions — Create a new mortgage position (auth required) routes.post("/api/mortgage/positions", async (c) => { const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); let claims; try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); } const body = await c.req.json() as Partial & { space?: string }; if (!body.principal || !body.termMonths || !body.interestRate) { return c.json({ error: "principal, termMonths, and interestRate required" }, 400); } const space = body.space || "demo"; const docId = flowsDocId(space); ensureFlowsDoc(space); const id = crypto.randomUUID(); const monthlyRate = body.interestRate / 100 / 12; const monthlyPayment = monthlyRate > 0 ? body.principal * (monthlyRate * Math.pow(1 + monthlyRate, body.termMonths)) / (Math.pow(1 + monthlyRate, body.termMonths) - 1) : body.principal / body.termMonths; const position: MortgagePosition = { id, borrower: body.borrower || claims.sub, borrowerDid: (claims as any).did || claims.sub, principal: body.principal, interestRate: body.interestRate, termMonths: body.termMonths, monthlyPayment: Math.round(monthlyPayment * 100) / 100, startDate: Date.now(), trustScore: body.trustScore || 0, status: "active", collateralType: body.collateralType || "trust-backed", }; _syncServer!.changeDoc(docId, "create mortgage position", (d) => { d.mortgagePositions[id] = position as any; }); return c.json(position, 201); }); // ── Page routes ── const lendingScripts = ` `; // GET / — Mortgage simulator dashboard (landing for the module) routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; return c.html(renderShell({ title: `${spaceSlug} — rLending | rSpace`, moduleId: "rlending", spaceSlug, modules: getModuleInfoList(), theme: "dark", body: ``, scripts: lendingScripts, })); }); // ── Seed demo data ── function seedTemplateLending(space: string) { if (!_syncServer) return; const doc = ensureFlowsDoc(space); if (Object.keys(doc.mortgagePositions || {}).length > 0) return; const docId = flowsDocId(space); const now = Date.now(); const demoMortgages: MortgagePosition[] = [ { id: crypto.randomUUID(), borrower: "alice.eth", borrowerDid: "did:key:alice123", principal: 250000, interestRate: 4.2, termMonths: 360, monthlyPayment: 1222.95, startDate: now - 86400000 * 120, trustScore: 92, status: "active", collateralType: "trust-backed", }, { id: crypto.randomUUID(), borrower: "bob.base", borrowerDid: "did:key:bob456", principal: 180000, interestRate: 3.8, termMonths: 240, monthlyPayment: 1079.19, startDate: now - 86400000 * 60, trustScore: 87, status: "active", collateralType: "hybrid", }, { id: crypto.randomUUID(), borrower: "carol.eth", borrowerDid: "did:key:carol789", principal: 75000, interestRate: 5.1, termMonths: 120, monthlyPayment: 799.72, startDate: now - 86400000 * 200, trustScore: 78, status: "active", collateralType: "trust-backed", }, { id: crypto.randomUUID(), borrower: "dave.base", borrowerDid: "did:key:dave012", principal: 320000, interestRate: 3.5, termMonths: 360, monthlyPayment: 1436.94, startDate: now - 86400000 * 30, trustScore: 95, status: "pending", collateralType: "asset-backed", }, ]; const demoReinvestments: ReinvestmentPosition[] = [ { protocol: "Aave v3", chain: "Base", asset: "USDC", deposited: 500000, currentValue: 512340, apy: 4.87, lastUpdated: now }, { protocol: "Morpho Blue", chain: "Ethereum", asset: "USDC", deposited: 200000, currentValue: 203120, apy: 3.12, lastUpdated: now }, ]; _syncServer.changeDoc(docId, "seed mortgage demo", (d) => { for (const m of demoMortgages) d.mortgagePositions[m.id] = m as any; for (const r of demoReinvestments) { const rid = `${r.protocol}:${r.chain}:${r.asset}`; d.reinvestmentPositions[rid] = r as any; } }); console.log(`[rlending] Mortgage demo seeded for "${space}"`); } // ── Module definition ── export const lendingModule: RSpaceModule = { id: "rlending", name: "rLending", icon: "🏦", description: "Trust-based distributed mortgages with DeFi reinvestment", scoping: { defaultScope: "space", userConfigurable: false }, routes, landingPage: renderLanding, seedTemplate: seedTemplateLending, async onInit(ctx) { _syncServer = ctx.syncServer; }, outputPaths: [ { path: "", name: "Simulator", icon: "🧮", description: "Distributed mortgage simulator with tranches, reinvestment, and secondary market" }, ], onboardingActions: [ { label: "Open Mortgage Simulator", icon: "🏦", description: "Model community-funded mortgages with variable terms and tranches", type: "create", href: "/{space}/rlending" }, ], };