Skip to main content

fedimint_walletv2_client/
cli.rs

1use std::{ffi, iter};
2
3use bitcoin::Address;
4use bitcoin::address::NetworkUnchecked;
5use clap::{Parser, Subcommand};
6use fedimint_api_client::api::FederationError;
7use fedimint_client_module::error::{ModuleLookupError, OperationLookupError};
8use fedimint_core::BitcoinAmountOrAll;
9use fedimint_eventlog::EventLogId;
10use serde::Serialize;
11use serde_json::Value;
12
13use crate::{AwaitReceiveError, ReceiveError, SendError, WalletClientModule};
14
15#[derive(Parser, Serialize)]
16enum Opts {
17    /// Subcommands for operator to retrieve information about the wallet state.
18    #[command(subcommand)]
19    Info(InfoOpts),
20    /// Fetch the current fee required to send an onchain payment.
21    SendFee,
22    /// Fetch the current fee required to claim an onchain deposit (peg-in).
23    ReceiveFee,
24    /// Send an onchain payment.
25    Send {
26        address: Address<NetworkUnchecked>,
27        /// Value to send, or "all" to sweep the entire balance.
28        value: BitcoinAmountOrAll,
29        #[arg(long)]
30        fee: Option<bitcoin::Amount>,
31    },
32    /// Return the next unused receive address.
33    ///
34    /// To wait for a payment to this address, read the current event log
35    /// position with `dev next-event-log-id` *before* running this, then pass
36    /// that position to `await-receive`.
37    Receive,
38    /// Block until the next payment is received, starting from the given event
39    /// log position. Returns the receive's final state and the event log
40    /// position to pass to the following `await-receive`.
41    AwaitReceive {
42        /// Event log position to start scanning from, as returned by
43        /// `dev next-event-log-id` or a prior `await-receive`.
44        position: EventLogId,
45    },
46}
47
48#[derive(Clone, Subcommand, Serialize)]
49enum InfoOpts {
50    /// Fetch the total value of bitcoin controlled by the federation.
51    TotalValue,
52    /// Fetch the consensus block count of the federation.
53    BlockCount,
54    /// Fetch the current consensus feerate.
55    Feerate,
56    /// Display the chain of pending bitcoin transactions.
57    PendingTxChain,
58    /// Display the chain of bitcoin transactions.
59    TxChain,
60}
61
62pub(crate) async fn handle_cli_command(
63    wallet: &WalletClientModule,
64    args: &[ffi::OsString],
65) -> Result<Value, CliCommandError> {
66    let opts = Opts::parse_from(iter::once(&ffi::OsString::from("walletv2")).chain(args.iter()));
67
68    let value = match opts {
69        Opts::Info(subcommand) => match subcommand {
70            InfoOpts::TotalValue => json(wallet.total_value().await?),
71            InfoOpts::BlockCount => json(wallet.block_count().await?),
72            InfoOpts::Feerate => json(wallet.feerate().await?),
73            InfoOpts::PendingTxChain => json(wallet.pending_tx_chain().await?),
74            InfoOpts::TxChain => json(wallet.tx_chain().await?),
75        },
76        Opts::SendFee => json(wallet.send_fee().await?),
77        Opts::ReceiveFee => json(wallet.receive_fee().await?),
78        Opts::Send {
79            address,
80            value,
81            fee,
82        } => {
83            // Resolve the on-chain fee up front so the same value sizes the
84            // sweep and funds the send: the required feerate rises with each
85            // pending federation transaction, and a value computed against a
86            // stale fee would be rejected.
87            let fee = match fee {
88                Some(fee) => fee,
89                None => wallet.send_fee().await?,
90            };
91
92            let value = match value {
93                // The on-chain fee is only part of the cost of sending
94                // everything: funding the wallet output also incurs the
95                // federation's per-note fees.
96                BitcoinAmountOrAll::All => {
97                    let balance = wallet.client_ctx.get_balance_for_btc().await?;
98                    wallet.max_sendable_amount(balance, fee).await?
99                }
100                BitcoinAmountOrAll::Amount(value) => value,
101            };
102
103            json(
104                wallet
105                    .await_final_send_operation_state(
106                        wallet
107                            .send(address, value, Some(fee), serde_json::Value::Null)
108                            .await?,
109                    )
110                    .await?,
111            )
112        }
113        Opts::Receive => json(wallet.receive().await),
114        Opts::AwaitReceive { position } => json(wallet.await_receive(position).await?),
115    };
116
117    Ok(value)
118}
119
120fn json<T: Serialize>(value: T) -> Value {
121    serde_json::to_value(value).expect("JSON serialization failed")
122}
123
124/// A failure of a `walletv2` module command.
125#[derive(Debug, thiserror::Error)]
126pub(crate) enum CliCommandError {
127    /// The federation did not serve the request.
128    #[error(transparent)]
129    Federation(#[from] FederationError),
130
131    /// The payment could not be quoted or sent.
132    #[error(transparent)]
133    Send(#[from] SendError),
134
135    /// The receive fee could not be quoted.
136    #[error(transparent)]
137    Receive(#[from] ReceiveError),
138
139    /// The client's bitcoin balance could not be read.
140    #[error(transparent)]
141    Balance(#[from] ModuleLookupError),
142
143    /// The send operation could not be looked up.
144    #[error(transparent)]
145    OperationLookup(#[from] OperationLookupError),
146
147    /// The next receive could not be awaited.
148    #[error(transparent)]
149    AwaitReceive(#[from] AwaitReceiveError),
150}