Skip to content

Conversation

@kumawatkaran523
Copy link
Contributor

@kumawatkaran523 kumawatkaran523 commented Sep 20, 2025

image image image image

Summary by CodeRabbit

  • New Features

    • Batch invoice creation and all-in-one batch payments with event summaries.
    • New Batch Create and Batch Payment pages with smart batch suggestions.
    • Token Picker with search, custom-token verification, and copy-to-clipboard.
    • Admin controls for fee setting and treasury withdrawals.
  • Enhancements

    • Safer payment flow with balance/allowance checks and improved error messages.
    • Richer token metadata and resilient logo fallbacks across Sent/Received/Generate Link/Create Invoice.
    • Improved invoice detail drawer, printing, and pagination.
  • UI

    • New reusable components: Dialog, Avatar, Separator, CopyButton; simplified Badge.
  • Chores

    • Added/updated dependencies (Radix UI, framer-motion, react-window).

@coderabbitai
Copy link

coderabbitai bot commented Sep 20, 2025

Walkthrough

Adds batch invoice creation and payment to the Chainvoice contract with fees/treasury management and new errors/events. Frontend introduces batch create and batch pay pages, dynamic token selection via hooks, removes hardcoded token presets, and adds UI primitives (dialog, avatar, separator, copy). Routing updated, token verification flows added, and multiple pages refactored to use token list.

Changes

Cohort / File(s) Summary
Smart contract: batch ops, fees, treasury
contracts/src/Chainvoice.sol
Adds batch create/pay functions, new errors, events, nonReentrant, fee/treasury state and admin setters, fee withdrawal, helpers, expanded views, and CEI updates to single-invoice flows.
Frontend dependencies
frontend/package.json
Adds Radix UI components, react-window; updates @radix-ui/react-slot and framer-motion.
App routing
frontend/src/App.jsx
Registers new route for batch invoice creation; imports batch pages.
Token data migration and hooks
frontend/src/hooks/useTokenList.js, frontend/src/hooks/useTokenSearch.js, frontend/src/utils/erc20_token.js
Introduces token list fetch/cache hook and search hook; removes TOKEN_PRESETS export.
Token carousel shift to hook
frontend/src/components/TokenCrousel.jsx
Replaces preset-based tokens with useTokenList; updates image fallback logic.
Token picker component
frontend/src/components/TokenPicker.jsx
Adds modal-based token selector with search, highlight, copy, optional custom token handling; exports ToggleSwitch.
UI primitives
frontend/src/components/ui/avatar.jsx, .../ui/dialog.jsx, .../ui/separator.jsx, .../ui/copyButton.jsx, .../ui/badge.jsx
Adds Avatar, Dialog, Separator, CopyButton; simplifies Badge and removes badgeVariants export.
Batch create page
frontend/src/page/CreateInvoicesBatch.jsx
New batch invoice creation page with Lit encryption, ERC-20 verification, wallet connect, and contract submission.
Batch payment page
frontend/src/page/BatchPayment.jsx
New batch payment UI with suggestions, grouping by token, allowance handling, and invoice inspector drawer.
Received invoices enhancements
frontend/src/page/ReceivedInvoice.jsx
Adds batch selection, suggestions, grouped payments, ERC-20 allowance flow, improved errors and UI.
Sent invoices token enrichment
frontend/src/page/SentInvoice.jsx
Enriches invoices with token metadata via useTokenList and on-chain fallbacks; improves image resilience.
Generate link refactor
frontend/src/page/GenerateLink.jsx
Replaces preset selection with TokenPicker and custom token verification; updates link params.
Create invoice refactor
frontend/src/page/CreateInvoice.jsx
Swaps token selector to TokenPicker with toggle and custom token verification; adjusts totals display.
Home menu update
frontend/src/page/Home.jsx
Adds “Batch Create” nav item and icon imports; minor style change.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Frontend UI
  participant Wallet as Wallet/Signer
  participant Chainvoice as Chainvoice.sol
  rect rgb(235, 245, 255)
    note over User,UI: Batch Invoice Creation
    User->>UI: Enter invoices, select token
    UI->>UI: Encrypt payloads (Lit)
    UI->>Wallet: Request tx signature
    Wallet-->>UI: Signed tx
    UI->>Chainvoice: createInvoicesBatch(tos, amounts, token, payloads, hashes)
    Chainvoice-->>UI: InvoiceBatchCreated(ids)
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as BatchPayment Page
  participant Wallet as Wallet/Signer
  participant ERC20 as ERC20 Token
  participant Chainvoice as Chainvoice.sol
  rect rgb(245, 255, 245)
    note over User,UI: Batch Payment (grouped by token)
    User->>UI: Select invoices
    UI->>UI: Group by token, sum amounts
    alt Native token
      UI->>Wallet: Send tx with value
      Wallet->>Chainvoice: payInvoicesBatch(ids) [value]
    else ERC-20
      UI->>ERC20: allowance(payer, Chainvoice)
      alt Insufficient
        UI->>Wallet: approve(Chainvoice, total)
        Wallet->>ERC20: approve
      end
      UI->>Wallet: Send tx
      Wallet->>Chainvoice: payInvoicesBatch(ids)
    end
    Chainvoice-->>UI: InvoicePaid events
    Chainvoice-->>UI: InvoiceBatchPaid summary
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • adityabhattad2021

