From c5d107ce3190c353c0d3a6e9a84f0f858de3ca5b Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 16 Mar 2026 12:08:41 +0000 Subject: [PATCH 1/2] [#159] Add balance display, validation, and MAX button to Trade/Donate - Show relevant token balance below input (reserve for buy/donate, storyline token for sell) - Inline "Insufficient balance" error when amount exceeds balance, button disabled - MAX button fills input with full available balance - Balance auto-refreshes every 15s and after each successful transaction Fixes #159 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/DonateWidget.tsx | 67 ++++++++++++++++++++++------- src/components/TradingWidget.tsx | 72 +++++++++++++++++++++++++------- 2 files changed, 111 insertions(+), 28 deletions(-) diff --git a/src/components/DonateWidget.tsx b/src/components/DonateWidget.tsx index 247b8926..c3374521 100644 --- a/src/components/DonateWidget.tsx +++ b/src/components/DonateWidget.tsx @@ -2,6 +2,7 @@ import { useState, useCallback } from "react"; import { useAccount, useWriteContract } from "wagmi"; +import { useQuery } from "@tanstack/react-query"; import { parseUnits, formatUnits } from "viem"; import { publicClient } from "../../lib/rpc"; import { erc20Abi } from "../../lib/price"; @@ -29,6 +30,24 @@ export function DonateWidget({ storylineId }: DonateWidgetProps) { ? parseUnits(amount, 18) : BigInt(0); + // Fetch reserve token balance + const { data: balance, refetch: refetchBalance } = useQuery({ + queryKey: ["token-balance", PLOT_TOKEN, address], + queryFn: async () => { + return publicClient.readContract({ + address: PLOT_TOKEN, + abi: erc20Abi, + functionName: "balanceOf", + args: [address!], + }); + }, + enabled: !!address, + refetchInterval: 15000, + }); + + const insufficientBalance = + balance !== undefined && parsedAmount > BigInt(0) && parsedAmount > balance; + const executeDonate = useCallback(async () => { if (!address || parsedAmount === BigInt(0)) return; @@ -82,11 +101,12 @@ export function DonateWidget({ storylineId }: DonateWidgetProps) { setTxState("done"); setAmount(""); + refetchBalance(); } catch (err) { setError(err instanceof Error ? err.message : "Transaction failed"); setTxState("error"); } - }, [address, parsedAmount, storylineId, writeContractAsync]); + }, [address, parsedAmount, storylineId, writeContractAsync, refetchBalance]); const reset = useCallback(() => { setTxState("idle"); @@ -107,18 +127,37 @@ export function DonateWidget({ storylineId }: DonateWidgetProps) { - { - setAmount(e.target.value); - if (txState !== "idle") reset(); - }} - disabled={txState !== "idle" && txState !== "error" && txState !== "done"} - className="border-border bg-background text-foreground mt-1 w-full rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none disabled:opacity-50" - /> +
+ { + setAmount(e.target.value); + if (txState !== "idle") reset(); + }} + disabled={txState !== "idle" && txState !== "error" && txState !== "done"} + className="border-border bg-background text-foreground w-full rounded border px-3 py-2 pr-14 text-sm focus:border-accent focus:outline-none disabled:opacity-50" + /> + {balance !== undefined && ( + + )} +
+ {balance !== undefined && ( +

+ Balance: {formatUnits(balance, 18)} {reserveLabel} +

+ )} + {insufficientBalance && ( +

Insufficient balance

+ )} {parsedAmount > BigInt(0) && ( @@ -134,7 +173,7 @@ export function DonateWidget({ storylineId }: DonateWidgetProps) { + )} + + {balance !== undefined && ( +

+ Balance: {formatUnits(balance, 18)} {tab === "buy" ? reserveLabel : "tokens"} +

+ )} + {insufficientBalance && ( +

Insufficient balance

+ )} {/* Estimate */} @@ -208,7 +252,7 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) {