import { useState, useCallback } from 'react';
import {
createPsbt,
signAndFinalizePsbt,
broadcastTx,
type WalletInfo,
type FeeEstimate,
type PsbtResult,
} from '../api';
function shortenTxid(txid: string) {
if (txid.length < 20) return txid;
return txid.slice(0, 10) + '…' + txid.slice(-6);
}
interface SendModalProps {
wallet: any;
addresses: any[];
utxos: any[];
password: string;
feeEstimate: FeeEstimate | null;
onClose: () => void;
onBroadcast: () => void;
}
type Phase = 'form' | 'preview' | 'signing' | 'broadcasting' | 'done' | 'error';
export default function SendModal({
wallet,
addresses: addressesRaw,
utxos,
password,
feeEstimate,
onClose,
onBroadcast,
}: SendModalProps) {
// Extract address strings for API calls
const addresses = addressesRaw.map((a: any) => a.address ?? a);
const [phase, setPhase] = useState<Phase>('form');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [feeRate, setFeeRate] = useState(feeEstimate?.feerate_sats_vb || 10);
const [coinSelection, setCoinSelection] = useState<'largest' | 'manual'>('largest');
const [selectedUtxos, setSelectedUtxos] = useState<Set<number>>(new Set());
const [psbtResult, setPsbtResult] = useState<PsbtResult | null>(null);
const [broadcastTxid, setBroadcastTxid] = useState('');
const showToast = (msg: string) => {
const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2000);
};
const copyToClipboard = (text: string, label: string) => {
navigator.clipboard.writeText(text).then(() => showToast(`${label} copied!`));
};
const satToBtc = (sats: number) => (sats / 100_000_000).toFixed(8);
// Handle coin selection
const toggleUtxo = (idx: number) => {
setSelectedUtxos(prev => {
const next = new Set(prev);
if (next.has(idx)) next.delete(idx);
else next.add(idx);
return next;
});
};
const handleCreate = async () => {
if (!recipient || !amount) {
setError('Fill in recipient and amount');
return;
}
setLoading(true);
setError('');
try {
const selected = coinSelection === 'manual'
? Array.from(selectedUtxos).map(i => utxos[i])
: null;
const result = await createPsbt(wallet, [{ address: recipient, amount: parseFloat(amount) }], feeRate, addresses, password, coinSelection, selected);
if (result.error) {
setError(result.error);
setPhase('error');
} else {
setPsbtResult(result);
setPhase('preview');
}
} catch (e: any) {
setError(e.message || 'Failed to create PSBT');
setPhase('error');
} finally {
setLoading(false);
}
};
const handleSignBroadcast = async () => {
if (!psbtResult) return;
setLoading(true);
setError('');
try {
// Sign and finalize
const signed = await signAndFinalizePsbt(wallet, psbtResult.psbt, password);
if (signed.error) {
setError(signed.error);
setPhase('error');
return;
}
if (!signed.raw_tx_hex) {
setError('Signing did not produce a raw transaction');
setPhase('error');
return;
}
setPhase('broadcasting');
// Broadcast
const result = await broadcastTx(signed.raw_tx_hex);
if (result.error) {
setError(result.error);
setPhase('error');
} else {
setBroadcastTxid(result.txid);
setPhase('done');
}
} catch (e: any) {
setError(e.message || 'Failed to sign/broadcast');
setPhase('error');
} finally {
setLoading(false);
}
};
// ---- Form ----
if (phase === 'form') {
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
<div className="modal">
<div className="modal-header">
<h3 className="modal-title">Send Bitcoin</h3>
<button className="modal-close" onClick={onClose}>✕</button>
</div>
<div className="modal-body">
<div className="send-form">
{error && <div className="error-box">{error}</div>}
<div className="input-group">
<label className="input-label">Recipient Address</label>
<input
className="input"
value={recipient}
onChange={e => setRecipient(e.target.value)}
placeholder="bc1q..."
/>
</div>
<div className="input-group">
<label className="input-label">Amount</label>
<div className="input-suffix">
<input
className="input"
type="number"
step="0.00000001"
min="0"
value={amount}
onChange={e => setAmount(e.target.value)}
placeholder="0.00000000"
/>
<span className="input-suffix-text">BTC</span>
</div>
</div>
<div className="input-group">
<label className="input-label">
Fee Rate: {feeRate} sats/vB
{feeRate > 100 ? ' ⚡' : feeRate < 5 ? ' 🐢' : ''}
</label>
<input
type="range"
min="1"
max="100"
step="1"
value={feeRate}
onChange={e => setFeeRate(parseInt(e.target.value))}
className="fee-slider"
/>
</div>
<div className="input-group">
<label className="input-label">Coin Selection</label>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className={`btn ${coinSelection === 'largest' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setCoinSelection('largest')}
>
Largest First
</button>
<button
className={`btn ${coinSelection === 'manual' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setCoinSelection('manual')}
>
Manual ({selectedUtxos.size}/{utxos.length})
</button>
</div>
</div>
{coinSelection === 'manual' && utxos.length > 0 && (
<div style={{ maxHeight: '160px', overflowY: 'auto' }}>
{utxos.map((u, i) => (
<label
key={i}
className="checkbox-label"
style={{ padding: '6px 0', borderBottom: '1px solid var(--border)' }}
>
<input
type="checkbox"
checked={selectedUtxos.has(i)}
onChange={() => toggleUtxo(i)}
/>
<span className="mono" style={{ fontSize: '12px' }}>
{u.txid.slice(0, 8)}…v{u.vout}
</span>
<span className="mono" style={{ marginLeft: 'auto', fontSize: '12px' }}>
{(u.amount ?? 0).toFixed(8)} BTC
</span>
</label>
))}
</div>
)}
<button
className="btn btn-primary"
style={{ width: '100%', marginTop: '8px' }}
onClick={handleCreate}
disabled={loading || !recipient || !amount}
>
{loading ? 'Creating…' : 'Create Transaction →'}
</button>
</div>
</div>
</div>
</div>
);
}
// ---- Preview ----
if (phase === 'preview' && psbtResult) {
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
<div className="modal" style={{ maxWidth: '580px' }}>
<div className="modal-header">
<h3 className="modal-title">Review Transaction</h3>
<button className="modal-close" onClick={onClose}>✕</button>
</div>
<div className="modal-body">
{error && <div className="error-box">{error}</div>}
<div className="send-summary">
<div className="send-summary-row">
<span className="label">To</span>
<span className="value">{recipient}</span>
</div>
<div className="send-summary-row">
<span className="label">Amount</span>
<span className="value">{parseFloat(amount).toFixed(8)} BTC</span>
</div>
<div className="send-summary-row">
<span className="label">Fee</span>
<span className="value">{psbtResult.fee_btc.toFixed(8)} BTC</span>
</div>
<div className="send-summary-row">
<span className="label">Total</span>
<span className="value">{(parseFloat(amount) + psbtResult.fee_btc).toFixed(8)} BTC</span>
</div>
<div className="send-summary-row">
<span className="label">Inputs</span>
<span className="value">{psbtResult.inputs}</span>
</div>
{psbtResult.change_address && (
<div className="send-summary-row">
<span className="label">Change</span>
<span className="value">{satToBtc(psbtResult.change_amount)} BTC</span>
</div>
)}
</div>
<div style={{ marginTop: '12px' }}>
<div className="input-label" style={{ marginBottom: '6px' }}>PSBT (click to copy)</div>
<div className="psbt-display" onClick={() => copyToClipboard(psbtResult.psbt, 'PSBT')}>
{psbtResult.psbt.slice(0, 200)}…
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-ghost" onClick={() => setPhase('form')}>Back</button>
<button className="btn btn-secondary" onClick={() => copyToClipboard(psbtResult.psbt, 'PSBT')}>
Copy PSBT
</button>
<button className="btn btn-primary" onClick={handleSignBroadcast} disabled={loading}>
{loading ? 'Processing…' : 'Sign & Broadcast'}
</button>
</div>
</div>
</div>
);
}
// ---- Broadcasting ----
if (phase === 'broadcasting') {
return (
<div className="modal-overlay">
<div className="modal" style={{ maxWidth: '400px', textAlign: 'center' }}>
<div className="modal-body" style={{ padding: '48px 24px' }}>
<div style={{ fontSize: '32px', marginBottom: '16px' }}>⏳</div>
<h3 style={{ marginBottom: '8px' }}>Broadcasting…</h3>
<p className="text-secondary" style={{ fontSize: '14px' }}>Sending transaction to the network</p>
</div>
</div>
</div>
);
}
// ---- Done ----
if (phase === 'done') {
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onDone(broadcastTxid); }}>
<div className="modal" style={{ maxWidth: '440px' }}>
<div className="modal-body" style={{ textAlign: 'center', padding: '40px 24px' }}>
<div className="success-icon">✓</div>
<h3 style={{ marginBottom: '4px' }}>Broadcasted!</h3>
<p className="text-secondary" style={{ fontSize: '14px', marginBottom: '20px' }}>
Transaction sent to the network
</p>
<div className="send-summary" style={{ textAlign: 'left' }}>
<div className="send-summary-row">
<span className="label">TXID</span>
<span className="value" style={{ cursor: 'pointer' }} onClick={() => copyToClipboard(broadcastTxid, 'TXID')}>
{shortenTxid(broadcastTxid)}
</span>
</div>
</div>
<div style={{ marginTop: '20px', display: 'flex', gap: '8px', justifyContent: 'center' }}>
<button className="btn btn-secondary" onClick={() => copyToClipboard(broadcastTxid, 'TXID')}>
Copy TXID
</button>
<button className="btn btn-primary" onClick={() => onDone(broadcastTxid)}>
Done
</button>
</div>
</div>
</div>
</div>
);
}
// ---- Error ----
if (phase === 'error') {
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
<div className="modal" style={{ maxWidth: '440px' }}>
<div className="modal-body" style={{ textAlign: 'center', padding: '40px 24px' }}>
<div className="error-icon">✕</div>
<h3 style={{ marginBottom: '4px' }}>Error</h3>
<p className="mono text-red" style={{ fontSize: '13px', marginBottom: '20px', wordBreak: 'break-word' }}>{error}</p>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
<button className="btn btn-secondary" onClick={() => setPhase('form')}>Try Again</button>
<button className="btn btn-ghost" onClick={onClose}>Close</button>
</div>
</div>
</div>
</div>
);
}
return null;
}