diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..72708f4 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Clippy check + run: cargo clippy -- -D warnings + + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Format check + run: cargo fmt --check \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0155ac9..4fc5f04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index fc0ade8..2c5e489 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,12 @@ A Bitcoin CLI tool that detects dust attack UTXOs in your wallet and sweeps them safely using PSBTs ([BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki)). -Built in Rust! ---- + +## Demo + +Example of scanning for dust UTXOs and performing a dry-run sweep: + +[![asciicast](https://asciinema.org/a/911545.svg)](https://asciinema.org/a/911545) ## What is a Dust Attack? @@ -146,6 +150,17 @@ cHNidP8BA... Send: bitcoin-cli sendrawtransaction ``` +### Sweep using OP_RETURN (burn to miner fees) +```bash +dust-cleaner --rpc-user --rpc-pass sweep --method op-return +``` + +Example output: +``` +๐Ÿ”ฅ Method: op-return โ€” dust burned to miner fees + Output: OP_RETURN ("ash" โ€” ashes to ashes, dust to dust) +``` + ### Custom threshold ```bash diff --git a/docs/demo.cast b/docs/demo.cast new file mode 100644 index 0000000..d90d424 --- /dev/null +++ b/docs/demo.cast @@ -0,0 +1,141 @@ +{"version": 2, "width": 82, "height": 44, "timestamp": 1775798717, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} +[0.820534, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[2.795947, "o", "c"] +[2.980767, "o", "a"] +[3.139852, "o", "r"] +[3.392408, "o", "g"] +[3.835027, "o", "o"] +[4.186488, "o", " "] +[5.40972, "o", "r"] +[5.701888, "o", "u"] +[6.069876, "o", "n"] +[6.946351, "o", " "] +[7.654731, "o", "-"] +[7.837404, "o", "-"] +[8.257093, "o", " "] +[9.03623, "o", "-"] +[9.230662, "o", "-"] +[9.907083, "o", "h"] +[10.337625, "o", "e"] +[10.51073, "o", "l"] +[10.830549, "o", "p"] +[12.792466, "o", "\r\n\u001b[?2004l\r"] +[12.967165, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.10s\r\n"] +[12.973381, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --help`\r\n"] +[12.981323, "o", "Detect and sweep dust attack UTXOs from your Bitcoin wallet\r\n\r\n\u001b[1m\u001b[4mUsage:\u001b[0m \u001b[1mdust-cleaner\u001b[0m [OPTIONS] \u001b[1m--rpc-user\u001b[0m \u001b[1m--rpc-pass\u001b[0m \r\n\r\n\u001b[1m\u001b[4mCommands:\u001b[0m\r\n \u001b[1mscan\u001b[0m Scan wallet for dust UTXOs\r\n \u001b[1msweep\u001b[0m Create a PSBT sweeping all dust UTXOs\r\n \u001b[1mhelp\u001b[0m Print this message or the help of the given subcommand(s)\r\n\r\n\u001b[1m\u001b[4mOptions:\u001b[0m\r\n \u001b[1m--rpc-url\u001b[0m Bitcoin Core RPC URL [default: http://127.0.0.1:18443]\r\n \u001b[1m--rpc-user\u001b[0m \r\n \u001b[1m--rpc-pass\u001b[0m \r\n \u001b[1m--threshold\u001b[0m Custom dust threshold in sats. If omitted, uses per-script-type thresholds\r\n \u001b[1m-h\u001b[0m, \u001b[1m--help\u001b[0m Print help\r\n"] +[12.982709, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[15.274456, "o", "c"] +[15.510141, "o", "a"] +[15.687383, "o", "r"] +[15.980204, "o", "g"] +[16.508834, "o", "o"] +[17.879149, "o", " "] +[18.482939, "o", "r"] +[18.748675, "o", "u"] +[19.060231, "o", "n"] +[19.412957, "o", " "] +[22.445408, "o", "-"] +[22.642001, "o", "-"] +[22.924427, "o", " "] +[23.237142, "o", "-"] +[23.458555, "o", "-"] +[25.72683, "o", "p"] +[26.257663, "o", "\b\u001b[K"] +[26.766145, "o", "r"] +[27.368655, "o", "p"] +[27.735021, "o", "c"] +[30.695653, "o", "-"] +[31.182338, "o", "p"] +[33.338241, "o", "\b\u001b[K"] +[33.578218, "o", "\b\u001b[K"] +[34.803893, "o", "-"] +[35.324833, "o", "u"] +[35.888364, "o", "s"] +[36.083225, "o", "e"] +[36.398795, "o", "r"] +[37.953145, "o", " "] +[38.34731, "o", "u"] +[38.676875, "o", "s"] +[38.937002, "o", "e"] +[39.227316, "o", "r"] +[39.696359, "o", " "] +[40.23407, "o", "-"] +[40.448878, "o", "-"] +[41.333096, "o", "r"] +[41.636531, "o", "p"] +[42.040403, "o", "c"] +[43.378474, "o", "-"] +[43.876399, "o", "p"] +[44.168749, "o", "a"] +[44.48426, "o", "s"] +[44.710614, "o", "s \r"] +[44.947755, "o", " "] +[45.496174, "o", "p"] +[45.79453, "o", "a"] +[46.116484, "o", "s"] +[46.332291, "o", "s"] +[46.501898, "o", " "] +[47.26367, "o", "-"] +[47.459406, "o", "-"] +[48.382401, "o", "t"] +[48.699388, "o", "h"] +[49.072755, "o", "r"] +[49.441081, "o", "e"] +[50.802838, "o", "s"] +[50.933947, "o", "h"] +[51.624798, "o", "o"] +[51.896608, "o", "l"] +[52.192583, "o", "d"] +[52.876343, "o", " "] +[53.534242, "o", "1"] +[53.955866, "o", "0"] +[54.145991, "o", "0"] +[54.384159, "o", "0"] +[55.356373, "o", " "] +[55.933721, "o", "s"] +[56.281976, "o", "c"] +[56.899056, "o", "a"] +[57.043523, "o", "n"] +[61.326621, "o", "\r\n\u001b[?2004l\r"] +[61.51143, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.09s\r\n"] +[61.519195, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 scan`\r\n"] +[61.575349, "o", "Found 9 total UTXOs (threshold: 1000 sats (custom))\r\n\r\n"] +[61.576409, "o", "โš ๏ธ DUST UTXOs (3 found):\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0 | P2WPKH | bcrt1q4x2qz6a7xrau48hflygphv75vmwx60v6xg9dqz\r\n"] +[61.576935, "o", " 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0 | P2WPKH | bcrt1q62sx9egww58us7up2f5w3lf2vspeyrjqwupr2q\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0 | P2WPKH | bcrt1qyk3sa24pnfrex8gneldjdwe2n8ed0yhj83uy8s\r\n\r\nโœ… CLEAN UTXOs (6 found):\r\n 4999973630 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:1\r\n 5000000000 sats | f50406678677be7e407d55ad7eda96df662237555cc5da5b26cc740838eca7ec:0\r\n 4999999860 sats | 81452410ca2bb3de54916a51a0c1b00f1c723bcef470d812895a01f33abb6500:0\r\n 5000000000 sats | e16c27cbd32cbbeb7f1e3d03fd26c77ae4f2c42f8bb122b77dc9d61cdddc8bcd:0\r\n 4999994210 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:1\r\n 5000000000 sats | 8789c76955d1107de5731d618424372b5c6467254ae960a214938c16a6852740:0\r\n\r\nโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\r\n๐Ÿ“Š Summary\r\n Total UTXOs: 9"] +[61.577538, "o", "\r\n Dust UTXOs: 3 (1600 sats)\r\n Clean UTXOs: 6 (29999967700 sats)\r\n Threshold: 1000 sats (custom)\r\n\r\n ๐Ÿ’ก Run 'sweep' to consolidate dust into a single UTXO\r\nโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\r\n"] +[61.578418, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[70.912916, "o", "c"] +[71.007195, "o", "l"] +[71.611786, "o", "e"] +[73.418187, "o", "a"] +[73.593535, "o", "r"] +[75.662852, "o", "\r\n\u001b[?2004l\r"] +[75.667244, "o", "\u001b[H\u001b[2J\u001b[3J"] +[75.66789, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[87.016649, "o", "\u001b[7mcargo run -- --rpc-user user --rpc-pass\u001b[27m\u001b[7m \u001b[27m\u001b[7mpass --threshold 1000 sweep --dry-run\u001b[27m"] +[87.973369, "o", "\u0007\u001b[A\r\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ cargo run -- --rpc-user user --rpc-pass pass --threshold 1000 sweep --dry-run"] +[91.891958, "o", "\r\n\u001b[?2004l\r"] +[92.04665, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.07s\r\n"] +[92.053677, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 sweep --dry-run`\r\n"] +[92.070096, "o", "Found 3 dust UTXOs to sweep:\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0\r\n 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0\r\n\r\n๐Ÿ” Dry Run โ€” no PSBT created\r\n\r\n Method: Consolidate\r\n Dust inputs: 3\r\n Total dust: 1600 sats\r\n Funder UTXO: 5000000000 sats\r\n Estimated fee: 626 sats\r\n Estimated output: 5000000974 sats\r\n\r\n Run without --dry-run to create the PSBT.\r\n"] +[92.071323, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[116.248237, "o", "\u001b[7mcargo run -- --rpc-user user --rpc-pass\u001b[27m\u001b[7m \u001b[27m\u001b[7mpass --threshold 1000 sweep --method op-return --dry-run\u001b[27m"] +[118.796131, "o", "\u0007\u001b[A\b\b\b\b\b\b\b\b\b\b\b\b\b\bcargo run -- --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return --dry-run"] +[119.62632, "o", "\r\n"] +[119.626848, "o", "\u001b[?2004l\r"] +[119.78859, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.07s\r\n"] +[119.794165, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return --dry-run`\r\n"] +[119.815471, "o", "Found 3 dust UTXOs to sweep:\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0\r\n 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0\r\n\r\n๐Ÿ” Dry Run โ€” no PSBT created\r\n\r\n Method: OpReturn\r\n Dust inputs: 3\r\n Total dust: 1600 sats\r\n Funder UTXO: 5000000000 sats\r\n Estimated fee: 626 sats\r\n Estimated output: 5000000974 sats\r\n\r\n Run without --dry-run to create the PSBT.\r\n"] +[119.81687, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[138.60205, "o", "\u001b[7mcargo run -- --rpc-user user --rpc-pass\u001b[27m\u001b[7m \u001b[27m\u001b[7mpass --threshold 1000 sweep --method op-return\u001b[27m"] +[139.284238, "o", "\u0007\u001b[A\b\b\b\bcargo run -- --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return"] +[142.210555, "o", "\r\n"] +[142.21145, "o", "\u001b[?2004l\r"] +[142.36781, "o", "\u001b[1m\u001b[92m Finished\u001b[0m `dev` profile [unoptimized + debuginfo] target(s) in 0.07s\r\n"] +[142.373133, "o", "\u001b[1m\u001b[92m Running\u001b[0m `target/debug/dust-cleaner --rpc-user user --rpc-pass pass --threshold 1000 sweep --method op-return`\r\n"] +[142.406946, "o", "Found 3 dust UTXOs to sweep:\r\n 500 sats | 84e32362d15b14d069934ef2fe7a5d7864323d59f6936ef361bce812c2a8c10e:0\r\n 300 sats | 1b6cceaba9b1555ad385f3273e3a726a7f3e4a7d1091128d8628caa38d8514cd:0\r\n 800 sats | 1a51b81e315379f2b036f5bdaf4862edfe3b3527c7596c475918e7f8b97ba6a1:0\r\n\r\n๐Ÿ”ฅ Method: op-return โ€” dust burned to miner fees\r\n Output: OP_RETURN (\"ash\" โ€” ashes to ashes, dust to dust)\r\n\r\n โ„น๏ธ Using clean UTXO to fund fees: 5000000000 sats\r\n"] +[142.54227, "o", "\r\n๐Ÿ“Š Sweep Summary:\r\n Dust inputs: 3\r\n Total dust: 1600 sats\r\n\r\n๐Ÿงน Sweep PSBT (base64):\r\ncHNidP8BAPoCAAAABEAnhaYWjJMUomDpSiVnZFwrNySEYR1z5X0Q0VVpx4mHAAAAAAD9////DsGowhLovGHzbpP2WT0yZHhdev7yTpNp0BRb0WIj44QAAAAAAP3////NFIWNo8ooho0SkRB9Sj5/anI6PifzhdNaVbGpq85sGwAAAAAA/f///6Gme7n45xhZR2xZxyc1O/7tYkivvfU2sPJ5UzEeuFEaAAAAAAD9////AwAAAAAAAAAABWoDYXNoQAYAAAAAAAAWABSttCLfgG4/NURqfAHEpBsd1fcygQTkBSoBAAAAFgAUQJeNT5jyl6LaNiWtzN2MWndlLLYAAAAAAAEAgwIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AlUA/////wIA8gUqAQAAABYAFF6XxDXy7piDz1AhPspCGSm9yBVQAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQEfAPIFKgEAAAAWABRel8Q18u6Yg89QIT7KQhkpvcgVUCIGAoENJdbT6mMTuD2hDkCwv1V4S5QlOyEdjjW5Ppn9OjIqGJi7BkpUAACAAQAAgAAAAIAAAAAAAAAAAAABAHECAAAAAe4a44e+jKVcfWdeXARqGank2Mtorlv5e/5x4ZMeK5Q4AAAAAAD9////AvQBAAAAAAAAFgAUqZQBa74w+8qe6fkQG7PUZtxtPZr+igUqAQAAABYAFDfwRlg6Y/RLC+tMJGvwLMmNTq8AaQAAAAEBH/QBAAAAAAAAFgAUqZQBa74w+8qe6fkQG7PUZtxtPZoiBgMJaNSmn50loDjR98nh/gMrTvFXaGo7NPI2Xl19VMnY6xiYuwZKVAAAgAEAAIAAAACAAAAAABkAAAAAAQBSAgAAAAHsI1uFz5DnI/y7jeQT9iTIKt0jaOpEnApHws6hSNGO8AIAAAAA/f///wEsAQAAAAAAABYAFNKgYuUOdQ/Ie4FSaOj9KmQDkg5AaQAAAAEBHywBAAAAAAAAFgAU0qBi5Q51D8h7gVJo6P0qZAOSDkAiBgLzk3PR1gOWaC0X7bvc/nHr3+5wc5+zPvgPYa1F6FzImhiYuwZKVAAAgAEAAIAAAACAAAAAABgAAAAAAQBxAgAAAAHsI1uFz5DnI/y7jeQT9iTIKt0jaOpEnApHws6hSNGO8AEAAAAA/f///wIgAwAAAAAAABYAFCWjDqqhmkeTHRPP2ya7Kpny15LyYtsFKgEAAAAWABTLA2JCoOsM139b+ft5JSupavi5H2kAAAABAR8gAwAAAAAAABYAFCWjDqqhmkeTHRPP2ya7Kpny15LyIgYDSYFWXz0zDtQ8nb819SBLfP+t/N6mCOsWq2X0ROrHHtkYmLsGSlQAAIABAACAAAAAgAAAAAAaAAAAAAAiAgNjLZZToK8U19OxlBXF1gJK6lJaPQ7gG6/izvhnnt4m6RiYuwZKVAAAgAEAAIAAAACAAQAAABIAAAAAIgICS/cUTihGMLrbjuES/wYIQR9uEn88RIEQ3tD9NdHVr08YmLsGSlQAAIABAACAAAAAgAAAAAAbAAAAAA==\r\n\r\n๐Ÿ’ก Next steps:\r\n Inspect: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass decodepsbt \r\n Sign: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass walletprocesspsbt \r\n Send: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass sendrawtransaction \r\n"] +[142.54348, "o", "\u001b[?2004h\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[150.79263, "r", "170x44"] +[150.79337, "o", "\r\u001b[K\r\u001b]0;jolah1@DESKTOP-N4M1LE0: ~/dev/dust-cleaner\u0007\u001b[01;32mjolah1@DESKTOP-N4M1LE0\u001b[00m:\u001b[01;34m~/dev/dust-cleaner\u001b[00m$ "] +[171.879902, "o", "\u001b[?2004l\r\r\nexit\r\n"] diff --git a/src/analyzer.rs b/src/analyzer.rs index adddb15..0103f8c 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -1,5 +1,5 @@ -use bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry; use crate::types::Utxo; +use bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry; /// Dust thresholds per script type based on fee cost to spend pub const DUST_P2PKH: u64 = 546; @@ -35,9 +35,15 @@ pub fn detect_script_type(address: &str) -> ScriptType { ScriptType::P2PKH } else if address.starts_with("3") { ScriptType::P2SH - } else if address.starts_with("bc1q") || address.starts_with("tb1q") || address.starts_with("bcrt1q") { + } else if address.starts_with("bc1q") + || address.starts_with("tb1q") + || address.starts_with("bcrt1q") + { ScriptType::P2WPKH - } else if address.starts_with("bc1p") || address.starts_with("tb1p") || address.starts_with("bcrt1p") { + } else if address.starts_with("bc1p") + || address.starts_with("tb1p") + || address.starts_with("bcrt1p") + { ScriptType::P2TR } else { ScriptType::Unknown @@ -53,12 +59,10 @@ pub fn is_dust(amount_sats: u64, threshold: u64) -> bool { pub fn is_dust_smart(amount_sats: u64, address: Option<&str>, user_threshold: Option) -> bool { let threshold = match user_threshold { Some(t) => t, - None => { - match address { - Some(addr) => detect_script_type(addr).dust_threshold(), - None => DUST_DEFAULT, - } - } + None => match address { + Some(addr) => detect_script_type(addr).dust_threshold(), + None => DUST_DEFAULT, + }, }; amount_sats < threshold } @@ -93,9 +97,10 @@ pub fn classify_utxos_smart( let mut clean = vec![]; for utxo in utxos { - let address = utxo.address - .as_ref() - .map(|a| a.clone().assume_checked().to_string()); + let address = utxo + .address + .as_ref() + .map(|a| a.clone().assume_checked().to_string()); let addr_str = address.as_deref(); @@ -110,10 +115,7 @@ pub fn classify_utxos_smart( } #[allow(dead_code)] -pub fn classify_owned_utxos( - utxos: Vec, - threshold: u64, -) -> (Vec, Vec) { +pub fn classify_owned_utxos(utxos: Vec, threshold: u64) -> (Vec, Vec) { let mut dust = vec![]; let mut clean = vec![]; @@ -161,23 +163,38 @@ mod tests { #[test] fn test_detect_script_type_p2pkh() { - assert_eq!(detect_script_type("1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf"), ScriptType::P2PKH); + assert_eq!( + detect_script_type("1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf"), + ScriptType::P2PKH + ); } #[test] fn test_detect_script_type_p2sh() { - assert_eq!(detect_script_type("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"), ScriptType::P2SH); + assert_eq!( + detect_script_type("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"), + ScriptType::P2SH + ); } #[test] fn test_detect_script_type_p2wpkh() { - assert_eq!(detect_script_type("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"), ScriptType::P2WPKH); - assert_eq!(detect_script_type("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), ScriptType::P2WPKH); + assert_eq!( + detect_script_type("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"), + ScriptType::P2WPKH + ); + assert_eq!( + detect_script_type("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), + ScriptType::P2WPKH + ); } #[test] fn test_detect_script_type_p2tr() { - assert_eq!(detect_script_type("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"), ScriptType::P2TR); + assert_eq!( + detect_script_type("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"), + ScriptType::P2TR + ); } #[test] @@ -191,16 +208,32 @@ mod tests { #[test] fn test_is_dust_smart_p2wpkh() { // 294 is NOT dust for P2WPKH (equal to threshold) - assert!(!is_dust_smart(294, Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), None)); + assert!(!is_dust_smart( + 294, + Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), + None + )); // 293 IS dust for P2WPKH - assert!(is_dust_smart(293, Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), None)); + assert!(is_dust_smart( + 293, + Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), + None + )); } #[test] fn test_is_dust_smart_user_override() { // user sets threshold to 1000, overrides per-type - assert!(is_dust_smart(500, Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), Some(1000))); - assert!(!is_dust_smart(1000, Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), Some(1000))); + assert!(is_dust_smart( + 500, + Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), + Some(1000) + )); + assert!(!is_dust_smart( + 1000, + Some("bcrt1q60z267jynd9swr3dk7k3tmjqmlmsr8xsn4c8mz"), + Some(1000) + )); } #[test] @@ -221,10 +254,7 @@ mod tests { #[test] fn test_classify_owned_utxos_all_dust() { - let utxos = vec![ - make_utxo("txid1", 0, 100), - make_utxo("txid2", 0, 200), - ]; + let utxos = vec![make_utxo("txid1", 0, 100), make_utxo("txid2", 0, 200)]; let (dust, clean) = classify_owned_utxos(utxos, 1000); assert_eq!(dust.len(), 2); assert_eq!(clean.len(), 0); @@ -259,4 +289,4 @@ mod tests { assert_eq!(dust.len(), 2); assert_eq!(clean.len(), 1); } -} \ No newline at end of file +} diff --git a/src/cli.rs b/src/cli.rs index 32fd8b0..8e92d5d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,18 +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, #[command(subcommand)] @@ -43,8 +44,8 @@ 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, }, -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 52ae9f1..51e1dbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ +pub mod analyzer; pub mod cli; +pub mod psbt_builder; pub mod rpc; pub mod scanner; -pub mod analyzer; -pub mod psbt_builder; pub mod types; diff --git a/src/main.rs b/src/main.rs index de7667b..334c511 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,137 +1,167 @@ +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}; +use cli::{Cli, Commands, SweepMethod}; + +fn handle_scan(client: &Client, user_threshold: Option) -> anyhow::Result<()> { + let utxos = scanner::fetch_utxos(client)?; + let total_utxos = utxos.len(); + + let threshold_display = match user_threshold { + 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 + ); + + let (dust_utxos, clean_utxos) = analyzer::classify_utxos_smart(utxos, user_threshold); + + println!("โš ๏ธ DUST UTXOs ({} found):", dust_utxos.len()); + if dust_utxos.is_empty() { + println!(" none"); + } + for utxo in &dust_utxos { + 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); + println!( + " {} sats | {}:{} | {:?} | {}", + utxo.amount.to_sat(), + utxo.txid, + utxo.vout, + script_type, + addr + ); + } + + println!("\nโœ… CLEAN UTXOs ({} found):", clean_utxos.len()); + if clean_utxos.is_empty() { + println!(" none"); + } + for utxo in &clean_utxos { + println!( + " {} sats | {}:{}", + utxo.amount.to_sat(), + utxo.txid, + utxo.vout + ); + } + + let total_dust_sats: u64 = dust_utxos.iter().map(|u| u.amount.to_sat()).sum(); + let total_clean_sats: u64 = clean_utxos.iter().map(|u| u.amount.to_sat()).sum(); + + 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!(" Threshold: {}", threshold_display); + + if !dust_utxos.is_empty() { + println!("\n ๐Ÿ’ก Run 'sweep' to consolidate dust into a single UTXO"); + } else { + println!("\n โœ… Wallet is clean โ€” no dust detected"); + } + println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€"); + + Ok(()) +} + +fn handle_sweep( + client: &Client, + user_threshold: Option, + dry_run: bool, + method: SweepMethod, +) -> anyhow::Result<()> { + let utxos = scanner::fetch_utxos(client)?; + let (dust_utxos, clean_utxos) = analyzer::classify_utxos_smart(utxos, user_threshold); + + if dust_utxos.is_empty() { + println!("โœ… No dust UTXOs found. Wallet is clean!"); + return Ok(()); + } + + println!("Found {} dust UTXOs to sweep:", dust_utxos.len()); + for utxo in &dust_utxos { + println!( + " {} sats | {}:{}", + utxo.amount.to_sat(), + utxo.txid, + utxo.vout + ); + } + + if dry_run { + let result = psbt_builder::dry_run_sweep(&dust_utxos, &clean_utxos)?; + println!("\n๐Ÿ” Dry Run โ€” no PSBT created\n"); + println!(" Method: {:?}", method); + println!(" Dust inputs: {}", result.dust_input_count); + 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!("\n Run without --dry-run to create the PSBT."); + return Ok(()); + } + + let result = match method { + SweepMethod::Consolidate => { + println!("\n๐Ÿ“Ž Method: consolidate โ€” dust swept to fresh address"); + psbt_builder::build_sweep_psbt(client, &dust_utxos, &clean_utxos)? + } + SweepMethod::OpReturn => { + println!("\n๐Ÿ”ฅ Method: op-return โ€” dust burned to miner fees"); + println!(" Output: OP_RETURN (\"ash\" โ€” ashes to ashes, dust to dust)"); + psbt_builder::build_op_return_psbt(client, &dust_utxos, &clean_utxos)? + } + }; + + println!("\n๐Ÿ“Š Sweep Summary:"); + println!(" Dust inputs: {}", result.dust_input_count); + println!(" Total dust: {} sats", result.total_dust_sats); + println!("\n๐Ÿงน Sweep PSBT (base64):"); + println!("{}", result.psbt); + println!("\n๐Ÿ’ก Next steps:"); + println!(" Inspect: bitcoin-cli decodepsbt "); + println!(" Sign: bitcoin-cli walletprocesspsbt "); + println!(" Send: bitcoin-cli sendrawtransaction "); + + Ok(()) +} fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let client = rpc::connect(&cli.rpc_url, &cli.rpc_user, &cli.rpc_pass)?; let user_threshold = cli.threshold; match cli.command { - Commands::Scan => { - let utxos = scanner::fetch_utxos(&client)?; - let total_utxos = utxos.len(); - - let threshold_display = match user_threshold { - 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); - - let (dust_utxos, clean_utxos) = analyzer::classify_utxos_smart(utxos, user_threshold); - - println!("โš ๏ธ DUST UTXOs ({} found):", dust_utxos.len()); - if dust_utxos.is_empty() { - println!(" none"); - } - for utxo in &dust_utxos { - 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); - println!( - " {} sats | {}:{} | {:?} | {}", - utxo.amount.to_sat(), - utxo.txid, - utxo.vout, - script_type, - addr - ); - } - - println!("\nโœ… CLEAN UTXOs ({} found):", clean_utxos.len()); - if clean_utxos.is_empty() { - println!(" none"); - } - for utxo in &clean_utxos { - println!( - " {} sats | {}:{}", - utxo.amount.to_sat(), - utxo.txid, - utxo.vout - ); - } - - let total_dust_sats: u64 = dust_utxos.iter().map(|u| u.amount.to_sat()).sum(); - let total_clean_sats: u64 = clean_utxos.iter().map(|u| u.amount.to_sat()).sum(); - - 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!(" Threshold: {}", threshold_display); - - if !dust_utxos.is_empty() { - println!("\n ๐Ÿ’ก Run 'sweep' to consolidate dust into a single UTXO"); - } else { - println!("\n โœ… Wallet is clean โ€” no dust detected"); - } - println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€"); - } - + Commands::Scan => handle_scan(&client, user_threshold)?, Commands::Sweep { dry_run, method } => { - let utxos = scanner::fetch_utxos(&client)?; - let (dust_utxos, clean_utxos) = analyzer::classify_utxos_smart(utxos, user_threshold); - - if dust_utxos.is_empty() { - println!("โœ… No dust UTXOs found. Wallet is clean!"); - return Ok(()); - } - - println!("Found {} dust UTXOs to sweep:", dust_utxos.len()); - for utxo in &dust_utxos { - println!( - " {} sats | {}:{}", - utxo.amount.to_sat(), - utxo.txid, - utxo.vout - ); - } - - if dry_run { - let result = psbt_builder::dry_run_sweep(&dust_utxos, &clean_utxos)?; - println!("\n๐Ÿ” Dry Run โ€” no PSBT created\n"); - println!(" Method: {:?}", method); - println!(" Dust inputs: {}", result.dust_input_count); - 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!("\n Run without --dry-run to create the PSBT."); - return Ok(()); - } - - let result = match method { - cli::SweepMethod::Consolidate => { - println!("\n๐Ÿ“Ž Method: consolidate โ€” dust swept to fresh address"); - psbt_builder::build_sweep_psbt(&client, &dust_utxos, &clean_utxos)? - } - cli::SweepMethod::OpReturn => { - println!("\n๐Ÿ”ฅ Method: op-return โ€” dust burned to miner fees"); - println!(" Output: OP_RETURN (\"ash\" โ€” ashes to ashes, dust to dust)"); - psbt_builder::build_op_return_psbt(&client, &dust_utxos, &clean_utxos)? - } - }; - - println!("\n๐Ÿ“Š Sweep Summary:"); - println!(" Dust inputs: {}", result.dust_input_count); - println!(" Total dust: {} sats", result.total_dust_sats); - println!("\n๐Ÿงน Sweep PSBT (base64):"); - println!("{}", result.psbt); - println!("\n๐Ÿ’ก Next steps:"); - println!(" Inspect: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass decodepsbt "); - println!(" Sign: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass walletprocesspsbt "); - println!(" Send: bitcoin-cli -rpcport=18443 -rpcuser=user -rpcpassword=pass sendrawtransaction "); + handle_sweep(&client, user_threshold, dry_run, method)? } } Ok(()) -} \ No newline at end of file +} diff --git a/src/psbt_builder.rs b/src/psbt_builder.rs index d1aa1b0..5e7bace 100644 --- a/src/psbt_builder.rs +++ b/src/psbt_builder.rs @@ -15,35 +15,47 @@ pub struct DryRunResult { pub estimated_output_sats: u64, } -pub fn build_sweep_psbt( - client: &Client, - dust_utxos: &[ListUnspentResultEntry], - clean_utxos: &[ListUnspentResultEntry], -) -> anyhow::Result { - if dust_utxos.is_empty() { - anyhow::bail!("No dust UTXOs to sweep"); - } +//helpers - let funder = clean_utxos +fn select_funder( + clean_utxos: &[ListUnspentResultEntry], +) -> anyhow::Result<&ListUnspentResultEntry> { + clean_utxos .iter() .max_by_key(|u| u.amount.to_sat()) .ok_or_else(|| anyhow::anyhow!( "Cannot sweep: no clean UTXOs available to fund transaction fees.\nTip: fund your wallet first with a non-dust amount." - ))?; - - let mut all_inputs: Vec = vec![ - serde_json::json!({ - "txid": funder.txid.to_string(), - "vout": funder.vout, - }) - ]; + )) +} +fn build_inputs( + funder: &ListUnspentResultEntry, + dust_utxos: &[ListUnspentResultEntry], +) -> Vec { + let mut inputs = vec![serde_json::json!({ + "txid": funder.txid.to_string(), + "vout": funder.vout, + })]; for utxo in dust_utxos { - all_inputs.push(serde_json::json!({ + inputs.push(serde_json::json!({ "txid": utxo.txid.to_string(), "vout": utxo.vout, })); } + inputs +} + +pub fn build_sweep_psbt( + client: &Client, + dust_utxos: &[ListUnspentResultEntry], + clean_utxos: &[ListUnspentResultEntry], +) -> anyhow::Result { + if dust_utxos.is_empty() { + anyhow::bail!("No dust UTXOs to sweep"); + } + + let funder = select_funder(clean_utxos)?; + let all_inputs = build_inputs(funder, dust_utxos); let out_address = client.get_new_address(None, None)?; let out_address = out_address.assume_checked(); @@ -88,13 +100,7 @@ pub fn dry_run_sweep( anyhow::bail!("No dust UTXOs to sweep"); } - let funder = clean_utxos - .iter() - .max_by_key(|u| u.amount.to_sat()) - .ok_or_else(|| anyhow::anyhow!( - "Cannot sweep: no clean UTXOs available to fund transaction fees." - ))?; - + let funder = select_funder(clean_utxos)?; let total_dust_sats: u64 = dust_utxos.iter().map(|u| u.amount.to_sat()).sum(); let funder_sats = funder.amount.to_sat(); @@ -123,53 +129,26 @@ pub fn build_op_return_psbt( anyhow::bail!("No dust UTXOs to sweep"); } - let funder = clean_utxos - .iter() - .max_by_key(|u| u.amount.to_sat()) - .ok_or_else(|| anyhow::anyhow!( - "Cannot sweep: no clean UTXOs available to fund transaction fees." - ))?; - + let funder = select_funder(clean_utxos)?; println!( "\n โ„น๏ธ Using clean UTXO to fund fees: {} sats", funder.amount.to_sat() ); - // Build inputs โ€” funder first, then all dust UTXOs - let mut all_inputs: Vec = vec![ - serde_json::json!({ - "txid": funder.txid.to_string(), - "vout": funder.vout, - }) - ]; + let all_inputs = build_inputs(funder, dust_utxos); - for utxo in dust_utxos { - all_inputs.push(serde_json::json!({ - "txid": utxo.txid.to_string(), - "vout": utxo.vout, - })); - } - - // OP_RETURN output โ€” "ashes to ashes, dust to dust" - // data: 617368 = "ash" in hex + // "ash" in hex โ€” ashes to ashes, dust to dust let op_return_data = "617368"; - // Change output to receive funder amount back minus fees let change_address = client.get_new_address(None, None)?; let change_address = change_address.assume_checked(); let funder_btc = funder.amount.to_btc(); let outputs = serde_json::json!([ - { - "data": op_return_data - }, - { - change_address.to_string(): format!("{:.8}", funder_btc) - } + { "data": op_return_data }, + { change_address.to_string(): format!("{:.8}", funder_btc) } ]); - // subtractFeeFromOutputs: [1] means subtract fee from the change output (index 1) - // OP_RETURN is index 0 and carries no value let response = client.call::( "walletcreatefundedpsbt", &[ @@ -195,4 +174,4 @@ pub fn build_op_return_psbt( dust_input_count: dust_utxos.len(), total_dust_sats, }) -} \ No newline at end of file +} diff --git a/src/rpc.rs b/src/rpc.rs index af22308..8e579ca 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -1,11 +1,11 @@ use bitcoincore_rpc::{Auth, Client}; pub fn connect(url: &str, user: &str, pass: &str) -> anyhow::Result { - let client = Client::new( - url, - Auth::UserPass(user.into(), pass.into()), - ).map_err(|e| anyhow::anyhow!( - "Failed to create RPC client: {}\nCheck your --rpc-url, --rpc-user and --rpc-pass", e - ))?; + let client = Client::new(url, Auth::UserPass(user.into(), pass.into())).map_err(|e| { + anyhow::anyhow!( + "Failed to create RPC client: {}\nCheck your --rpc-url, --rpc-user and --rpc-pass", + e + ) + })?; Ok(client) -} \ No newline at end of file +} diff --git a/src/scanner.rs b/src/scanner.rs index 0df5e1c..d880037 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -1,5 +1,5 @@ -use bitcoincore_rpc::{Client, RpcApi}; use bitcoincore_rpc::bitcoincore_rpc_json::ListUnspentResultEntry; +use bitcoincore_rpc::{Client, RpcApi}; pub fn fetch_utxos(client: &Client) -> anyhow::Result> { let utxos = client.list_unspent(None, None, None, None, None) @@ -8,10 +8,10 @@ pub fn fetch_utxos(client: &Client) -> anyhow::Result -rpcuser= -rpcpassword= loadwallet ") } else { anyhow::anyhow!("Failed to fetch UTXOs: {}", msg) } })?; Ok(utxos) -} \ No newline at end of file +} diff --git a/src/types.rs b/src/types.rs index bd493d6..3d7372b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -17,4 +17,4 @@ impl Utxo { address: address.map(|a| a.to_string()), } } -} \ No newline at end of file +}