-
Notifications
You must be signed in to change notification settings - Fork 19
Batch Processing (Batch Creation and Batch Payment) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this 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 ifaddressis 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).
Badgeis 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_PRESETSno longer exists. Fall back to custom-token verification when a URLtokenAddressis 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 polymorphismWithout
...props, callers can’t setaria-*,title,data-*, or handlers. Anas/Tagprop 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/themingUsing 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 consistencyIf 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 confirmationnavigator.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 stateMake 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 tweaksYou 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 semanticsDefaulting 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 routeEither 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 routeslocation.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 noiseHarden 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.
hasMoreis inferred from equality against the page cap, which yields false positives when results equal the cap exactly. Compute total matches inside the memo and derivehasMorefrom 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, orcontract_addressto 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.
loadinganderrorare 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
Setand 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
useTokenSearchbut don’t expose “Load more” UI here. Add a small “Load more” button/auto-load on scroll whenhasMoreis 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
setErrorandtoast.errorinstead ofalertfor 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.
getGroupedInvoicessumsparseFloat(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
.sliceon 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.
totalAmountis aggregated viaparseFloatand thenparseUnits(...), 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().chainIdis 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 withtoast.*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) ingroupand 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
invoiceRowswhenever 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/getReceivedInvoicesreturn 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
📒 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 — removingvariant/badgeVariantsis not a breaking change for this repoRepo scan shows no imports of
badgeVariantsand no JSX using<Badge variant=...>. Only Badge imports are in frontend/src/components/TokenPicker.jsx and frontend/src/page/GenerateLink.jsx; neither passes avariantprop.Likely an incorrect or invalid review comment.
frontend/src/components/ui/dialog.jsx (1)
26-45: LGTM — solid Radix compositionOverlay/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.decimalsfrom the list may be undefined until the hook preserves it. After applying theuseTokenListfix, this path will use real decimals; until then, keep|| parsed.paymentToken.decimals || 18to be safe.frontend/src/page/ReceivedInvoice.jsx (1)
486-507: Fee dependency in balance checks.
checkBalancerelies onfeefrom component state; if not yet fetched, ETH fee checks may undercount. Either fetchfeeinline 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 listsRepo 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 withonlyOwner.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.
Summary by CodeRabbit
New Features
Enhancements
UI
Chores