Skip to content

Peanut wallet#1641

Merged
21 commits merged intopeanut-wallet-devfrom
peanut-wallet
Jan 27, 2026
Merged

Peanut wallet#1641
21 commits merged intopeanut-wallet-devfrom
peanut-wallet

Conversation

@jjramirezn
Copy link
Contributor

@jjramirezn jjramirezn commented Jan 26, 2026

Note

Introduces a new payment graph and refactors the invites graph to support multiple modes and scalable filtering, plus maintenance-based withdraw restrictions.

  • Adds /dev/payment-graph page with password access, performance mode, and P2P-focused overlays; marks it public in routes while backend/password-gated
  • Replaces invite graph page with auth-gated /dev/full-graph; adds (mobile-ui)/dev/layout to restrict dev routes in prod and updates dev tools index
  • Major InvitesGraph overhaul: new mode (full/payment/user), backend-driven topNodes limit (replaces showAllNodes), payment-safe anonymized data (size/frequency/volume), improved external node linking/direction, new force defaults, search for external nodes, revised legends/labels, and separate saved preferences per mode
  • Points page: show minimal personal graph in dev for all users; badge-gated in prod
  • KYC flow: prefill missing fullName/email from bank form before KYC; KYC modal now resets errors on open
  • Token selector: honors underMaintenance.disableSquidWithdraw to limit withdraws to USDC on Arbitrum with UI notice
  • Misc: simplify logout button rendering; expose resetError in useBridgeKycFlow; extend public routes regex

Written by Cursor Bugbot for commit 6bb7377. This will update automatically on new commits. Configure here.

Hugo0 and others added 21 commits January 20, 2026 13:48
- Restored "new users" feature: green nodes for recent signups (within activity window)
- Three-state activity status: New (green), Active (purple), Inactive (gray)
- Progressive inactive fade with exponential time bands (1w, 2w, 4w, 8w, 16w, 32w, 64w+)
- Each band gets progressively lighter gray + lower opacity for visual hierarchy
- Updated legend to include "New" status
- Fixed Grafana link format (teampeanut.grafana.net/explore-peanut-wallet-user)
- Fix external edge tooltips: show per-user amounts instead of "undefined - Invalid Date"
- Display accurate transaction counts/amounts per user-external node pair
- Update ExternalNode type to include userTxData for edge weight calculation
- Change minConnections from slider to discrete buttons (1, 2, 3, 5, 10)
- Lower default minConnections from 2 to 1 to show all external nodes
- Add fallback logic for missing userTxData

Now edges show "External: 5 txs ($200.50)" with correct per-user data
instead of showing node totals on every edge.
- Search now includes external nodes (banks, wallets, merchants)
- Searches both custom labels ("Manteca BR Deposit") and original IDs
- Search results show emoji indicators for external node types (🏦 🏪 💳)
- External results show user count and total USD instead of points
- Allow right-click selection of external nodes for camera zoom
- "Focused on" banner shows custom labels for external nodes with orange styling
- Results styled with orange hover for external nodes vs purple for users

Users can now search for "Manteca", "Bridge SEPA", bank IDs, etc.
Activity Time Window Filtering:
- Filter external nodes by activityDays (e.g., 30d filter hides old merchants)
- Add lastTxDate to ExternalNode type for client-side filtering
- External nodes now respect same time window as user nodes
- Fixes bug: devcon-swag-shop (last tx Nov 21) no longer shows with 30d filter

Logarithmic Edge Animation Scaling:
- OLD: Linear scaling made 9 txs vs 32 txs barely distinguishable
- NEW: Log10 scaling for dramatic visual distinction between activity levels

P2P Edge Scaling (logarithmic):
- Line width: 0.4px → 3.0px (7.5x variance)
- Particle speed: 0.0003 → 0.001 (3.3x variance, log-scaled by tx count)
- Particle size: 1.5px → 6.0px (4x variance, log-scaled by USD volume)
- Particle count: 1 → 5 particles (log-scaled)

External Edge Scaling (now matching P2P):
- Line width: 0.4px → 3.0px (same formula as P2P)
- Particle speed: 0.0002 → 0.0008 (70% of P2P max for visual distinction)
- Particle size: 1.5px → 6.0px (same 4x variance, log-scaled by USD)
- Particle count: 1 → 4 particles (one less than P2P to reduce clutter)

Visual Impact Examples:
- 1 tx: slow, tiny, single particle (barely visible)
- 9 txs: 2.2x faster, 2 particles, medium (clearly visible)
- 32 txs: 2.8x faster, 3 particles, larger (dramatically different)
- 100 txs: 3.3x max speed, 5 particles, huge (very obvious)

