Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ edition = "2021"
[dependencies]
bitcoincore-rpc = "0.19"
bitcoin = "0.32"
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "env"] }
anyhow = "1"
serde_json = "1.0.149"
14 changes: 8 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ use clap::{Parser, Subcommand, ValueEnum};
)]
pub struct Cli {
/// Bitcoin Core RPC URL
#[arg(long, default_value = "http://127.0.0.1:18443")]
#[arg(long, default_value = "http://127.0.0.1:18443", env = "DUST_RPC_URL")]
pub rpc_url: String,

#[arg(long)]
/// Bitcoin Core RPC username
#[arg(long, env = "DUST_RPC_USER")]
pub rpc_user: String,

#[arg(long)]
/// Bitcoin Core RPC password
#[arg(long, env = "DUST_RPC_PASS")]
pub rpc_pass: String,

/// Custom dust threshold in sats. If omitted, uses per-script-type thresholds
#[arg(long)]
/// Dust threshold in sats. If omitted, uses per-script-type thresholds
#[arg(long, env = "DUST_THRESHOLD")]
pub threshold: Option<u64>,

#[command(subcommand)]
Expand All @@ -42,7 +44,7 @@ pub enum Commands {
#[arg(long, default_value = "false")]
dry_run: bool,

/// Sweep method: op-return (burn to fees)
/// Sweep method: consolidate (default) or op-return (burn to fees)
#[arg(long, value_enum, default_value = "consolidate")]
method: SweepMethod,
},
Expand Down
34 changes: 25 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
mod analyzer;
mod cli;
mod psbt_builder;
mod rpc;
mod scanner;
mod analyzer;
mod psbt_builder;
mod types;

use bitcoincore_rpc::Client;
use clap::Parser;
use cli::{Cli, Commands, SweepMethod};
use bitcoincore_rpc::Client;

fn handle_scan(client: &Client, user_threshold: Option<u64>) -> anyhow::Result<()> {
let utxos = scanner::fetch_utxos(client)?;
Expand All @@ -17,7 +17,10 @@ fn handle_scan(client: &Client, user_threshold: Option<u64>) -> anyhow::Result<(
Some(t) => format!("{} sats (custom)", t),
None => "per-script-type (P2PKH:546, P2WPKH:294, P2TR:294, P2SH:540)".to_string(),
};
println!("Found {} total UTXOs (threshold: {})\n", total_utxos, threshold_display);
println!(
"Found {} total UTXOs (threshold: {})\n",
total_utxos, threshold_display
);

let (dust_utxos, clean_utxos) = analyzer::classify_utxos_smart(utxos, user_threshold);

Expand All @@ -26,7 +29,9 @@ fn handle_scan(client: &Client, user_threshold: Option<u64>) -> anyhow::Result<(
println!(" none");
}
for utxo in &dust_utxos {
let addr = utxo.address.as_ref()
let addr = utxo
.address
.as_ref()
.map(|a| a.clone().assume_checked().to_string())
.unwrap_or_else(|| "unknown".to_string());
let script_type = analyzer::detect_script_type(&addr);
Expand Down Expand Up @@ -59,8 +64,16 @@ fn handle_scan(client: &Client, user_threshold: Option<u64>) -> anyhow::Result<(
println!("\n─────────────────────────────────────────");
println!("📊 Summary");
println!(" Total UTXOs: {}", total_utxos);
println!(" Dust UTXOs: {} ({} sats)", dust_utxos.len(), total_dust_sats);
println!(" Clean UTXOs: {} ({} sats)", clean_utxos.len(), total_clean_sats);
println!(
" Dust UTXOs: {} ({} sats)",
dust_utxos.len(),
total_dust_sats
);
println!(
" Clean UTXOs: {} ({} sats)",
clean_utxos.len(),
total_clean_sats
);
println!(" Threshold: {}", threshold_display);

if !dust_utxos.is_empty() {
Expand Down Expand Up @@ -105,7 +118,10 @@ fn handle_sweep(
println!(" Total dust: {} sats", result.total_dust_sats);
println!(" Funder UTXO: {} sats", result.funder_sats);
println!(" Estimated fee: {} sats", result.estimated_fee_sats);
println!(" Estimated output: {} sats", result.estimated_output_sats);
println!(
" Estimated output: {} sats",
result.estimated_output_sats
);
println!("\n Run without --dry-run to create the PSBT.");
return Ok(());
}
Expand Down Expand Up @@ -148,4 +164,4 @@ fn main() -> anyhow::Result<()> {
}

Ok(())
}
}
17 changes: 9 additions & 8 deletions src/psbt_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ fn build_inputs(
funder: &ListUnspentResultEntry,
dust_utxos: &[ListUnspentResultEntry],
) -> Vec<serde_json::Value> {
let mut inputs = vec![
serde_json::json!({
"txid": funder.txid.to_string(),
"vout": funder.vout,
})
];
let mut inputs = vec![serde_json::json!({
"txid": funder.txid.to_string(),
"vout": funder.vout,
})];
for utxo in dust_utxos {
inputs.push(serde_json::json!({
"txid": utxo.txid.to_string(),
Expand Down Expand Up @@ -132,7 +130,10 @@ pub fn build_op_return_psbt(
}

let funder = select_funder(clean_utxos)?;
println!("\n ℹ️ Using clean UTXO to fund fees: {} sats", funder.amount.to_sat());
println!(
"\n ℹ️ Using clean UTXO to fund fees: {} sats",
funder.amount.to_sat()
);

let all_inputs = build_inputs(funder, dust_utxos);

Expand Down Expand Up @@ -173,4 +174,4 @@ pub fn build_op_return_psbt(
dust_input_count: dust_utxos.len(),
total_dust_sats,
})
}
}
Loading