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, fees) = match amount {
105 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}