Browse Source

Merge #462

462: Add `withdraw` subcommand to taker and maker r=da-kami a=da-kami

Withdraw is an optional subcommand of the network subcommand, so the command reads like this, e.g. taker:
`./taker mainnet withdraw --address ...`

Internally, we use the wallet actor for withdrawing for now (simplifies the implementation, otherwise we would have to extract the wallet construction outside the actor).

ToDo inside the wallet actor:

- [x] wallet drain if no amount is given
- [x] construct transaction based on amount and address
- [x] broadcast transaction and return `Txid`

Co-authored-by: Daniel Karzel <daniel@comit.network>
burn-down-handle
bors[bot] 3 years ago
committed by GitHub
parent
commit
edd584e967
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      daemon/src/maker.rs
  2. 71
      daemon/src/taker.rs
  3. 85
      daemon/src/wallet.rs

65
daemon/src/maker.rs

@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use bdk::bitcoin;
use bdk::bitcoin::secp256k1::schnorrsig;
use clap::Parser;
use bdk::{bitcoin, FeeRate};
use clap::{Parser, Subcommand};
use daemon::auth::{self, MAKER_USERNAME};
use daemon::db::{self};
@ -20,6 +20,7 @@ use std::net::SocketAddr;
use std::path::PathBuf;
use std::str::FromStr;
use bdk::bitcoin::Amount;
use std::task::Poll;
use tokio::sync::watch;
use tracing_subscriber::filter::LevelFilter;
@ -66,27 +67,53 @@ enum Network {
/// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:50002")]
electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
},
/// Run on testnet.
Testnet {
/// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:60002")]
electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
},
/// Run on signet
Signet {
/// URL to the electrum backend to use for the wallet.
#[clap(long)]
electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
},
}
#[derive(Subcommand)]
enum Withdraw {
Withdraw {
/// Optionally specify the amount of Bitcoin to be withdrawn. If not specified the wallet
/// will be drained. Amount is to be specified with denomination, e.g. "0.1 BTC"
#[clap(long)]
amount: Option<Amount>,
/// Optionally specify the fee-rate for the transaction. The fee-rate is specified as sats
/// per vbyte, e.g. 5.0
#[clap(long)]
fee: Option<f32>,
/// The address to receive the Bitcoin.
#[clap(long)]
address: bdk::bitcoin::Address,
},
}
impl Network {
fn electrum(&self) -> &str {
match self {
Network::Mainnet { electrum } => electrum,
Network::Testnet { electrum } => electrum,
Network::Signet { electrum } => electrum,
Network::Mainnet { electrum, .. } => electrum,
Network::Testnet { electrum, .. } => electrum,
Network::Signet { electrum, .. } => electrum,
}
}
@ -105,6 +132,14 @@ impl Network {
Network::Signet { .. } => base.join("signet"),
}
}
fn withdraw(&self) -> &Option<Withdraw> {
match self {
Network::Mainnet { withdraw, .. } => withdraw,
Network::Testnet { withdraw, .. } => withdraw,
Network::Signet { withdraw, .. } => withdraw,
}
}
}
#[rocket::main]
@ -139,8 +174,28 @@ async fn main() -> Result<()> {
.create(None)
.spawn_global();
// do this before withdraw to ensure the wallet is synced
let wallet_info = wallet.send(wallet::Sync).await??;
if let Some(Withdraw::Withdraw {
amount,
address,
fee,
}) = opts.network.withdraw()
{
let txid = wallet
.send(wallet::Withdraw {
amount: *amount,
address: address.clone(),
fee: fee.map(FeeRate::from_sat_per_vb),
})
.await??;
tracing::info!(%txid, "Withdraw successful");
return Ok(());
}
let auth_password = seed.derive_auth_password::<auth::Password>();
tracing::info!(

71
daemon/src/taker.rs

@ -1,24 +1,20 @@
use anyhow::{Context, Result};
use bdk::bitcoin;
use bdk::bitcoin::secp256k1::schnorrsig;
use clap::Parser;
use bdk::bitcoin::{Address, Amount};
use bdk::{bitcoin, FeeRate};
use clap::{Parser, Subcommand};
use daemon::db::{self};
use daemon::model::WalletInfo;
use daemon::seed::Seed;
use daemon::{
bitmex_price_feed, connection, housekeeping, logger, monitor, oracle, taker_cfd, wallet,
wallet_sync, TakerActorSystem,
};
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::SqlitePool;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::str::FromStr;
use tokio::sync::watch;
use tracing_subscriber::filter::LevelFilter;
use xtra::prelude::MessageChannel;
@ -61,26 +57,52 @@ enum Network {
/// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:50002")]
electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
},
Testnet {
/// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:60002")]
electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
},
/// Run on signet
Signet {
/// URL to the electrum backend to use for the wallet.
#[clap(long)]
electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
},
}
#[derive(Subcommand)]
enum Withdraw {
Withdraw {
/// Optionally specify the amount of Bitcoin to be withdrawn. If not specified the wallet
/// will be drained. Amount is to be specified with denomination, e.g. "0.1 BTC"
#[clap(long)]
amount: Option<Amount>,
/// Optionally specify the fee-rate for the transaction. The fee-rate is specified as sats
/// per vbyte, e.g. 5.0
#[clap(long)]
fee: Option<f32>,
/// The address to receive the Bitcoin.
#[clap(long)]
address: Address,
},
}
impl Network {
fn electrum(&self) -> &str {
match self {
Network::Mainnet { electrum } => electrum,
Network::Testnet { electrum } => electrum,
Network::Signet { electrum } => electrum,
Network::Mainnet { electrum, .. } => electrum,
Network::Testnet { electrum, .. } => electrum,
Network::Signet { electrum, .. } => electrum,
}
}
@ -99,6 +121,14 @@ impl Network {
Network::Signet { .. } => base.join("signet"),
}
}
fn withdraw(&self) -> &Option<Withdraw> {
match self {
Network::Mainnet { withdraw, .. } => withdraw,
Network::Testnet { withdraw, .. } => withdraw,
Network::Signet { withdraw, .. } => withdraw,
}
}
}
#[rocket::main]
@ -132,8 +162,29 @@ async fn main() -> Result<()> {
.await?
.create(None)
.spawn_global();
// do this before withdraw to ensure the wallet is synced
let wallet_info = wallet.send(wallet::Sync).await??;
if let Some(Withdraw::Withdraw {
amount,
address,
fee,
}) = opts.network.withdraw()
{
let txid = wallet
.send(wallet::Withdraw {
amount: *amount,
address: address.clone(),
fee: fee.map(FeeRate::from_sat_per_vb),
})
.await??;
tracing::info!(%txid, "Withdraw successful");
return Ok(());
}
// TODO: Actually fetch it from Olivia
let oracle = schnorrsig::PublicKey::from_str(
"ddd4636845a90185991826be5a494cde9f4a6947b1727217afedc6292fa4caf7",

85
daemon/src/wallet.rs

@ -1,12 +1,12 @@
use crate::model::{Timestamp, WalletInfo};
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use bdk::bitcoin::consensus::encode::serialize_hex;
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{Amount, PublicKey, Transaction, Txid};
use bdk::bitcoin::{Address, Amount, PublicKey, Script, Transaction, Txid};
use bdk::blockchain::{ElectrumBlockchain, NoopProgress};
use bdk::wallet::AddressIndex;
use bdk::{electrum_client, KeychainKind, SignOptions};
use bdk::{electrum_client, FeeRate, KeychainKind, SignOptions};
use cfd_protocol::{PartyParams, WalletExt};
use rocket::serde::json::Value;
use std::path::Path;
@ -14,6 +14,8 @@ use std::sync::Arc;
use tokio::sync::Mutex;
use xtra_productivity::xtra_productivity;
const DUST_AMOUNT: u64 = 546;
#[derive(Clone)]
pub struct Actor {
wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::database::SqliteDatabase>>>,
@ -46,6 +48,45 @@ impl Actor {
Ok(Self { wallet })
}
/// Calculates the maximum "giveable" amount of this wallet.
///
/// We define this as the maximum amount we can pay to a single output,
/// given a fee rate.
pub async fn max_giveable(
&self,
locking_script_size: usize,
fee_rate: FeeRate,
) -> Result<Amount> {
let wallet = self.wallet.lock().await;
let balance = wallet.get_balance()?;
// TODO: Do we have to deal with the min_relay_fee here as well, i.e. if balance below
// min_relay_fee we should return Amount::ZERO?
if balance < DUST_AMOUNT {
return Ok(Amount::ZERO);
}
let mut tx_builder = wallet.build_tx();
let dummy_script = Script::from(vec![0u8; locking_script_size]);
tx_builder.drain_to(dummy_script);
tx_builder.fee_rate(fee_rate);
tx_builder.drain_wallet();
let response = tx_builder.finish();
match response {
Ok((_, details)) => {
let max_giveable = details.sent
- details
.fee
.expect("fees are always present with Electrum backend");
Ok(Amount::from_sat(max_giveable))
}
Err(bdk::Error::InsufficientFunds { .. }) => Ok(Amount::ZERO),
Err(e) => bail!("Failed to build transaction. {:#}", e),
}
}
}
#[xtra_productivity]
@ -133,6 +174,38 @@ impl Actor {
Ok(txid)
}
pub async fn handle_withdraw(&self, msg: Withdraw) -> Result<Txid> {
let fee_rate = msg.fee.unwrap_or_else(FeeRate::default_min_relay_fee);
let address = msg.address;
let amount = if let Some(amount) = msg.amount {
amount
} else {
self.max_giveable(address.script_pubkey().len(), fee_rate)
.await
.context("Unable to drain wallet")?
};
tracing::info!(%amount, %address, "Amount to be sent to address");
let wallet = self.wallet.lock().await;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(address.script_pubkey(), amount.as_sat())
.fee_rate(fee_rate)
// Turn on RBF signaling
.enable_rbf();
let (mut psbt, _) = tx_builder.finish()?;
wallet.sign(&mut psbt, SignOptions::default())?;
let txid = wallet.broadcast(psbt.extract_tx())?;
Ok(txid)
}
}
impl xtra::Actor for Actor {}
@ -152,6 +225,12 @@ pub struct TryBroadcastTransaction {
pub tx: Transaction,
}
pub struct Withdraw {
pub amount: Option<Amount>,
pub fee: Option<FeeRate>,
pub address: Address,
}
fn parse_rpc_protocol_error_code(error_value: &Value) -> Result<i64> {
let json = error_value
.as_str()

Loading…
Cancel
Save