Browse Source

Add `withdraw` subcommand to taker and maker

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).

Fee rate can either be passed by user or is set to default fee rate of `1.0`.
We enable RBF (replace by fee) signalling so that users can bump the transaction fee.
burn-down-handle
Daniel Karzel 3 years ago
parent
commit
50daebaed0
No known key found for this signature in database GPG Key ID: 30C3FC2E438ADB6E
  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 anyhow::{Context, Result};
use bdk::bitcoin;
use bdk::bitcoin::secp256k1::schnorrsig; use bdk::bitcoin::secp256k1::schnorrsig;
use clap::Parser; use bdk::{bitcoin, FeeRate};
use clap::{Parser, Subcommand};
use daemon::auth::{self, MAKER_USERNAME}; use daemon::auth::{self, MAKER_USERNAME};
use daemon::db::{self}; use daemon::db::{self};
@ -20,6 +20,7 @@ use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use bdk::bitcoin::Amount;
use std::task::Poll; use std::task::Poll;
use tokio::sync::watch; use tokio::sync::watch;
use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::filter::LevelFilter;
@ -66,27 +67,53 @@ enum Network {
/// URL to the electrum backend to use for the wallet. /// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:50002")] #[clap(long, default_value = "ssl://electrum.blockstream.info:50002")]
electrum: String, electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
}, },
/// Run on testnet. /// Run on testnet.
Testnet { Testnet {
/// URL to the electrum backend to use for the wallet. /// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:60002")] #[clap(long, default_value = "ssl://electrum.blockstream.info:60002")]
electrum: String, electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
}, },
/// Run on signet /// Run on signet
Signet { Signet {
/// URL to the electrum backend to use for the wallet. /// URL to the electrum backend to use for the wallet.
#[clap(long)] #[clap(long)]
electrum: String, 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 btc
/// per kvb, e.g. 0.00001
#[clap(long)]
fee: Option<f32>,
/// The address to receive the Bitcoin.
#[clap(long)]
address: bdk::bitcoin::Address,
}, },
} }
impl Network { impl Network {
fn electrum(&self) -> &str { fn electrum(&self) -> &str {
match self { match self {
Network::Mainnet { electrum } => electrum, Network::Mainnet { electrum, .. } => electrum,
Network::Testnet { electrum } => electrum, Network::Testnet { electrum, .. } => electrum,
Network::Signet { electrum } => electrum, Network::Signet { electrum, .. } => electrum,
} }
} }
@ -105,6 +132,14 @@ impl Network {
Network::Signet { .. } => base.join("signet"), 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] #[rocket::main]
@ -139,8 +174,28 @@ async fn main() -> Result<()> {
.create(None) .create(None)
.spawn_global(); .spawn_global();
// do this before withdraw to ensure the wallet is synced
let wallet_info = wallet.send(wallet::Sync).await??; 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_btc_per_kvb),
})
.await??;
tracing::info!(%txid, "Withdraw successful");
return Ok(());
}
let auth_password = seed.derive_auth_password::<auth::Password>(); let auth_password = seed.derive_auth_password::<auth::Password>();
tracing::info!( tracing::info!(

71
daemon/src/taker.rs

@ -1,24 +1,20 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use bdk::bitcoin;
use bdk::bitcoin::secp256k1::schnorrsig; 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::db::{self};
use daemon::model::WalletInfo; use daemon::model::WalletInfo;
use daemon::seed::Seed; use daemon::seed::Seed;
use daemon::{ use daemon::{
bitmex_price_feed, connection, housekeeping, logger, monitor, oracle, taker_cfd, wallet, bitmex_price_feed, connection, housekeeping, logger, monitor, oracle, taker_cfd, wallet,
wallet_sync, TakerActorSystem, wallet_sync, TakerActorSystem,
}; };
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use tokio::sync::watch; use tokio::sync::watch;
use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::filter::LevelFilter;
use xtra::prelude::MessageChannel; use xtra::prelude::MessageChannel;
@ -61,26 +57,52 @@ enum Network {
/// URL to the electrum backend to use for the wallet. /// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:50002")] #[clap(long, default_value = "ssl://electrum.blockstream.info:50002")]
electrum: String, electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
}, },
Testnet { Testnet {
/// URL to the electrum backend to use for the wallet. /// URL to the electrum backend to use for the wallet.
#[clap(long, default_value = "ssl://electrum.blockstream.info:60002")] #[clap(long, default_value = "ssl://electrum.blockstream.info:60002")]
electrum: String, electrum: String,
#[clap(subcommand)]
withdraw: Option<Withdraw>,
}, },
/// Run on signet /// Run on signet
Signet { Signet {
/// URL to the electrum backend to use for the wallet. /// URL to the electrum backend to use for the wallet.
#[clap(long)] #[clap(long)]
electrum: String, 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 btc
/// per kvb, e.g. 0.00001
#[clap(long)]
fee: Option<f32>,
/// The address to receive the Bitcoin.
#[clap(long)]
address: Address,
}, },
} }
impl Network { impl Network {
fn electrum(&self) -> &str { fn electrum(&self) -> &str {
match self { match self {
Network::Mainnet { electrum } => electrum, Network::Mainnet { electrum, .. } => electrum,
Network::Testnet { electrum } => electrum, Network::Testnet { electrum, .. } => electrum,
Network::Signet { electrum } => electrum, Network::Signet { electrum, .. } => electrum,
} }
} }
@ -99,6 +121,14 @@ impl Network {
Network::Signet { .. } => base.join("signet"), 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] #[rocket::main]
@ -132,8 +162,29 @@ async fn main() -> Result<()> {
.await? .await?
.create(None) .create(None)
.spawn_global(); .spawn_global();
// do this before withdraw to ensure the wallet is synced
let wallet_info = wallet.send(wallet::Sync).await??; 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_btc_per_kvb),
})
.await??;
tracing::info!(%txid, "Withdraw successful");
return Ok(());
}
// TODO: Actually fetch it from Olivia // TODO: Actually fetch it from Olivia
let oracle = schnorrsig::PublicKey::from_str( let oracle = schnorrsig::PublicKey::from_str(
"ddd4636845a90185991826be5a494cde9f4a6947b1727217afedc6292fa4caf7", "ddd4636845a90185991826be5a494cde9f4a6947b1727217afedc6292fa4caf7",

85
daemon/src/wallet.rs

@ -1,12 +1,12 @@
use crate::model::{Timestamp, WalletInfo}; use crate::model::{Timestamp, WalletInfo};
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use bdk::bitcoin::consensus::encode::serialize_hex; use bdk::bitcoin::consensus::encode::serialize_hex;
use bdk::bitcoin::util::bip32::ExtendedPrivKey; use bdk::bitcoin::util::bip32::ExtendedPrivKey;
use bdk::bitcoin::util::psbt::PartiallySignedTransaction; 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::blockchain::{ElectrumBlockchain, NoopProgress};
use bdk::wallet::AddressIndex; use bdk::wallet::AddressIndex;
use bdk::{electrum_client, KeychainKind, SignOptions}; use bdk::{electrum_client, FeeRate, KeychainKind, SignOptions};
use cfd_protocol::{PartyParams, WalletExt}; use cfd_protocol::{PartyParams, WalletExt};
use rocket::serde::json::Value; use rocket::serde::json::Value;
use std::path::Path; use std::path::Path;
@ -14,6 +14,8 @@ use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use xtra_productivity::xtra_productivity; use xtra_productivity::xtra_productivity;
const DUST_AMOUNT: u64 = 546;
#[derive(Clone)] #[derive(Clone)]
pub struct Actor { pub struct Actor {
wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::database::SqliteDatabase>>>, wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::database::SqliteDatabase>>>,
@ -46,6 +48,45 @@ impl Actor {
Ok(Self { wallet }) 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] #[xtra_productivity]
@ -133,6 +174,38 @@ impl Actor {
Ok(txid) 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 {} impl xtra::Actor for Actor {}
@ -152,6 +225,12 @@ pub struct TryBroadcastTransaction {
pub tx: Transaction, 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> { fn parse_rpc_protocol_error_code(error_value: &Value) -> Result<i64> {
let json = error_value let json = error_value
.as_str() .as_str()

Loading…
Cancel
Save