Poem

A hop, a skip, a batch I weave,
Invoices lined like autumn leaves.
Tokens dance, approvals sing,
Fees accrue—treasury’s ring.
Click, approve, then whoosh away—
Paid in bundles, night to day.
— A busy bun at deploy ballet 🐇💸

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely and accurately summarizes the primary feature introduced — batch processing with both batch creation and batch payment — and directly reflects the main changes in the diff. It is specific, focused, and readable for a reviewer scanning PR history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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: 6

Caution

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

⚠️ Outside diff range comments (4)
frontend/src/page/SentInvoice.jsx (1)

422-426: Harden formatAddress to handle undefined/short strings.

formatAddress(invoice.client?.address) will throw if address is undefined/null.

Apply this diff:

-  const formatAddress = (address) => {
-    return `${address.substring(0, 10)}...${address.substring(
-      address.length - 10
-    )}`;
-  };
+  const formatAddress = (addr) => {
+    if (typeof addr !== "string") return "-";
+    if (addr.length <= 15) return addr;
+    return `${addr.slice(0, 10)}...${addr.slice(-10)}`;
+  };
frontend/src/page/ReceivedInvoice.jsx (1)

871-875: Harden formatAddress to handle undefined/short strings.

Same crash risk as in SentInvoice.

Apply this diff:

-  const formatAddress = (address) => {
-    return `${address.substring(0, 10)}...${address.substring(
-      address.length - 10
-    )}`;
-  };
+  const formatAddress = (addr) => {
+    if (typeof addr !== "string") return "-";
+    if (addr.length <= 15) return addr;
+    return `${addr.slice(0, 10)}...${addr.slice(-10)}`;
+  };
frontend/src/page/CreateInvoice.jsx (2)

20-27: Incorrect Badge import (runtime failure).

Badge is not a lucide icon; import from your UI lib.

Apply this diff:

-import {
-  Badge,
-  CalendarIcon,
-  CheckCircle2,
-  Coins,
-  Loader2,
-  PlusIcon,
-  XCircle,
-} from "lucide-react";
+import {
+  CalendarIcon,
+  CheckCircle2,
+  Coins,
+  Loader2,
+  PlusIcon,
+  XCircle,
+} from "lucide-react";
+import { Badge } from "../components/ui/badge";

Also applies to: 46-48


97-116: Reference to removed TOKEN_PRESETS (ReferenceError).

TOKEN_PRESETS no longer exists. Fall back to custom-token verification when a URL tokenAddress is provided.

Apply this diff:

-    if (urlTokenAddress) {
-      if (isCustomFromURL) {
-        setUseCustomToken(true);
-        setCustomTokenAddress(urlTokenAddress);
-        verifyToken(urlTokenAddress);
-      } else {
-        const preselectedToken = TOKEN_PRESETS.find(
-          (token) =>
-            token.address.toLowerCase() === urlTokenAddress.toLowerCase()
-        );
-        if (preselectedToken) {
-          setSelectedToken(preselectedToken);
-          setUseCustomToken(false);
-        } else {
-          setUseCustomToken(true);
-          setCustomTokenAddress(urlTokenAddress);
-          verifyToken(urlTokenAddress);
-        }
-      }
-    }
+    if (urlTokenAddress) {
+      if (isCustomFromURL) {
+        setUseCustomToken(true);
+        setCustomTokenAddress(urlTokenAddress);
+        verifyToken(urlTokenAddress);
+      } else {
+        // Without TOKEN_PRESETS, default to custom verification for safety.
+        setUseCustomToken(true);
+        setCustomTokenAddress(urlTokenAddress);
+        verifyToken(urlTokenAddress);
+      }
+    }
🧹 Nitpick comments (35)
frontend/src/components/ui/badge.jsx (3)

1-7: Forward props and allow element polymorphism

Without ...props, callers can’t set aria-*, title, data-*, or handlers. An as/Tag prop enables anchors/divs when needed (e.g., clickable badges). If you don’t adopt the full variant patch above, at least do this:

-const Badge = ({ className = "", children }) => (
-  <span
-    className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600 ${className}`}
-  >
+const Badge = ({ className = "", children, as: Tag = "span", ...props }) => (
+  <Tag
+    className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600 ${className}`}
+    {...props}
+  >
     {children}
-  </span>
+  </Tag>
 );

3-3: Hardcoded colors; prefer theme tokens for dark mode/theming

Using Tailwind tokens (e.g., bg-muted text-muted-foreground) or CSS vars keeps the badge consistent with app themes.

-    className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600 ${className}`}
+    className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted text-muted-foreground ${className}`}

9-9: Export surface consistency

If other files import { badgeVariants } or expect named exports, keep exporting it (or ship a deprecation window). If you intend the breaking change, bump a major version and include a migration note.

frontend/src/components/ui/copyButton.jsx (2)

8-17: Clipboard fallback for non-secure contexts + longer visible confirmation

navigator.clipboard fails on http/iframes. Provide a DOM fallback and keep “Copied” visible a bit longer.

