fedimint_wallet_client/
cli.rs1use 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 AwaitDeposit {
23 addr: Option<String>,
24 #[arg(long)]
25 operation_id: Option<OperationId>,
26 #[arg(long)]
27 tweak_idx: Option<TweakIdx>,
28 #[arg(long, default_value = "1")]
30 num: usize,
31 },
32 GetConsensusBlockCount,
33 GetBitcoinRpcKind {
35 peer_id: u16,
36 },
37 GetBitcoinRpcConfig,
39
40 NewDepositAddress,
41 Withdraw {
43 #[clap(long)]
44 amount: BitcoinAmountOrAll,
45 #[clap(long)]
46 address: bitcoin::Address<NetworkUnchecked>,
47 },
48 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}