File size: 7,973 Bytes
945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 383d246 945e815 383d246 5f5514e 945e815 5f5514e 945e815 5f5514e 383d246 5f5514e 945e815 383d246 945e815 383d246 5f5514e 383d246 5f5514e 383d246 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 383d246 5f5514e 383d246 5f5514e 383d246 945e815 5f5514e 945e815 5f5514e 383d246 5f5514e 383d246 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | import React, { useState } from 'react';
import { Num, useToast } from '../components/ui/index';
interface Props { balance: { sol: number; usdt: number }; onSent: () => void; }
export default function SendPage({ balance, onSent }: Props) {
const [token, setToken] = useState<'SOL' | 'USDT'>('SOL');
const [to, setTo] = useState('');
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState<any>(null);
const [step, setStep] = useState<'form' | 'confirm' | 'done'>('form');
const [risk, setRisk] = useState<any>(null);
const { addToast } = useToast();
const max = token === 'SOL' ? balance.sol : balance.usdt;
const amt = parseFloat(amount) || 0;
const review = async () => {
setError('');
if (!to.trim()) return setError('Recipient address required');
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(to.trim())) return setError('Invalid Solana address');
if (!amt || amt <= 0) return setError('Enter a valid amount');
if (amt > max) return setError(`Insufficient balance. Max: ${max.toFixed(token === 'SOL' ? 4 : 2)} ${token}`);
// AI risk assessment before showing confirmation
if (window.solvox) {
try {
const r = await window.solvox.ai.assessRisk({ amount: amt, token, to: to.trim() });
if (r.success) setRisk(r.risk);
} catch {}
}
setStep('confirm');
};
const send = async () => {
setLoading(true); setError('');
try {
const r = window.solvox
? await window.solvox.ai.executeConfirmed({ token, amount: amt, to: to.trim() })
: { success: true, signature: 'dev_' + Date.now(), explorer: '#' };
if (r.success) {
setSuccess(r); setStep('done'); onSent();
addToast({ type: 'success', title: `${amount} ${token} sent` });
} else { setError(r.error || 'Transaction failed'); setStep('form'); }
} catch (e: any) { setError(e.message); setStep('form'); }
setLoading(false);
};
const reset = () => { setTo(''); setAmount(''); setError(''); setSuccess(null); setRisk(null); setStep('form'); };
return (
<div className="max-w-md mx-auto px-8 py-section">
<h2 className="text-title-lg display-text text-ink mb-2">Send {token}</h2>
<p className="text-body-sm text-body mb-8">Balance: <span className="number-mono"><Num value={max} decimals={token === 'SOL' ? 4 : 2} /></span> {token}</p>
{step === 'form' && (
<div className="card space-y-5">
{/* Token */}
<div className="flex gap-2 p-1 bg-surface-soft rounded-pill">
{(['SOL', 'USDT'] as const).map(t => (
<button key={t} onClick={() => setToken(t)} className={`flex-1 py-2 rounded-pill text-button transition-colors ${token === t ? 'bg-primary text-on-primary' : 'text-body hover:text-ink'}`}>
{t === 'SOL' ? '◎' : '₮'} {t}
</button>
))}
</div>
{/* Recipient */}
<div>
<label className="text-caption-strong text-muted uppercase tracking-wider block mb-1.5">Recipient</label>
<input value={to} onChange={e => setTo(e.target.value)} placeholder="Solana address" className="input-field font-mono text-body-sm" />
</div>
{/* Amount */}
<div>
<div className="flex justify-between items-center mb-1.5">
<label className="text-caption-strong text-muted uppercase tracking-wider">Amount</label>
<button onClick={() => setAmount(max.toString())} className="btn-text text-body-sm py-0 px-1">Max</button>
</div>
<input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0.00" step="any" className="input-field number-mono text-title-lg text-center" />
<div className="flex gap-2 mt-2">
{[25, 50, 75, 100].map(p => (
<button key={p} onClick={() => setAmount((max * p / 100).toFixed(token === 'SOL' ? 4 : 2))} className="flex-1 py-1.5 rounded-pill text-caption-strong text-muted bg-surface-soft hover:bg-surface-strong transition-colors">{p}%</button>
))}
</div>
</div>
{error && <div className="text-body-sm text-semantic-down bg-semantic-down/5 rounded-md p-3">{error}</div>}
<button onClick={review} className="btn-primary w-full">Review transaction</button>
</div>
)}
{step === 'confirm' && (
<div className="card space-y-5 page-enter">
<div className="text-center mb-2">
<div className="text-body text-muted">Confirm transaction</div>
<div className="display-text text-display-sm text-ink mt-1">{amount} {token}</div>
</div>
<div className="bg-surface-soft rounded-xl p-4 space-y-3 text-body-sm">
<div className="flex justify-between"><span className="text-muted">To</span><span className="font-mono text-caption">{to.slice(0, 10)}…{to.slice(-6)}</span></div>
<div className="hairline-soft" />
<div className="flex justify-between"><span className="text-muted">Fee</span><span className="number-mono text-caption">~0.000005 SOL</span></div>
<div className="flex justify-between"><span className="text-muted">Network</span><span className="badge-pill text-[10px]">DEVNET</span></div>
</div>
{/* AI Risk Assessment */}
{risk && (
<div className={`rounded-xl p-4 text-body-sm ${risk.level === 'safe' ? 'bg-semantic-up/5' : risk.level === 'danger' ? 'bg-semantic-down/5' : 'bg-accent-yellow/5'}`}>
<div className="flex items-center gap-2 mb-1">
<span className={`badge-pill text-[10px] ${risk.level === 'safe' ? 'badge-pill-green' : 'badge-pill-red'}`}>AI RISK: {risk.level.toUpperCase()}</span>
<span className="number-mono text-caption text-muted">{risk.score}/100</span>
</div>
{risk.factors?.length > 0 && <ul className="text-caption text-body mt-1 space-y-0.5">{risk.factors.map((f: string, i: number) => <li key={i}>· {f}</li>)}</ul>}
{risk.recommendation && <div className="text-caption text-muted mt-1">{risk.recommendation}</div>}
<div className="text-caption text-muted-soft mt-1 flex items-center gap-1">
<div className="w-1 h-1 rounded-full bg-primary" />
Analyzed by @qvac/llm-llamacpp + @qvac/embed-llamacpp
</div>
</div>
)}
{error && <div className="text-body-sm text-semantic-down">{error}</div>}
<div className="flex gap-3">
<button onClick={() => setStep('form')} className="btn-secondary flex-1">Cancel</button>
<button onClick={send} disabled={loading} className="btn-primary flex-1 disabled:opacity-50">{loading ? 'Sending…' : `Send ${token}`}</button>
</div>
</div>
)}
{step === 'done' && success && (
<div className="card text-center space-y-5 page-enter">
<div className="w-14 h-14 mx-auto rounded-full bg-semantic-up/10 flex items-center justify-center">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#05b169" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<div className="display-text text-display-sm text-ink">Sent</div>
<p className="text-body text-muted">{amount} {token} sent successfully</p>
<div className="bg-surface-soft rounded-xl p-3 font-mono text-caption text-muted break-all">{success.signature}</div>
<a href={success.explorer} target="_blank" rel="noopener noreferrer" className="btn-text text-body-sm">View on Solscan →</a>
<button onClick={reset} className="btn-primary w-full">Send another</button>
</div>
)}
</div>
);
}
|