Skip to main content

fedimint_wallet_client/
cli.rs

1use std::str::FromStr as _;
2use std::{ffi, iter};
3
4use anyhow::bail;
5use bitcoin::address::NetworkUnchecked;
6use clap::Parser;
7use fedimint_core::BitcoinAmountOrAll;
8use fedimint_core::core::OperationId;
9use fedimint_core::encoding::Encodable;
10use futures::StreamExt;
11use serde::Serialize;
12use tracing::{debug, info};
13
14use super::WalletClientModule;
15use crate::WithdrawState;
16use crate::api::WalletFederationApi;
17use crate::client_db::TweakIdx;
18
19#[derive(Parser, Serialize)]
20enum Opts {
21    /// Await a deposit on a given deposit address
22    AwaitDeposit {
23        addr: Option<String>,
24        #[arg(long)]
25        operation_id: Option<OperationId>,
26        #[arg(long)]
27        tweak_idx: Option<TweakIdx>,
28        /// Await more than just one deposit
29        #[arg(long, default_value = "1")]
30        num: usize,
31    },
32    GetConsensusBlockCount,
33    /// Returns the Bitcoin RPC kind
34    GetBitcoinRpcKind {
35        peer_id: u16,
36    },
37    /// Returns the Bitcoin RPC kind and URL, if authenticated
38    GetBitcoinRpcConfig,
39
40    NewDepositAddress,
41    /// Withdraw funds from the federation
42    Withdraw {
43        #[clap(long)]
44        amount: BitcoinAmountOrAll,
45        #[clap(long)]
46        address: bitcoin::Address<NetworkUnchecked>,
47    },
48    /// Trigger wallet address check (in the background)
49    RecheckDepositAddress {
50        addr: Option<bitcoin::Address<NetworkUnchecked>>,
51        #[arg(long)]
52        operation_id: Option<OperationId>,
53        #[arg(long)]
54        tweak_idx: Option<TweakIdx>,
55    },
56}
57
58async fn await_deposit(
59    module: &WalletClientModule,
60    addr: Option<String>,
61    operation_id: Option<OperationId>,
62    tweak_idx: Option<TweakIdx>,
63    num: usize,
64) -> anyhow::Result<()> {
65    if u32::from(addr.is_some())
66        + u32::from(operation_id.is_some())
67        + u32::from(tweak_idx.is_some())
68        != 1
69    {
70        bail!("One and only one of the selector arguments must be set")
71    }
72    if let Some(tweak_idx) = tweak_idx {
73        module.await_num_deposits(tweak_idx, num).await?;
74    } else if let Some(operation_id) = operation_id {
75        module
76            .await_num_deposits_by_operation_id(operation_id, num)
77            .await?;
78    } else if let Some(addr) = addr {
79        if addr.len() == 64 {
80            eprintln!(
81                "Interpreting addr as an operation_id for backward compatibility. \
82                Use `--operation-id` from now on."
83            );
84            let operation_id = OperationId::from_str(&addr)?;
85            module
86                .await_num_deposits_by_operation_id(operation_id, num)
87                .await?;
88        } else {
89            let addr = bitcoin::Address::from_str(&addr)?;
90            module.await_num_deposits_by_address(addr, num).await?;
91        }
92    } else {
93        unreachable!()
94    }
95    Ok(())
96}
97
98async fn withdraw(
99    module: &WalletClientModule,
100    amount: BitcoinAmountOrAll,
101    address: bitcoin::Address<NetworkUnchecked>,
102) -> anyhow::Result<serde_json::Value> {
103    let address = address.require_network(module.get_network())?;
104    let (amount, fees) = match amount {
105        // The on-chain fee is only part of the cost of withdrawing everything:
106        // funding the peg-out output also incurs the federation's per-note
107        // fees. The returned fees are quoted at the returned amount, so they
108        // must be used together.
109        BitcoinAmountOrAll::All => {
110            let balance = module.client_ctx.get_balance_for_btc().await?;
111            module.max_withdrawable_amount(&address, balance).await?
112        }
113        BitcoinAmountOrAll::Amount(amount) => {
114            (amount, module.get_withdraw_fees(&address, amount).await?)
115        }
116    };
117    let absolute_fees = fees.amount();
118
119    info!("Attempting withdraw with fees: {fees:?}");
120
121    let operation_id = module.withdraw(&address, amount, fees, ()).await?;
122
123    let mut updates = module
124        .subscribe_withdraw_updates(operation_id)
125        .await?
126        .into_stream();
127
128    while let Some(update) = updates.next().await {
129        debug!(?update, "Withdraw state update");
130
131        match update {
132            WithdrawState::Succeeded(txid) => {
133                return Ok(serde_json::json!({
134                    "txid": txid.consensus_encode_to_hex(),
135                    "fees_sat": absolute_fees.to_sat(),
136                }));
137            }
138            WithdrawState::Failed(e) => {
139                bail!("Withdraw failed: {e}");
140            }
141            WithdrawState::Created => {}
142        }
143    }
144
145    unreachable!("Update stream ended without outcome");
146}
147
148pub(crate) async fn handle_cli_command(
149    module: &WalletClientModule,
150    args: &[ffi::OsString],
151) -> anyhow::Result<serde_json::Value> {
152    let opts = Opts::parse_from(iter::once(&ffi::OsString::from("wallet")).chain(args.iter()));
153
154    let res = match opts {
155        Opts::AwaitDeposit {
156            operation_id,
157            num,
158            addr,
159            tweak_idx,
160        } => {
161            await_deposit(module, addr, operation_id, tweak_idx, num).await?;
162            serde_json::Value::Bool(true)
163        }
164        Opts::GetBitcoinRpcKind { peer_id } => {
165            let kind = module
166                .module_api
167                .fetch_bitcoin_rpc_kind(peer_id.into())
168                .await?;
169            serde_json::to_value(kind).expect("JSON serialization failed")
170        }
171        Opts::GetBitcoinRpcConfig => {
172            let auth = module
173                .admin_auth
174                .clone()
175                .ok_or(anyhow::anyhow!("Admin auth not set"))?;
176            serde_json::to_value(module.module_api.fetch_bitcoin_rpc_config(auth).await?)
177                .expect("JSON serialization failed")
178        }
179        Opts::GetConsensusBlockCount => {
180            serde_json::to_value(module.module_api.fetch_consensus_block_count().await?)
181                .expect("JSON serialization failed")
182        }
183        Opts::RecheckDepositAddress {
184            addr,
185            operation_id,
186            tweak_idx,
187        } => {
188            if u32::from(addr.is_some())
189                + u32::from(operation_id.is_some())
190                + u32::from(tweak_idx.is_some())
191                != 1
192            {
193                bail!("One and only one of the selector arguments must be set")
194            }
195            if let Some(tweak_idx) = tweak_idx {
196                module.recheck_pegin_address(tweak_idx).await?;
197            } else if let Some(operation_id) = operation_id {
198                module.recheck_pegin_address_by_op_id(operation_id).await?;
199            } else if let Some(addr) = addr {
200                module.recheck_pegin_address_by_address(addr).await?;
201            } else {
202                unreachable!()
203            }
204            serde_json::Value::Bool(true)
205        }
206        Opts::NewDepositAddress => {
207            let deposit_address = module.allocate_deposit_address_expert_only(()).await?;
208            serde_json::json! {
209                {
210                    "address": deposit_address.address,
211                    "operation_id": deposit_address.operation_id,
212                    "tweak_idx": deposit_address.tweak_idx.0
213                }
214            }
215        }
216        Opts::Withdraw { amount, address } => return withdraw(module, amount, address).await,
217    };
218
219    Ok(res)
220}