From 03151043928b1cd7ede065596f339d33148f3b58 Mon Sep 17 00:00:00 2001 From: The Joel Date: Sat, 29 Aug 2026 21:21:57 +0100 Subject: [PATCH 1/5] feat: add Stellar address validation and balance check to TransactionPanel --- src/components/TransactionPanel.test.tsx | 66 +++++++++++++++++++++++- src/components/TransactionPanel.tsx | 15 ++++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/components/TransactionPanel.test.tsx b/src/components/TransactionPanel.test.tsx index c25f5b6..b2aa590 100644 --- a/src/components/TransactionPanel.test.tsx +++ b/src/components/TransactionPanel.test.tsx @@ -20,6 +20,7 @@ describe("TransactionPanel", () => { vi.mocked(useSorokit).mockReturnValue({ address: "GABC", isConnected: true, + balances: [{ asset: "XLM", balance: "100" }], } as unknown as ReturnType); }); @@ -116,7 +117,8 @@ describe("TransactionPanel", () => { vi.mocked(useSorokit).mockReturnValue({ address: null, isConnected: true, - }); + balances: [{ asset: "XLM", balance: "100" }], + } as unknown as ReturnType); render(); @@ -138,7 +140,8 @@ describe("TransactionPanel", () => { vi.mocked(useSorokit).mockReturnValue({ address: "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", isConnected: true, - }); + balances: [{ asset: "XLM", balance: "100" }], + } as unknown as ReturnType); render(); @@ -180,4 +183,63 @@ describe("TransactionPanel", () => { ); expect(submitBtn).not.toBeDisabled(); }); + + it("shows insufficient balance error when amount exceeds XLM balance", async () => { + vi.mocked(useSorokit).mockReturnValue({ + address: "GABC", + isConnected: true, + balances: [{ asset: "XLM", balance: "10" }], + } as unknown as ReturnType); + + render(); + + const destInput = screen.getByLabelText("Destination Address"); + const amountInput = screen.getByLabelText("Amount (XLM)"); + const submitBtn = screen.getByRole("button", { name: "Send Payment" }); + + const validDest = "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + fireEvent.change(destInput, { target: { value: validDest } }); + + // Type amount exceeding balance (10 XLM) + fireEvent.change(amountInput, { target: { value: "15" } }); + + expect(screen.getByText("Insufficient balance")).toBeInTheDocument(); + expect(submitBtn).toBeDisabled(); + + // Type amount within balance + fireEvent.change(amountInput, { target: { value: "5" } }); + expect(screen.getByText("Insufficient balance")).toHaveClass("opacity-0"); + expect(submitBtn).not.toBeDisabled(); + }); + + it("allows submission when amount is within XLM balance", async () => { + const mockSubmit = vi.fn().mockResolvedValue({ data: { hash: "txhash123", ledger: 100 }, error: null }); + + vi.mocked(getClient).mockReturnValue({ + transaction: { + submit: mockSubmit, + }, + } as unknown as ReturnType); + + vi.mocked(useSorokit).mockReturnValue({ + address: "GABC", + isConnected: true, + balances: [{ asset: "XLM", balance: "100" }], + } as unknown as ReturnType); + + render(); + + const destInput = screen.getByLabelText("Destination Address"); + const amountInput = screen.getByLabelText("Amount (XLM)"); + const submitBtn = screen.getByRole("button", { name: "Send Payment" }); + + const validDest = "GCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + fireEvent.change(destInput, { target: { value: validDest } }); + fireEvent.change(amountInput, { target: { value: "50" } }); + + expect(submitBtn).not.toBeDisabled(); + fireEvent.click(submitBtn); + + expect(await screen.findByText("Transaction submitted")).toBeInTheDocument(); + }); }); diff --git a/src/components/TransactionPanel.tsx b/src/components/TransactionPanel.tsx index cee3f16..6abe89b 100644 --- a/src/components/TransactionPanel.tsx +++ b/src/components/TransactionPanel.tsx @@ -14,7 +14,7 @@ import { getClient, type TxResult } from "@/lib/client"; type State = "idle" | "loading" | "success" | "error"; export function TransactionPanel() { - const { address, isConnected } = useSorokit(); + const { address, isConnected, balances } = useSorokit(); const [dest, setDest] = useState(""); const [destDirty, setDestDirty] = useState(false); const [amount, setAmount] = useState(""); @@ -29,11 +29,17 @@ export function TransactionPanel() { const parsedAmount = parseFloat(amount); const isAmountValid = !isNaN(parsedAmount) && parsedAmount >= 0.0000001; + // Get XLM balance from balances array + const xlmBalance = balances.find((b) => b.asset === "XLM")?.balance || "0"; + const xlmBalanceNumber = parseFloat(xlmBalance); + const hasSufficientBalance = !isNaN(parsedAmount) && parsedAmount <= xlmBalanceNumber; + const canSubmit = isConnected && isDestValid && amount.trim() !== "" && - isAmountValid; + isAmountValid && + hasSufficientBalance; async function submit(e: React.FormEvent) { e.preventDefault(); @@ -169,6 +175,7 @@ export function TransactionPanel() { type="number" placeholder="0.00" min="0.0000001" + max={xlmBalanceNumber || undefined} step="0.0000001" value={amount} onChange={(e) => { @@ -183,7 +190,9 @@ export function TransactionPanel() { ? "Amount must be greater than 0" : parsedAmount < 0.0000001 ? "Minimum amount is 0.0000001 XLM" - : undefined + : !hasSufficientBalance + ? "Insufficient balance" + : undefined : undefined } disabled={state === "loading"} From 10325f8cc4a1f341bf7cbd167f4bb692778c879d Mon Sep 17 00:00:00 2001 From: The Joel Date: Sat, 29 Aug 2026 21:41:31 +0100 Subject: [PATCH 2/5] fix: change outDir to outDirs in vite.lib.config.ts for dts plugin --- vite.lib.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.lib.config.ts b/vite.lib.config.ts index bd46dd7..c520695 100644 --- a/vite.lib.config.ts +++ b/vite.lib.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ plugins: [react(), tailwindcss(), dts({ tsconfigPath: path.resolve(__dirname, "tsconfig.app.json"), entryRoot: path.resolve(__dirname, "src"), - outDir: "dist", + outDirs: ["dist"], include: ["src/components", "src/lib"], })], build: { From 14eabebea5bf1e209f2c10eeb71bdd53f829eb5a Mon Sep 17 00:00:00 2001 From: The Joel Date: Sat, 29 Aug 2026 22:10:55 +0100 Subject: [PATCH 3/5] fix: remove unused useRef and fix type error in TransactionPanel --- src/components/TransactionHistory.tsx | 2 +- src/components/TransactionPanel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TransactionHistory.tsx b/src/components/TransactionHistory.tsx index 92596f1..889976b 100644 --- a/src/components/TransactionHistory.tsx +++ b/src/components/TransactionHistory.tsx @@ -5,7 +5,7 @@ import { CheckmarkCircle01Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useEffect, useState } from "react"; +import { memo, useEffect, useMemo, useState } from "react"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; diff --git a/src/components/TransactionPanel.tsx b/src/components/TransactionPanel.tsx index 6abe89b..4bb685c 100644 --- a/src/components/TransactionPanel.tsx +++ b/src/components/TransactionPanel.tsx @@ -201,7 +201,7 @@ export function TransactionPanel() { label="Memo (optional)" placeholder="Text memo" value={memo} - onChange={(e) => setMemo(e.target.value)} + onChange={(e) => setMemo(e.target.value || "")} disabled={state === "loading"} /> From e895b59552b41f8d2d6ef7f23845335f3946b30d Mon Sep 17 00:00:00 2001 From: The Joel Date: Sat, 29 Aug 2026 22:21:18 +0100 Subject: [PATCH 4/5] fix: change vite.config.ts import from vitest/config to vite --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 7412145..daee5a3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import path from "path"; -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vite"; export default defineConfig({ plugins: [react(), tailwindcss()], From 8db74b8dace82fdf74ecec1b94bad9242022c7a3 Mon Sep 17 00:00:00 2001 From: The Joel Date: Sat, 29 Aug 2026 22:35:54 +0100 Subject: [PATCH 5/5] fix: remove test property from vite.config.ts --- vite.config.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index daee5a3..41cf4f3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -33,9 +33,4 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, - test: { - globals: true, - environment: "jsdom", - setupFiles: ["./src/setupTests.ts"], - }, });