Mona#8
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Greptile SummaryThis PR introduces a full-stack AI shopping app — MONA — built on Monad Testnet, with a Gemini-powered chat interface, x402 on-chain checkout, a Zustand cart store, and a product catalog. Two blocking issues need attention before this can function correctly in production:
Confidence Score: 3/5Not safe to merge — the order confirmation modal will crash on every successful checkout, and the payment price is client-manipulable. Two P0/P1 defects affect the primary user path: (1) the order confirmation modal throws a TypeError on every successful purchase, and (2) the x402 checkout price is sourced from a client-controlled query parameter. Both must be fixed before the app can work correctly.
|
| Filename | Overview |
|---|---|
| src/components/OrderConfirmationModal.tsx | Runtime crash: accesses item.product.name / item.product.priceUsdc but order.items is CartItem[] which has no nested product object |
| src/app/api/checkout/route.ts | x402 payment price is sourced from the client-supplied ?total query param, enabling price manipulation; also echoes back unvalidated request body |
| src/components/ChatUI.tsx | Stale error message references OpenAI; addItem included in useEffect dep array against project conventions; otherwise checkout and AI stream integration look correct |
| src/lib/checkout.ts | x402 fetch wrapper and error handling are well-structured; total is passed as query param (origin of the server-side price manipulation issue) |
| src/app/api/chat/route.ts | Gemini-backed streaming chat with searchProducts and addToCart tools; looks correct and well-structured |
| src/store/cartStore.ts | Zustand cart store with add/remove/updateQuantity/clearCart — logic is correct; CartItem shape lacks a nested product field (source of modal bug) |
| src/components/Toast.tsx | onDismiss in useEffect dep array may reset timer on parent re-renders; otherwise straightforward auto-dismiss toast |
| src/lib/monad.ts | Correct Monad Testnet chain definition (id: 10143) used by the app; root-level lib/monad.ts with wrong id (41454) is dead code but confusing |
Sequence Diagram
sequenceDiagram
participant User
participant ChatUI
participant ChatAPI as /api/chat (Gemini)
participant CheckoutLib as src/lib/checkout.ts
participant CheckoutAPI as /api/checkout (x402)
participant Blockchain as Monad Testnet
User->>ChatUI: Types query
ChatUI->>ChatAPI: POST /api/chat {messages}
ChatAPI-->>ChatUI: Stream (searchProducts / addToCart tool calls)
ChatUI->>ChatUI: addItem() → CartStore
User->>ChatUI: Click Checkout
ChatUI->>CheckoutLib: checkout(cart, total, walletClient)
CheckoutLib->>CheckoutAPI: POST /api/checkout?total=X (x402 wrapped fetch)
CheckoutAPI-->>CheckoutLib: 402 Payment Required
CheckoutLib->>Blockchain: signTypedData → USDC transfer
Blockchain-->>CheckoutLib: txHash
CheckoutLib->>CheckoutAPI: POST /api/checkout?total=X + payment header
CheckoutAPI-->>CheckoutLib: {success, txHash, items}
CheckoutLib-->>ChatUI: OrderConfirmation
ChatUI->>ChatUI: OrderConfirmationModal (crashes on item.product.name)
Reviews (1): Last reviewed commit: "First Commit" | Re-trigger Greptile
| <span className="text-neutral-500 mr-1">{item.quantity}x</span> {item.product.name} | ||
| </span> | ||
| <span className="text-white">${(item.product.priceUsdc * item.quantity).toFixed(2)}</span> |
There was a problem hiding this comment.
Runtime crash:
item.product is undefined on CartItem
order.items is the CartItem[] array echoed back by the checkout API. CartItem has productName and priceUsdc at the top level — it has no nested product object. Accessing item.product.name and item.product.priceUsdc will throw TypeError: Cannot read properties of undefined (reading 'name') every time the modal is opened after a successful checkout.
| <span className="text-neutral-500 mr-1">{item.quantity}x</span> {item.product.name} | |
| </span> | |
| <span className="text-white">${(item.product.priceUsdc * item.quantity).toFixed(2)}</span> | |
| <span className="text-neutral-300"> | |
| <span className="text-neutral-500 mr-1">{item.quantity}x</span> {item.productName} | |
| </span> | |
| <span className="text-white">${(item.priceUsdc * item.quantity).toFixed(2)}</span> |
| scheme: "exact", | ||
| price: (context) => { | ||
| const total = context.adapter.getQueryParam?.('total'); | ||
| const priceValue = Array.isArray(total) ? total[0] : total; | ||
| return `$${priceValue || '0'}`; |
There was a problem hiding this comment.
Client-controlled price allows payment manipulation
The payment amount is read entirely from the client-supplied ?total query parameter. Any user can craft a request with ?total=0.01 and the x402 payment requirement will be set to $0.01 regardless of the real cart value. The server should derive the authoritative price from the items in the request body (summing item.priceUsdc * item.quantity) rather than trusting the client.
| const errorMessage: Message = { | ||
| id: Date.now().toString(), | ||
| role: 'assistant', | ||
| parts: [{ type: 'text', text: 'Sorry, I encountered an error. Please check your OpenAI API key and credits.' }] | ||
| } |
There was a problem hiding this comment.
Stale error message references OpenAI
The chat API uses Google Gemini (@ai-sdk/google, gemini-2.0-flash-001), but this catch-block tells users to check their "OpenAI API key and credits." This will confuse users and developers debugging production errors.
| const errorMessage: Message = { | |
| id: Date.now().toString(), | |
| role: 'assistant', | |
| parts: [{ type: 'text', text: 'Sorry, I encountered an error. Please check your OpenAI API key and credits.' }] | |
| } | |
| const errorMessage: Message = { | |
| id: Date.now().toString(), | |
| role: 'assistant', | |
| parts: [{ type: 'text', text: 'Sorry, I encountered an error. Please try again.' }] | |
| } |
| useEffect(() => { | ||
| const lastMessage = messages[messages.length - 1] as ExtendedMessage | ||
| if (lastMessage?.role === 'assistant' && lastMessage.toolInvocations) { | ||
| // Check for searchProducts results | ||
| const searchInv = lastMessage.toolInvocations.find(inv => inv.toolName === 'searchProducts') | ||
| if (searchInv?.state === 'result') { | ||
| setCurrentProducts(searchInv.result as Product[]) | ||
| setLastSearchQuery((searchInv.args as SearchArgs).query) | ||
| setIsSearching(false) | ||
| } else if (searchInv?.state === 'call') { | ||
| setIsSearching(true) | ||
| } | ||
|
|
||
| // Check for addToCart results | ||
| const addInv = lastMessage.toolInvocations.find(inv => inv.toolName === 'addToCart') | ||
| if (addInv?.state === 'result' && (addInv.result as AddToCartResult).success) { | ||
| addItem((addInv.result as AddToCartResult).product, (addInv.result as AddToCartResult).quantity) | ||
| setIsCartOpen(true) | ||
| } | ||
| } | ||
| }, [messages, addItem]) |
There was a problem hiding this comment.
State setter
addItem in useEffect dependency array
Per project conventions, state setters should not be included in useEffect dependency arrays when the effect calls that setter. While Zustand actions are generally stable references, including addItem here is inconsistent with this pattern.
| useEffect(() => { | |
| const lastMessage = messages[messages.length - 1] as ExtendedMessage | |
| if (lastMessage?.role === 'assistant' && lastMessage.toolInvocations) { | |
| // Check for searchProducts results | |
| const searchInv = lastMessage.toolInvocations.find(inv => inv.toolName === 'searchProducts') | |
| if (searchInv?.state === 'result') { | |
| setCurrentProducts(searchInv.result as Product[]) | |
| setLastSearchQuery((searchInv.args as SearchArgs).query) | |
| setIsSearching(false) | |
| } else if (searchInv?.state === 'call') { | |
| setIsSearching(true) | |
| } | |
| // Check for addToCart results | |
| const addInv = lastMessage.toolInvocations.find(inv => inv.toolName === 'addToCart') | |
| if (addInv?.state === 'result' && (addInv.result as AddToCartResult).success) { | |
| addItem((addInv.result as AddToCartResult).product, (addInv.result as AddToCartResult).quantity) | |
| setIsCartOpen(true) | |
| } | |
| } | |
| }, [messages, addItem]) | |
| }, [messages]) |
Rule Used: Avoid including state setters in useEffect depende... (source)
Learnt From
monad-developers/monapp#251
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| import { defineChain } from 'viem' | ||
|
|
||
| export const monadTestnet = defineChain({ | ||
| id: 41454, |
There was a problem hiding this comment.
Conflicting Monad Testnet chain ID
lib/monad.ts at the repo root defines id: 41454, whereas src/lib/monad.ts (the file actually used by the application) defines id: 10143. The checkout route and x402 server both register on "eip155:10143". The root-level file appears to be dead code with a wrong chain ID, but risks being imported accidentally.
Consider removing lib/monad.ts or aligning it with the correct chain ID.
| useEffect(() => { | ||
| setIsVisible(true); | ||
| const timer = setTimeout(() => { | ||
| setIsVisible(false); | ||
| setTimeout(onDismiss, 300); // Wait for fade out | ||
| }, duration); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| }, [duration, onDismiss]); |
There was a problem hiding this comment.
onDismiss reference instability resets timer on re-render
onDismiss is passed as () => setPaymentError(null) in ChatUI, creating a new function reference on every parent render. Because onDismiss is in this effect's dependency array, any re-render of ChatUI while the toast is visible will reset the countdown timer. Wrap the caller with useCallback or omit onDismiss from the dep array since the intent is to run once on mount.
No description provided.