1use bitcoin::{
2 Transaction, TxIn, TxOut, OutPoint, ScriptBuf, Witness, Sequence, transaction::Version, Address
3};
4use bitcoin::psbt::Psbt;
5use bitcoin::Amount;
6use bitcoincore_rpc::{Client, RpcApi};
7use base64::{engine::general_purpose, Engine as _};
8use std::collections::HashMap;
9use std::str::FromStr;
10
11pub struct DustSweeper {
32 rpc_client: Client,
33 threshold: u64,
34}
35
36impl DustSweeper {
37 pub fn new(rpc_client: Client, threshold: u64) -> Self {
43 Self { rpc_client, threshold }
44 }
45
46 pub fn get_dust_utxos(&self) -> Result<Vec<bitcoincore_rpc::json::ListUnspentResultEntry>, bitcoincore_rpc::Error> {
51 let utxos = self.rpc_client.list_unspent(None, None, None, None, None)?;
52 Ok(utxos.into_iter()
53 .filter(|u| u.amount.to_sat() < self.threshold)
54 .collect())
55 }
56
57 fn group_utxos_by_address(&self, utxos: Vec<bitcoincore_rpc::json::ListUnspentResultEntry>) -> HashMap<String, Vec<bitcoincore_rpc::json::ListUnspentResultEntry>> {
65 let mut grouped: HashMap<String, Vec<bitcoincore_rpc::json::ListUnspentResultEntry>> = HashMap::new();
66
67 for utxo in utxos {
68 if let Some(address) = &utxo.address {
69 grouped.entry(address.clone().assume_checked().to_string()).or_default().push(utxo);
70 }
71 }
72
73 grouped
74 }
75
76 pub fn build_psbts_burn(&self, dust_utxos: Vec<bitcoincore_rpc::json::ListUnspentResultEntry>, fee: u64, burn_addr: &str) -> Result<Vec<Psbt>, bitcoin::psbt::Error> {
86 let utxos_by_address = self.group_utxos_by_address(dust_utxos);
87 let mut psbts = Vec::new();
88
89 for (_address, utxos) in utxos_by_address {
90 let psbt = self.build_psbt(utxos, fee, burn_addr)?;
91 psbts.push(psbt);
92 }
93
94 Ok(psbts)
95 }
96
97 fn build_psbt(&self, dust_utxos: Vec<bitcoincore_rpc::json::ListUnspentResultEntry>, fee: u64, burn_addr: &str) -> Result<Psbt, bitcoin::psbt::Error> {
107 let inputs: Vec<TxIn> = dust_utxos.iter().map(|u| {
108 TxIn {
109 previous_output: OutPoint { txid: u.txid, vout: u.vout },
110 script_sig: ScriptBuf::new(),
111 sequence: Sequence(0xFFFFFFFD), witness: Witness::new(),
113 }
114 }).collect();
115
116 let total_input_amount: u64 = dust_utxos.iter().map(|u| u.amount.to_sat()).sum();
117 let output_value = total_input_amount.saturating_sub(fee);
118
119 let burn_address = Address::from_str(burn_addr)
120 .expect("❌ Invalid burn address")
121 .assume_checked();
122
123 let txout = TxOut {
124 value: Amount::from_sat(output_value),
125 script_pubkey: burn_address.script_pubkey(),
126 };
127
128 let outputs: Vec<TxOut> = vec![txout];
129
130 let unsigned_tx = Transaction {
131 version: Version(2),
132 lock_time: bitcoin::absolute::LockTime::ZERO,
133 input: inputs,
134 output: outputs,
135 };
136
137 Psbt::from_unsigned_tx(unsigned_tx)
138 }
139
140 pub fn psbt_to_base64(psbt: &Psbt) -> String {
148 general_purpose::STANDARD.encode(psbt.serialize())
149 }
150}