Skip to content

feat(console): rewire treasury to org-scoped backend#65

Merged
hitakshiA merged 1 commit into
mainfrom
codex/64-treasury
Jul 11, 2026
Merged

feat(console): rewire treasury to org-scoped backend#65
hitakshiA merged 1 commit into
mainfrom
codex/64-treasury

Conversation

@hitakshiA

@hitakshiA hitakshiA commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Epic: #58 · Step D (Treasury) · Closes #64

Rewires the Treasury screen + store read model off the demo-era (Stellar-legacy) endpoints onto the real org-scoped backend. Builds on auth (#60) and onboarding (#63).

What changed

  • lib/api.tsorgTreasury(orgId)GET /orgs/:id/treasury; depositToTreasury(orgId, {amount, token, idempotencyKey})POST /orgs/:id/treasury/deposit; treasuryDeposits(orgId, {limit?, before?})GET /orgs/:id/treasury/deposits. Removed the no-backend fns/paths (treasury, treasuryPublicBalance, treasuryReceive, treasurySendPublic, fundTreasury, proveBalance/Total/Solvency) — zero dead references remain.
  • lib/store.tsx — the treasury read model is org-scoped: api.orgTreasury(activeOrg.id), fetched only when there's an active org; independent-read resilience preserved. (+ new store.test.tsx.)
  • screens/Treasury.tsx — real managed treasury: address (copy), registered badge, per-token encrypted balances; Add fundsdepositToTreasury (USDC → minor units + fresh idempotencyKey, reflects submitted/confirmed + tx); Recent deposits from treasuryDeposits. Dropped the no-backend actions. Trimmed ~892 → ~391 lines.
  • Dashboard.tsx / Pay.tsx — updated for the new treasury shape (they read it).
  • packages/types — treasury view + deposit + deposits-history types matching the backend.
  • Demo — stubs reseeded to the new shapes; VITE_DEMO_MODE=1 renders a populated Treasury.

CCTP funding (/orgs/:id/treasury/fund-intent) is intentionally left as a TODO(cctp) seam.

Verification (all green)

typecheck · test (72) · build · VITE_DEMO_MODE=1 build · biome lint .

Summary by CodeRabbit

  • New Features

    • Introduced organization-scoped managed treasury views with encrypted USDC and EURC balances.
    • Added direct USDC deposits with idempotency protection and deposit results.
    • Added recent deposit history with statuses, transaction links, and pagination.
    • Added custody, registration, consent, and treasury address details.
    • Updated dashboards and payment views to use the new balance model.
  • Bug Fixes

    • Treasury data now loads correctly for the active workspace.
    • Restricted treasury deposits to authorized owners.

Greptile Summary

This PR moves Treasury onto the org-scoped backend. The main changes are:

  • Adds org-scoped treasury read, deposit, and deposit-history API calls.
  • Replaces the Treasury screen with managed address, encrypted balances, add-funds, and recent deposits.
  • Updates the console store, demo data, Dashboard, Pay, and shared treasury types for the new shape.

Confidence Score: 5/5

The changed flow looks safe to merge after a small cleanup to Pay balance display.

  • Treasury API paths, store loading, demo data, and shared types line up with the new org-scoped shape.
  • The remaining issue is limited to Pay showing every selected source balance as private instead of showing an available decoded value.

apps/console/src/screens/Pay.tsx

Important Files Changed

Filename Overview
apps/console/src/lib/api.ts Adds org-scoped treasury read, deposit, and deposit-history calls while removing legacy treasury helpers.
apps/console/src/lib/store.tsx Loads treasury through the active org and skips the read when no org is selected.
apps/console/src/screens/Treasury.tsx Replaces legacy treasury actions with managed treasury details, add-funds, balances, and deposit history.
apps/console/src/screens/Pay.tsx Updates Pay for the new treasury shape but now hides every selected account balance.
packages/types/src/api.ts Defines the new managed treasury view and deposit DTOs.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant UI as Treasury screen
  participant Store as Console store
  participant API as lib/api.ts
  participant Backend as Org treasury backend

  Store->>API: orgTreasury(activeOrg.id)
  API->>Backend: GET /orgs/:id/treasury
  Backend-->>API: TreasuryView
  API-->>Store: Managed treasury state
  Store-->>UI: Address and balances

  UI->>API: "treasuryDeposits(orgId, { limit })"
  API->>Backend: GET /orgs/:id/treasury/deposits
  Backend-->>API: Deposit history
  API-->>UI: Recent deposits

  UI->>API: depositToTreasury(orgId, amount, token, idempotencyKey)
  API->>Backend: POST /orgs/:id/treasury/deposit
  Backend-->>API: Submitted or confirmed deposit
  API-->>UI: Deposit result
  UI->>Store: refresh()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant UI as Treasury screen
  participant Store as Console store
  participant API as lib/api.ts
  participant Backend as Org treasury backend

  Store->>API: orgTreasury(activeOrg.id)
  API->>Backend: GET /orgs/:id/treasury
  Backend-->>API: TreasuryView
  API-->>Store: Managed treasury state
  Store-->>UI: Address and balances

  UI->>API: "treasuryDeposits(orgId, { limit })"
  API->>Backend: GET /orgs/:id/treasury/deposits
  Backend-->>API: Deposit history
  API-->>UI: Recent deposits

  UI->>API: depositToTreasury(orgId, amount, token, idempotencyKey)
  API->>Backend: POST /orgs/:id/treasury/deposit
  Backend-->>API: Submitted or confirmed deposit
  API-->>UI: Deposit result
  UI->>Store: refresh()
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/console/src/screens/Pay.tsx:50
**Source Balance Always Hidden**

When an account is selected, this value is always `undefined`, so the Pay form now renders `Balance: Private` for every source account. Users lose the decoded balance check that previously warned them before submitting an overdrawn payment, and the failure only appears after the backend rejects or cannot settle the payment.

Reviews (1): Last reviewed commit: "feat(console): rewire treasury to org-sc..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Treasury screen + store read model now use the real org-scoped endpoints:
GET /orgs/:id/treasury (managed address, registered, per-token encrypted
balances), POST /orgs/:id/treasury/deposit (Add funds — minor-unit USDC +
fresh idempotency key, reflects submitted/confirmed), and GET
/orgs/:id/treasury/deposits (recent deposits). Removed the no-backend
Stellar-era actions (public balance, receive, send-public, prove-*). Dashboard
+ Pay updated for the new shape; demo stubs reseeded.

Epic #58 step D. Closes #64.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The console treasury flow now uses org-scoped treasury views, managed encrypted balances, direct deposits, and deposit history. Legacy public-balance, funding, and proof operations are removed from the API, demo implementation, store, and Treasury screen.

Changes

Org-scoped treasury flow

Layer / File(s) Summary
Treasury contracts and endpoints
packages/types/src/api.ts
Defines managed treasury, encrypted balance, deposit, and deposit-history types, and registers org-scoped treasury endpoints.
Demo treasury data
apps/console/src/demo/api.ts, apps/console/src/demo/data.ts
Seeds treasury deposits and implements org-scoped treasury reads, deposit creation, and paginated deposit history.
API client and org-scoped store
apps/console/src/lib/api.ts, apps/console/src/lib/store.tsx, apps/console/src/lib/*.test.*
Replaces legacy treasury methods with org-scoped methods and loads treasury data using the active organization.
Managed treasury screen
apps/console/src/screens/Treasury.tsx, apps/console/src/screens/Treasury.test.tsx
Displays managed balances and deposits, validates and submits USDC deposits with idempotency keys, and restricts deposits by role.
Dashboard and payment views
apps/console/src/screens/Dashboard.tsx, apps/console/src/screens/Dashboard.test.tsx, apps/console/src/screens/Pay.tsx
Reads the primary USDC balance from the new treasury shape and removes numeric treasury balance display from payments.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TreasuryScreen
  participant ConsoleApi
  participant OrgTreasuryEndpoints
  TreasuryScreen->>ConsoleApi: Load treasury and deposits
  ConsoleApi->>OrgTreasuryEndpoints: GET org-scoped treasury data
  OrgTreasuryEndpoints-->>ConsoleApi: Treasury view and deposit history
  ConsoleApi-->>TreasuryScreen: Render balances and deposits
  TreasuryScreen->>ConsoleApi: Submit deposit with idempotency key
  ConsoleApi->>OrgTreasuryEndpoints: POST treasury deposit
  OrgTreasuryEndpoints-->>ConsoleApi: Deposit result and transaction hash
  ConsoleApi-->>TreasuryScreen: Refresh treasury and show result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the org-scoped treasury APIs, UI, types, and demo updates required by #64, including deposits, history, and active-org gating.
Out of Scope Changes check ✅ Passed No clear out-of-scope code changes appear beyond the requested treasury rewrite; the CCTP path is explicitly left as a TODO seam.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: moving console treasury behavior to org-scoped backend endpoints.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/64-treasury

Comment @coderabbitai help to get the list of available commands.


const fromName = accounts.find((a) => a.id === fromAccountId)?.name ?? "";
const fromBalance = treasury?.accounts.find((a) => a.account.id === fromAccountId)?.balance?.amount;
const fromBalance: string | undefined = undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Source Balance Always Hidden

When an account is selected, this value is always undefined, so the Pay form now renders Balance: Private for every source account. Users lose the decoded balance check that previously warned them before submitting an overdrawn payment, and the failure only appears after the backend rejects or cannot settle the payment.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/console/src/screens/Pay.tsx
Line: 50

Comment:
**Source Balance Always Hidden**

When an account is selected, this value is always `undefined`, so the Pay form now renders `Balance: Private` for every source account. Users lose the decoded balance check that previously warned them before submitting an overdrawn payment, and the failure only appears after the backend rejects or cannot settle the payment.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

@hitakshiA

Copy link
Copy Markdown
Contributor Author

Greptile (Pay source balance always undefined): acknowledged, deferred to step B by design. The old fromBalance read treasury.accounts[].balance — the org-scoped treasury (this PR) has per-token encrypted balances, not per-account balances, so that lookup no longer exists. Pay (one-off payments) is a no-backend domain (no /payments engine on the eERC backend); step B hides it in real mode and decides its demo-only fate. Codex stubbed the value to keep it compiling; in demo it now shows "Balance: Private" (acceptable for a privacy console, though it loses the overdraw pre-check). Tracking the proper Pay handling in step B rather than bolting a per-account balance back onto the treasury model here.

@hitakshiA
hitakshiA merged commit 04a3b80 into main Jul 11, 2026
3 checks passed
@hitakshiA
hitakshiA deleted the codex/64-treasury branch July 11, 2026 03:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/console/src/demo/data.ts (1)

436-438: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Source chain-scoped tokenIds from @benzo/config instead of hardcoding.

The "avalanche-fuji:usdc" / "avalanche-fuji:eurc" identifiers (and sourceChain: "avalanche-fuji" in the seeded deposits) embed the chain directly. Per the project convention these chain/token definitions should come from @benzo/config with a TODO(@benzo/config) fallback until that package is published, so demo and real paths stay in sync when the chain changes.

As per coding guidelines: "Use chain definitions and addresses from @benzo/config (keep the TODO(@benzo/config) vendor fallback until that package is published)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/demo/data.ts` around lines 436 - 438, Replace hardcoded
Avalanche Fuji token IDs and seeded deposit sourceChain values in the demo data
with the corresponding chain-scoped definitions from `@benzo/config`, retaining
the TODO(`@benzo/config`) vendor fallback until the package is available. Update
the balances entries and seeded deposits consistently so demo and production
paths share the same chain configuration.

Source: Coding guidelines

apps/console/src/demo/api.ts (1)

159-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded tokenId/sourceChain should come from @benzo/config.

tokenId: avalanche-fuji:${body.token} (and sourceChain: "avalanche-fuji" on Line 154) embed the chain. Same convention as flagged in data.ts — prefer @benzo/config chain/token definitions with the TODO(@benzo/config) fallback.

As per coding guidelines: "Use chain definitions and addresses from @benzo/config (keep the TODO(@benzo/config) vendor fallback until that package is published)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/demo/api.ts` at line 159, Replace the hardcoded
"avalanche-fuji" values in the demo API’s sourceChain and tokenId construction
with the corresponding chain and token definitions from `@benzo/config`,
preserving the TODO(`@benzo/config`) vendor fallback until the package is
available. Update the affected handler around the returned object and
sourceChain assignment consistently.

Source: Coding guidelines

apps/console/src/screens/Pay.tsx (1)

50-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

fromBalance is now a permanent no-op; the "Balance" UI branch is dead code.

Since fromBalance is hardcoded to undefined and never reassigned, the fromBalance != null ? <Amount .../> : <span>Private</span> branch at Line 169 will always take the else path. This matches the PR intent (no numeric treasury balance shown), but leaving the always-false condition and unused string | undefined type annotation as-is obscures that this is an intentional placeholder rather than a bug. Consider collapsing to the literal "Private" render and adding a short // TODO: note tying it to the follow-up work for per-account balances (or the CCTP funding seam) so future readers don't mistake it for leftover dead code.

♻️ Optional simplification
-  const fromBalance: string | undefined = undefined;
+  // TODO(treasury): wire up real per-account balance once available; until then, balances are always private.
-                  {fromBalance != null ? <Amount minor={fromBalance} className="font-medium text-fg" /> : <span className="text-fg">Private</span>}
+                  <span className="text-fg">Private</span>

Also applies to: 165-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/screens/Pay.tsx` at line 50, Replace the unused
`fromBalance` variable and its always-false conditional in the payment balance
rendering with the literal “Private” output. Add a brief TODO comment
documenting the intended follow-up for per-account balances or the CCTP funding
seam, so the placeholder is clearly intentional.
apps/console/src/screens/Dashboard.test.tsx (1)

25-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate treasury fixture literal.

The same detailed treasury object (address/custody/registered/consented/custodyConsent/balances) is repeated verbatim in beforeEach and again in the "collapses setup into a compact banner" test. Extracting a shared makeTreasury()/constant fixture would remove the duplication and make future contract changes (e.g. to TreasuryView) a one-line update instead of a multi-site edit.

Also applies to: 59-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/screens/Dashboard.test.tsx` around lines 25 - 30, The
detailed treasury fixture is duplicated between beforeEach and the “collapses
setup into a compact banner” test. Extract the shared address, custody, consent,
and balances data into a makeTreasury() helper or constant, then reuse it in
both locations so TreasuryView contract changes require updating only one
fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/console/src/demo/api.ts`:
- Around line 161-168: Handle an unknown before cursor explicitly in
treasuryDeposits: when query.before is provided and findIndex returns -1, return
an empty deposits array with no nextCursor; otherwise use index + 1 for the
start position, while retaining 0 when before is unset. Remove the unnecessary
Math.max call and preserve normal pagination for valid cursors.

In `@apps/console/src/screens/Treasury.tsx`:
- Line 65: Change the initial value of the amount state in the Treasury
component from "1000" to an empty string so the money-movement form starts with
no prefilled deposit amount.
- Around line 118-129: Persist the deposit idempotency key across retries
instead of generating it inside each addFunds submission. Add a useRef near the
other state in the Treasury component, initialize it with
randomIdempotencyKey(), reuse its value in api.depositToTreasury, and clear or
replace the ref only after a successful deposit; retain the same key when the
request fails or times out.
- Around line 83-103: Prevent stale deposit requests from updating state after
an organization switch. In loadDeposits and its useEffect, add a request-scoped
cancellation flag or generation ref tied to activeOrgId, and guard every
setDeposits, setDepositsError, and setDepositsLoading call—including success,
error, and finally paths—so only the latest request can update state.
- Around line 384-388: Update the KV component’s value prop to use an imported
ReactNode type from "react" instead of React.ReactNode, matching the typing
pattern used by other console components and avoiding reliance on the React UMD
global.

---

Nitpick comments:
In `@apps/console/src/demo/api.ts`:
- Line 159: Replace the hardcoded "avalanche-fuji" values in the demo API’s
sourceChain and tokenId construction with the corresponding chain and token
definitions from `@benzo/config`, preserving the TODO(`@benzo/config`) vendor
fallback until the package is available. Update the affected handler around the
returned object and sourceChain assignment consistently.

In `@apps/console/src/demo/data.ts`:
- Around line 436-438: Replace hardcoded Avalanche Fuji token IDs and seeded
deposit sourceChain values in the demo data with the corresponding chain-scoped
definitions from `@benzo/config`, retaining the TODO(`@benzo/config`) vendor
fallback until the package is available. Update the balances entries and seeded
deposits consistently so demo and production paths share the same chain
configuration.

In `@apps/console/src/screens/Dashboard.test.tsx`:
- Around line 25-30: The detailed treasury fixture is duplicated between
beforeEach and the “collapses setup into a compact banner” test. Extract the
shared address, custody, consent, and balances data into a makeTreasury() helper
or constant, then reuse it in both locations so TreasuryView contract changes
require updating only one fixture.

In `@apps/console/src/screens/Pay.tsx`:
- Line 50: Replace the unused `fromBalance` variable and its always-false
conditional in the payment balance rendering with the literal “Private” output.
Add a brief TODO comment documenting the intended follow-up for per-account
balances or the CCTP funding seam, so the placeholder is clearly intentional.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 89bf2a86-6a4c-4e20-a5c4-4b2bb1004118

📥 Commits

Reviewing files that changed from the base of the PR and between 4f0ad8a and 6009f9d.

📒 Files selected for processing (12)
  • apps/console/src/demo/api.ts
  • apps/console/src/demo/data.ts
  • apps/console/src/lib/api.test.ts
  • apps/console/src/lib/api.ts
  • apps/console/src/lib/store.test.tsx
  • apps/console/src/lib/store.tsx
  • apps/console/src/screens/Dashboard.test.tsx
  • apps/console/src/screens/Dashboard.tsx
  • apps/console/src/screens/Pay.tsx
  • apps/console/src/screens/Treasury.test.tsx
  • apps/console/src/screens/Treasury.tsx
  • packages/types/src/api.ts

Comment on lines +161 to +168
treasuryDeposits: async (_orgId: string, query: { limit?: number; before?: string } = {}): Promise<TreasuryDepositsResponse> => {
await delay(READ);
const start = query.before ? db.treasuryDeposits.findIndex((d) => d.id === query.before) + 1 : 0;
const from = Math.max(0, start);
const limit = query.limit ?? 20;
const deposits = db.treasuryDeposits.slice(from, from + limit);
const next = db.treasuryDeposits[from + limit]?.id;
return { deposits: clone(deposits), nextCursor: next };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unknown before cursor silently returns the first page.

When query.before is provided but no matching deposit exists, findIndex(...) returns -1, so start becomes 0 and the method returns the first page (with a nextCursor) rather than an empty result. A stale/invalid cursor then re-serves page 1, which can loop a "load more" flow. Math.max(0, start) is also dead once before is unset (0) or found (idx+1 ≥ 1).

🐛 Proposed fix
-    const start = query.before ? db.treasuryDeposits.findIndex((d) => d.id === query.before) + 1 : 0;
-    const from = Math.max(0, start);
-    const limit = query.limit ?? 20;
+    const limit = query.limit ?? 20;
+    if (query.before) {
+      const idx = db.treasuryDeposits.findIndex((d) => d.id === query.before);
+      if (idx === -1) return { deposits: [], nextCursor: undefined };
+    }
+    const from = query.before
+      ? db.treasuryDeposits.findIndex((d) => d.id === query.before) + 1
+      : 0;
     const deposits = db.treasuryDeposits.slice(from, from + limit);
     const next = db.treasuryDeposits[from + limit]?.id;
     return { deposits: clone(deposits), nextCursor: next };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
treasuryDeposits: async (_orgId: string, query: { limit?: number; before?: string } = {}): Promise<TreasuryDepositsResponse> => {
await delay(READ);
const start = query.before ? db.treasuryDeposits.findIndex((d) => d.id === query.before) + 1 : 0;
const from = Math.max(0, start);
const limit = query.limit ?? 20;
const deposits = db.treasuryDeposits.slice(from, from + limit);
const next = db.treasuryDeposits[from + limit]?.id;
return { deposits: clone(deposits), nextCursor: next };
treasuryDeposits: async (_orgId: string, query: { limit?: number; before?: string } = {}): Promise<TreasuryDepositsResponse> => {
await delay(READ);
const limit = query.limit ?? 20;
if (query.before) {
const idx = db.treasuryDeposits.findIndex((d) => d.id === query.before);
if (idx === -1) return { deposits: [], nextCursor: undefined };
}
const from = query.before
? db.treasuryDeposits.findIndex((d) => d.id === query.before) + 1
: 0;
const deposits = db.treasuryDeposits.slice(from, from + limit);
const next = db.treasuryDeposits[from + limit]?.id;
return { deposits: clone(deposits), nextCursor: next };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/demo/api.ts` around lines 161 - 168, Handle an unknown
before cursor explicitly in treasuryDeposits: when query.before is provided and
findIndex returns -1, return an empty deposits array with no nextCursor;
otherwise use index + 1 for the start position, while retaining 0 when before is
unset. Remove the unnecessary Math.max call and preserve normal pagination for
valid cursors.

const activeOrgId = activeOrg?.id;
const canAddFunds = canManageTreasury(session?.role);

const [amount, setAmount] = useState("1000");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Money-movement form defaults to a prefilled 1000 USDC amount.

Prefilling a nonzero, sizable amount on a deposit form increases the risk of an accidental large deposit if a user clicks "Add funds" without adjusting it first. Consider defaulting to an empty string.

🔧 Proposed fix
-  const [amount, setAmount] = useState("1000");
+  const [amount, setAmount] = useState("");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [amount, setAmount] = useState("1000");
const [amount, setAmount] = useState("");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/screens/Treasury.tsx` at line 65, Change the initial value
of the amount state in the Treasury component from "1000" to an empty string so
the money-movement form starts with no prefilled deposit amount.

Comment on lines +83 to +103
const loadDeposits = useCallback(async () => {
if (!activeOrgId) {
setDeposits([]);
setDepositsError(null);
return;
}
// Refresh OUTSIDE the try: a settled shield must not be flipped to failed by a
// transient refresh error.
if (confirmed) {
await Promise.all([refresh(), loadPublic()]).catch(() => {});
}
}

// ---- Send to a wallet (real public on-chain USDC payment) -----------------
const [sendTo, setSendTo] = useState("");
const [sendAmt, setSendAmt] = useState("");
const [confirmSend, setConfirmSend] = useState(false);
const [busySend, setBusySend] = useState(false);
const [sendResult, setSendResult] = useState<{ onChain: boolean; txHash?: string; error?: string } | null>(null);
const addrLooksValid = /^0x[a-fA-F0-9]{40}$/.test(sendTo.trim());

async function sendPublic(): Promise<boolean> {
setBusySend(true);
setSendResult(null);
setDepositsLoading(true);
try {
const r = await api.treasurySendPublic(sendTo.trim(), sendAmt);
if (r.onChain) {
setSendResult({ onChain: true, txHash: r.txHash });
toast({ title: `Sent ${sendAmt} USDC to a wallet`, tone: "success" });
setSendTo("");
setSendAmt("");
await loadPublic();
return true;
}
setSendResult({ onChain: false, error: r.error });
toast({ title: r.error ?? "Couldn't send", tone: "danger" });
return false;
const response = await api.treasuryDeposits(activeOrgId, { limit: DEPOSIT_LIMIT });
setDeposits(response.deposits);
setDepositsError(null);
} catch (e) {
toast({ title: friendlyError(e), tone: "danger" });
setSendResult({ onChain: false, error: friendlyError(e) });
return false;
setDepositsError(friendlyError(e, "Couldn't load treasury deposits."));
} finally {
setBusySend(false);
setDepositsLoading(false);
}
}
}, [activeOrgId]);

// ---- Receive (address + QR) ----------------------------------------------
const [receiveOpen, setReceiveOpen] = useState(false);
const [recv, setRecv] = useState<{ address: string; asset: string; issuer: string; live: boolean } | null>(null);
const [recvLoading, setRecvLoading] = useState(false);
useEffect(() => {
void loadDeposits();
}, [loadDeposits]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and nearby relevant files.
ast-grep outline apps/console/src/screens/Treasury.tsx --view expanded || true
ast-grep outline apps/console/src/screens/store.tsx --view expanded || true

# Read the relevant ranges with line numbers.
echo "---- Treasury.tsx ----"
sed -n '1,220p' apps/console/src/screens/Treasury.tsx | cat -n

echo "---- store.tsx ----"
sed -n '1,260p' apps/console/src/screens/store.tsx | cat -n

# Find any cancellation / request guard patterns in the screen directory.
rg -n "cancelled|requestOrgIdRef|useRef\\(|activeOrgId" apps/console/src/screens -g '*.tsx'

Repository: Miny-Labs/benzo-console

Length of output: 11029


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the console store implementation and any cancelled-flag refresh pattern.
fd -a 'store.tsx' apps/console/src
fd -a 'store.ts' apps/console/src
fd -a 'store.*' apps/console/src

rg -n "cancelled|refresh\\s*=|useConsole|activeOrgId" apps/console/src -g '*.ts' -g '*.tsx'

# Read the most likely store file if found.
for f in $(fd -a 'store.tsx' apps/console/src; fd -a 'store.ts' apps/console/src); do
  echo "---- $f ----"
  sed -n '1,260p' "$f" | cat -n
done

Repository: Miny-Labs/benzo-console

Length of output: 28921


Guard deposit-history requests on org switch.

loadDeposits can still write an older org’s response after activeOrgId changes. Add a request-scoped ref or cancel flag so setDeposits, setDepositsError, and setDepositsLoading only update for the latest org.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/screens/Treasury.tsx` around lines 83 - 103, Prevent stale
deposit requests from updating state after an organization switch. In
loadDeposits and its useEffect, add a request-scoped cancellation flag or
generation ref tied to activeOrgId, and guard every setDeposits,
setDepositsError, and setDepositsLoading call—including success, error, and
finally paths—so only the latest request can update state.

Comment on lines +118 to +129
setBusyDeposit(true);
setDepositResult(null);
try {
const result = await api.depositToTreasury(activeOrgId, {
amount: minor,
token: FUNDING_TOKEN,
idempotencyKey: randomIdempotencyKey(),
});
setDepositResult(result);
toast({ title: `${depositStatusLabel(result.status)} - ${formatMoney(result.amount, TOKEN_META[result.token].decimals, TOKEN_META[result.token].symbol)}`, tone: "success" });
setAmount("");
await Promise.allSettled([refresh(), loadDeposits()]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Idempotency key is regenerated on every submit, defeating retry protection.

randomIdempotencyKey() is called fresh inside addFunds() each time it runs. If a deposit request succeeds server-side but the client never sees the response (timeout/dropped connection), a user retry generates a brand-new key, so the backend has no way to recognize it as the same logical deposit — risking a duplicate on-chain deposit. Idempotency keys are only useful if they persist across retries of the same logical operation.

🔧 Proposed fix: persist the key until success
+  const pendingIdempotencyKey = useRef<string | null>(null);
+
   async function addFunds() {
     if (!activeOrgId) return;
     let minor = "0";
     try {
       minor = usdcToMinor(amount);
     } catch {
       setDepositResult(null);
       return;
     }
     if (!MINOR_UNITS.test(minor)) {
       setDepositResult(null);
       return;
     }
     setBusyDeposit(true);
     setDepositResult(null);
     try {
+      if (!pendingIdempotencyKey.current) pendingIdempotencyKey.current = randomIdempotencyKey();
       const result = await api.depositToTreasury(activeOrgId, {
         amount: minor,
         token: FUNDING_TOKEN,
-        idempotencyKey: randomIdempotencyKey(),
+        idempotencyKey: pendingIdempotencyKey.current,
       });
       setDepositResult(result);
+      pendingIdempotencyKey.current = null;
       toast(...);

(requires adding useRef to the react import)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/screens/Treasury.tsx` around lines 118 - 129, Persist the
deposit idempotency key across retries instead of generating it inside each
addFunds submission. Add a useRef near the other state in the Treasury
component, initialize it with randomIdempotencyKey(), reuse its value in
api.depositToTreasury, and clear or replace the ref only after a successful
deposit; retain the same key when the request fails or times out.

Comment on lines +384 to +388
function KV({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 font-semibold text-white">
<ShieldCheck size={15} /> {result.headline}
</div>
{result.ref?.root ? (
<motion.div
className="rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2"
style={{ transformPerspective: 640 }}
initial={reduce ? false : { rotateX: 90, opacity: 0 }}
animate={{ rotateX: 0, opacity: 1 }}
transition={{ duration: 0.5, ease: EASE }}
data-testid="prove-merkle-root"
>
<div className="text-[11px] uppercase tracking-wide text-white/45">Merkle root</div>
<div className="break-all font-mono text-[12px] text-white/80">{result.ref.root}</div>
</motion.div>
) : null}
<div className="flex items-center justify-between gap-2 text-white/60">
<span>Verified on-chain. Anyone can re-check it.</span>
{result.ref ? <OnChainDetail refData={result.ref} /> : null}
</div>
<div className="flex items-center justify-between gap-4 py-2">
<dt className="text-muted">{label}</dt>
<dd className="font-medium text-fg">{value}</dd>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd tsconfig.json --exec cat {}
rg -n 'allowUmdGlobalAccess' .
rg -n 'React\.' apps/console/src --type=ts --type=tsx -g '!**/node_modules/**'

Repository: Miny-Labs/benzo-console

Length of output: 3560


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== apps/console/tsconfig.json ==\n'
cat -n apps/console/tsconfig.json

printf '\n== apps/console/src/screens/Treasury.tsx (relevant excerpt) ==\n'
sed -n '360,410p' apps/console/src/screens/Treasury.tsx

printf '\n== ReactNode usage in apps/console/src ==\n'
rg -n 'React\.ReactNode|type ReactNode|import .*ReactNode' apps/console/src

printf '\n== tsconfig chain search for allowUmdGlobalAccess ==\n'
rg -n 'allowUmdGlobalAccess' .

Repository: Miny-Labs/benzo-console

Length of output: 4695


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package manifests mentioning react/typescript ==\n'
rg -n '"(`@types/react`|react|typescript)"' package.json apps/console/package.json packages/*/package.json -g '!**/node_modules/**'

printf '\n== local TypeScript probe ==\n'
tmpdir="$(mktemp -d)"
cat > "$tmpdir/tsconfig.json" <<'JSON'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["index.tsx"]
}
JSON
cat > "$tmpdir/index.tsx" <<'TSX'
function KV({ value }: { value: React.ReactNode }) {
  return value;
}
export {};
TSX

if command -v tsc >/dev/null 2>&1; then
  (cd "$tmpdir" && tsc -p tsconfig.json) || true
else
  echo "tsc not available"
fi

Repository: Miny-Labs/benzo-console

Length of output: 1347


Import ReactNode here instead of using React.ReactNode
apps/console/tsconfig.json doesn’t enable allowUmdGlobalAccess, so React.ReactNode won’t resolve in this module. Use a type import from "react" like the other console components.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/screens/Treasury.tsx` around lines 384 - 388, Update the KV
component’s value prop to use an imported ReactNode type from "react" instead of
React.ReactNode, matching the typing pattern used by other console components
and avoiding reliance on the React UMD global.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(console): treasury screen onto org-scoped /orgs/:id/treasury

1 participant