External Node Tooltips:
- Simplified to show only formatted label (no full account numbers)
- Bank accounts: "🏷️ Account: ES27 **** 7890"
- Wallets/Merchants: "🏷️ ID: devcon-swag-shop"

Code Cleanup:
- Removed all console.log statements (migration logs, force logs, recalc debug)
- Cleaner console output for production

Performance: Logarithmic calculations add negligible overhead (<0.01ms per frame).
- Add `/dev/payment-graph` page with anonymized P2P-only visualization
- Add `/dev/layout.tsx` for dev-only route protection (except full-graph and payment-graph)
- Rename invite-graph to full-graph
- Replace "All nodes" checkbox with "Top nodes" slider (0-10000, backend-filtered)
- Remove timestamps from payment mode response for full anonymization
- Remove unused `showAllNodes` prop, replace with `topNodes` (number)
- Payment mode defaults: merchants ON, minConnections=10, 5000 node limit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…chant issue

Cleaned up all debug console.log statements that were added to investigate the duplicate node ID issue. The root cause was identified and fixed in the backend (SHA-256 hash collision prevention).

Changes:
- Removed API response duplicate detection logging
- Removed external links duplicate ID checking
- Removed orphan merchant warning logs
- Removed link creation summary logs
- Cleaned up unused variables (txDataKeys, matchedLinks, unmatchedKeys)
- Remove verbose debug logs from graph rendering
- Add lightweight console.assert() for duplicate ID detection
- Remove unused simulation variable
- Clean up debug logging throughout InvitesGraph component
- Add safety nets for duplicate node/external node IDs

This reduces console noise in production while maintaining
runtime safety checks that only appear when assertions fail.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add SizeLabel export for node sizing in payment mode
- Make node fields optional to support both full and payment modes
- Add separate localStorage key for payment graph preferences
- Update payment graph to use topNodes=5000 with performanceMode

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…op-nodes-slider

feat(graph): Add payment-graph mode and top-nodes slider for full-graph
- Only update fullName if it's missing (not overwrite existing)
- Only update email if it's missing (not overwrite existing)
- Add proper error handling for fetchUser() call
- Trim email field like accountOwnerName
- Only reset error when modal opens (not when closing)
hot-fix: update user details if missing during KYC process and reset error state on modal open
- Replace API key auth with password query param for payment mode
- Read password from URL (?password=xxx) or show input form
- Support direct URL access: /dev/payment-graph?password=xxx
- Backend validates password against PAYMENT_GRAPH_PASSWORD env var
- API key still required for full-graph mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…op-nodes-slider

feat: payment graph with password auth and UI improvements
The external nodes API call was missing password param and was still
sending API key header for payment mode. This caused 401 errors when
loading external nodes in payment-graph.

- Add password option to getExternalNodes
- Pass password in query params for payment mode
- Skip api-key header for payment mode (uses password auth)
- Pass password from InvitesGraph component to API call

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Username was 'konrad' but should be 'kkonrad'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jan 26, 2026 5:38pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 26, 2026

Walkthrough

This PR introduces a multi-mode graph visualization system (full, payment, user modes) with corresponding dev pages, routing infrastructure, and enhanced data models. Key additions include a new Payment Graph feature with password gating, renaming Invite Graph to Full Graph, replacing showAllNodes with topNodes-based filtering, restructuring data types to support anonymized qualitative labels (frequency/volume/size), implementing dev route access control via layout middleware, and adding maintenance-based withdrawal gating.

Changes

