1use std::str::FromStr as _;
2use std::{ffi, iter};
3
4use bitcoin::address::NetworkUnchecked;
5use clap::Parser;
6use fedimint_api_client::api::FederationError;
7use fedimint_client_module::error::{ModuleLookupError, TransactionSubmitError};
8use fedimint_core::BitcoinAmountOrAll;
9use fedimint_core::core::OperationId;
10use fedimint_core::encoding::Encodable;
11use futures::StreamExt;
12use serde::Serialize;
13use tracing::{debug, info};
14
15use super::WalletClientModule;
16use crate::api::WalletFederationApi;
17use crate::client_db::TweakIdx;
18use crate::{
19 DepositAddressError, MaxWithdrawableAmountError, PegInError, SubscribeWithdrawError,
20 WithdrawFeesError, WithdrawState,
21};
22
23#[derive(Parser, Serialize)]
24enum Opts {
25 AwaitDeposit {
27 addr: Option<String>,
28 #[arg(long)]
29 operation_id: Option<OperationId>,
30 #[arg(long)]
31 tweak_idx: Option<TweakIdx>,
32 #[arg(long, default_value = "1")]
34 num: usize,
35 },
36 GetConsensusBlockCount,
37 GetBitcoinRpcKind {
39 peer_id: u16,
40 },
41 GetBitcoinRpcConfig,
43
44 NewDepositAddress,
45 Withdraw {
47 #[clap(long)]
48 amount: BitcoinAmountOrAll,
49 #[clap(long)]
50 address: bitcoin::Address<NetworkUnchecked>,
51 },
52 RecheckDepositAddress {
54 addr: Option<bitcoin::Address<NetworkUnchecked>>,
55 #[arg(long)]
56 operation_id: Option<OperationId>,
57 #[arg(long)]
58 tweak_idx: Option<TweakIdx>,
59 },
60}
61
62async fn await_deposit(
63 module: &WalletClientModule,
64 addr: Option<String>,
65 operation_id: Option<OperationId>,
66 tweak_idx: Option<TweakIdx>,
67 num: usize,
68) -> Result<(), CliCommandError> {
69 if u32::from(addr.is_some())
70 + u32::from(operation_id.is_some())
71 + u32::from(tweak_idx.is_some())
72 != 1
73 {
74 return Err(CliCommandError::SelectorCount);
75 }
76 if let Some(tweak_idx) = tweak_idx {
77 module.await_num_deposits(tweak_idx, num).await?;
78 } else if let Some(operation_id) = operation_id {
79 module
80 .await_num_deposits_by_operation_id(operation_id, num)
81 .await?;
82 } else if let Some(addr) = addr {
83 if addr.len() == 64 {
84 eprintln!(
85 "Interpreting addr as an operation_id for backward compatibility. \
86 Use `--operation-id` from now on."
87 );
88 let operation_id = OperationId::from_str(&addr)?;
89 module
90 .await_num_deposits_by_operation_id(operation_id, num)
91 .await?;
92 } else {
93 let addr = bitcoin::Address::from_str(&addr)?;
94 module.await_num_deposits_by_address(addr, num).await?;
95 }
96 } else {
97 unreachable!()
98 }
99 Ok(())
100}
101
102async fn withdraw(
103 module: &WalletClientModule,
104 amount: BitcoinAmountOrAll,
105 address: bitcoin::Address<NetworkUnchecked>,
106) -> Result<serde_json::Value, CliCommandError> {
107 let address = address.require_network(module.get_network())?;
108 let (amount, fees) = match amount {
109 BitcoinAmountOrAll::All => {
114 let balance = module.client_ctx.get_balance_for_btc().await?;
115 module.max_withdrawable_amount(&address, balance).await?
116 }
117 BitcoinAmountOrAll::Amount(amount) => {
118 (amount, module.get_withdraw_fees(&address, amount).await?)
119 }
120 };
121 let absolute_fees = fees.amount();
122
123 info!("Attempting withdraw with fees: {fees:?}");
124
125 let operation_id = module.withdraw(&address, amount, fees, ()).await?;
126
127 let mut updates = module
128 .subscribe_withdraw_updates(operation_id)
129 .await?
130 .into_stream();
131
132 while let Some(update) = updates.next().await {
133 debug!(?update, "Withdraw state update");
134
135 match update {
136 WithdrawState::Succeeded(txid) => {
137 return Ok(serde_json::json!({
138 "txid": txid.consensus_encode_to_hex(),
139 "fees_sat": absolute_fees.to_sat(),
140 }));
141 }
142 WithdrawState::Failed(e) => {
143 return Err(CliCommandError::WithdrawFailed(e));
144 }
145 WithdrawState::Created => {}
146 }
147 }
148
149 unreachable!("Update stream ended without outcome");
150}
151
152pub(crate) async fn handle_cli_command(
153 module: &WalletClientModule,
154 args: &[ffi::OsString],
155) -> Result<serde_json::Value, CliCommandError> {
156 let opts = Opts::parse_from(iter::once(&ffi::OsString::from("wallet")).chain(args.iter()));
157
158 let res = match opts {
159 Opts::AwaitDeposit {
160 operation_id,
161 num,
162 addr,
163 tweak_idx,
164 } => {
165 await_deposit(module, addr, operation_id, tweak_idx, num).await?;
166 serde_json::Value::Bool(true)
167 }
168 Opts::GetBitcoinRpcKind { peer_id } => {
169 let kind = module
170 .module_api
171 .fetch_bitcoin_rpc_kind(peer_id.into())
172 .await?;
173 serde_json::to_value(kind).expect("JSON serialization failed")
174 }
175 Opts::GetBitcoinRpcConfig => {
176 let auth = module
177 .admin_auth
178 .clone()
179 .ok_or(CliCommandError::AdminAuthNotSet)?;
180 serde_json::to_value(module.module_api.fetch_bitcoin_rpc_config(auth).await?)
181 .expect("JSON serialization failed")
182 }
183 Opts::GetConsensusBlockCount => {
184 serde_json::to_value(module.module_api.fetch_consensus_block_count().await?)
185 .expect("JSON serialization failed")
186 }
187 Opts::RecheckDepositAddress {
188 addr,
189 operation_id,
190 tweak_idx,
191 } => {
192 if u32::from(addr.is_some())
193 + u32::from(operation_id.is_some())
194 + u32::from(tweak_idx.is_some())
195 != 1
196 {
197 return Err(CliCommandError::SelectorCount);
198 }
199 if let Some(tweak_idx) = tweak_idx {
200 module.recheck_pegin_address(tweak_idx).await?;
201 } else if let Some(operation_id) = operation_id {
202 module.recheck_pegin_address_by_op_id(operation_id).await?;
203 } else if let Some(addr) = addr {
204 module.recheck_pegin_address_by_address(addr).await?;
205 } else {
206 unreachable!()
207 }
208 serde_json::Value::Bool(true)
209 }
210 Opts::NewDepositAddress => {
211 let deposit_address = module.allocate_deposit_address_expert_only(()).await?;
212 serde_json::json! {
213 {
214 "address": deposit_address.address,
215 "operation_id": deposit_address.operation_id,
216 "tweak_idx": deposit_address.tweak_idx.0
217 }
218 }
219 }
220 Opts::Withdraw { amount, address } => return withdraw(module, amount, address).await,
221 };
222
223 Ok(res)
224}
225
226#[derive(Debug, thiserror::Error)]
228pub(crate) enum CliCommandError {
229 #[error("One and only one of the selector arguments must be set")]
232 SelectorCount,
233
234 #[error(transparent)]
236 PegIn(#[from] PegInError),
237
238 #[error(transparent)]
240 OperationId(#[from] fedimint_core::hex::FromHexError),
241
242 #[error(transparent)]
244 Address(#[from] bitcoin::address::ParseError),
245
246 #[error(transparent)]
248 Balance(#[from] ModuleLookupError),
249
250 #[error(transparent)]
252 MaxWithdrawable(#[from] MaxWithdrawableAmountError),
253
254 #[error(transparent)]
256 WithdrawFees(#[from] WithdrawFeesError),
257
258 #[error(transparent)]
260 Withdraw(#[from] TransactionSubmitError),
261
262 #[error(transparent)]
264 SubscribeWithdraw(#[from] SubscribeWithdrawError),
265
266 #[error("Withdraw failed: {0}")]
268 WithdrawFailed(String),
269
270 #[error(transparent)]
272 Federation(#[from] FederationError),
273
274 #[error("Admin auth not set")]
277 AdminAuthNotSet,
278
279 #[error(transparent)]
281 DepositAddress(#[from] DepositAddressError),
282}