import { useState } from 'react'; import { View, Text, TextInput, Pressable, ScrollView, ActivityIndicator } from 'react-native'; import { seedFromPhrase, validateSeedPhrase, rootFromSeed, accountNode, accountXpub as toXpub, addressNode, p2wpkhAddress, digibyte, } from '@dgb-wallet/core'; import { buildPsbt, finalizeAndExtract, type Utxo } from '@dgb-wallet/psbt'; import { payments } from 'bitcoinjs-lib'; import { ECPairFactory } from 'ecpair'; import * as ecc from '@bitcoinerlab/secp256k1'; import { p2pBalance, p2pBroadcast, type P2PBroadcastResponse } from './rpc-native'; import { styles } from './styles'; const ECPair = ECPairFactory(ecc); const FIXED_FEE_SATS = 5000; const GAP = 20; type Status = | { kind: 'idle' } | { kind: 'busy'; msg: string } | { kind: 'signed'; hex: string; txid: string; fee: number } | { kind: 'broadcast'; res: P2PBroadcastResponse } | { kind: 'error'; msg: string }; export function SendScreen(): JSX.Element { const [phrase, setPhrase] = useState(''); const [dest, setDest] = useState(''); const [amtDgb, setAmtDgb] = useState(''); const [useDandelion, setUseDandelion] = useState(true); const [status, setStatus] = useState({ kind: 'idle' }); const okSeed = validateSeedPhrase(phrase.trim()); const okDest = dest.trim().length > 20; const okAmt = Number(amtDgb) > 0; const ready = okSeed && okDest && okAmt; async function onSign() { if (!ready) return; setStatus({ kind: 'busy', msg: 'Deriving keys and scanning P2P for UTXOs…' }); try { const seed = await seedFromPhrase(phrase.trim()); const root = rootFromSeed(seed); const acct = accountNode(root, 84, 0); const xpub = toXpub(acct); // Pre-derive every candidate script → private key so we can look // up each returned UTXO by its scriptPubKey hex in O(1). const spkToKey = new Map(); const spkToAddr = new Map(); for (const ch of [0, 1] as const) { for (let idx = 0; idx < GAP; idx++) { const node = addressNode(acct, ch, idx); const pubkey = Buffer.from(node.publicKey); const spk = payments.p2wpkh({ pubkey, network: digibyte }).output; if (!spk || !node.privateKey) continue; const spkHex = Buffer.from(spk).toString('hex'); spkToKey.set(spkHex, Buffer.from(node.privateKey)); spkToAddr.set(spkHex, p2wpkhAddress(node)); } } const scan = await p2pBalance({ xpub, purpose: 84, account: 0, gapLimit: GAP }); if (scan.utxos.length === 0) { throw new Error('No spendable UTXOs on any BIP84 address for this seed.'); } const amtSats = Math.round(Number(amtDgb) * 1e8); const utxosSorted = [...scan.utxos].sort((a, b) => b.value - a.value); const inputs: Utxo[] = []; const inputKeys: Buffer[] = []; let inputSats = 0; for (const u of utxosSorted) { const key = spkToKey.get(u.scriptPubKey); const addr = spkToAddr.get(u.scriptPubKey); if (!key || !addr) continue; inputs.push({ txid: u.txid, vout: u.vout, value: u.value, address: addr, scriptPubKey: u.scriptPubKey, witness: { scriptHex: u.scriptPubKey, value: u.value }, }); inputKeys.push(key); inputSats += u.value; if (inputSats >= amtSats + FIXED_FEE_SATS) break; } if (inputSats < amtSats + FIXED_FEE_SATS) { throw new Error(`Insufficient funds: have ${(inputSats / 1e8).toFixed(8)}, need ${((amtSats + FIXED_FEE_SATS) / 1e8).toFixed(8)}`); } setStatus({ kind: 'busy', msg: 'Building & signing PSBT…' }); const changeAmt = inputSats - amtSats - FIXED_FEE_SATS; const changeAddr = p2wpkhAddress(addressNode(acct, 1, 0)); const psbt = buildPsbt({ inputs, outputs: [ { address: dest.trim(), value: amtSats }, ...(changeAmt > 546 ? [{ address: changeAddr, value: changeAmt }] : []), ], }); for (const [i, key] of inputKeys.entries()) { const kp = ECPair.fromPrivateKey(key, { network: digibyte }); psbt.signInput(i, kp); } const { hex, txid } = finalizeAndExtract(psbt); setStatus({ kind: 'signed', hex, txid, fee: FIXED_FEE_SATS }); } catch (e) { setStatus({ kind: 'error', msg: (e as Error).message }); } } async function onBroadcast() { if (status.kind !== 'signed') return; setStatus({ kind: 'busy', msg: `Broadcasting${useDandelion ? ' (Dandelion++)' : ''}…` }); try { const res = await p2pBroadcast(status.hex, useDandelion); setStatus({ kind: 'broadcast', res }); } catch (e) { setStatus({ kind: 'error', msg: (e as Error).message }); } } return ( ⚠️ Demo scope: BIP84 only, single account, flat 5000-sat fee. Never paste a real-funds seed on an untrusted device. BIP39 seed Recipient (dgb1q… address) Amount (DGB) Fee is a flat {(FIXED_FEE_SATS / 1e8).toFixed(8)} DGB for the demo. {status.kind === 'busy' ? : Build & sign transaction} {status.kind === 'error' && {status.msg}} {status.kind === 'signed' && ( Signed transaction txid: {status.txid} fee: {(status.fee / 1e8).toFixed(8)} DGB Broadcast path setUseDandelion(true)}> Dandelion++ (private) setUseDandelion(false)}> Direct fan-out {useDandelion ? 'Relays via a random Dandelion-capable peer before fluff. Falls back to direct if none available.' : 'Announces to every ready peer. Faster but reveals your IP as the origin.'} Broadcast now )} {status.kind === 'broadcast' && ( Broadcast successful via: {status.res.via ?? 'p2p'} {status.res.stemPeer && stem: {status.res.stemPeer}} txid: {status.res.txid} {status.res.via !== 'dandelion' && ( {status.res.requestedBy}/{status.res.totalPeers} peers requested the tx. )} )} ); }