1use std::fs;
24use std::fs::File;
25use std::path::PathBuf;
26use std::process::exit;
27
28use bpstd::{Derive, IdxBase, Keychain, NormalIndex, Sats};
29use bpwallet::{coinselect, Amount, Beneficiary, OpType, StoreError, TxParams, WalletUtxo};
30use psbt::PsbtVer;
31use strict_encoding::Ident;
32
33use crate::opts::DescriptorOpts;
34use crate::{Args, Config, Exec, RuntimeError, WalletAddr};
35
36#[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)]
37pub enum Command {
38 #[display("list")]
40 List,
41
42 #[display("default")]
44 Default {
45 default: Option<Ident>,
47 },
48
49 #[display("create")]
51 Create {
52 name: Ident,
54 },
55
56 #[display("address")]
58 Address {
59 #[clap(short = '1', long)]
61 change: bool,
62
63 #[clap(short, long, conflicts_with = "change")]
65 keychain: Option<Keychain>,
66
67 #[clap(short, long)]
69 index: Option<NormalIndex>,
70
71 #[clap(short = 'D', long, conflicts_with_all = ["change", "index"])]
73 dry_run: bool,
74
75 #[clap(short = 'C', long, default_value = "1")]
77 count: u8,
78 },
79}
80
81#[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)]
82pub enum BpCommand {
83 #[clap(flatten)]
84 #[display(inner)]
85 General(Command),
86
87 #[display("balance")]
89 Balance {
90 #[clap(short, long)]
92 addr: bool,
93
94 #[clap(short, long)]
96 utxo: bool,
97 },
98
99 #[display("history")]
101 History {
102 #[clap(long)]
104 txid: bool,
105
106 #[clap(long)]
108 details: bool,
109 },
110
111 #[display("construct")]
113 Construct {
114 #[clap(short = '2')]
116 v2: bool,
117
118 #[clap(long)]
124 to: Vec<Beneficiary>,
125
126 fee: Sats,
128
129 psbt: Option<PathBuf>,
131 },
132}
133
134impl<O: DescriptorOpts> Exec for Args<Command, O> {
135 type Error = RuntimeError;
136 const CONF_FILE_NAME: &'static str = "bp.toml";
137
138 fn exec(self, mut config: Config, name: &'static str) -> Result<(), Self::Error> {
139 match &self.command {
140 Command::List => {
141 let dir = self.general.base_dir();
142 let Ok(dir) = fs::read_dir(dir).map_err(|err| {
143 error!("Error reading wallet directory: {err:?}");
144 eprintln!("System directory is not initialized");
145 }) else {
146 return Ok(());
147 };
148 println!("Known wallets:");
149 let mut count = 0usize;
150 for wallet in dir {
151 let Ok(wallet) = wallet else {
152 continue;
153 };
154 let Ok(meta) = wallet.metadata() else {
155 continue;
156 };
157 if !meta.is_dir() {
158 continue;
159 }
160 let name = wallet.file_name().into_string().expect("invalid directory name");
161 println!(
162 "{name}{}",
163 if config.default_wallet == name { "\t[default]" } else { "" }
164 );
165 count += 1;
166 }
167 if count == 0 {
168 println!("no wallets found");
169 }
170 }
171 Command::Default { default } => {
172 if let Some(default) = default {
173 config.default_wallet = default.to_string();
174 config.store(&self.conf_path(name));
175 } else {
176 println!("Default wallet is '{}'", config.default_wallet);
177 }
178 }
179 Command::Create { name } => {
180 if !self.wallet.descriptor_opts.is_some() {
181 eprintln!("Error: you must provide an argument specifying wallet descriptor");
182 exit(1);
183 }
184 let mut runtime = self.bp_runtime::<O::Descr>(&config)?;
185 let name = name.to_string();
186 print!("Saving the wallet as '{name}' ... ");
187 let dir = self.general.wallet_dir(&name);
188 runtime.set_name(name);
189 if let Err(err) = runtime.store(&dir) {
190 println!("error: {err}");
191 } else {
192 println!("success");
193 }
194 }
195 Command::Address {
196 change,
197 keychain,
198 index,
199 dry_run: no_shift,
200 count: no,
201 } => {
202 let mut runtime = self.bp_runtime::<O::Descr>(&config)?;
203 let keychain = match (change, keychain) {
204 (false, None) => runtime.default_keychain(),
205 (true, None) => (*change as u8).into(),
206 (false, Some(keychain)) => *keychain,
207 _ => unreachable!(),
208 };
209 if !runtime.keychains().contains(&keychain) {
210 eprintln!(
211 "Error: the specified keychain {keychain} is not a part of the descriptor"
212 );
213 exit(1);
214 }
215 let index =
216 index.unwrap_or_else(|| runtime.next_derivation_index(keychain, !*no_shift));
217 println!("\nTerm.\tAddress");
218 for derived_addr in
219 runtime.addresses(keychain).skip(index.index() as usize).take(*no as usize)
220 {
221 println!("{}\t{}", derived_addr.terminal, derived_addr.addr);
222 }
223 runtime.try_store()?;
224 }
225 }
226
227 Ok(())
228 }
229}
230
231impl<O: DescriptorOpts> Exec for Args<BpCommand, O> {
232 type Error = RuntimeError;
233 const CONF_FILE_NAME: &'static str = "bp.toml";
234
235 fn exec(mut self, config: Config, name: &'static str) -> Result<(), Self::Error> {
236 match &self.command {
237 BpCommand::General(cmd) => self.translate(cmd).exec(config, name)?,
238 BpCommand::Balance {
239 addr: false,
240 utxo: false,
241 } => {
242 let runtime = self.bp_runtime::<O::Descr>(&config)?;
243 println!("\nWallet total balance: {} ṩ", runtime.balance());
244 }
245 BpCommand::Balance {
246 addr: true,
247 utxo: false,
248 } => {
249 let runtime = self.bp_runtime::<O::Descr>(&config)?;
250 println!("\nTerm.\t{:62}\t# used\tVol., ṩ\tBalance, ṩ", "Address");
251 for info in runtime.address_balance() {
252 let WalletAddr {
253 addr,
254 terminal,
255 used,
256 volume,
257 balance,
258 } = info;
259 println!("{terminal}\t{:62}\t{used}\t{volume}\t{balance}", addr.to_string());
260 }
261 self.command = BpCommand::Balance {
262 addr: false,
263 utxo: false,
264 };
265 self.resolver.sync = false;
266 self.exec(config, name)?;
267 }
268 BpCommand::Balance {
269 addr: false,
270 utxo: true,
271 } => {
272 let runtime = self.bp_runtime::<O::Descr>(&config)?;
273 println!("\nHeight\t{:>12}\t{:68}\tAddress", "Amount, ṩ", "Outpoint");
274 for row in runtime.coins() {
275 println!(
276 "{}\t{: >12}\t{:68}\t{}",
277 row.height, row.amount, row.outpoint, row.address
278 );
279 }
280 self.command = BpCommand::Balance {
281 addr: false,
282 utxo: false,
283 };
284 self.resolver.sync = false;
285 self.exec(config, name)?;
286 }
287 BpCommand::Balance {
288 addr: true,
289 utxo: true,
290 } => {
291 let runtime = self.bp_runtime::<O::Descr>(&config)?;
292 println!("\nHeight\t{:>12}\t{:68}", "Amount, ṩ", "Outpoint");
293 for (derived_addr, utxos) in runtime.address_coins() {
294 println!("{}\t{}", derived_addr.addr, derived_addr.terminal);
295 for row in utxos {
296 println!("{}\t{: >12}\t{:68}", row.height, row.amount, row.outpoint);
297 }
298 println!()
299 }
300 self.command = BpCommand::Balance {
301 addr: false,
302 utxo: false,
303 };
304 self.resolver.sync = false;
305 self.exec(config, name)?;
306 }
307 BpCommand::History { txid, details } => {
308 let runtime = self.bp_runtime::<O::Descr>(&config)?;
309 println!(
310 "\nHeight\t{:<1$}\t Amount, ṩ\tFee rate, ṩ/vbyte",
311 "Txid",
312 if *txid { 64 } else { 18 }
313 );
314 let mut rows = runtime.history().collect::<Vec<_>>();
315 rows.sort_by_key(|row| row.height);
316 for row in rows {
317 println!(
318 "{}\t{}\t{}{: >12}\t{: >8.2}",
319 row.height,
320 if *txid { row.txid.to_string() } else { format!("{:#}", row.txid) },
321 row.operation,
322 row.amount,
323 row.fee.sats() as f64 * 4.0 / row.weight as f64
324 );
325 if *details {
326 for (cp, value) in &row.own {
327 println!(
328 "\t* {value: >-12}ṩ\t{}\t{cp}",
329 if *value < 0 {
330 "debit from"
331 } else if row.operation == OpType::Credit {
332 "credit to "
333 } else {
334 "change to "
335 }
336 );
337 }
338 for (cp, value) in &row.counterparties {
339 println!(
340 "\t* {value: >-12}ṩ\t{}\t{cp}",
341 if *value > 0 {
342 "paid from "
343 } else if row.operation == OpType::Credit {
344 "change to "
345 } else {
346 "sent to "
347 }
348 );
349 }
350 println!("\t* {: >-12}ṩ\tminer fee", -row.fee.sats_i64());
351 println!();
352 }
353 }
354 }
355 BpCommand::Construct {
356 v2,
357 to: beneficiaries,
358 fee,
359 psbt: psbt_file,
360 } => {
361 let mut runtime = self.bp_runtime::<O::Descr>(&config)?;
362
363 let total_amount =
365 beneficiaries.iter().try_fold(Sats::ZERO, |sats, b| match b.amount {
366 Amount::Max => Err(()),
367 Amount::Fixed(s) => sats.checked_add(s).ok_or(()),
368 });
369 let coins: Vec<_> = match total_amount {
370 Ok(sats) if sats > Sats::ZERO => {
371 runtime.wallet().coinselect(sats + *fee, coinselect::all).collect()
372 }
373 _ => {
374 eprintln!(
375 "Warning: you are not paying to anybody but just aggregating all your \
376 balances to a single UTXO",
377 );
378 runtime.wallet().all_utxos().map(WalletUtxo::into_outpoint).collect()
379 }
380 };
381
382 let params = TxParams::with(*fee);
384 let (psbt, _) =
385 runtime.wallet_mut().construct_psbt(coins, beneficiaries, params)?;
386 let ver = if *v2 { PsbtVer::V2 } else { PsbtVer::V0 };
387
388 eprintln!("{}", serde_yaml::to_string(&psbt).unwrap());
389 match psbt_file {
390 Some(file_name) => {
391 let mut psbt_file = File::create(file_name).map_err(StoreError::from)?;
392 psbt.encode(ver, &mut psbt_file).map_err(StoreError::from)?;
393 }
394 None => match ver {
395 PsbtVer::V0 => println!("{psbt}"),
396 PsbtVer::V2 => println!("{psbt:#}"),
397 },
398 }
399 }
400 };
401
402 println!();
403
404 Ok(())
405 }
406}