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>
  );
}