// SendModal — a focused little dialog for Transfer tx's. Accepts a // hex pub, DC-address, or @username and resolves to the Ed25519 pub // before submitting. Validates amount against balance + min fee. import React, { useEffect, useMemo, useState } from 'react'; import { useStore } from '@/lib/store'; import { getBalance, resolveAccount } from '@/lib/api'; import { buildTransferTx, submitTx, humanizeTxError } from '@/lib/tx'; const MIN_FEE_UT = 1_000; function parseAmountT(s: string): number | null { const n = parseFloat(s); if (!Number.isFinite(n) || n <= 0) return null; return Math.round(n * 1_000_000); } export function SendModal({ onClose, onSent, }: { onClose: () => void; onSent: () => void; }): React.ReactElement { const keyFile = useStore(s => s.keyFile); const [toInput, setToInput] = useState(''); const [amount, setAmount] = useState(''); const [memo, setMemo] = useState(''); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const [balance, setBalance] = useState(null); useEffect(() => { if (!keyFile) return; getBalance(keyFile.pub_key).then(setBalance).catch(() => setBalance(null)); }, [keyFile]); const amountUT = useMemo(() => parseAmountT(amount), [amount]); const totalUT = amountUT === null ? null : amountUT + MIN_FEE_UT; const canSend = !!keyFile && !busy && amountUT !== null && balance !== null && totalUT !== null && balance >= totalUT && toInput.trim().length > 0; const submit = async () => { if (!keyFile || !canSend || amountUT === null) return; setBusy(true); setErr(null); try { const to = await resolveAccount(toInput); if (!to) throw new Error('Can\'t resolve recipient'); if (to === keyFile.pub_key) throw new Error('Refusing self-transfer'); const tx = buildTransferTx({ from: keyFile.pub_key, to, amount: amountUT, fee: MIN_FEE_UT, privKey: keyFile.priv_key, memo: memo.trim() || undefined, }); await submitTx(tx); onSent(); onClose(); } catch (e) { setErr(humanizeTxError(e)); } finally { setBusy(false); } }; return ( {} : onClose}>
setToInput(e.target.value)} placeholder="@alice or DC… or " spellCheck={false} autoFocus style={inputStyle} /> setAmount(e.target.value)} placeholder="0.0" inputMode="decimal" style={inputStyle} />
Balance: {balance === null ? '…' : `${(balance / 1_000_000).toLocaleString(undefined, { maximumFractionDigits: 6 })} T`} {amountUT !== null && ( <> · Fee: {(MIN_FEE_UT / 1_000_000).toFixed(6)} T )}
setMemo(e.target.value)} placeholder="Invoice #42" style={inputStyle} />
{err && (
{err}
)}
); } // ─── Shared modal primitives used by Send/Receive ──────────────────────── function Backdrop({ children, onClose }: { children: React.ReactNode; onClose: () => void }) { return (
e.stopPropagation()} style={{ width: '100%', display: 'flex', justifyContent: 'center' }}> {children}
); } function Header({ title, onClose, busy }: { title: string; onClose: () => void; busy: boolean; }) { return (
{title}
); } function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode; }) { return (
{label}
{children} {hint && (
{hint}
)}
); } const inputStyle: React.CSSProperties = { width: '100%', boxSizing: 'border-box', background: '#000', border: '1px solid #1f1f1f', borderRadius: 8, padding: '10px 12px', color: '#fff', fontSize: 13, fontFamily: 'inherit', outline: 'none', }; const primaryBtnStyle = (disabled: boolean): React.CSSProperties => ({ padding: '9px 18px', borderRadius: 999, border: 'none', background: '#1d9bf0', color: '#fff', fontSize: 13, fontWeight: 700, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.5 : 1, }); const secondaryBtnStyle = (disabled: boolean): React.CSSProperties => ({ padding: '9px 14px', borderRadius: 999, background: 'transparent', border: '1px solid #1f1f1f', color: '#8b8b8b', fontSize: 13, fontWeight: 700, cursor: disabled ? 'default' : 'pointer', }); export { Backdrop, Header, Field, inputStyle, primaryBtnStyle, secondaryBtnStyle };