From 07174e9c99ab7310bc2e92932b0c47365319a7d2 Mon Sep 17 00:00:00 2001 From: valoryyaa-byte Date: Tue, 20 Oct 2026 01:13:37 +0100 Subject: [PATCH 1/4] Fix #910: cache USDC balance fetches with existing TTL cache layer fetchUsdcBalance() was hitting the RPC on every call (WalletConnect header polls it on connect, PayModal polls it on open), bypassing the blockchainCache module already used for quotes/invoices. Added a 'balance' cache kind with a 10s TTL and routed fetchUsdcBalance through blockchainCache.getOrFetch, so concurrent/rapid reads for the same wallet collapse onto one RPC simulation. --- src/lib/blockchainCache.ts | 4 ++++ src/lib/stellar.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/lib/blockchainCache.ts b/src/lib/blockchainCache.ts index 87a5a36..fdd127c 100644 --- a/src/lib/blockchainCache.ts +++ b/src/lib/blockchainCache.ts @@ -32,6 +32,10 @@ export const CACHE_TTL = { quote: 5_000, /** Contract/account metadata that essentially never changes post-deploy. */ staticMeta: 5 * 60_000, + /** USDC token balance for a wallet. Polled from multiple components + * (WalletConnect header, PayModal) on connect/mount; short TTL collapses + * those into one RPC simulation without serving noticeably stale data. */ + balance: 10_000, } as const; export type CacheKind = keyof typeof CACHE_TTL; diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts index bb8d65a..3ba8fa6 100644 --- a/src/lib/stellar.ts +++ b/src/lib/stellar.ts @@ -242,6 +242,16 @@ export async function payMilestone(params: { export async function fetchUsdcBalance( walletAddress: string, tokenContractId = USDC_CONTRACT_ID +): Promise { + const cacheKey = `${walletAddress}:${tokenContractId}`; + return blockchainCache.getOrFetch("balance", cacheKey, () => + fetchUsdcBalanceUncached(walletAddress, tokenContractId) + ); +} + +async function fetchUsdcBalanceUncached( + walletAddress: string, + tokenContractId: string ): Promise { const { Contract, From 180fd25769ac7614c8cc861d0fbb95cefbcf9d1d Mon Sep 17 00:00:00 2001 From: valoryyaa-byte Date: Tue, 20 Oct 2026 01:14:34 +0100 Subject: [PATCH 2/4] Fix #911: memoize StatusChip in InvoiceTable to cut per-row re-renders StatusChip is rendered once per invoice row (tables can have hundreds of rows) and only takes a primitive status string prop. Wrapping it in React.memo lets rows whose status hasn't changed skip re-rendering the chip when the table re-renders for unrelated reasons (sort, selection toggles on other rows, column visibility). --- src/components/InvoiceTable.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/InvoiceTable.tsx b/src/components/InvoiceTable.tsx index 8b37538..4fb5b6c 100644 --- a/src/components/InvoiceTable.tsx +++ b/src/components/InvoiceTable.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { useState, useCallback, useMemo, useEffect, useRef, memo } from "react"; import type { Invoice } from "@stellar-split/sdk"; import { formatAmount } from "@stellar-split/sdk"; import { getInvoiceDisplayStatus } from "@/lib/dashboardFilters"; @@ -100,7 +100,13 @@ const STATUS_STYLES: Record = { Refunded: "bg-gray-500/20 text-gray-400", }; -function StatusChip({ status }: { status: string }) { +// Rendered once per row (potentially hundreds of rows). Its only prop is a +// primitive status string, so React.memo's default shallow comparison lets +// unaffected rows skip re-rendering this chip when the table re-renders for +// an unrelated reason (sort change, selection toggle on another row, column +// visibility toggle, etc.) — those re-render the parent row but this +// specific status string is almost always unchanged. +const StatusChip = memo(function StatusChip({ status }: { status: string }) { const style = STATUS_STYLES[status] ?? "bg-gray-500/20 text-gray-400"; return ( ); -} +}); // ── Sort header button ──────────────────────────────────────────────────────── From a3e3c763c29571a12ea7ff3ab0d428cccf4530af Mon Sep 17 00:00:00 2001 From: valoryyaa-byte Date: Tue, 20 Oct 2026 01:15:43 +0100 Subject: [PATCH 3/4] Fix #909: lazy-load ReleaseBanner to defer canvas-confetti bundle cost ReleaseBanner was statically imported in the invoice detail page even though it only renders when an invoice transitions to 'released', and it pulls in canvas-confetti eagerly. Converted the import to next/dynamic (ssr: false) so that component and dependency are only fetched when the banner actually needs to show. --- src/app/invoice/[id]/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/invoice/[id]/page.tsx b/src/app/invoice/[id]/page.tsx index e17b284..72596f8 100644 --- a/src/app/invoice/[id]/page.tsx +++ b/src/app/invoice/[id]/page.tsx @@ -82,7 +82,10 @@ import InvoiceSentiment from "@/components/invoice/InvoiceSentiment"; import PayerWalletHealth from "@/components/invoice/PayerWalletHealth"; import ReconciliationPanel from "@/components/invoice/ReconciliationPanel"; import InvoiceExportButton from "@/components/InvoiceExportButton"; -import ReleaseBanner from "@/components/ReleaseBanner"; +// Only rendered once an invoice transitions to "released", and pulls in the +// canvas-confetti animation library — load it on demand instead of in the +// page's initial bundle. +const ReleaseBanner = dynamic(() => import("@/components/ReleaseBanner"), { ssr: false }); import { cancelReminder, setReminder } from "@/lib/reminders"; import { recordCooldown } from "@/lib/cooldown"; import { exportTimelineAsImage } from "@/lib/timelineImageExport"; From 4d4b897c8ee7f6f1cf820360190f1070e2bd5d3e Mon Sep 17 00:00:00 2001 From: valoryyaa-byte Date: Tue, 20 Oct 2026 01:16:04 +0100 Subject: [PATCH 4/4] Fix #908: document continuous testing scripts in README test:watch (vitest --changed) and test:watch:all already existed in package.json but weren't documented anywhere, so the continuous-testing workflow for local development wasn't discoverable. Added a README section pointing to them. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 5e65776..f5465c9 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ Open [http://localhost:3000](http://localhost:3000). | `/invoice/[id]` | Invoice detail, payment progress, Pay button | | `/verify/[id]` | Public on-chain verification (no login needed) | +## Continuous Testing (Local Dev) + +Run tests continuously while developing, instead of a single one-off run: + +```bash +npm run test:watch # vitest in watch mode, scoped to files changed vs. the last commit +npm run test:watch:all # vitest in watch mode, full suite +``` + ## Run Lint ```bash