bp_util/
command.rs

1// Modern, minimalistic & standard-compliant cold wallet library.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2020-2023 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2020-2023 LNP/BP Standards Association. All rights reserved.
9// Copyright (C) 2020-2023 Dr Maxim Orlovsky. All rights reserved.
10//
11// Licensed under the Apache License, Version 2.0 (the "License");
12// you may not use this file except in compliance with the License.
13// You may obtain a copy of the License at
14//
15//     http://www.apache.org/licenses/LICENSE-2.0
16//
17// Unless required by applicable law or agreed to in writing, software
18// distributed under the License is distributed on an "AS IS" BASIS,
19// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20// See the License for the specific language governing permissions and
21// limitations under the License.
22
23use 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    /// List known wallets
39    #[display("list")]
40    List,
41
42    /// Get or set default wallet
43    #[display("default")]
44    Default {
45        /// Name of the wallet to make it default
46        default: Option<Ident>,
47    },
48
49    /// Create a wallet
50    #[display("create")]
51    Create {
52        /// The name for the new wallet
53        name: Ident,
54    },
55
56    /// Generate a new wallet address(es)
57    #[display("address")]
58    Address {
59        /// Use change keychain
60        #[clap(short = '1', long)]
61        change: bool,
62
63        /// Use custom keychain
64        #[clap(short, long, conflicts_with = "change")]
65        keychain: Option<Keychain>,
66
67        /// Use custom address index
68        #[clap(short, long)]
69        index: Option<NormalIndex>,
70
71        /// Do not shift the last used index
72        #[clap(short = 'D', long, conflicts_with_all = ["change", "index"])]
73        dry_run: bool,
74
75        /// Number of addresses to generate
76        #[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    /// List wallet balance with additional optional details
88    #[display("balance")]
89    Balance {
90        /// Print balance for each individual address
91        #[clap(short, long)]
92        addr: bool,
93
94        /// Print information about individual UTXOs
95        #[clap(short, long)]
96        utxo: bool,
97    },
98
99    /// Display history of wallet operations
100    #[display("history")]
101    History {
102        /// Print full transaction ids
103        #[clap(long)]
104        txid: bool,
105
106        /// Print operation details
107        #[clap(long)]
108        details: bool,
109    },
110
111    /// Compose a new PSBT for bitcoin payment
112    #[display("construct")]
113    Construct {
114        /// Encode PSBT as V2
115        #[clap(short = '2')]
116        v2: bool,
117
118        /// Bitcoin invoice in form of `<sats>@<address>`. To spend full wallet balance use
119        /// `MAX` for the amount.
120        ///
121        /// If multiple `MAX` addresses provided the wallet balance is split between them in equal
122        /// proportions.
123        #[clap(long)]
124        to: Vec<Beneficiary>,
125
126        /// Fee
127        fee: Sats,
128
129        /// Name of PSBT file to save. If not given, prints PSBT to STDOUT
130        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                // Do coin selection
364                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                // TODO: Support lock time and RBFs
383                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}