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 = match amount {
105        BitcoinAmountOrAll::All => {
106            bail!(
107                "The 'all' option is not supported in the module CLI. \
108                Use `fedimint-cli withdraw --amount all` instead."
109            );
110        }
111        BitcoinAmountOrAll::Amount(amount) => amount,
112    };
113    let fees = module.get_withdraw_fees(&address, amount).await?;
114    let absolute_fees = fees.amount();
115
116    info!("Attempting withdraw with fees: {fees:?}");
117
118    let operation_id = module.withdraw(&address, amount, fees, ()).await?;
119
120    let mut updates = module
121        .subscribe_withdraw_updates(operation_id)
122        .await?
123        .into_stream();
124
125    while let Some(update) = updates.next().await {
126        debug!(?update, "Withdraw state update");
127
128        match update {
129            WithdrawState::Succeeded(txid) => {
130                return Ok(serde_json::json!({
131                    "txid": txid.consensus_encode_to_hex(),
132                    "fees_sat": absolute_fees.to_sat(),
133                }));
134            }
135            WithdrawState::Failed(e) => {
136                bail!("Withdraw failed: {e}");
137            }
138            WithdrawState::Created => {}
139        }
140    }
141
142    unreachable!("Update stream ended without outcome");
143}
144
145pub(crate) async fn handle_cli_command(
146    module: &WalletClientModule,
147    args: &[ffi::OsString],
148) -> anyhow::Result<serde_json::Value> {
149    let opts = Opts::parse_from(iter::once(&ffi::OsString::from("wallet")).chain(args.iter()));
150
151    let res = match opts {
152        Opts::AwaitDeposit {
153            operation_id,
154            num,
155            addr,
156            tweak_idx,
157        } => {
158            await_deposit(module, addr, operation_id, tweak_idx, num).await?;
159            serde_json::Value::Bool(true)
160        }
161        Opts::GetBitcoinRpcKind { peer_id } => {
162            let kind = module
163                .module_api
164                .fetch_bitcoin_rpc_kind(peer_id.into())
165                .await?;
166            serde_json::to_value(kind).expect("JSON serialization failed")
167        }
168        Opts::GetBitcoinRpcConfig => {
169            let auth = module
170                .admin_auth
171                .clone()
172                .ok_or(anyhow::anyhow!("Admin auth not set"))?;
173            serde_json::to_value(module.module_api.fetch_bitcoin_rpc_config(auth).await?)
174                .expect("JSON serialization failed")
175        }
176        Opts::GetConsensusBlockCount => {
177            serde_json::to_value(module.module_api.fetch_consensus_block_count().await?)
178                .expect("JSON serialization failed")
179        }
180        Opts::RecheckDepositAddress {
181            addr,
182            operation_id,
183            tweak_idx,
184        } => {
185            if u32::from(addr.is_some())
186                + u32::from(operation_id.is_some())
187                + u32::from(tweak_idx.is_some())
188                != 1
189            {
190                bail!("One and only one of the selector arguments must be set")
191            }
192            if let Some(tweak_idx) = tweak_idx {
193                module.recheck_pegin_address(tweak_idx).await?;
194            } else if let Some(operation_id) = operation_id {
195                module.recheck_pegin_address_by_op_id(operation_id).await?;
196            } else if let Some(addr) = addr {
197                module.recheck_pegin_address_by_address(addr).await?;
198            } else {
199                unreachable!()
200            }
201            serde_json::Value::Bool(true)
202        }
203        Opts::NewDepositAddress => {
204            let (operation_id, address, tweak_idx) =
205                module.allocate_deposit_address_expert_only(()).await?;
206            serde_json::json! {
207                {
208                    "address": address,
209                    "operation_id": operation_id,
210                    "tweak_idx": tweak_idx.0
211                }
212            }
213        }
214        Opts::Withdraw { amount, address } => return withdraw(module, amount, address).await,
215    };
216
217    Ok(res)
218}