Cohort / File(s) Summary
Dev Pages & Layout Infrastructure
src/app/(mobile-ui)/dev/layout.tsx, src/app/(mobile-ui)/dev/page.tsx, src/app/(mobile-ui)/dev/full-graph/page.tsx, src/app/(mobile-ui)/dev/payment-graph/page.tsx
Added new dev layout component with route-based access control for protected dev routes. Renamed Invite Graph tool to Full Graph with updated path and description. Created two new dev visualizations: Full Graph (with frontend auth gating and user-based access restrictions) and Payment Graph (with password-based entry, feature-rich control panel, and performance mode toggle).
Graph Component & Data Model Refactoring
src/components/Global/InvitesGraph/index.tsx, src/services/points.ts
Introduced multi-mode system (full, payment, user) with mode-driven defaults, visibility, and force configurations. Replaced showAllNodes with topNodes filtering. Added qualitative labeling types (FrequencyLabel, VolumeLabel, SizeLabel) and restructured P2PEdge/ExternalNode to support both full (exact counts) and anonymized (frequency/volume labels) representations. Enhanced API signatures for getInvitesGraph and getExternalNodes with mode, topNodes, and password parameters.
Graph Preferences Hook
src/hooks/useGraphPreferences.ts
Added mode-based persistence with separate storage keys (GRAPH_PREFS_KEY vs PAYMENT_GRAPH_PREFS_KEY). Expanded GraphPreferences interface to include topNodes field. Modified hook signature to accept mode parameter with 'full' default.
Maintenance & Withdrawal Controls
src/config/underMaintenance.config.ts, src/components/Global/TokenSelector/TokenSelector.tsx
Introduced disableSquidWithdraw maintenance flag to gate cross-chain withdrawals. Updated TokenSelector to conditionally restrict popular tokens list to USDC on Arbitrum and hide network selection, search, and chain-clearing UI when maintenance is active.
Points Page & User Management
src/app/(mobile-ui)/points/page.tsx, src/components/AddWithdraw/AddWithdrawCountriesList.tsx, src/components/Profile/index.tsx
Added IS_DEV flag usage for graph visibility control in points page. Implemented auto-fill logic for missing user fields (fullName, email) in AddWithdraw flow via updateUserById. Removed fallback 'Anonymous User' string in profile display name.
KYC & Navigation Updates
src/hooks/useBridgeKycFlow.ts, src/components/Kyc/InitiateBridgeKYCModal.tsx, src/components/Global/NavHeader/index.tsx
Added resetError hook to useBridgeKycFlow for explicit error state clearing. Updated InitiateBridgeKYCModal to reset errors on modal open and conditionally inject error-specific CTA. Simplified NavHeader logout button by using icon prop instead of child Icon element.
Configuration & Routing
src/constants/routes.ts, .cursorrules
Added dev/payment-graph to PUBLIC_ROUTES and PUBLIC_ROUTES_REGEX to allow API-key-based access. Updated cursor rules version (0.0.1 → 0.0.2) with new sections on URL as State and Export Rules; removed explicit imports guideline and expanded Testing section.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • #1630: Makes overlapping changes including renaming InviteGraph→FullGraph, adding dev/payment-graph route and page, replacing showAllNodes with topNodes, and restructuring graph data types with mode support across multiple shared files.
🚥 Pre-merge checks | ❌ 3
❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Peanut wallet' is vague and generic, providing no meaningful information about the specific changes in this substantial changeset. Use a more descriptive title that captures the main change, such as 'Add payment graph visualization and dev layout controls' or 'Introduce multi-mode graph system with payment/full modes'.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relatedness to the changeset. Add a pull request description that explains the purpose, motivation, and key changes in this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch peanut-wallet

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

@jjramirezn jjramirezn closed this Jan 26, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/Global/TokenSelector/TokenSelector.tsx (1)

62-70: Ensure maintenance mode actually enforces the USDC/Arbitrum selection.

Right now, if a non‑USDC token/chain is already selected in context, isSquidWithdrawDisabled only hides UI but doesn’t override the selection. That can let disallowed selections persist. Consider coercing the selection (and clearing UI state) when maintenance is active.

🛠️ Suggested fix
-import React, { type ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react'
+import React, { type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
@@
     } = useContext(tokenSelectorContext)
+
+    useEffect(() => {
+        if (!isSquidWithdrawDisabled) return
+        setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString())
+        setSelectedTokenAddress(PEANUT_WALLET_TOKEN)
+        setSearchValue('')
+        setNetworkSearchValue('')
+        setShowNetworkList(false)
+    }, [
+        isSquidWithdrawDisabled,
+        setSelectedChainID,
+        setSelectedTokenAddress,
+        setSearchValue,
+        setNetworkSearchValue,
+        setShowNetworkList,
+    ])
src/components/Global/InvitesGraph/index.tsx (1)

953-1031: Add missing dependencies to both useEffects and update external nodes cache key

The second useEffect fetches external nodes using topNodes and password, but neither is included in the dependency array. This causes stale external nodes when topNodes changes (e.g., full graph slider) or when password is updated. Additionally, the cache key (externalNodesFetchedLimitRef) only tracks the limit, not topNodes, preventing cache invalidation.

The first useEffect also fails to include props.password in its dependencies, though it's conditionally passed to the API.

Add topNodes and props.password to both useEffect dependency arrays, and update the cache ref to track both limit and topNodes:

