Epic: #58 · Step D (Treasury)
Rewire the console's Treasury screen off its demo-era (Stellar-legacy) endpoints onto the real org-scoped backend. Builds on the cookie-SIWE session (#60) and onboarding (#63, which already provisions the managed treasury).
Backend contract (source of truth — services/api/src/routes/orgs.ts)
GET /orgs/:id/treasury → 200 { address, custody:"managed", registered, consented,
custodyConsent:{consented,consentedAt,consentedBy},
balances:[{ token:"usdc"|"eurc", tokenId, symbol, decimals, amount }] }
POST /orgs/:id/treasury/deposit → 201|202|200 { amount, approvalTxHash?, source:"direct",
{ amount, token:"usdc"|"eurc", idempotencyKey } status:"confirmed"|"submitted", token, tokenId, txHash }
GET /orgs/:id/treasury/deposits → 200 { deposits:[{ id, kind:"direct"|"cctp", amount, token,
(query: limit?, before?) status:"pending"|"credited"|"failed", txHash, sourceChain, createdAt, updatedAt }],
nextCursor }
amount in the deposit request is minor units (/^[1-9][0-9]*$/, USDC 6dp). idempotencyKey is required (generate one per attempt). All monetary fields are strings.
Changes
lib/api.ts — replace the treasury client fns with the org-scoped ones: orgTreasury(orgId) → GET /orgs/:id/treasury; depositToTreasury(orgId, { amount, token, idempotencyKey }) → POST /orgs/:id/treasury/deposit; treasuryDeposits(orgId, { limit?, before? }) → GET /orgs/:id/treasury/deposits. REMOVE the no-backend fns/paths: treasury(GET /treasury), treasuryPublicBalance, treasuryReceive, treasurySendPublic, fundTreasury, proveBalance, proveTotal, proveSolvency (and their /treasury/* paths).
lib/store.tsx — the treasury read model is now org-scoped: only fetch it when session.activeOrg exists, calling api.orgTreasury(activeOrg.id). Keep the independent-read resilience (don't blank on a transient miss).
screens/Treasury.tsx — render the real treasury: the managed treasury address, registered status, and the per-token encrypted balances. Primary action = Add funds → depositToTreasury (direct ERC20 → encrypted balance) with a generated idempotencyKey, reflecting status (submitted vs confirmed) + the tx. Show recent deposits from treasuryDeposits (kind/amount/status/tx). REMOVE from real mode the actions with no backend (public balance, receive/QR, send-to-wallet, prove-balance/total/solvency). Money shown in USDC (6dp) via the existing formatters.
packages/types — treasury view + deposit + deposits-history response types matching the backend (drop the Stellar-legacy stroops/issuer shapes for real mode).
- Demo — update
demo/api.ts/demo/data.ts treasury stubs to the new shapes so VITE_DEMO_MODE=1 still renders a populated Treasury screen.
Out of scope
Cross-chain CCTP funding (/orgs/:id/treasury/fund-intent) — leave a clear TODO seam but don't build the CCTP UX in this PR. Payroll (E), hiding no-backend screens (B).
Definition of done — run ALL, make them pass:
pnpm --filter @benzo/console typecheck · test · build · VITE_DEMO_MODE=1 … build · pnpm exec biome lint .
Operating rules
No git (can't commit). Match the codebase style. End with (1) a one-line conventional-commit message and (2) the exact git add <paths> list.
Epic: #58 · Step D (Treasury)
Rewire the console's Treasury screen off its demo-era (Stellar-legacy) endpoints onto the real org-scoped backend. Builds on the cookie-SIWE session (#60) and onboarding (#63, which already provisions the managed treasury).
Backend contract (source of truth —
services/api/src/routes/orgs.ts)amountin the deposit request is minor units (/^[1-9][0-9]*$/, USDC 6dp).idempotencyKeyis required (generate one per attempt). All monetary fields are strings.Changes
lib/api.ts— replace the treasury client fns with the org-scoped ones:orgTreasury(orgId)→GET /orgs/:id/treasury;depositToTreasury(orgId, { amount, token, idempotencyKey })→POST /orgs/:id/treasury/deposit;treasuryDeposits(orgId, { limit?, before? })→GET /orgs/:id/treasury/deposits. REMOVE the no-backend fns/paths:treasury(GET /treasury),treasuryPublicBalance,treasuryReceive,treasurySendPublic,fundTreasury,proveBalance,proveTotal,proveSolvency(and their/treasury/*paths).lib/store.tsx— thetreasuryread model is now org-scoped: only fetch it whensession.activeOrgexists, callingapi.orgTreasury(activeOrg.id). Keep the independent-read resilience (don't blank on a transient miss).screens/Treasury.tsx— render the real treasury: the managed treasuryaddress,registeredstatus, and the per-token encryptedbalances. Primary action = Add funds →depositToTreasury(direct ERC20 → encrypted balance) with a generatedidempotencyKey, reflectingstatus(submitted vs confirmed) + the tx. Show recent deposits fromtreasuryDeposits(kind/amount/status/tx). REMOVE from real mode the actions with no backend (public balance, receive/QR, send-to-wallet, prove-balance/total/solvency). Money shown in USDC (6dp) via the existing formatters.packages/types— treasury view + deposit + deposits-history response types matching the backend (drop the Stellar-legacystroops/issuershapes for real mode).demo/api.ts/demo/data.tstreasury stubs to the new shapes soVITE_DEMO_MODE=1still renders a populated Treasury screen.Out of scope
Cross-chain CCTP funding (
/orgs/:id/treasury/fund-intent) — leave a clear TODO seam but don't build the CCTP UX in this PR. Payroll (E), hiding no-backend screens (B).Definition of done — run ALL, make them pass:
pnpm --filter @benzo/console typecheck·test·build·VITE_DEMO_MODE=1 … build·pnpm exec biome lint .Operating rules
No git (can't commit). Match the codebase style. End with (1) a one-line conventional-commit message and (2) the exact
git add <paths>list.