use std::str::FromStr as _;
use std::{ffi, iter};
use anyhow::bail;
use bitcoin::address::NetworkUnchecked;
use clap::Parser;
use fedimint_core::BitcoinAmountOrAll;
use fedimint_core::core::OperationId;
use fedimint_core::encoding::Encodable;
use futures::StreamExt;
use serde::Serialize;
use tracing::{debug, info};
use super::WalletClientModule;
use crate::WithdrawState;
use crate::api::WalletFederationApi;
use crate::client_db::TweakIdx;
#[derive(Parser, Serialize)]
enum Opts {
AwaitDeposit {
addr: Option<String>,
#[arg(long)]
operation_id: Option<OperationId>,
#[arg(long)]
tweak_idx: Option<TweakIdx>,
#[arg(long, default_value = "1")]
num: usize,
},
GetConsensusBlockCount,
GetBitcoinRpcKind {
peer_id: u16,
},
GetBitcoinRpcConfig,
NewDepositAddress,
Withdraw {
#[clap(long)]
amount: BitcoinAmountOrAll,
#[clap(long)]
address: bitcoin::Address<NetworkUnchecked>,
},
RecheckDepositAddress {
addr: Option<bitcoin::Address<NetworkUnchecked>>,
#[arg(long)]
operation_id: Option<OperationId>,
#[arg(long)]
tweak_idx: Option<TweakIdx>,
},
}
async fn await_deposit(
module: &WalletClientModule,
addr: Option<String>,
operation_id: Option<OperationId>,
tweak_idx: Option<TweakIdx>,
num: usize,
) -> anyhow::Result<()> {
if u32::from(addr.is_some())
+ u32::from(operation_id.is_some())
+ u32::from(tweak_idx.is_some())
!= 1
{
bail!("One and only one of the selector arguments must be set")
}
if let Some(tweak_idx) = tweak_idx {
module.await_num_deposits(tweak_idx, num).await?;
} else if let Some(operation_id) = operation_id {
module
.await_num_deposits_by_operation_id(operation_id, num)
.await?;
} else if let Some(addr) = addr {
if addr.len() == 64 {
eprintln!(
"Interpreting addr as an operation_id for backward compatibility. \
Use `--operation-id` from now on."
);
let operation_id = OperationId::from_str(&addr)?;
module
.await_num_deposits_by_operation_id(operation_id, num)
.await?;
} else {
let addr = bitcoin::Address::from_str(&addr)?;
module.await_num_deposits_by_address(addr, num).await?;
}
} else {
unreachable!()
}
Ok(())
}
async fn withdraw(
module: &WalletClientModule,
amount: BitcoinAmountOrAll,
address: bitcoin::Address<NetworkUnchecked>,
) -> anyhow::Result<serde_json::Value> {
let address = address.require_network(module.get_network())?;
let amount = match amount {
BitcoinAmountOrAll::All => {
bail!(
"The 'all' option is not supported in the module CLI. \
Use `fedimint-cli withdraw --amount all` instead."
);
}
BitcoinAmountOrAll::Amount(amount) => amount,
};
let fees = module.get_withdraw_fees(&address, amount).await?;
let absolute_fees = fees.amount();
info!("Attempting withdraw with fees: {fees:?}");
let operation_id = module.withdraw(&address, amount, fees, ()).await?;
let mut updates = module
.subscribe_withdraw_updates(operation_id)
.await?
.into_stream();
while let Some(update) = updates.next().await {
debug!(?update, "Withdraw state update");
match update {
WithdrawState::Succeeded(txid) => {
return Ok(serde_json::json!({
"txid": txid.consensus_encode_to_hex(),
"fees_sat": absolute_fees.to_sat(),
}));
}
WithdrawState::Failed(e) => {
bail!("Withdraw failed: {e}");
}
WithdrawState::Created => {}
}
}
unreachable!("Update stream ended without outcome");
}
pub(crate) async fn handle_cli_command(
module: &WalletClientModule,
args: &[ffi::OsString],
) -> anyhow::Result<serde_json::Value> {
let opts = Opts::parse_from(iter::once(&ffi::OsString::from("wallet")).chain(args.iter()));
let res = match opts {
Opts::AwaitDeposit {
operation_id,
num,
addr,
tweak_idx,
} => {
await_deposit(module, addr, operation_id, tweak_idx, num).await?;
serde_json::Value::Bool(true)
}
Opts::GetBitcoinRpcKind { peer_id } => {
let kind = module
.module_api
.fetch_bitcoin_rpc_kind(peer_id.into())
.await?;
serde_json::to_value(kind).expect("JSON serialization failed")
}
Opts::GetBitcoinRpcConfig => {
let auth = module
.admin_auth
.clone()
.ok_or(anyhow::anyhow!("Admin auth not set"))?;
serde_json::to_value(module.module_api.fetch_bitcoin_rpc_config(auth).await?)
.expect("JSON serialization failed")
}
Opts::GetConsensusBlockCount => {
serde_json::to_value(module.module_api.fetch_consensus_block_count().await?)
.expect("JSON serialization failed")
}
Opts::RecheckDepositAddress {
addr,
operation_id,
tweak_idx,
} => {
if u32::from(addr.is_some())
+ u32::from(operation_id.is_some())
+ u32::from(tweak_idx.is_some())
!= 1
{
bail!("One and only one of the selector arguments must be set")
}
if let Some(tweak_idx) = tweak_idx {
module.recheck_pegin_address(tweak_idx).await?;
} else if let Some(operation_id) = operation_id {
module.recheck_pegin_address_by_op_id(operation_id).await?;
} else if let Some(addr) = addr {
module.recheck_pegin_address_by_address(addr).await?;
} else {
unreachable!()
}
serde_json::Value::Bool(true)
}
Opts::NewDepositAddress => {
let deposit_address = module.allocate_deposit_address_expert_only(()).await?;
serde_json::json! {
{
"address": deposit_address.address,
"operation_id": deposit_address.operation_id,
"tweak_idx": deposit_address.tweak_idx.0
}
}
}
Opts::Withdraw { amount, address } => return withdraw(module, amount, address).await,
};
Ok(res)
}