Proposed fix
-const externalNodesFetchedLimitRef = useRef<number | null>(null)
+const externalNodesFetchedRef = useRef<{ limit: number; topNodes?: number } | null>(null)

 useEffect(() => {
     if (isMinimal) return
-}, [isMinimal, !isMinimal && props.apiKey, mode, topNodes])
+}, [isMinimal, props.apiKey, mode, topNodes, props.password])

 useEffect(() => {
     if (isMinimal) return
     if (!externalNodesConfig.enabled) return
-    const lastLimit = externalNodesFetchedLimitRef.current
-    if (lastLimit !== null && lastLimit >= externalNodesConfig.limit) return
+    const effectiveTopNodes = topNodes > 0 ? topNodes : undefined
+    const lastFetch = externalNodesFetchedRef.current
+    if (lastFetch && lastFetch.limit >= externalNodesConfig.limit && lastFetch.topNodes === effectiveTopNodes) return
  
             if (result.success && result.data) {
                 setExternalNodesData(result.data.nodes)
-                externalNodesFetchedLimitRef.current = externalNodesConfig.limit
+                externalNodesFetchedRef.current = { limit: externalNodesConfig.limit, topNodes: effectiveTopNodes }
  
-}, [isMinimal, !isMinimal && props.apiKey, externalNodesConfig.enabled, mode, externalNodesConfig.limit])
+}, [isMinimal, props.apiKey, externalNodesConfig.enabled, mode, externalNodesConfig.limit, topNodes, props.password])
🤖 Fix all issues with AI agents
In @.cursorrules:
- Line 10: Typo: change "explicity" to "explicitly" in the .cursorrules text
line that reads "**Do not generate .md files** unless explicity told to do so."
— update that phrase to "**Do not generate .md files** unless explicitly told to
do so." to correct the spelling.

In `@src/app/`(mobile-ui)/dev/layout.tsx:
- Around line 1-19: The DevLayout client component currently calls notFound(),
which throws in client-side code; either move the guard out of the client module
(e.g., implement the check in a Server Component or middleware using IS_DEV and
PRODUCTION_ALLOWED_ROUTES) or replace the client-side behavior: remove
notFound(), import useRouter from 'next/navigation', call const router =
useRouter(), and inside a useEffect check the pathname against
PRODUCTION_ALLOWED_ROUTES and call router.replace('/404') (or another safe
route) when not allowed, returning null while redirecting; reference DevLayout,
PRODUCTION_ALLOWED_ROUTES, IS_DEV, usePathname, and notFound to locate the
change.

