import { PrivateKey, type WalletInterface } from '@bsv/sdk' import { useCallback, useEffect, useMemo, useState } from 'react' import { Toaster, toast } from 'sonner' import { legacyBurnOrdinals, legacySendBsv, legacySendOrdinals, } from '../lib/legacy-send' import { type EnrichedOrdinal, type ScannedAssets, deriveAddress, scanAddresses, } from '../lib/scanner' import { executeSweep, sweepBsv21Token } from '../lib/sweeper' import { getWallet } from '../lib/wallet' import type { LegacyKeys } from '../types' import { Bsv20Section, Bsv21Section, FundingSection, LockedSection, OrdinalsSection, RunSection, } from './asset-preview' import { ConnectWallet } from './connect-wallet' import { OpnsSection } from './opns-section' import { TxHistory, type TxRecord } from './tx-history' import { Badge } from './ui/badge' import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs' import { WifInput } from './wif-input' type TabId = 'ordinals' | 'opns' | 'bsv21' | 'bsv20' | 'locks' | 'run' export interface SweepAppProps { legacyKeys?: LegacyKeys wallet?: WalletInterface | null sweepOnly?: boolean } export function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly, }: SweepAppProps) { const [walletConnected, setWalletConnected] = useState(!!externalWallet) const [scanning, setScanning] = useState(false) const [scanProgress, setScanProgress] = useState('') const [assets, setAssets] = useState(null) const [legacyKeys, setLegacyKeys] = useState(null) const [sweeping, setSweeping] = useState(false) const [sweepProgress, setSweepProgress] = useState('') const [txHistory, setTxHistory] = useState([]) const [selectedOrdinals, setSelectedOrdinals] = useState>( new Set(), ) const [selectedOpns, setSelectedOpns] = useState>(new Set()) const [sweepAmount, setSweepAmount] = useState(null) const [activeTab, setActiveTab] = useState('ordinals') useEffect(() => { setWalletConnected(!!externalWallet) }, [externalWallet]) const resolveWallet = useCallback((): WalletInterface | null => { return externalWallet ?? getWallet() }, [externalWallet]) const keyMap = useMemo(() => { if (!legacyKeys) return new Map() const map = new Map() map.set( deriveAddress(legacyKeys.payPk), PrivateKey.fromWif(legacyKeys.payPk), ) map.set( deriveAddress(legacyKeys.ordPk), PrivateKey.fromWif(legacyKeys.ordPk), ) if (legacyKeys.identityPk) { map.set( deriveAddress(legacyKeys.identityPk), PrivateKey.fromWif(legacyKeys.identityPk), ) } return map }, [legacyKeys]) const addTx = useCallback((label: string, txid: string, error?: string) => { setTxHistory((prev) => [ ...prev, { label, txid, timestamp: new Date(), error }, ]) }, []) const tabs = useMemo(() => { if (!assets) return [] const t: { id: TabId; label: string; count: number }[] = [] if (assets.ordinals.length > 0) t.push({ id: 'ordinals', label: 'Ordinals', count: assets.ordinals.length, }) if (assets.opnsNames.length > 0) t.push({ id: 'opns', label: 'OpNS', count: assets.opnsNames.length }) if (assets.bsv21Tokens.length > 0) t.push({ id: 'bsv21', label: 'BSV-21', count: assets.bsv21Tokens.length }) if (assets.bsv20Tokens.length > 0) t.push({ id: 'bsv20', label: 'BSV-20', count: assets.bsv20Tokens.length }) if (assets.locked.length > 0) t.push({ id: 'locks', label: 'Locks', count: assets.locked.length }) if (assets.run.length > 0) t.push({ id: 'run', label: 'RUN', count: assets.run.length }) return t }, [assets]) const handleToggleOrdinal = useCallback((outpoint: string) => { setSelectedOrdinals((prev) => { const next = new Set(prev) if (next.has(outpoint)) next.delete(outpoint) else next.add(outpoint) return next }) }, []) const handleSelectOrdinalPage = useCallback((outpoints: string[]) => { setSelectedOrdinals(new Set(outpoints)) }, []) const handleDeselectAllOrdinals = useCallback( () => setSelectedOrdinals(new Set()), [], ) const handleToggleOpns = useCallback((outpoint: string) => { setSelectedOpns((prev) => { const next = new Set(prev) if (next.has(outpoint)) next.delete(outpoint) else next.add(outpoint) return next }) }, []) const handleSelectOpnsPage = useCallback((outpoints: string[]) => { setSelectedOpns(new Set(outpoints)) }, []) const handleDeselectAllOpns = useCallback( () => setSelectedOpns(new Set()), [], ) const refreshAssets = useCallback(async () => { if (!legacyKeys) return const addresses = [ ...new Set([ deriveAddress(legacyKeys.payPk), deriveAddress(legacyKeys.ordPk), ...(legacyKeys.identityPk ? [deriveAddress(legacyKeys.identityPk)] : []), ]), ] const result = await scanAddresses(addresses) setAssets(result) setSelectedOrdinals(new Set()) setSelectedOpns(new Set()) setSweepAmount(null) }, [legacyKeys]) const handleScan = useCallback(async (keys: LegacyKeys) => { setScanning(true) setAssets(null) setSelectedOrdinals(new Set()) setSelectedOpns(new Set()) setSweepAmount(null) setLegacyKeys(keys) try { const addresses = [ ...new Set([ deriveAddress(keys.payPk), deriveAddress(keys.ordPk), ...(keys.identityPk ? [deriveAddress(keys.identityPk)] : []), ]), ] const result = await scanAddresses(addresses, (p) => setScanProgress(p.detail ?? p.phase), ) setAssets(result) const total = result.funding.length + result.ordinals.length + result.opnsNames.length + result.bsv21Tokens.reduce((n, t) => n + t.outputs.length, 0) + result.bsv20Tokens.length + result.locked.length + result.run.length if (total === 0) toast.info('No assets found at legacy addresses') if (result.ordinals.length > 0) setActiveTab('ordinals') else if (result.opnsNames.length > 0) setActiveTab('opns') else if (result.bsv21Tokens.length > 0) setActiveTab('bsv21') else if (result.bsv20Tokens.length > 0) setActiveTab('bsv20') else if (result.locked.length > 0) setActiveTab('locks') } catch (e) { console.error('Scan failed:', e) toast.error(e instanceof Error ? e.message : 'Scan failed') } finally { setScanning(false) } }, []) useEffect(() => { if (initialKeys) handleScan(initialKeys) }, [initialKeys, handleScan]) const runOperation = useCallback( async (label: string, op: () => Promise) => { setSweeping(true) setSweepProgress(label + '...') try { const txid = await op() addTx(label, txid) toast.success(label) await refreshAssets() } catch (e) { const msg = e instanceof Error ? e.message : 'Operation failed' addTx(label, '', msg) toast.error(msg) } finally { setSweeping(false) } }, [addTx, refreshAssets], ) const getSelectedFunding = useCallback(() => { if (!assets) return [] if (sweepAmount === null) return assets.funding const selected: typeof assets.funding = [] let accumulated = 0 for (const utxo of assets.funding) { selected.push(utxo) accumulated += utxo.satoshis ?? 0 if (accumulated >= sweepAmount) break } return selected }, [assets, sweepAmount]) const handleSweepBsv = useCallback(async () => { const wallet = resolveWallet() if (!wallet || !legacyKeys || !assets) return await runOperation('Sweep BSV', async () => { const result = await executeSweep({ wallet, keys: keyMap, funding: getSelectedFunding(), ordinals: [], amount: sweepAmount ?? undefined, onProgress: setSweepProgress, }) if (result.errors.length > 0) throw new Error(result.errors[0]) return result.bsvTxid ?? '' }) }, [ resolveWallet, legacyKeys, assets, sweepAmount, getSelectedFunding, runOperation, ]) const handleSendBsv = useCallback( async (destination: string) => { if (!legacyKeys || !assets) return await runOperation('Send BSV', async () => { const result = await legacySendBsv({ funding: getSelectedFunding(), keys: legacyKeys, destination, amount: sweepAmount ?? undefined, }) return result.txid }) }, [legacyKeys, assets, sweepAmount, getSelectedFunding, runOperation], ) const sweepOrdinalList = useCallback( async (list: EnrichedOrdinal[], label: string) => { const wallet = resolveWallet() if (!wallet || !legacyKeys || list.length === 0) return await runOperation(label, async () => { const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: list, onProgress: setSweepProgress, }) if (result.sweptOutpoints.length > 0) { setSelectedOrdinals((prev) => { const next = new Set(prev) for (const op of result.sweptOutpoints) next.delete(op) return next }) } if (result.errors.length > 0) { for (const txid of result.ordinalTxids) addTx(label, txid) if (result.sweptOutpoints.length > 0) await refreshAssets() throw new Error(result.errors[0]) } for (let i = 0; i < result.ordinalTxids.length - 1; i++) { addTx(label, result.ordinalTxids[i]) } return result.ordinalTxids[result.ordinalTxids.length - 1] ?? '' }) }, [resolveWallet, legacyKeys, keyMap, runOperation, addTx, refreshAssets], ) const handleSweepOrdinals = useCallback(async () => { if (!assets) return const selected = assets.ordinals.filter((o) => selectedOrdinals.has(o.outpoint), ) if (selected.length === 0) return await sweepOrdinalList( selected, `Sweep ${selected.length} ordinal${selected.length !== 1 ? 's' : ''}`, ) }, [assets, selectedOrdinals, sweepOrdinalList]) const handleSweepAllOrdinals = useCallback(async () => { if (!assets || assets.ordinals.length === 0) return await sweepOrdinalList( assets.ordinals, `Sweep all ${assets.ordinals.length} ordinal${assets.ordinals.length !== 1 ? 's' : ''}`, ) }, [assets, sweepOrdinalList]) const handleSendOrdinals = useCallback( async (destination: string) => { if (!legacyKeys || !assets) return const selected = assets.ordinals.filter((o) => selectedOrdinals.has(o.outpoint), ) if (selected.length === 0) return await runOperation( `Send ${selected.length} ordinal${selected.length !== 1 ? 's' : ''}`, async () => { const result = await legacySendOrdinals({ ordinals: selected, funding: assets.funding, keys: legacyKeys, destination, }) return result.txid }, ) }, [legacyKeys, assets, selectedOrdinals, runOperation], ) const handleBurnOrdinals = useCallback(async () => { if (!legacyKeys || !assets) return const selected = assets.ordinals.filter((o) => selectedOrdinals.has(o.outpoint), ) if (selected.length === 0) return await runOperation( `Burn ${selected.length} ordinal${selected.length !== 1 ? 's' : ''}`, async () => { const result = await legacyBurnOrdinals({ ordinals: selected, funding: assets.funding, keys: legacyKeys, }) return result.txid }, ) }, [legacyKeys, assets, selectedOrdinals, runOperation]) const sweepOpnsList = useCallback( async (list: EnrichedOrdinal[], label: string) => { const wallet = resolveWallet() if (!wallet || !legacyKeys || list.length === 0) return await runOperation(label, async () => { const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: list, onProgress: setSweepProgress, }) if (result.sweptOutpoints.length > 0) { setSelectedOpns((prev) => { const next = new Set(prev) for (const op of result.sweptOutpoints) next.delete(op) return next }) } if (result.errors.length > 0) { for (const txid of result.ordinalTxids) addTx(label, txid) if (result.sweptOutpoints.length > 0) await refreshAssets() throw new Error(result.errors[0]) } for (let i = 0; i < result.ordinalTxids.length - 1; i++) { addTx(label, result.ordinalTxids[i]) } return result.ordinalTxids[result.ordinalTxids.length - 1] ?? '' }) }, [resolveWallet, legacyKeys, keyMap, runOperation, addTx, refreshAssets], ) const handleSweepOpns = useCallback(async () => { if (!assets) return const selected = assets.opnsNames.filter((o) => selectedOpns.has(o.outpoint), ) if (selected.length === 0) return await sweepOpnsList( selected, `Sweep ${selected.length} domain${selected.length !== 1 ? 's' : ''}`, ) }, [assets, selectedOpns, sweepOpnsList]) const handleSweepAllOpns = useCallback(async () => { if (!assets || assets.opnsNames.length === 0) return await sweepOpnsList( assets.opnsNames, `Sweep all ${assets.opnsNames.length} domain${assets.opnsNames.length !== 1 ? 's' : ''}`, ) }, [assets, sweepOpnsList]) const handleSendOpns = useCallback( async (destination: string) => { if (!legacyKeys || !assets) return const selected = assets.opnsNames.filter((o) => selectedOpns.has(o.outpoint), ) if (selected.length === 0) return await runOperation( `Send ${selected.length} domain${selected.length !== 1 ? 's' : ''}`, async () => { const result = await legacySendOrdinals({ ordinals: selected, funding: assets.funding, keys: legacyKeys, destination, }) return result.txid }, ) }, [legacyKeys, assets, selectedOpns, runOperation], ) const handleBurnOpns = useCallback(async () => { if (!legacyKeys || !assets) return const selected = assets.opnsNames.filter((o) => selectedOpns.has(o.outpoint), ) if (selected.length === 0) return await runOperation( `Burn ${selected.length} domain${selected.length !== 1 ? 's' : ''}`, async () => { const result = await legacyBurnOrdinals({ ordinals: selected, funding: assets.funding, keys: legacyKeys, }) return result.txid }, ) }, [legacyKeys, assets, selectedOpns, runOperation]) const handleSweepBsv21Token = useCallback( async (tokenId: string) => { const wallet = resolveWallet() if (!wallet || !assets) return const token = assets.bsv21Tokens.find((t) => t.tokenId === tokenId) if (!token) return await runOperation( `Sweep ${token.symbol ?? tokenId.slice(0, 8)}`, async () => { const result = await sweepBsv21Token({ wallet, keys: keyMap, token, onProgress: setSweepProgress, }) if (result.error) throw new Error(result.error) return result.txid ?? '' }, ) }, [resolveWallet, assets, keyMap, runOperation], ) return (

1Sat Sweep

Transfer or sweep legacy assets

{!externalWallet && ( setWalletConnected(true)} onDisconnected={() => setWalletConnected(false)} connected={walletConnected} /> )} {!initialKeys && ( )} {scanning && (

{scanProgress}

)} {assets && !sweeping && (
{tabs.length > 0 && ( setActiveTab(v as TabId)} > {tabs.map((tab) => ( {tab.label} {tab.count} ))} )}
)}
) }