-  const handleCopy = async (e) => {
+  const handleCopy = async (e) => {
     e.stopPropagation();
     try {
-      await navigator.clipboard.writeText(textToCopy);
-      setCopied(true);
-      setTimeout(() => setCopied(false), 500);
+      if (window.isSecureContext && navigator.clipboard?.writeText) {
+        await navigator.clipboard.writeText(textToCopy);
+      } else {
+        const ta = document.createElement("textarea");
+        ta.value = textToCopy ?? "";
+        ta.setAttribute("readonly", "");
+        ta.style.position = "absolute";
+        ta.style.left = "-9999px";
+        document.body.appendChild(ta);
+        ta.select();
+        document.execCommand("copy");
+        document.body.removeChild(ta);
+      }
+      setCopied(true);
+      setTimeout(() => setCopied(false), 1500);
     } catch (err) {
       console.error("Failed to copy:", err);
     }
   };

19-37: A11y: dynamic title/label + announce copy state

Make the label generic and expose state to screen readers. Also disable when text is empty.

-    <button
+    <button
       type="button"
       onClick={handleCopy}
-      className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors ${className}`}
-      title="Copy address"
+      className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
+      title={copied ? "Copied" : "Copy to clipboard"}
+      aria-label={copied ? "Copied" : "Copy to clipboard"}
+      disabled={!textToCopy}
     >
+      <span className="sr-only" aria-live="polite">
+        {copied ? "Copied" : "Copy to clipboard"}
+      </span>
frontend/src/components/ui/avatar.jsx (1)

1-18: Align with Radix Avatar or remove dependency; add small a11y/perf tweaks

You added @radix-ui/react-avatar but this component reimplements it. Either switch to Radix for consistency or drop the dep. Also consider lazy-loading and better fallback semantics.

-const Avatar = ({ src, alt, className = "", children, onError }) => (
+const Avatar = ({ src, alt = "", className = "", children, onError }) => (
   <div
     className={`inline-flex items-center justify-center rounded-full bg-gray-100 overflow-hidden ${className}`}
   >
     {src ? (
       <img
         src={src}
         alt={alt}
         className="w-full h-full object-cover rounded-full"
-        onError={onError}
+        loading="lazy"
+        decoding="async"
+        referrerPolicy="no-referrer"
+        onError={(e) => {
+          if (onError) onError(e);
+          // avoid infinite loop
+          e.currentTarget.onerror = null;
+          e.currentTarget.style.display = "none";
+        }}
       />
     ) : (
-      <div className="flex items-center justify-center w-full h-full text-xs font-medium text-gray-600">
+      <div
+        className="flex items-center justify-center w-full h-full text-xs font-medium text-gray-600"
+        role="img"
+        aria-label={alt}
+      >
         {children}
       </div>
     )}
   </div>
 );

If you prefer Radix, I can draft a swap to @radix-ui/react-avatar.

frontend/src/components/ui/separator.jsx (1)

6-21: Decorative default may hide semantics

Defaulting decorative to true hides the separator from AT. For semantic separators, ensure callers set decorative={false}. Consider flipping default or documenting.

frontend/src/App.jsx (2)

27-33: Use env for WalletConnect projectId (avoid committing placeholders)

Replace the hard-coded "YOUR_PROJECT_ID" with an env var.

-export const config = getDefaultConfig({
-  appName: "My RainbowKit App",
-  projectId: "YOUR_PROJECT_ID",
+const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID;
+export const config = getDefaultConfig({
+  appName: "My RainbowKit App",
+  projectId: walletConnectProjectId,

36-38: Unused BatchPayment import or missing route

Either remove the import or wire the route.

 import GenerateLink from "./page/GenerateLink";
 import CreateInvoicesBatch from "./page/CreateInvoicesBatch";
-import BatchPayment from "./page/BatchPayment"; // New import needed
+import BatchPayment from "./page/BatchPayment";

 ...
                     <Route path="dashboard" element={<Home />}>
                       <Route path="create" element={<CreateInvoice />} />
                       <Route path="sent" element={<SentInvoice />} />
                       <Route path="pending" element={<ReceivedInvoice />} />
                       <Route path="generate-link" element={<GenerateLink />} />
                       <Route
                         path="batch-invoice"
                         element={<CreateInvoicesBatch />}
                       />
+                      <Route path="batch-payment" element={<BatchPayment />} />

Also applies to: 88-92

frontend/src/page/Home.jsx (1)

90-112: Selection logic could over-match nested routes

location.pathname.includes(item.route) can mark multiple items selected (e.g., "sent" vs "resent"). Consider startsWith or exact matching.

- selected={location.pathname.includes(item.route)}
+ selected={location.pathname.split("/").pop() === item.route}
frontend/src/components/TokenCrousel.jsx (1)

67-69: Avoid infinite onError loops and generic console noise

Harden the image fallback and drop console logs.

-                    onError={(e) => {
-                      e.target.src = "/tokenImages/generic.png";
-                    }}
+                    onError={(e) => {
+                      e.currentTarget.onerror = null;
+                      e.currentTarget.src = "/tokenImages/generic.png";
+                    }}

Additionally, remove the console.log(tokens) above.

frontend/package.json (1)

21-28: Unify Avatar dependency and confirm React Router v7 compatibility

  • @radix-ui/react-avatar is present in frontend/package.json but the repo uses a local Avatar at frontend/src/components/ui/avatar.jsx (used in ReceivedInvoice.jsx, SentInvoice.jsx, TokenPicker.jsx). Remove the unused dependency or replace the local Avatar with Radix to avoid shipping dead code.
  • frontend/src/App.jsx imports BrowserRouter/Routes/Route from "react-router-dom". React Router v7 still re-exports the v6-style primitives but the v7 upgrade recommends importing from "react-router" and requires Node ≥20 + React 18 — either update imports/CI to v7 requirements or pin to v6 and confirm CI passes. (reactrouter.com)
frontend/src/hooks/useTokenSearch.js (4)

64-101: Fix hasMore false positives; compute totalMatches inside memo.

hasMore is inferred from equality against the page cap, which yields false positives when results equal the cap exactly. Compute total matches inside the memo and derive hasMore from a strict comparison.

Apply this diff:

-  const filteredTokens = useMemo(() => {
-    if (!debouncedQuery.trim()) {
-      return tokens.slice(0, page * pageSize);
-    }
-    const lowerQuery = debouncedQuery.toLowerCase();
-    const results = new Set();
-    const exactSymbol = indexes.bySymbol.get(lowerQuery);
-    if (exactSymbol) {
-      results.add(exactSymbol);
-    }
-    const symbolPrefixMatches = indexes.bySymbolPrefix.get(lowerQuery);
-    if (symbolPrefixMatches) {
-      symbolPrefixMatches.forEach((token) => results.add(token));
-    }
-    const namePrefixMatches = indexes.byNamePrefix.get(lowerQuery);
-    if (namePrefixMatches) {
-      namePrefixMatches.forEach((token) => results.add(token));
-    }
-    if (lowerQuery.length >= 2) {
-      for (const [address, token] of indexes.byAddress.entries()) {
-        if (address.includes(lowerQuery)) {
-          results.add(token);
-        }
-      }
-    }
-    return Array.from(results).slice(0, page * pageSize);
-  }, [tokens, debouncedQuery, indexes, page, pageSize]);
+  const { list: filteredTokens, totalMatches } = useMemo(() => {
+    if (!debouncedQuery.trim()) {
+      const list = tokens.slice(0, page * pageSize);
+      return { list, totalMatches: tokens.length };
+    }
+    const lowerQuery = debouncedQuery.toLowerCase();
+    const results = new Set();
+    const exactSymbol = indexes.bySymbol.get(lowerQuery);
+    if (exactSymbol) results.add(exactSymbol);
+    const symbolPrefixMatches = indexes.bySymbolPrefix.get(lowerQuery);
+    if (symbolPrefixMatches) symbolPrefixMatches.forEach((t) => results.add(t));
+    const namePrefixMatches = indexes.byNamePrefix.get(lowerQuery);
+    if (namePrefixMatches) namePrefixMatches.forEach((t) => results.add(t));
+    if (lowerQuery.length >= 2) {
+      for (const [address, token] of indexes.byAddress.entries()) {
+        if (address.includes(lowerQuery)) results.add(token);
+      }
+    }
+    const all = Array.from(results);
+    return { list: all.slice(0, page * pageSize), totalMatches: all.length };
+  }, [tokens, debouncedQuery, indexes, page, pageSize]);
@@
-  const hasMore = filteredTokens.length === page * pageSize;
+  const hasMore = filteredTokens.length < totalMatches;

Also applies to: 107-108


12-16: Harden indexing against missing fields; support address fallback.

Guard against tokens lacking symbol, name, or contract_address to avoid runtime errors and index only present fields.

Apply this diff:

-  tokens.forEach((token) => {
-    const symbol = token.symbol.toLowerCase();
-    const name = token.name.toLowerCase();
-    const address = token.contract_address.toLowerCase();
+  tokens.forEach((token) => {
+    const symbol = (token.symbol || "").toLowerCase();
+    const name = (token.name || "").toLowerCase();
+    const address = (token.contract_address || token.address || "").toLowerCase();
@@
-    indexes.bySymbol.set(symbol, token);
+    if (symbol) {
+      indexes.bySymbol.set(symbol, token);
+    }
@@
-    for (let i = 1; i <= symbol.length; i++) {
+    if (symbol) for (let i = 1; i <= symbol.length; i++) {
       const prefix = symbol.slice(0, i);
       if (!indexes.bySymbolPrefix.has(prefix)) {
         indexes.bySymbolPrefix.set(prefix, new Set());
       }
       indexes.bySymbolPrefix.get(prefix).add(token);
     }
@@
-    for (let i = 1; i <= name.length; i++) {
+    if (name) for (let i = 1; i <= name.length; i++) {
       const prefix = name.slice(0, i);
       if (!indexes.byNamePrefix.has(prefix)) {
         indexes.byNamePrefix.set(prefix, new Set());
       }
       indexes.byNamePrefix.get(prefix).add(token);
     }
@@
-    indexes.byAddress.set(address, token);
+    if (address) {
+      indexes.byAddress.set(address, token);
+    }

Also applies to: 17-19, 21-27, 29-36, 38-40


45-48: Remove unused loading/error state or wire them up.

loading and error are never set. Either remove them from the hook API or set them appropriately (e.g., while building indexes). Keeping unused state adds noise.

Apply this minimal diff to remove them:

-export function useTokenSearch(tokens, pageSize = 250) {
-  const [loading, setLoading] = useState(false);
-  const [error, setError] = useState(null);
+export function useTokenSearch(tokens, pageSize = 250) {
@@
   return {
     tokens: filteredTokens,
-    loading,
-    error,
     query,
     setQuery,
     loadMore,
     hasMore,
   };

Also applies to: 109-117


17-19: Optional: support duplicate symbols (map symbol -> Set).

Exact symbol index currently stores only one token per symbol, dropping duplicates. If your lists can contain multiple tokens sharing a symbol, store a Set and merge into results similar to prefix hits.

Also applies to: 72-76

frontend/src/components/TokenPicker.jsx (1)

339-353: Consider using hasMore/loadMore for long lists.

You already compute pagination in useTokenSearch but don’t expose “Load more” UI here. Add a small “Load more” button/auto-load on scroll when hasMore is true to prevent rendering very large lists at once.

frontend/src/hooks/useTokenList.js (2)

61-61: Remove debug logging.

console.log(data); is noisy in production.

Apply this diff:

-        console.log(data);

23-24: Abort in-flight fetches on chainId change/unmount.

Avoid setting state after unmount when users rapidly switch chains.

Apply this diff:

   useEffect(() => {
-    const fetchTokens = async () => {
+    const controller = new AbortController();
+    const fetchTokens = async () => {
@@
-        const response = await fetch(dataUrl, {
+        const response = await fetch(dataUrl, {
           headers: {
             Accept: "application/json",
           },
+          signal: controller.signal,
         });
@@
-    fetchTokens();
-  }, [chainId]);
+    fetchTokens();
+    return () => controller.abort();
+  }, [chainId]);

Also applies to: 50-54, 83-85

frontend/src/page/SentInvoice.jsx (1)

163-167: Avoid alert; use existing error UI/toast for consistency.

Use setError and toast.error instead of alert for non-blocking UX.

Apply this diff:

-        if (!litNodeClient) {
-          alert("Lit client not initialized");
-          return;
-        }
+        if (!litNodeClient) {
+          setError("Lit client not initialized. Please refresh the page.");
+          toast.error("Lit client not initialized. Please refresh the page.");
+          return;
+        }
frontend/src/page/ReceivedInvoice.jsx (3)

276-304: Accumulate amounts in smallest units to avoid rounding errors.

getGroupedInvoices sums parseFloat(inv.amountDue), which can under/over-estimate and later diverge from on-chain units. Track totals in wei (BigInt) using known decimals, and format for display.

Apply this diff:

-  const getGroupedInvoices = () => {
+  const getGroupedInvoices = () => {
     const grouped = new Map();
     receivedInvoices.forEach((invoice) => {
       if (!selectedInvoices.has(invoice.id)) return;
       const tokenAddress = invoice.paymentToken?.address || ethers.ZeroAddress;
       const tokenKey = `${tokenAddress}_${ 
         invoice.paymentToken?.symbol || "ETH"
       }`;
       if (!grouped.has(tokenKey)) {
         grouped.set(tokenKey, {
           tokenAddress,
           symbol: invoice.paymentToken?.symbol || "ETH",
           logo: invoice.paymentToken?.logo,
           decimals: invoice.paymentToken?.decimals || 18,
           invoices: [],
-          totalAmount: 0,
+          totalAmountWei: 0n,
         });
       }
       const group = grouped.get(tokenKey);
       group.invoices.push(invoice);
-      group.totalAmount += parseFloat(invoice.amountDue);
+      const dec = group.decimals ?? 18;
+      group.totalAmountWei += ethers.parseUnits(String(invoice.amountDue), dec);
     });
     return grouped;
   };

And in the payment summary:

-                          {group.totalAmount.toFixed(6)} {group.symbol}
+                          {ethers.formatUnits(group.totalAmountWei, group.decimals)} {group.symbol}

Also applies to: 1145-1146


1331-1335: batchId may be non-string; guard before slice.

Avoid .slice on numeric IDs.

Apply this diff:

-                                        label={`Batch #${invoice.batchInfo.batchId.slice(
+                                        label={`Batch #${String(invoice.batchInfo.batchId).slice(
                                           -4
                                         )} (${invoice.batchInfo.index + 1}/${
                                           invoice.batchInfo.batchSize
                                         })`}

531-550: Approval amount could be insufficient if existing allowance > 0 but < total.

You handle that. Consider resetting to 0 first for non‑compliant ERC‑20s (USDT‑style). If targeting broad token support, add a safe path with try‑catch falling back to increase.

frontend/src/page/BatchPayment.jsx (6)

174-187: Avoid float rounding in balance checks.

totalAmount is aggregated via parseFloat and then parseUnits(...), which can under/over-estimate required funds. Sum BigInt amounts per invoice instead.

Apply this diff in checkPaymentCapability:

-      const totalFee = BigInt(fee) * BigInt(invoices.length);
-      const totalRequired =
-        ethers.parseUnits(totalAmount.toString(), 18) + totalFee;
+      const totalFee = fee * BigInt(invoices.length);
+      const totalAmountWei = invoices.reduce(
+        (acc, inv) => acc + ethers.parseUnits(String(inv.amountDue), 18),
+        0n
+      );
+      const totalRequired = totalAmountWei + totalFee;
@@
-      const decimals = await tokenContract.decimals();
-      const requiredAmount = ethers.parseUnits(
-        totalAmount.toString(),
-        decimals
-      );
+      const decimals = await tokenContract.decimals();
+      const requiredAmount = invoices.reduce(
+        (acc, inv) => acc + ethers.parseUnits(String(inv.amountDue), decimals),
+        0n
+      );

Also applies to: 193-201


346-353: Fix BigInt vs number comparison for chainId.

provider.getNetwork().chainId is bigint in ethers v6. Coerce before comparing.

Apply this diff:

-        if (network.chainId != 11155111) {
+        if (Number(network.chainId) !== 11155111) {

816-821: Null‑safe address formatting.

Guard against undefined to avoid runtime errors.

Apply this diff:

-  const formatAddress = (address) => {
-    return `${address.substring(0, 10)}...${address.substring(
-      address.length - 10
-    )}`;
-  };
+  const formatAddress = (addr) => {
+    if (!addr) return "-";
+    return `${addr.slice(0, 10)}...${addr.slice(-10)}`;
+  };

800-814: Defensive check for window.ethereum; unify UX (avoid alert).

Add a guard and use toasts consistently instead of alert.

Apply this diff:

   const switchNetwork = async () => {
     try {
       setNetworkLoading(true);
+      if (!window?.ethereum) {
+        toast.error("No injected wallet detected.");
+        return;
+      }
       await window.ethereum.request({
         method: "wallet_switchEthereumChain",
         params: [{ chainId: "0xaa36a7" }], // Sepolia chain ID
       });
       setError(null);
-    } catch (error) {
-      console.error("Network switch failed:", error);
-      alert("Failed to switch network. Please switch to Sepolia manually.");
+    } catch (error) {
+      console.error("Network switch failed:", error);
+      toast.error("Failed to switch network. Please switch to Sepolia manually.");
     } finally {
       setNetworkLoading(false);
     }
   };

Also replace remaining alert(...) calls in this file with toast.* for consistency.


9-15: Use stable Lit imports to avoid bundling fragility.

Import from package entrypoints instead of deep paths.

Apply this diff:

-import { decryptToString } from "@lit-protocol/encryption/src/lib/encryption.js";
+import { decryptToString } from "@lit-protocol/encryption";

Also applies to: 446-455


276-305: Optional: track totals in wei to remove float usage entirely.

Consider storing totalAmountWei (BigInt) in group and formatting only for UI.

frontend/src/page/CreateInvoice.jsx (1)

701-706: Unify success UX (avoid alert).

Replace alert(...) with toasts for consistency with the rest of the app.

Also applies to: 733-739

frontend/src/page/CreateInvoicesBatch.jsx (2)

112-132: Avoid effect churn from derived-state writes.

The totals effect rewrites invoiceRows whenever any item changes, creating new objects each time. Prefer computing totals on change handlers only, or memoize totals to reduce renders.


821-851: Success UI: consider toasts instead of inline green panels.

For consistency with rest of app.

contracts/src/Chainvoice.sol (3)

61-69: NonReentrancy guard is fine; consider using OpenZeppelin ReentrancyGuard for readability.

Current hand-rolled guard works but relying on OZ improves clarity and audit-friendliness.


202-244: Use custom errors consistently.

Single-invoice paths still use require(..., "Not authorized") style while batch uses custom errors. Align for gas and clarity.

Apply this diff pattern:

-        require(msg.sender == invoice.to, "Not authorized");
-        require(!invoice.isPaid, "Already paid");
-        require(!invoice.isCancelled, "Invoice is cancelled");
+        if (msg.sender != invoice.to) revert NotAuthorizedPayer();
+        if (invoice.isPaid || invoice.isCancelled) revert AlreadySettled();

Similarly in cancelInvoice:

-        require(!invoice.isPaid && !invoice.isCancelled, "Invoice not cancellable");
+        if (invoice.isPaid || invoice.isCancelled) revert AlreadySettled();

320-352: View helpers can be heavy for large sets.

getSentInvoices/getReceivedInvoices return full structs; consider pagination or returning IDs to reduce gas on RPCs and client decode time.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94cd95c and 7d2f6a4.

📒 Files selected for processing (20)
  • contracts/src/Chainvoice.sol (5 hunks)
  • frontend/package.json (3 hunks)
  • frontend/src/App.jsx (2 hunks)
  • frontend/src/components/TokenCrousel.jsx (2 hunks)
  • frontend/src/components/TokenPicker.jsx (1 hunks)
  • frontend/src/components/ui/avatar.jsx (1 hunks)
  • frontend/src/components/ui/badge.jsx (1 hunks)
  • frontend/src/components/ui/copyButton.jsx (1 hunks)
  • frontend/src/components/ui/dialog.jsx (1 hunks)
  • frontend/src/components/ui/separator.jsx (1 hunks)
  • frontend/src/hooks/useTokenList.js (1 hunks)
  • frontend/src/hooks/useTokenSearch.js (1 hunks)
  • frontend/src/page/BatchPayment.jsx (1 hunks)
  • frontend/src/page/CreateInvoice.jsx (7 hunks)
  • frontend/src/page/CreateInvoicesBatch.jsx (1 hunks)
  • frontend/src/page/GenerateLink.jsx (8 hunks)
  • frontend/src/page/Home.jsx (4 hunks)
  • frontend/src/page/ReceivedInvoice.jsx (24 hunks)
  • frontend/src/page/SentInvoice.jsx (12 hunks)
  • frontend/src/utils/erc20_token.js (0 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/utils/erc20_token.js
🧰 Additional context used
🧬 Code graph analysis (13)
frontend/src/hooks/useTokenSearch.js (2)
frontend/src/hooks/useTokenList.js (3)
  • tokens (19-19)
  • loading (20-20)
  • error (21-21)
frontend/src/components/TokenPicker.jsx (1)
  • filteredTokens (193-193)
frontend/src/components/TokenCrousel.jsx (5)
frontend/src/page/BatchPayment.jsx (1)
  • useTokenList (62-62)
frontend/src/page/ReceivedInvoice.jsx (1)
  • useTokenList (101-101)
frontend/src/page/GenerateLink.jsx (1)
  • useTokenList (38-38)
frontend/src/page/SentInvoice.jsx (1)
  • useTokenList (79-79)
frontend/src/hooks/useTokenList.js (1)
  • tokens (19-19)
frontend/src/hooks/useTokenList.js (2)
frontend/src/page/ReceivedInvoice.jsx (1)
  • useTokenList (101-101)
frontend/src/page/GenerateLink.jsx (1)
  • useTokenList (38-38)
frontend/src/page/CreateInvoicesBatch.jsx (6)
frontend/src/page/BatchPayment.jsx (6)
  • walletClient (39-39)
  • useAccount (40-40)
  • loading (41-41)
  • litClientRef (48-48)
  • showWalletAlert (51-51)
  • error (46-46)
frontend/src/page/ReceivedInvoice.jsx (6)
  • walletClient (74-74)
  • useAccount (75-75)
  • loading (76-76)
  • litClientRef (81-81)
  • showWalletAlert (84-84)
  • error (79-79)
frontend/src/components/WalletConnectionAlert.jsx (1)
  • WalletConnectionAlert (7-56)
frontend/src/components/TokenPicker.jsx (1)
  • TokenPicker (175-382)
frontend/src/components/ui/copyButton.jsx (1)
  • CopyButton (5-39)
frontend/src/lib/utils.js (1)
  • cn (4-6)
frontend/src/page/BatchPayment.jsx (1)
frontend/src/page/ReceivedInvoice.jsx (15)
  • page (72-72)
  • walletClient (74-74)
  • receivedInvoices (77-77)
  • selectedInvoices (91-91)
  • fee (78-78)
  • error (79-79)
  • drawerState (96-99)
  • getTokenSymbol (184-187)
  • handleBatchPayment (469-590)
  • getGroupedInvoices (276-302)
  • grouped (886-886)
  • payInvoice (367-466)
  • toggleDrawer (819-831)
  • handlePrint (833-850)
  • switchNetwork (852-869)
frontend/src/components/ui/copyButton.jsx (1)
frontend/src/page/GenerateLink.jsx (1)
  • copied (32-32)
frontend/src/page/SentInvoice.jsx (3)
frontend/src/page/ReceivedInvoice.jsx (8)
  • useAccount (75-75)
  • useTokenList (101-101)
  • getTokenInfo (175-182)
  • handleChangePage (103-105)
  • handleChangeRowsPerPage (107-110)
  • error (79-79)
  • walletClient (74-74)
  • litReady (80-80)
frontend/src/hooks/useTokenList.js (1)
  • tokens (19-19)
frontend/src/contractsABI/ERC20_ABI.js (2)
  • ERC20_ABI (1-10)
  • ERC20_ABI (1-10)
frontend/src/components/ui/separator.jsx (1)
frontend/src/lib/utils.js (1)
  • cn (4-6)
frontend/src/components/TokenPicker.jsx (8)
frontend/src/lib/utils.js (1)
  • cn (4-6)
frontend/src/hooks/useTokenSearch.js (3)
  • query (48-48)
  • filteredTokens (64-101)
  • useTokenSearch (45-118)
frontend/src/components/ui/avatar.jsx (1)
  • Avatar (1-18)
frontend/src/components/ui/badge.jsx (1)
  • Badge (1-7)
frontend/src/components/ui/copyButton.jsx (1)
  • CopyButton (5-39)
frontend/src/hooks/useTokenList.js (2)
  • useTokenList (18-86)
  • tokens (19-19)
frontend/src/components/ui/button.jsx (1)
  • Button (37-45)
frontend/src/components/ui/input.jsx (1)
  • Input (5-16)
frontend/src/page/ReceivedInvoice.jsx (3)
frontend/src/page/BatchPayment.jsx (16)
  • selectedInvoices (43-43)
  • useTokenList (62-62)
  • error (46-46)
  • getTokenSymbol (84-87)
  • findBatchSuggestions (103-167)
  • fee (45-45)
  • getGroupedInvoices (277-305)
  • grouped (831-831)
  • receivedInvoices (42-42)
  • handleSelectInvoice (250-263)
  • handleSelectAll (265-270)
  • unpaidInvoices (827-829)
  • handleClearAll (272-274)
  • handleBatchPayment (540-678)
  • payInvoice (681-770)
  • walletClient (39-39)
frontend/src/hooks/useTokenList.js (3)
  • useTokenList (18-86)
  • error (21-21)
  • tokens (19-19)
frontend/src/contractsABI/ERC20_ABI.js (2)
  • ERC20_ABI (1-10)
  • ERC20_ABI (1-10)
frontend/src/components/ui/dialog.jsx (1)
frontend/src/lib/utils.js (1)
  • cn (4-6)
frontend/src/page/CreateInvoice.jsx (5)
frontend/src/page/GenerateLink.jsx (2)
  • loading (46-46)
  • verifyToken (104-124)
frontend/src/components/TokenPicker.jsx (3)
  • ToggleSwitch (50-84)
  • ToggleSwitch (50-84)
  • TokenPicker (175-382)
frontend/src/components/ui/input.jsx (1)
  • Input (5-16)
frontend/src/components/ui/badge.jsx (1)
  • Badge (1-7)
frontend/src/components/ui/copyButton.jsx (1)
  • CopyButton (5-39)
frontend/src/page/GenerateLink.jsx (3)
frontend/src/components/ui/copyButton.jsx (2)
  • copied (6-6)
  • CopyButton (5-39)
frontend/src/hooks/useTokenList.js (3)
  • useTokenList (18-86)
  • loading (20-20)
  • tokens (19-19)
frontend/src/components/TokenPicker.jsx (3)
  • ToggleSwitch (50-84)
  • ToggleSwitch (50-84)
  • TokenPicker (175-382)
🔇 Additional comments (9)
frontend/src/components/ui/badge.jsx (1)

1-9: Incorrect — removing variant / badgeVariants is not a breaking change for this repo

Repo scan shows no imports of badgeVariants and no JSX using <Badge variant=...>. Only Badge imports are in frontend/src/components/TokenPicker.jsx and frontend/src/page/GenerateLink.jsx; neither passes a variant prop.

Likely an incorrect or invalid review comment.

frontend/src/components/ui/dialog.jsx (1)

26-45: LGTM — solid Radix composition

Overlay/content structure, focus handling, and close affordance follow shadcn/Radix patterns. No blockers.

frontend/src/components/ui/separator.jsx (1)

4-4: Configure path alias "@/lib/utils"

frontend/src/components/ui/separator.jsx imports "@/lib/utils":

import { cn } from "@/lib/utils"

No vite.config.* or tsconfig.json / jsconfig.json found to verify alias—confirm either tsconfig/jsconfig has "paths": { "@/": ["src/"] } or vite.config.{js,ts} defines resolve.alias "@" -> "". Otherwise this import will break.

frontend/src/page/SentInvoice.jsx (1)

281-313: Decimals may be undefined; ensure fallback is correct.

tokenInfo.decimals from the list may be undefined until the hook preserves it. After applying the useTokenList fix, this path will use real decimals; until then, keep || parsed.paymentToken.decimals || 18 to be safe.

frontend/src/page/ReceivedInvoice.jsx (1)

486-507: Fee dependency in balance checks.

checkBalance relies on fee from component state; if not yet fetched, ETH fee checks may undercount. Either fetch fee inline or guard with a ref that’s updated with latest value before checks.

frontend/src/page/GenerateLink.jsx (2)

273-289: Minor: unify verification UX and copy behavior with other pages.

Looks consistent; no blockers.

Also applies to: 299-334, 336-351


48-61: Default token selection: verify ETH/WETH detection and external token lists

Repo search found no JSON token lists or entries for "ETH"/"WETH" or the zero contract address (only: biome.json, frontend/components.json, frontend/jsconfig.json, frontend/package.json). If tokens are supplied at runtime or from an external source, confirm those lists include native ETH or a wrapped-ETH entry; otherwise falling back to tokens[0] can select an unintended token.

File: frontend/src/page/GenerateLink.jsx
Lines: 48-61

  // Set default token when tokens are loaded
  useEffect(() => {
    if (tokens.length > 0 && !selectedToken && !useCustomToken) {
      // Find ETH or use first token as default
      const ethToken = tokens.find(
        (token) =>
          token.symbol.toLowerCase() === "eth" ||
          token.contract_address ===
            "0x0000000000000000000000000000000000000000"
      );
      setSelectedToken(ethToken || tokens[0]);
    }
  }, [tokens, selectedToken, useCustomToken]);
contracts/src/Chainvoice.sol (2)

380-395: Anyone-can-withdraw pattern: confirm intent.

withdrawFees() is callable by anyone (sends to treasury). If that’s intended, OK. If not, gate with onlyOwner.

Would you like this restricted?

-function withdrawFees() external {
+function withdrawFees() external onlyOwner {

246-318: Batch CEI is correct; add explicit comments on all‑or‑nothing semantics.

Implementation already reverts on any failure; consider documenting in NatSpec.

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.

2 participants