In `@src/app/`(mobile-ui)/dev/page.tsx:
- Line 81: The text in the dev page ("• These tools are only available in
development mode") is inconsistent with route access rules defined in
src/constants/routes.ts (notably the /dev/payment-graph public API-key route);
update the copy in src/app/(mobile-ui)/dev/page.tsx to accurately reflect access
control (either change the line to note that some tools require development mode
while /dev/payment-graph is public via API key, or indicate per-tool access like
"Development-only; some tools (e.g. /dev/payment-graph) are public via API
key"). Locate the string in the Dev page component and adjust the wording to
match the actual policies or add a short per-tool note linking to the
routes/constants for clarity.

In `@src/app/`(mobile-ui)/dev/payment-graph/page.tsx:
- Around line 16-23: The useEffect that reads the password from searchParams
(the block using useEffect, searchParams.get('password'), setPassword and
setPasswordSubmitted) must remove/scrub the password query param immediately
after reading it to avoid leaking credentials; after calling setPassword and
setPasswordSubmitted, programmatically replace the URL (e.g., via Next.js
router.replace or history.replaceState) with the same path but without the
password param so the credential is not retained in browser history or
referrers, and ensure this scrub happens only once (guarded by the same effect)
to preserve current behavior.

In `@src/config/underMaintenance.config.ts`:
- Around line 40-45: The config underMaintenanceConfig currently sets
disableSquidWithdraw: true which globally disables cross-chain withdrawals;
change the default to false in underMaintenanceConfig (MaintenanceConfig) so
x‑chain withdrawals remain enabled by default, and if desired read an
environment flag or feature flag to toggle disableSquidWithdraw per-deployment
(e.g., pull from process.env or a runtime feature manager) so ops can disable it
without shipping a global default of true.

In `@src/services/points.ts`:
- Around line 316-318: The code currently adds sensitive passwords to the URL
querystring via params.set('password', options.password), which leaks secrets
into logs and Sentry; change the request to send the password in a secure place
instead: remove any params.set('password', ...) usage and instead attach the
secret in a request header (e.g., 'Authorization' or 'X-Password') or in the
POST body when calling fetchWithSentry; update all occurrences that build params
(the params variable and any code paths referenced around options?.password) to
stop including password in the URL and ensure fetchWithSentry is passed the
header/body value; if the backend cannot accept headers/body, change
fetchWithSentry to redact the password from logged URLs before sending to Sentry
(redact in fetchWithSentry by replacing the password query value with
'[REDACTED]') and update callers accordingly.
- Around line 308-311: Add an early guard that returns/throws a local error when
payment mode is requested but no password is provided: check the computed
isPaymentMode (options?.mode === 'payment') and if true and options?.password is
falsy, immediately return or throw a clear validation error instead of
proceeding (before creating URLSearchParams or making any network calls). Apply
the same short-circuit in the other payment-related branch referenced by the
code around where isPaymentMode/params are used (the second flow at the other
occurrence).
🧹 Nitpick comments (3)
src/components/AddWithdraw/AddWithdrawCountriesList.tsx (1)

148-154: Consider validating trimmed values before adding to payload.

The truthiness check happens before .trim(), so whitespace-only strings like " " would pass the check but result in empty values being sent to the API.

Suggested defensive approach
-                    if (!hasNameOnLoad && rawData.accountOwnerName) {
-                        updatePayload.fullName = rawData.accountOwnerName.trim()
+                    const trimmedName = rawData.accountOwnerName?.trim()
+                    if (!hasNameOnLoad && trimmedName) {
+                        updatePayload.fullName = trimmedName
                     }

-                    if (!hasEmailOnLoad && rawData.email) {
-                        updatePayload.email = rawData.email.trim()
+                    const trimmedEmail = rawData.email?.trim()
+                    if (!hasEmailOnLoad && trimmedEmail) {
+                        updatePayload.email = trimmedEmail
                     }
src/constants/routes.ts (1)

66-66: Ensure PUBLIC_ROUTES array and regex stay in sync.

The regex pattern now includes dev\/payment-graph, which correctly matches the PUBLIC_ROUTES array entry. This is good practice. Consider adding a comment or test to ensure these stay synchronized when routes are added/removed.

src/services/points.ts (1)

6-23: Centralize P2PEdge to avoid type drift.

This duplicates src/components/Global/InvitesGraph/index.tsx (Line 127-139). Consider moving the type to a shared module (e.g., src/services/services.types or a graph.types file) and importing it in both places to prevent divergence.

- **Never open SVG files** - it crashes you. Only read jpeg, png, gif, or webp.
- **Never run jq command** - it crashes you.
- **Never run sleep** from command line - it hibernates pc.
- **Do not generate .md files** unless explicity told to do so.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "explicity" should be "explicitly".

📝 Proposed fix
-- **Do not generate .md files** unless explicity told to do so.
+- **Do not generate .md files** unless explicitly told to do so.
📝 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
- **Do not generate .md files** unless explicity told to do so.
- **Do not generate .md files** unless explicitly told to do so.
🤖 Prompt for AI Agents
In @.cursorrules at line 10, Typo: change "explicity" to "explicitly" in the
.cursorrules text line that reads "**Do not generate .md files** unless
explicity told to do so." — update that phrase to "**Do not generate .md files**
unless explicitly told to do so." to correct the spelling.

Comment on lines +1 to +19
'use client'

import { usePathname } from 'next/navigation'
import { notFound } from 'next/navigation'
import { IS_DEV } from '@/constants/general.consts'

// Routes that are allowed in production (protected by API key / user check)
const PRODUCTION_ALLOWED_ROUTES = ['/dev/full-graph', '/dev/payment-graph']

export default function DevLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()

// In production, only allow specific routes (full-graph, payment-graph)
// Other dev tools (leaderboard, shake-test, dev index) are dev-only
if (!IS_DEV) {
const isAllowedInProd = PRODUCTION_ALLOWED_ROUTES.some((route) => pathname?.startsWith(route))
if (!isAllowedInProd) {
notFound()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Next.js notFound() client component 'use client' error behavior

💡 Result:

notFound() is not a “client-side navigation” API in the App Router. It’s implemented by throwing a special error (NEXT_HTTP_ERROR_FALLBACK;404) to terminate rendering of the current route segment, so Next.js can render that segment’s not-found.js UI. This mechanism is designed for server-side rendering paths (e.g., Server Components during render, and other server-controlled flows), not for Client Component event handlers. [1]

Because "use client" makes the file part of the client module graph (rendered on the client after navigation/hydration), calling notFound() there typically results in a runtime error / red error overlay, rather than showing not-found.js, since there’s no server render to “bail out of” and swap in the segment’s Not Found boundary. [1], [2]

What to do in a Client Component instead

  • Render your own “not found” UI conditionally, or
  • Navigate with the client router (e.g., router.replace(...)) to a dedicated route, rather than calling notFound(). (The not-found.js convention is specifically for when notFound() is thrown during segment rendering.) [3]

Note: Tutorials showing notFound() inside a "use client" component are misleading for real App Router behavior. [1], [4]

Sources:
[1] Next.js API Reference: notFound() (throws NEXT_HTTP_ERROR_FALLBACK;404)
[2] Next.js Docs: Server vs Client Components ("use client" boundary; Client Components render on the client)
[3] Next.js API Reference: not-found.js convention (rendered when notFound() is thrown in a segment)
[4] Example article demonstrating notFound() inside a Client Component (non-official; conflicts with intended mechanism)


🏁 Script executed:

# First, let's examine the actual file to understand the context
cat -n src/app/\(mobile-ui\)/dev/layout.tsx 2>/dev/null || find . -name "layout.tsx" -path "*dev*" | head -5

Repository: peanutprotocol/peanut-ui

Length of output: 1028


notFound() throws a runtime error in client components and won't render the not-found page

notFound() is implemented by throwing a special error during server-side rendering to bail out of a route segment. Since this file is marked with 'use client', the component renders on the client after navigation, so calling notFound() will result in a runtime error instead of showing not-found.js. Either move this guard to a Server Component or middleware, or replace with a client-side redirect using the router.

🤖 Prompt for AI Agents
In `@src/app/`(mobile-ui)/dev/layout.tsx around lines 1 - 19, The DevLayout client
component currently calls notFound(), which throws in client-side code; either
move the guard out of the client module (e.g., implement the check in a Server
Component or middleware using IS_DEV and PRODUCTION_ALLOWED_ROUTES) or replace
the client-side behavior: remove notFound(), import useRouter from
'next/navigation', call const router = useRouter(), and inside a useEffect check
the pathname against PRODUCTION_ALLOWED_ROUTES and call router.replace('/404')
(or another safe route) when not allowed, returning null while redirecting;
reference DevLayout, PRODUCTION_ALLOWED_ROUTES, IS_DEV, usePathname, and
notFound to locate the change.

<h3 className="font-bold text-blue-900">ℹ️ Info</h3>
<ul className="space-y-1 text-sm text-blue-800">
<li>• These tools are publicly accessible (no login required)</li>
<li>• These tools are only available in development mode</li>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent info text with actual access control.

This text states tools are "only available in development mode", but according to src/constants/routes.ts, /dev/payment-graph is a public route accessible via API key. Consider updating the text to accurately reflect access policies, or clarifying which tools have different access levels.

🤖 Prompt for AI Agents
In `@src/app/`(mobile-ui)/dev/page.tsx at line 81, The text in the dev page ("•
These tools are only available in development mode") is inconsistent with route
access rules defined in src/constants/routes.ts (notably the /dev/payment-graph
public API-key route); update the copy in src/app/(mobile-ui)/dev/page.tsx to
accurately reflect access control (either change the line to note that some
tools require development mode while /dev/payment-graph is public via API key,
or indicate per-tool access like "Development-only; some tools (e.g.
/dev/payment-graph) are public via API key"). Locate the string in the Dev page
component and adjust the wording to match the actual policies or add a short
per-tool note linking to the routes/constants for clarity.

Comment on lines +16 to +23
// Check for password in URL on mount
useEffect(() => {
const urlPassword = searchParams.get('password')
if (urlPassword) {
setPassword(urlPassword)
setPasswordSubmitted(true)
}
}, [searchParams])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid keeping the payment-graph password in the URL

Query params can leak via browser history, referrers, and logs. Since this is a password gate, scrub the password param after reading it (or avoid query params entirely).

🔒 Proposed fix (scrub query param after reading)
 useEffect(() => {
     const urlPassword = searchParams.get('password')
     if (urlPassword) {
         setPassword(urlPassword)
         setPasswordSubmitted(true)
+        // Scrub password from the URL to avoid leaks via history/referrer/logs
+        if (typeof window !== 'undefined') {
+            const url = new URL(window.location.href)
+            url.searchParams.delete('password')
+            window.history.replaceState({}, '', url.toString())
+        }
     }
 }, [searchParams])
📝 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
// Check for password in URL on mount
useEffect(() => {
const urlPassword = searchParams.get('password')
if (urlPassword) {
setPassword(urlPassword)
setPasswordSubmitted(true)
}
}, [searchParams])
// Check for password in URL on mount
useEffect(() => {
const urlPassword = searchParams.get('password')
if (urlPassword) {
setPassword(urlPassword)
setPasswordSubmitted(true)
// Scrub password from the URL to avoid leaks via history/referrer/logs
if (typeof window !== 'undefined') {
const url = new URL(window.location.href)
url.searchParams.delete('password')
window.history.replaceState({}, '', url.toString())
}
}
}, [searchParams])
🤖 Prompt for AI Agents
In `@src/app/`(mobile-ui)/dev/payment-graph/page.tsx around lines 16 - 23, The
useEffect that reads the password from searchParams (the block using useEffect,
searchParams.get('password'), setPassword and setPasswordSubmitted) must
remove/scrub the password query param immediately after reading it to avoid
leaking credentials; after calling setPassword and setPasswordSubmitted,
programmatically replace the URL (e.g., via Next.js router.replace or
history.replaceState) with the same path but without the password param so the
credential is not retained in browser history or referrers, and ensure this
scrub happens only once (guarded by the same effect) to preserve current
behavior.

Comment on lines 40 to 45
const underMaintenanceConfig: MaintenanceConfig = {
enableFullMaintenance: false, // set to true to redirect all pages to /maintenance
enableMaintenanceBanner: false, // set to true to show maintenance banner on all pages
disabledPaymentProviders: [], // set to ['MANTECA'] to disable Manteca QR payments
disableSquidWithdraw: true, // set to true to disable cross-chain withdrawals (only allows USDC on Arbitrum)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Defaulting disableSquidWithdraw to true disables x‑chain withdrawals globally.

Unless this is intended for all environments, this will ship with cross‑chain withdrawals disabled. Consider defaulting to false and toggling via env/ops when needed.

🛠️ Suggested change
 const underMaintenanceConfig: MaintenanceConfig = {
     enableFullMaintenance: false, // set to true to redirect all pages to /maintenance
     enableMaintenanceBanner: false, // set to true to show maintenance banner on all pages
     disabledPaymentProviders: [], // set to ['MANTECA'] to disable Manteca QR payments
-    disableSquidWithdraw: true, // set to true to disable cross-chain withdrawals (only allows USDC on Arbitrum)
+    disableSquidWithdraw: false, // set to true to disable cross-chain withdrawals (only allows USDC on Arbitrum)
 }
📝 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 underMaintenanceConfig: MaintenanceConfig = {
enableFullMaintenance: false, // set to true to redirect all pages to /maintenance
enableMaintenanceBanner: false, // set to true to show maintenance banner on all pages
disabledPaymentProviders: [], // set to ['MANTECA'] to disable Manteca QR payments
disableSquidWithdraw: true, // set to true to disable cross-chain withdrawals (only allows USDC on Arbitrum)
}
const underMaintenanceConfig: MaintenanceConfig = {
enableFullMaintenance: false, // set to true to redirect all pages to /maintenance
enableMaintenanceBanner: false, // set to true to show maintenance banner on all pages
disabledPaymentProviders: [], // set to ['MANTECA'] to disable Manteca QR payments
disableSquidWithdraw: false, // set to true to disable cross-chain withdrawals (only allows USDC on Arbitrum)
}
🤖 Prompt for AI Agents
In `@src/config/underMaintenance.config.ts` around lines 40 - 45, The config
underMaintenanceConfig currently sets disableSquidWithdraw: true which globally
disables cross-chain withdrawals; change the default to false in
underMaintenanceConfig (MaintenanceConfig) so x‑chain withdrawals remain enabled
by default, and if desired read an environment flag or feature flag to toggle
disableSquidWithdraw per-deployment (e.g., pull from process.env or a runtime
feature manager) so ops can disable it without shipping a global default of
true.

Comment on lines +308 to +311
const isPaymentMode = options?.mode === 'payment'
const params = new URLSearchParams()
if (isPaymentMode) {
params.set('mode', 'payment')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Short-circuit when payment mode is missing a password.

Both flows allow payment mode without a password, which results in avoidable network calls and less clear errors. Consider returning a local error immediately.

💡 Suggested guard
         const isPaymentMode = options?.mode === 'payment'
+        if (isPaymentMode && !options?.password) {
+            return { success: false, data: null, error: 'Invalid or missing password.' }
+        }
             // Payment mode uses password auth, full mode requires JWT
             const isPaymentMode = options?.mode === 'payment'
+            if (isPaymentMode && !options?.password) {
+                return { success: false, data: null, error: 'Invalid or missing password.' }
+            }
             if (!isPaymentMode && !jwtToken) {
                 return { success: false, data: null, error: 'Not authenticated. Please log in.' }
             }

Also applies to: 355-357

🤖 Prompt for AI Agents
In `@src/services/points.ts` around lines 308 - 311, Add an early guard that
returns/throws a local error when payment mode is requested but no password is
provided: check the computed isPaymentMode (options?.mode === 'payment') and if
true and options?.password is falsy, immediately return or throw a clear
validation error instead of proceeding (before creating URLSearchParams or
making any network calls). Apply the same short-circuit in the other
payment-related branch referenced by the code around where isPaymentMode/params
are used (the second flow at the other occurrence).

Comment on lines +316 to +318
if (options?.password) {
params.set('password', options.password)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid sending payment passwords via query string (secret leakage risk).

Query params are logged in fetchWithSentry (URL is captured in Sentry and console logs), so the password can leak via logs, browser history, proxies, and referrers. Prefer sending it via a header or POST body. If the backend only accepts query params, consider redacting password in fetchWithSentry before logging.

🔒 Proposed fix (header-based password)
-        if (options?.password) {
-            params.set('password', options.password)
-        }
         const endpoint = `/invites/graph${params.toString() ? `?${params}` : ''}`
         // Payment mode uses password auth (no API key needed), full mode requires API key + JWT
-        const headers: Record<string, string> = isPaymentMode ? {} : { 'api-key': apiKey }
+        const headers: Record<string, string> = isPaymentMode ? {} : { 'api-key': apiKey }
+        if (options?.password) {
+            headers['x-payment-password'] = options.password
+        }
-            // Password is required for payment mode
-            if (options?.password) {
-                params.set('password', options.password)
-            }
             const url = `${PEANUT_API_URL}/invites/graph/external${params.toString() ? `?${params}` : ''}`
@@
             const headers: Record<string, string> = {
                 'Content-Type': 'application/json',
             }
+            if (options?.password) {
+                headers['x-payment-password'] = options.password
+            }

Also applies to: 321-322, 374-380, 387-395

🤖 Prompt for AI Agents
In `@src/services/points.ts` around lines 316 - 318, The code currently adds
sensitive passwords to the URL querystring via params.set('password',
options.password), which leaks secrets into logs and Sentry; change the request
to send the password in a secure place instead: remove any
params.set('password', ...) usage and instead attach the secret in a request
header (e.g., 'Authorization' or 'X-Password') or in the POST body when calling
fetchWithSentry; update all occurrences that build params (the params variable
and any code paths referenced around options?.password) to stop including
password in the URL and ensure fetchWithSentry is passed the header/body value;
if the backend cannot accept headers/body, change fetchWithSentry to redact the
password from logged URLs before sending to Sentry (redact in fetchWithSentry by
replacing the password query value with '[REDACTED]') and update callers
accordingly.

@Hugo0 Hugo0 reopened this Jan 26, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

>
<Icon name="logout" size={32} className="size-8" />
</Button>
/>
Copy link

Choose a reason for hiding this comment

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

Logout icon hidden during loading state

Low Severity

The refactor from using Icon as a child to using the icon prop changes the loading behavior. The Button component's renderIcon() function returns null when loading is true, meaning the logout icon now disappears entirely while isLoggingOut is true. Previously, the icon was passed as children and remained visible alongside the loading spinner. Users will now see only a spinner during logout instead of both the spinner and the logout icon.

Fix in Cursor Fix in Web

fetchExternalNodes()
}, [isMinimal, props.apiKey, externalNodesConfig.enabled])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMinimal, !isMinimal && props.apiKey, externalNodesConfig.enabled, mode, externalNodesConfig.limit])
Copy link

Choose a reason for hiding this comment

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

External nodes fetch missing topNodes dependency

Medium Severity

The external nodes fetch useEffect uses topNodes in the API call (line 1007: topNodes: topNodes > 0 ? topNodes : undefined) but topNodes is not included in the dependency array. When users increase the top-N slider, the main graph refetches correctly, but external nodes don't refetch. This means external nodes (wallets/banks/merchants) connected only to users in the expanded range won't appear, causing incomplete visualization of the network.

Additional Locations (1)

Fix in Cursor Fix in Web

@Hugo0 Hugo0 closed this pull request by merging all changes into peanut-wallet-dev in 8d0e1bc Jan 27, 2026
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.

3 participants