From c852480b23e5e7e5cc223355d55aabf4ed7c4436 Mon Sep 17 00:00:00 2001 From: "Jenola.base.eth" Date: Tue, 30 Jun 2026 07:32:09 +0100 Subject: [PATCH 1/4] feat: add NetworkSwitcher component to allow toggling between network environments --- src/components/NetworkSwitcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index 981f31a..b18d91e 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -19,7 +19,7 @@ export function NetworkSwitcher() { const current = NETWORKS.find((n) => n.name === network?.name) ?? NETWORKS[1]; const handleSelect = async (name: NetworkName) => { - if (isSwitching) return; + if (isSwitching || name === network?.name) return; setIsSwitching(true); try { await switchNetwork(name); From c05d81cdf24e245595c2b708a6efde5919d64ea2 Mon Sep 17 00:00:00 2001 From: "Jenola.base.eth" Date: Tue, 30 Jun 2026 07:45:22 +0100 Subject: [PATCH 2/4] feat: add TransactionPanel component for submitting XLM payments on Stellar --- src/components/TransactionPanel.tsx | 258 ++++++++++++++++++++++++---- 1 file changed, 227 insertions(+), 31 deletions(-) diff --git a/src/components/TransactionPanel.tsx b/src/components/TransactionPanel.tsx index f3d8995..be235f4 100644 --- a/src/components/TransactionPanel.tsx +++ b/src/components/TransactionPanel.tsx @@ -1,33 +1,229 @@ -/** - * TransactionPanel Component - * - * Displays transaction history and management interface for the connected wallet. - * Shows pending, confirmed, and failed transactions with detailed information. - * - * @component - * @example - * ```tsx - * import { TransactionPanel } from 'sorokit-ui'; - * - * export function Dashboard() { - * return ( - *
- * - *
- * ); - * } - * ``` - * - * @returns The rendered TransactionPanel component - * - * @remarks - * - Auto-refreshes every 5 seconds - * - Shows transaction status updates in real-time - * - Filters by date range available - * - Requires SorokitProvider context - * - * @see {@link SorokitProvider} for setup - */ +import { useState } from "react"; +import { useSorokit } from "@/context/useSorokit"; +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import { Badge } from "@/components/ui/Badge"; +import { getClient, type TxResult } from "@/lib/client"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + CheckmarkCircle01Icon, + AlertCircleIcon, +} from "@hugeicons/core-free-icons"; + +type State = "idle" | "loading" | "success" | "error"; + export function TransactionPanel() { - // Component implementation + const { address, isConnected } = useSorokit(); + const [dest, setDest] = useState(""); + const [destDirty, setDestDirty] = useState(false); + const [amount, setAmount] = useState(""); + const [amountDirty, setAmountDirty] = useState(false); + const [memo, setMemo] = useState(""); + const [state, setState] = useState("idle"); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const isDestValid = /^G[A-Z2-7]{55}$/.test(dest.trim()); + const isSelfPayment = dest.trim() === address; + const parsedAmount = parseFloat(amount); + const isAmountValid = !isNaN(parsedAmount) && parsedAmount >= 0.0000001; + + const canSubmit = + isConnected && + isDestValid && + amount.trim() !== "" && + isAmountValid; + + async function submit(e: React.FormEvent) { + e.preventDefault(); + if (!address) { + setError("Wallet not connected"); + setState("error"); + return; + } + if (!canSubmit) return; + setState("loading"); + setError(null); + setResult(null); + try { + const { data, error: err } = await getClient().transaction.submit({ + source: address, + destination: dest.trim(), + amount: amount.trim(), + memo: memo.trim() || undefined, + asset: "XLM", + }); + if (err) { + setError(err); + setState("error"); + return; + } + setResult(data); + setState("success"); + setDest(""); + setAmount(""); + setMemo(""); + setDestDirty(false); + setAmountDirty(false); + } catch (e) { + setError(e instanceof Error ? e.message : "Unknown error"); + setState("error"); + } + } + + const handleSendClick = () => { + submit({ preventDefault: () => {} } as React.FormEvent); + }; + + return ( +
+
+

Send Payment

+

+ Submit a payment on the Stellar network +

+
+ +
+ {!isConnected ? ( +
+

+ Connect your wallet to send transactions +

+
+ ) : state === "success" && result ? ( +
+
+
+ +
+
+

+ Transaction submitted +

+

+ Ledger #{result.ledger} +

+
+
+
+

+ Transaction Hash +

+ + {result.hash} + + + Successful + +
+
+ ) : state === "error" ? ( +
+
+ +
+
+

+ Transaction failed +

+

{error}

+
+
+ ) : ( +
+ { + setDest(e.target.value); + setDestDirty(true); + }} + error={ + destDirty + ? !isDestValid + ? "Invalid Stellar address" + : isSelfPayment + ? "Destination is the same as your wallet address" + : undefined + : undefined + } + disabled={state === "loading"} + /> + { + setAmount(e.target.value); + setAmountDirty(true); + }} + error={ + amountDirty + ? amount.trim() === "" + ? "Amount is required" + : isNaN(parsedAmount) || parsedAmount <= 0 + ? "Amount must be greater than 0" + : parsedAmount < 0.0000001 + ? "Minimum amount is 0.0000001 XLM" + : undefined + : undefined + } + disabled={state === "loading"} + /> + setMemo(e.target.value)} + disabled={state === "loading"} + /> +
+ )} +
+ +
+ {state === "success" || state === "error" ? ( + + ) : ( + + )} +
+
+ ); } From 66864ade948a279c6353cd5206d1eadfc77f7435 Mon Sep 17 00:00:00 2001 From: "Jenola.base.eth" Date: Tue, 30 Jun 2026 07:56:11 +0100 Subject: [PATCH 3/4] feat: add SorobanPanel and TransactionPanel components to dashboard UI --- src/components/SorobanPanel.tsx | 202 +++++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 28 deletions(-) diff --git a/src/components/SorobanPanel.tsx b/src/components/SorobanPanel.tsx index 85ac5e3..c53ad8f 100644 --- a/src/components/SorobanPanel.tsx +++ b/src/components/SorobanPanel.tsx @@ -1,29 +1,175 @@ -/** - * SorobanPanel Component - * - * Provides a user-friendly interface for interacting with Soroban smart contracts. - * Handles contract invocation, parameter input, and result display. - * - * @component - * @example - * ```tsx - * import { SorobanPanel } from 'sorokit-ui'; - * - * export function App() { - * return ; - * } - * ``` - * - * @returns The rendered SorobanPanel component - * - * @remarks - * - Requires SorokitProvider context - * - Automatically handles wallet connection - * - Supports all Soroban contract types - * - * @see {@link SorokitProvider} for context setup - * @see {@link useClient} for custom client access - */ -export function SorobanPanel() { - // Component implementation +import { useState } from "react"; +import { useSorokit } from "@/context/useSorokit"; +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; +import { Badge } from "@/components/ui/Badge"; +import { getClient } from "@/lib/client"; + +type State = "idle" | "loading" | "success" | "error"; + +interface SorobanPanelProps { + contractId: string; + onContractIdChange: (contractId: string) => void; +} + +export function SorobanPanel({ + contractId, + onContractIdChange, +}: SorobanPanelProps) { + const { isConnected, address } = useSorokit(); + const [method, setMethod] = useState(""); + const [args, setArgs] = useState(""); + const [state, setState] = useState("idle"); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const canInvoke = isConnected && contractId.trim() && method.trim(); + + async function doInvoke() { + if (!canInvoke) return; + setState("loading"); + setError(null); + setResult(null); + try { + let parsedArgs: unknown[] = []; + if (args.trim()) { + try { + const parsed = JSON.parse(args.trim()); + if (!Array.isArray(parsed)) { + setError('Arguments must be a JSON array (e.g. ["arg1", 42])'); + setState("error"); + return; + } + parsedArgs = parsed; + } catch { + setError("Invalid JSON in arguments"); + setState("error"); + return; + } + } + const { data, error: err } = await getClient().soroban.invokeContract({ + contractId: contractId.trim(), + method: method.trim(), + args: parsedArgs, + sourceAccount: address ?? undefined, + }); + if (err) { + setError(err); + setState("error"); + return; + } + setResult(data); + setState("success"); + } catch (e) { + setError(e instanceof Error ? e.message : "Unknown error"); + setState("error"); + } + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + doInvoke(); + } + + function handleClick() { + doInvoke(); + } + + return ( +
+
+
+

+ Contract Invoke +

+

+ Call a Soroban smart contract method +

+
+ Soroban +
+ +
+ {!isConnected ? ( +

+ Connect your wallet to invoke contracts +

+ ) : ( +
+ onContractIdChange(e.target.value)} + disabled={state === "loading"} + /> + setMethod(e.target.value)} + disabled={state === "loading"} + /> +
+ +