mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-25 15:39:25 -05:00
Merge #307
307: Reduce load on electrum r=thomaseizinger a=rishflab . Co-authored-by: rishflab <rishflab@hotmail.com> Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
commit
95acbc6277
@ -12,7 +12,6 @@ use tracing::info;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939";
|
const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939";
|
||||||
const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/";
|
|
||||||
const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002";
|
const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002";
|
||||||
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
|
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
|
||||||
|
|
||||||
@ -52,7 +51,6 @@ pub struct Network {
|
|||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Bitcoin {
|
pub struct Bitcoin {
|
||||||
pub electrum_http_url: Url,
|
|
||||||
pub electrum_rpc_url: Url,
|
pub electrum_rpc_url: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,12 +118,6 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
|||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
let listen_address = listen_address.as_str().parse()?;
|
let listen_address = listen_address.as_str().parse()?;
|
||||||
|
|
||||||
let electrum_http_url: String = Input::with_theme(&ColorfulTheme::default())
|
|
||||||
.with_prompt("Enter Electrum HTTP URL or hit return to use default")
|
|
||||||
.default(DEFAULT_ELECTRUM_HTTP_URL.to_owned())
|
|
||||||
.interact_text()?;
|
|
||||||
let electrum_http_url = Url::parse(electrum_http_url.as_str())?;
|
|
||||||
|
|
||||||
let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default())
|
let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt("Enter Electrum RPC URL or hit return to use default")
|
.with_prompt("Enter Electrum RPC URL or hit return to use default")
|
||||||
.default(DEFAULT_ELECTRUM_RPC_URL.to_owned())
|
.default(DEFAULT_ELECTRUM_RPC_URL.to_owned())
|
||||||
@ -144,10 +136,7 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
|||||||
network: Network {
|
network: Network {
|
||||||
listen: listen_address,
|
listen: listen_address,
|
||||||
},
|
},
|
||||||
bitcoin: Bitcoin {
|
bitcoin: Bitcoin { electrum_rpc_url },
|
||||||
electrum_http_url,
|
|
||||||
electrum_rpc_url,
|
|
||||||
},
|
|
||||||
monero: Monero {
|
monero: Monero {
|
||||||
wallet_rpc_url: monero_wallet_rpc_url,
|
wallet_rpc_url: monero_wallet_rpc_url,
|
||||||
},
|
},
|
||||||
@ -170,7 +159,6 @@ mod tests {
|
|||||||
dir: Default::default(),
|
dir: Default::default(),
|
||||||
},
|
},
|
||||||
bitcoin: Bitcoin {
|
bitcoin: Bitcoin {
|
||||||
electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(),
|
|
||||||
electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
|
electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
|
||||||
},
|
},
|
||||||
network: Network {
|
network: Network {
|
||||||
|
@ -152,8 +152,8 @@ async fn init_wallets(
|
|||||||
) -> Result<(bitcoin::Wallet, monero::Wallet)> {
|
) -> Result<(bitcoin::Wallet, monero::Wallet)> {
|
||||||
let bitcoin_wallet = bitcoin::Wallet::new(
|
let bitcoin_wallet = bitcoin::Wallet::new(
|
||||||
config.bitcoin.electrum_rpc_url,
|
config.bitcoin.electrum_rpc_url,
|
||||||
config.bitcoin.electrum_http_url,
|
|
||||||
BITCOIN_NETWORK,
|
BITCOIN_NETWORK,
|
||||||
|
execution_params.bitcoin_finality_confirmations,
|
||||||
bitcoin_wallet_data_dir,
|
bitcoin_wallet_data_dir,
|
||||||
key,
|
key,
|
||||||
)
|
)
|
||||||
|
@ -21,9 +21,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use swap::bitcoin::{Amount, TxLock};
|
use swap::bitcoin::{Amount, TxLock};
|
||||||
use swap::cli::command::{
|
use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams};
|
||||||
AliceConnectParams, Arguments, BitcoinParams, Command, Data, MoneroParams,
|
|
||||||
};
|
|
||||||
use swap::database::Database;
|
use swap::database::Database;
|
||||||
use swap::execution_params::{ExecutionParams, GetExecutionParams};
|
use swap::execution_params::{ExecutionParams, GetExecutionParams};
|
||||||
use swap::network::quote::BidQuote;
|
use swap::network::quote::BidQuote;
|
||||||
@ -95,11 +93,7 @@ async fn main() -> Result<()> {
|
|||||||
receive_monero_address,
|
receive_monero_address,
|
||||||
monero_daemon_host,
|
monero_daemon_host,
|
||||||
},
|
},
|
||||||
bitcoin_params:
|
electrum_rpc_url,
|
||||||
BitcoinParams {
|
|
||||||
electrum_http_url,
|
|
||||||
electrum_rpc_url,
|
|
||||||
},
|
|
||||||
} => {
|
} => {
|
||||||
if receive_monero_address.network != monero_network {
|
if receive_monero_address.network != monero_network {
|
||||||
bail!(
|
bail!(
|
||||||
@ -112,9 +106,9 @@ async fn main() -> Result<()> {
|
|||||||
let bitcoin_wallet = init_bitcoin_wallet(
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
bitcoin_network,
|
bitcoin_network,
|
||||||
electrum_rpc_url,
|
electrum_rpc_url,
|
||||||
electrum_http_url,
|
|
||||||
seed,
|
seed,
|
||||||
data_dir.clone(),
|
data_dir.clone(),
|
||||||
|
execution_params,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let (monero_wallet, _process) = init_monero_wallet(
|
let (monero_wallet, _process) = init_monero_wallet(
|
||||||
@ -196,11 +190,7 @@ async fn main() -> Result<()> {
|
|||||||
receive_monero_address,
|
receive_monero_address,
|
||||||
monero_daemon_host,
|
monero_daemon_host,
|
||||||
},
|
},
|
||||||
bitcoin_params:
|
electrum_rpc_url,
|
||||||
BitcoinParams {
|
|
||||||
electrum_http_url,
|
|
||||||
electrum_rpc_url,
|
|
||||||
},
|
|
||||||
} => {
|
} => {
|
||||||
if receive_monero_address.network != monero_network {
|
if receive_monero_address.network != monero_network {
|
||||||
bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network)
|
bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network)
|
||||||
@ -209,9 +199,9 @@ async fn main() -> Result<()> {
|
|||||||
let bitcoin_wallet = init_bitcoin_wallet(
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
bitcoin_network,
|
bitcoin_network,
|
||||||
electrum_rpc_url,
|
electrum_rpc_url,
|
||||||
electrum_http_url,
|
|
||||||
seed,
|
seed,
|
||||||
data_dir.clone(),
|
data_dir.clone(),
|
||||||
|
execution_params,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let (monero_wallet, _process) = init_monero_wallet(
|
let (monero_wallet, _process) = init_monero_wallet(
|
||||||
@ -255,18 +245,14 @@ async fn main() -> Result<()> {
|
|||||||
Command::Cancel {
|
Command::Cancel {
|
||||||
swap_id,
|
swap_id,
|
||||||
force,
|
force,
|
||||||
bitcoin_params:
|
electrum_rpc_url,
|
||||||
BitcoinParams {
|
|
||||||
electrum_http_url,
|
|
||||||
electrum_rpc_url,
|
|
||||||
},
|
|
||||||
} => {
|
} => {
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
bitcoin_network,
|
bitcoin_network,
|
||||||
electrum_rpc_url,
|
electrum_rpc_url,
|
||||||
electrum_http_url,
|
|
||||||
seed,
|
seed,
|
||||||
data_dir,
|
data_dir,
|
||||||
|
execution_params,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -290,32 +276,20 @@ async fn main() -> Result<()> {
|
|||||||
Command::Refund {
|
Command::Refund {
|
||||||
swap_id,
|
swap_id,
|
||||||
force,
|
force,
|
||||||
bitcoin_params:
|
electrum_rpc_url,
|
||||||
BitcoinParams {
|
|
||||||
electrum_http_url,
|
|
||||||
electrum_rpc_url,
|
|
||||||
},
|
|
||||||
} => {
|
} => {
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(
|
let bitcoin_wallet = init_bitcoin_wallet(
|
||||||
bitcoin_network,
|
bitcoin_network,
|
||||||
electrum_rpc_url,
|
electrum_rpc_url,
|
||||||
electrum_http_url,
|
|
||||||
seed,
|
seed,
|
||||||
data_dir,
|
data_dir,
|
||||||
|
execution_params,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
||||||
|
|
||||||
bob::refund(
|
bob::refund(swap_id, resume_state, Arc::new(bitcoin_wallet), db, force).await??;
|
||||||
swap_id,
|
|
||||||
resume_state,
|
|
||||||
execution_params,
|
|
||||||
Arc::new(bitcoin_wallet),
|
|
||||||
db,
|
|
||||||
force,
|
|
||||||
)
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -324,16 +298,16 @@ async fn main() -> Result<()> {
|
|||||||
async fn init_bitcoin_wallet(
|
async fn init_bitcoin_wallet(
|
||||||
network: bitcoin::Network,
|
network: bitcoin::Network,
|
||||||
electrum_rpc_url: Url,
|
electrum_rpc_url: Url,
|
||||||
electrum_http_url: Url,
|
|
||||||
seed: Seed,
|
seed: Seed,
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
|
execution_params: ExecutionParams,
|
||||||
) -> Result<bitcoin::Wallet> {
|
) -> Result<bitcoin::Wallet> {
|
||||||
let wallet_dir = data_dir.join("wallet");
|
let wallet_dir = data_dir.join("wallet");
|
||||||
|
|
||||||
let wallet = bitcoin::Wallet::new(
|
let wallet = bitcoin::Wallet::new(
|
||||||
electrum_rpc_url.clone(),
|
electrum_rpc_url.clone(),
|
||||||
electrum_http_url.clone(),
|
|
||||||
network,
|
network,
|
||||||
|
execution_params.bitcoin_finality_confirmations,
|
||||||
&wallet_dir,
|
&wallet_dir,
|
||||||
seed.derive_extended_private_key(network)?,
|
seed.derive_extended_private_key(network)?,
|
||||||
)
|
)
|
||||||
|
@ -20,6 +20,7 @@ pub use ecdsa_fun::fun::Scalar;
|
|||||||
pub use ecdsa_fun::Signature;
|
pub use ecdsa_fun::Signature;
|
||||||
pub use wallet::Wallet;
|
pub use wallet::Wallet;
|
||||||
|
|
||||||
|
use crate::bitcoin::wallet::ScriptStatus;
|
||||||
use ::bitcoin::hashes::hex::ToHex;
|
use ::bitcoin::hashes::hex::ToHex;
|
||||||
use ::bitcoin::hashes::Hash;
|
use ::bitcoin::hashes::Hash;
|
||||||
use ::bitcoin::{secp256k1, SigHash};
|
use ::bitcoin::{secp256k1, SigHash};
|
||||||
@ -110,6 +111,15 @@ impl From<PublicKey> for Point {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PublicKey> for ::bitcoin::PublicKey {
|
||||||
|
fn from(from: PublicKey) -> Self {
|
||||||
|
::bitcoin::PublicKey {
|
||||||
|
compressed: true,
|
||||||
|
key: from.0.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Point> for PublicKey {
|
impl From<Point> for PublicKey {
|
||||||
fn from(p: Point) -> Self {
|
fn from(p: Point) -> Self {
|
||||||
Self(p)
|
Self(p)
|
||||||
@ -209,46 +219,21 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu
|
|||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn poll_until_block_height_is_gte(
|
pub fn current_epoch(
|
||||||
client: &crate::bitcoin::Wallet,
|
|
||||||
target: BlockHeight,
|
|
||||||
) -> Result<()> {
|
|
||||||
while client.get_block_height().await? < target {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn current_epoch(
|
|
||||||
bitcoin_wallet: &crate::bitcoin::Wallet,
|
|
||||||
cancel_timelock: CancelTimelock,
|
cancel_timelock: CancelTimelock,
|
||||||
punish_timelock: PunishTimelock,
|
punish_timelock: PunishTimelock,
|
||||||
lock_tx_id: ::bitcoin::Txid,
|
tx_lock_status: ScriptStatus,
|
||||||
) -> Result<ExpiredTimelocks> {
|
tx_cancel_status: ScriptStatus,
|
||||||
let current_block_height = bitcoin_wallet.get_block_height().await?;
|
) -> ExpiredTimelocks {
|
||||||
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
|
if tx_cancel_status.is_confirmed_with(punish_timelock) {
|
||||||
let cancel_timelock_height = lock_tx_height + cancel_timelock;
|
return ExpiredTimelocks::Punish;
|
||||||
let punish_timelock_height = cancel_timelock_height + punish_timelock;
|
|
||||||
|
|
||||||
match (
|
|
||||||
current_block_height < cancel_timelock_height,
|
|
||||||
current_block_height < punish_timelock_height,
|
|
||||||
) {
|
|
||||||
(true, _) => Ok(ExpiredTimelocks::None),
|
|
||||||
(false, true) => Ok(ExpiredTimelocks::Cancel),
|
|
||||||
(false, false) => Ok(ExpiredTimelocks::Punish),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wait_for_cancel_timelock_to_expire(
|
if tx_lock_status.is_confirmed_with(cancel_timelock) {
|
||||||
bitcoin_wallet: &crate::bitcoin::Wallet,
|
return ExpiredTimelocks::Cancel;
|
||||||
cancel_timelock: CancelTimelock,
|
}
|
||||||
lock_tx_id: ::bitcoin::Txid,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
|
|
||||||
|
|
||||||
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?;
|
ExpiredTimelocks::None
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||||
@ -266,3 +251,53 @@ pub struct EmptyWitnessStack;
|
|||||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||||
#[error("input has {0} witnesses, expected 3")]
|
#[error("input has {0} witnesses, expected 3")]
|
||||||
pub struct NotThreeWitnesses(usize);
|
pub struct NotThreeWitnesses(usize);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() {
|
||||||
|
let tx_lock_status = ScriptStatus::from_confirmations(4);
|
||||||
|
let tx_cancel_status = ScriptStatus::Unseen;
|
||||||
|
|
||||||
|
let expired_timelock = current_epoch(
|
||||||
|
CancelTimelock::new(5),
|
||||||
|
PunishTimelock::new(5),
|
||||||
|
tx_lock_status,
|
||||||
|
tx_cancel_status,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(expired_timelock, ExpiredTimelocks::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() {
|
||||||
|
let tx_lock_status = ScriptStatus::from_confirmations(5);
|
||||||
|
let tx_cancel_status = ScriptStatus::Unseen;
|
||||||
|
|
||||||
|
let expired_timelock = current_epoch(
|
||||||
|
CancelTimelock::new(5),
|
||||||
|
PunishTimelock::new(5),
|
||||||
|
tx_lock_status,
|
||||||
|
tx_cancel_status,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(expired_timelock, ExpiredTimelocks::Cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() {
|
||||||
|
let tx_lock_status = ScriptStatus::from_confirmations(10);
|
||||||
|
let tx_cancel_status = ScriptStatus::from_confirmations(5);
|
||||||
|
|
||||||
|
let expired_timelock = current_epoch(
|
||||||
|
CancelTimelock::new(5),
|
||||||
|
PunishTimelock::new(5),
|
||||||
|
tx_lock_status,
|
||||||
|
tx_cancel_status,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(expired_timelock, ExpiredTimelocks::Punish)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::bitcoin::wallet::Watchable;
|
||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
|
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
|
||||||
TX_FEE,
|
TX_FEE,
|
||||||
@ -5,9 +6,11 @@ use crate::bitcoin::{
|
|||||||
use ::bitcoin::util::bip143::SigHashCache;
|
use ::bitcoin::util::bip143::SigHashCache;
|
||||||
use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
|
use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use bitcoin::Script;
|
||||||
use ecdsa_fun::Signature;
|
use ecdsa_fun::Signature;
|
||||||
use miniscript::{Descriptor, DescriptorTrait};
|
use miniscript::{Descriptor, DescriptorTrait};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
|
|
||||||
@ -33,6 +36,18 @@ impl Add<CancelTimelock> for BlockHeight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialOrd<CancelTimelock> for u32 {
|
||||||
|
fn partial_cmp(&self, other: &CancelTimelock) -> Option<Ordering> {
|
||||||
|
self.partial_cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<CancelTimelock> for u32 {
|
||||||
|
fn eq(&self, other: &CancelTimelock) -> bool {
|
||||||
|
self.eq(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represent a timelock, expressed in relative block height as defined in
|
/// Represent a timelock, expressed in relative block height as defined in
|
||||||
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
|
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
|
||||||
/// E.g. The timelock expires 10 blocks after the reference transaction is
|
/// E.g. The timelock expires 10 blocks after the reference transaction is
|
||||||
@ -55,7 +70,19 @@ impl Add<PunishTimelock> for BlockHeight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
impl PartialOrd<PunishTimelock> for u32 {
|
||||||
|
fn partial_cmp(&self, other: &PunishTimelock) -> Option<Ordering> {
|
||||||
|
self.partial_cmp(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<PunishTimelock> for u32 {
|
||||||
|
fn eq(&self, other: &PunishTimelock) -> bool {
|
||||||
|
self.eq(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct TxCancel {
|
pub struct TxCancel {
|
||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
digest: SigHash,
|
digest: SigHash,
|
||||||
@ -180,3 +207,13 @@ impl TxCancel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Watchable for TxCancel {
|
||||||
|
fn id(&self) -> Txid {
|
||||||
|
self.txid()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(&self) -> Script {
|
||||||
|
self.output_descriptor.script_pubkey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
use crate::bitcoin::wallet::Watchable;
|
||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE,
|
build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE,
|
||||||
};
|
};
|
||||||
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use bitcoin::Script;
|
||||||
use ecdsa_fun::fun::Point;
|
use ecdsa_fun::fun::Point;
|
||||||
use miniscript::{Descriptor, DescriptorTrait};
|
use miniscript::{Descriptor, DescriptorTrait};
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
@ -55,6 +57,10 @@ impl TxLock {
|
|||||||
.len()
|
.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn script_pubkey(&self) -> Script {
|
||||||
|
self.output_descriptor.script_pubkey()
|
||||||
|
}
|
||||||
|
|
||||||
/// Retreive the index of the locked output in the transaction outputs
|
/// Retreive the index of the locked output in the transaction outputs
|
||||||
/// vector
|
/// vector
|
||||||
fn lock_output_vout(&self) -> usize {
|
fn lock_output_vout(&self) -> usize {
|
||||||
@ -100,3 +106,13 @@ impl From<TxLock> for PartiallySignedTransaction {
|
|||||||
from.inner
|
from.inner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Watchable for TxLock {
|
||||||
|
fn id(&self) -> Txid {
|
||||||
|
self.txid()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(&self) -> Script {
|
||||||
|
self.output_descriptor.script_pubkey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use crate::bitcoin::{Address, PublicKey, PunishTimelock, Transaction, TxCancel};
|
use crate::bitcoin::wallet::Watchable;
|
||||||
|
use crate::bitcoin::{self, Address, PunishTimelock, Transaction, TxCancel, Txid};
|
||||||
use ::bitcoin::util::bip143::SigHashCache;
|
use ::bitcoin::util::bip143::SigHashCache;
|
||||||
use ::bitcoin::{SigHash, SigHashType};
|
use ::bitcoin::{SigHash, SigHashType};
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use ecdsa_fun::Signature;
|
use bdk::bitcoin::Script;
|
||||||
use miniscript::{Descriptor, DescriptorTrait};
|
use miniscript::{Descriptor, DescriptorTrait};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ pub struct TxPunish {
|
|||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
digest: SigHash,
|
digest: SigHash,
|
||||||
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||||
|
watch_script: Script,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TxPunish {
|
impl TxPunish {
|
||||||
@ -32,6 +34,7 @@ impl TxPunish {
|
|||||||
inner: tx_punish,
|
inner: tx_punish,
|
||||||
digest,
|
digest,
|
||||||
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
|
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
|
||||||
|
watch_script: punish_address.script_pubkey(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,22 +42,20 @@ impl TxPunish {
|
|||||||
self.digest
|
self.digest
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_signatures(
|
pub fn complete(
|
||||||
self,
|
self,
|
||||||
(A, sig_a): (PublicKey, Signature),
|
tx_punish_sig_bob: bitcoin::Signature,
|
||||||
(B, sig_b): (PublicKey, Signature),
|
a: bitcoin::SecretKey,
|
||||||
|
B: bitcoin::PublicKey,
|
||||||
) -> Result<Transaction> {
|
) -> Result<Transaction> {
|
||||||
|
let sig_a = a.sign(self.digest());
|
||||||
|
let sig_b = tx_punish_sig_bob;
|
||||||
|
|
||||||
let satisfier = {
|
let satisfier = {
|
||||||
let mut satisfier = HashMap::with_capacity(2);
|
let mut satisfier = HashMap::with_capacity(2);
|
||||||
|
|
||||||
let A = ::bitcoin::PublicKey {
|
let A = a.public().into();
|
||||||
compressed: true,
|
let B = B.into();
|
||||||
key: A.0.into(),
|
|
||||||
};
|
|
||||||
let B = ::bitcoin::PublicKey {
|
|
||||||
compressed: true,
|
|
||||||
key: B.0.into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// The order in which these are inserted doesn't matter
|
// The order in which these are inserted doesn't matter
|
||||||
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
|
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
|
||||||
@ -65,8 +66,19 @@ impl TxPunish {
|
|||||||
|
|
||||||
let mut tx_punish = self.inner;
|
let mut tx_punish = self.inner;
|
||||||
self.cancel_output_descriptor
|
self.cancel_output_descriptor
|
||||||
.satisfy(&mut tx_punish.input[0], satisfier)?;
|
.satisfy(&mut tx_punish.input[0], satisfier)
|
||||||
|
.context("Failed to satisfy inputs with given signatures")?;
|
||||||
|
|
||||||
Ok(tx_punish)
|
Ok(tx_punish)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Watchable for TxPunish {
|
||||||
|
fn id(&self) -> Txid {
|
||||||
|
self.inner.txid()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(&self) -> Script {
|
||||||
|
self.watch_script.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
|
use crate::bitcoin::wallet::Watchable;
|
||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs,
|
||||||
Transaction, TxLock,
|
NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock,
|
||||||
};
|
};
|
||||||
use ::bitcoin::util::bip143::SigHashCache;
|
use ::bitcoin::util::bip143::SigHashCache;
|
||||||
use ::bitcoin::{SigHash, SigHashType, Txid};
|
use ::bitcoin::{SigHash, SigHashType, Txid};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
use bitcoin::Script;
|
||||||
|
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||||
|
use ecdsa_fun::fun::Scalar;
|
||||||
|
use ecdsa_fun::nonce::Deterministic;
|
||||||
use ecdsa_fun::Signature;
|
use ecdsa_fun::Signature;
|
||||||
use miniscript::{Descriptor, DescriptorTrait};
|
use miniscript::{Descriptor, DescriptorTrait};
|
||||||
|
use sha2::Sha256;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct TxRedeem {
|
pub struct TxRedeem {
|
||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
digest: SigHash,
|
digest: SigHash,
|
||||||
lock_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
lock_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||||
|
watch_script: Script,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TxRedeem {
|
impl TxRedeem {
|
||||||
@ -33,6 +40,7 @@ impl TxRedeem {
|
|||||||
inner: tx_redeem,
|
inner: tx_redeem,
|
||||||
digest,
|
digest,
|
||||||
lock_output_descriptor: tx_lock.output_descriptor.clone(),
|
lock_output_descriptor: tx_lock.output_descriptor.clone(),
|
||||||
|
watch_script: redeem_address.script_pubkey(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,17 +52,31 @@ impl TxRedeem {
|
|||||||
self.digest
|
self.digest
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_signatures(
|
pub fn complete(
|
||||||
self,
|
mut self,
|
||||||
(A, sig_a): (PublicKey, Signature),
|
encrypted_signature: EncryptedSignature,
|
||||||
(B, sig_b): (PublicKey, Signature),
|
a: SecretKey,
|
||||||
|
s_a: Scalar,
|
||||||
|
B: PublicKey,
|
||||||
) -> Result<Transaction> {
|
) -> Result<Transaction> {
|
||||||
|
verify_encsig(
|
||||||
|
B,
|
||||||
|
PublicKey::from(s_a.clone()),
|
||||||
|
&self.digest(),
|
||||||
|
&encrypted_signature,
|
||||||
|
)
|
||||||
|
.context("Invalid encrypted signature received")?;
|
||||||
|
|
||||||
|
let sig_a = a.sign(self.digest());
|
||||||
|
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
||||||
|
let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature);
|
||||||
|
|
||||||
let satisfier = {
|
let satisfier = {
|
||||||
let mut satisfier = HashMap::with_capacity(2);
|
let mut satisfier = HashMap::with_capacity(2);
|
||||||
|
|
||||||
let A = ::bitcoin::PublicKey {
|
let A = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
key: A.0.into(),
|
key: a.public.into(),
|
||||||
};
|
};
|
||||||
let B = ::bitcoin::PublicKey {
|
let B = ::bitcoin::PublicKey {
|
||||||
compressed: true,
|
compressed: true,
|
||||||
@ -68,11 +90,11 @@ impl TxRedeem {
|
|||||||
satisfier
|
satisfier
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tx_redeem = self.inner;
|
|
||||||
self.lock_output_descriptor
|
self.lock_output_descriptor
|
||||||
.satisfy(&mut tx_redeem.input[0], satisfier)?;
|
.satisfy(&mut self.inner.input[0], satisfier)
|
||||||
|
.context("Failed to sign Bitcoin redeem transaction")?;
|
||||||
|
|
||||||
Ok(tx_redeem)
|
Ok(self.inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_signature_by_key(
|
pub fn extract_signature_by_key(
|
||||||
@ -112,3 +134,13 @@ impl TxRedeem {
|
|||||||
Ok(sig)
|
Ok(sig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Watchable for TxRedeem {
|
||||||
|
fn id(&self) -> Txid {
|
||||||
|
self.txid()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(&self) -> Script {
|
||||||
|
self.watch_script.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::bitcoin::wallet::Watchable;
|
||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
||||||
Transaction, TxCancel,
|
Transaction, TxCancel,
|
||||||
@ -5,6 +6,7 @@ use crate::bitcoin::{
|
|||||||
use ::bitcoin::util::bip143::SigHashCache;
|
use ::bitcoin::util::bip143::SigHashCache;
|
||||||
use ::bitcoin::{SigHash, SigHashType, Txid};
|
use ::bitcoin::{SigHash, SigHashType, Txid};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
|
use bitcoin::Script;
|
||||||
use ecdsa_fun::Signature;
|
use ecdsa_fun::Signature;
|
||||||
use miniscript::{Descriptor, DescriptorTrait};
|
use miniscript::{Descriptor, DescriptorTrait};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -14,6 +16,7 @@ pub struct TxRefund {
|
|||||||
inner: Transaction,
|
inner: Transaction,
|
||||||
digest: SigHash,
|
digest: SigHash,
|
||||||
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||||
|
watch_script: Script,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TxRefund {
|
impl TxRefund {
|
||||||
@ -31,6 +34,7 @@ impl TxRefund {
|
|||||||
inner: tx_punish,
|
inner: tx_punish,
|
||||||
digest,
|
digest,
|
||||||
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
|
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
|
||||||
|
watch_script: refund_address.script_pubkey(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,3 +114,13 @@ impl TxRefund {
|
|||||||
Ok(sig)
|
Ok(sig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Watchable for TxRefund {
|
||||||
|
fn id(&self) -> Txid {
|
||||||
|
self.txid()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(&self) -> Script {
|
||||||
|
self.watch_script.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use bdk::electrum_client::HeaderNotification;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
|
|
||||||
/// Represent a block height, or block number, expressed in absolute block
|
/// Represent a block height, or block number, expressed in absolute block
|
||||||
@ -26,6 +29,19 @@ impl BlockHeight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<HeaderNotification> for BlockHeight {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: HeaderNotification) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self(
|
||||||
|
value
|
||||||
|
.height
|
||||||
|
.try_into()
|
||||||
|
.context("Failed to fit usize into u32")?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Add<u32> for BlockHeight {
|
impl Add<u32> for BlockHeight {
|
||||||
type Output = BlockHeight;
|
type Output = BlockHeight;
|
||||||
fn add(self, rhs: u32) -> Self::Output {
|
fn add(self, rhs: u32) -> Self::Output {
|
||||||
@ -33,7 +49,7 @@ impl Add<u32> for BlockHeight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum ExpiredTimelocks {
|
pub enum ExpiredTimelocks {
|
||||||
None,
|
None,
|
||||||
Cancel,
|
Cancel,
|
||||||
|
@ -1,60 +1,46 @@
|
|||||||
use crate::bitcoin::timelocks::BlockHeight;
|
use crate::bitcoin::timelocks::BlockHeight;
|
||||||
use crate::bitcoin::{Address, Amount, Transaction};
|
use crate::bitcoin::{Address, Amount, Transaction};
|
||||||
use crate::execution_params::ExecutionParams;
|
|
||||||
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||||
use ::bitcoin::Txid;
|
use ::bitcoin::Txid;
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use backoff::backoff::Constant as ConstantBackoff;
|
|
||||||
use backoff::future::retry;
|
|
||||||
use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain};
|
use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain};
|
||||||
use bdk::descriptor::Segwitv0;
|
use bdk::descriptor::Segwitv0;
|
||||||
use bdk::electrum_client::{self, Client, ElectrumApi};
|
use bdk::electrum_client::{self, ElectrumApi, GetHistoryRes};
|
||||||
use bdk::keys::DerivableKey;
|
use bdk::keys::DerivableKey;
|
||||||
use bdk::{FeeRate, KeychainKind};
|
use bdk::{FeeRate, KeychainKind};
|
||||||
use bitcoin::Script;
|
use bitcoin::Script;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::{Deserialize, Serialize};
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::future::Future;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::interval;
|
|
||||||
|
|
||||||
const SLED_TREE_NAME: &str = "default_tree";
|
const SLED_TREE_NAME: &str = "default_tree";
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
enum Error {
|
|
||||||
#[error("Sending the request failed")]
|
|
||||||
Io(reqwest::Error),
|
|
||||||
#[error("Conversion to Integer failed")]
|
|
||||||
Parse(std::num::ParseIntError),
|
|
||||||
#[error("The transaction is not minded yet")]
|
|
||||||
NotYetMined,
|
|
||||||
#[error("Deserialization failed")]
|
|
||||||
JsonDeserialization(reqwest::Error),
|
|
||||||
#[error("Electrum client error")]
|
|
||||||
ElectrumClient(electrum_client::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Wallet {
|
pub struct Wallet {
|
||||||
inner: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
|
client: Arc<Mutex<Client>>,
|
||||||
http_url: Url,
|
wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
|
||||||
rpc_url: Url,
|
bitcoin_finality_confirmations: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wallet {
|
impl Wallet {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
electrum_rpc_url: Url,
|
electrum_rpc_url: Url,
|
||||||
electrum_http_url: Url,
|
|
||||||
network: bitcoin::Network,
|
network: bitcoin::Network,
|
||||||
|
bitcoin_finality_confirmations: u32,
|
||||||
wallet_dir: &Path,
|
wallet_dir: &Path,
|
||||||
key: impl DerivableKey<Segwitv0> + Clone,
|
key: impl DerivableKey<Segwitv0> + Clone,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47.
|
// Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47.
|
||||||
let config = electrum_client::ConfigBuilder::default().retry(2).build();
|
let config = electrum_client::ConfigBuilder::default().retry(2).build();
|
||||||
|
|
||||||
let client = Client::from_config(electrum_rpc_url.as_str(), config)
|
let client =
|
||||||
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?;
|
bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config.clone())
|
||||||
|
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?;
|
||||||
|
|
||||||
let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
||||||
|
|
||||||
@ -66,16 +52,21 @@ impl Wallet {
|
|||||||
ElectrumBlockchain::from(client),
|
ElectrumBlockchain::from(client),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let electrum = bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config)
|
||||||
|
.map_err(|e| anyhow!("Failed to init electrum rpc client {:?}", e))?;
|
||||||
|
|
||||||
|
let interval = Duration::from_secs(5);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: Arc::new(Mutex::new(bdk_wallet)),
|
wallet: Arc::new(Mutex::new(bdk_wallet)),
|
||||||
http_url: electrum_http_url,
|
client: Arc::new(Mutex::new(Client::new(electrum, interval)?)),
|
||||||
rpc_url: electrum_rpc_url,
|
bitcoin_finality_confirmations,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn balance(&self) -> Result<Amount> {
|
pub async fn balance(&self) -> Result<Amount> {
|
||||||
let balance = self
|
let balance = self
|
||||||
.inner
|
.wallet
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.get_balance()
|
.get_balance()
|
||||||
@ -86,7 +77,7 @@ impl Wallet {
|
|||||||
|
|
||||||
pub async fn new_address(&self) -> Result<Address> {
|
pub async fn new_address(&self) -> Result<Address> {
|
||||||
let address = self
|
let address = self
|
||||||
.inner
|
.wallet
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.get_new_address()
|
.get_new_address()
|
||||||
@ -96,13 +87,14 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_tx(&self, txid: Txid) -> Result<Option<Transaction>> {
|
pub async fn get_tx(&self, txid: Txid) -> Result<Option<Transaction>> {
|
||||||
let tx = self.inner.lock().await.client().get_tx(&txid)?;
|
let tx = self.wallet.lock().await.client().get_tx(&txid)?;
|
||||||
|
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||||
let fees = self
|
let fees = self
|
||||||
.inner
|
.wallet
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.list_transactions(true)?
|
.list_transactions(true)?
|
||||||
@ -117,7 +109,7 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sync(&self) -> Result<()> {
|
pub async fn sync(&self) -> Result<()> {
|
||||||
self.inner
|
self.wallet
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.sync(noop_progress(), None)
|
.sync(noop_progress(), None)
|
||||||
@ -131,7 +123,7 @@ impl Wallet {
|
|||||||
address: Address,
|
address: Address,
|
||||||
amount: Amount,
|
amount: Amount,
|
||||||
) -> Result<PartiallySignedTransaction> {
|
) -> Result<PartiallySignedTransaction> {
|
||||||
let wallet = self.inner.lock().await;
|
let wallet = self.wallet.lock().await;
|
||||||
|
|
||||||
let mut tx_builder = wallet.build_tx();
|
let mut tx_builder = wallet.build_tx();
|
||||||
tx_builder.add_recipient(address.script_pubkey(), amount.as_sat());
|
tx_builder.add_recipient(address.script_pubkey(), amount.as_sat());
|
||||||
@ -147,7 +139,7 @@ impl Wallet {
|
|||||||
/// already accounting for the fees we need to spend to get the
|
/// already accounting for the fees we need to spend to get the
|
||||||
/// transaction confirmed.
|
/// transaction confirmed.
|
||||||
pub async fn max_giveable(&self, locking_script_size: usize) -> Result<Amount> {
|
pub async fn max_giveable(&self, locking_script_size: usize) -> Result<Amount> {
|
||||||
let wallet = self.inner.lock().await;
|
let wallet = self.wallet.lock().await;
|
||||||
|
|
||||||
let mut tx_builder = wallet.build_tx();
|
let mut tx_builder = wallet.build_tx();
|
||||||
|
|
||||||
@ -163,15 +155,28 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_network(&self) -> bitcoin::Network {
|
pub async fn get_network(&self) -> bitcoin::Network {
|
||||||
self.inner.lock().await.network()
|
self.wallet.lock().await.network()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broadcast the given transaction to the network and emit a log statement
|
/// Broadcast the given transaction to the network and emit a log statement
|
||||||
/// if done so successfully.
|
/// if done so successfully.
|
||||||
pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result<Txid> {
|
///
|
||||||
|
/// Returns the transaction ID and a future for when the transaction meets
|
||||||
|
/// the configured finality confirmations.
|
||||||
|
pub async fn broadcast(
|
||||||
|
&self,
|
||||||
|
transaction: Transaction,
|
||||||
|
kind: &str,
|
||||||
|
) -> Result<(Txid, impl Future<Output = Result<()>> + '_)> {
|
||||||
let txid = transaction.txid();
|
let txid = transaction.txid();
|
||||||
|
|
||||||
self.inner
|
// to watch for confirmations, watching a single output is enough
|
||||||
|
let watcher = self.wait_for_transaction_finality(
|
||||||
|
(txid, transaction.output[0].script_pubkey.clone()),
|
||||||
|
kind.to_owned(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.wallet
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.broadcast(transaction)
|
.broadcast(transaction)
|
||||||
@ -181,11 +186,11 @@ impl Wallet {
|
|||||||
|
|
||||||
tracing::info!(%txid, "Published Bitcoin {} transaction", kind);
|
tracing::info!(%txid, "Published Bitcoin {} transaction", kind);
|
||||||
|
|
||||||
Ok(txid)
|
Ok((txid, watcher))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
|
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
|
||||||
let (signed_psbt, finalized) = self.inner.lock().await.sign(psbt, None)?;
|
let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
|
||||||
|
|
||||||
if !finalized {
|
if !finalized {
|
||||||
bail!("PSBT is not finalized")
|
bail!("PSBT is not finalized")
|
||||||
@ -202,110 +207,72 @@ impl Wallet {
|
|||||||
.ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid))
|
.ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
pub async fn status_of_script<T>(&self, tx: &T) -> Result<ScriptStatus>
|
||||||
tracing::debug!("watching for tx: {}", txid);
|
where
|
||||||
let tx = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
T: Watchable,
|
||||||
let client = Client::new(self.rpc_url.as_ref())
|
{
|
||||||
.map_err(|err| backoff::Error::Permanent(Error::ElectrumClient(err)))?;
|
self.client.lock().await.status_of_script(tx)
|
||||||
|
|
||||||
let tx = client.transaction_get(&txid).map_err(|err| match err {
|
|
||||||
electrum_client::Error::Protocol(err) => {
|
|
||||||
tracing::debug!("Received protocol error {} from Electrum, retrying...", err);
|
|
||||||
backoff::Error::Transient(Error::NotYetMined)
|
|
||||||
}
|
|
||||||
err => backoff::Error::Permanent(Error::ElectrumClient(err)),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Result::<_, backoff::Error<Error>>::Ok(tx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.context("Transient errors should be retried")?;
|
|
||||||
|
|
||||||
Ok(tx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_block_height(&self) -> Result<BlockHeight> {
|
pub async fn watch_until_status<T>(
|
||||||
let url = make_blocks_tip_height_url(&self.http_url)?;
|
|
||||||
|
|
||||||
let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
|
||||||
let height = reqwest::get(url.clone())
|
|
||||||
.await
|
|
||||||
.map_err(Error::Io)?
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.map_err(Error::Io)?
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|err| backoff::Error::Permanent(Error::Parse(err)))?;
|
|
||||||
Result::<_, backoff::Error<Error>>::Ok(height)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.context("Transient errors should be retried")?;
|
|
||||||
|
|
||||||
Ok(BlockHeight::new(height))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn transaction_block_height(&self, txid: Txid) -> Result<BlockHeight> {
|
|
||||||
let status_url = make_tx_status_url(&self.http_url, txid)?;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
struct TransactionStatus {
|
|
||||||
block_height: Option<u32>,
|
|
||||||
confirmed: bool,
|
|
||||||
}
|
|
||||||
let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
|
||||||
let block_height = reqwest::get(status_url.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|err| backoff::Error::Transient(Error::Io(err)))?
|
|
||||||
.json::<TransactionStatus>()
|
|
||||||
.await
|
|
||||||
.map_err(|err| backoff::Error::Permanent(Error::JsonDeserialization(err)))?
|
|
||||||
.block_height
|
|
||||||
.ok_or(backoff::Error::Transient(Error::NotYetMined))?;
|
|
||||||
|
|
||||||
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.context("Transient errors should be retried")?;
|
|
||||||
|
|
||||||
Ok(BlockHeight::new(height))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wait_for_transaction_finality(
|
|
||||||
&self,
|
&self,
|
||||||
txid: Txid,
|
tx: &T,
|
||||||
execution_params: ExecutionParams,
|
mut status_fn: impl FnMut(ScriptStatus) -> bool,
|
||||||
) -> Result<()> {
|
) -> Result<()>
|
||||||
let conf_target = execution_params.bitcoin_finality_confirmations;
|
where
|
||||||
|
T: Watchable,
|
||||||
|
{
|
||||||
|
let txid = tx.id();
|
||||||
|
|
||||||
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" });
|
let mut last_status = None;
|
||||||
|
|
||||||
// Divide by 4 to not check too often yet still be aware of the new block early
|
|
||||||
// on.
|
|
||||||
let mut interval = interval(execution_params.bitcoin_avg_block_time / 4);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let tx_block_height = self.transaction_block_height(txid).await?;
|
let new_status = self.client.lock().await.status_of_script(tx)?;
|
||||||
tracing::debug!("tx_block_height: {:?}", tx_block_height);
|
|
||||||
|
|
||||||
let block_height = self.get_block_height().await?;
|
if Some(new_status) != last_status {
|
||||||
tracing::debug!("latest_block_height: {:?}", block_height);
|
tracing::debug!(%txid, "Transaction is {}", new_status);
|
||||||
|
|
||||||
if let Some(confirmations) = block_height.checked_sub(
|
|
||||||
tx_block_height
|
|
||||||
.checked_sub(BlockHeight::new(1))
|
|
||||||
.expect("transaction must be included in block with height >= 1"),
|
|
||||||
) {
|
|
||||||
tracing::debug!(%txid, "confirmations: {:?}", confirmations);
|
|
||||||
if u32::from(confirmations) >= conf_target {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
interval.tick().await;
|
last_status = Some(new_status);
|
||||||
|
|
||||||
|
if status_fn(new_status) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_transaction_finality<T>(&self, tx: T, kind: String) -> Result<()>
|
||||||
|
where
|
||||||
|
T: Watchable,
|
||||||
|
{
|
||||||
|
let conf_target = self.bitcoin_finality_confirmations;
|
||||||
|
let txid = tx.id();
|
||||||
|
|
||||||
|
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin {} transaction", conf_target, if conf_target > 1 { "s" } else { "" }, kind);
|
||||||
|
|
||||||
|
let mut seen_confirmations = 0;
|
||||||
|
|
||||||
|
self.watch_until_status(&tx, |status| match status {
|
||||||
|
ScriptStatus::Confirmed(inner) => {
|
||||||
|
let confirmations = inner.confirmations();
|
||||||
|
|
||||||
|
if confirmations > seen_confirmations {
|
||||||
|
tracing::info!(%txid, "Bitcoin {} tx has {} out of {} confirmation{}", kind, confirmations, conf_target, if conf_target > 1 { "s" } else { "" });
|
||||||
|
seen_confirmations = confirmations;
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.meets_target(conf_target)
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Selects an appropriate [`FeeRate`] to be used for getting transactions
|
/// Selects an appropriate [`FeeRate`] to be used for getting transactions
|
||||||
/// confirmed within a reasonable amount of time.
|
/// confirmed within a reasonable amount of time.
|
||||||
fn select_feerate(&self) -> FeeRate {
|
fn select_feerate(&self) -> FeeRate {
|
||||||
@ -314,42 +281,285 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result<Url> {
|
/// Defines a watchable transaction.
|
||||||
let url = base_url.join(&format!("tx/{}/status", txid))?;
|
///
|
||||||
|
/// For a transaction to be watchable, we need to know two things: Its
|
||||||
Ok(url)
|
/// transaction ID and the specific output script that is going to change.
|
||||||
|
/// A transaction can obviously have multiple outputs but our protocol purposes,
|
||||||
|
/// we are usually interested in a specific one.
|
||||||
|
pub trait Watchable {
|
||||||
|
fn id(&self) -> Txid;
|
||||||
|
fn script(&self) -> Script;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_blocks_tip_height_url(base_url: &Url) -> Result<Url> {
|
impl Watchable for (Txid, Script) {
|
||||||
let url = base_url.join("blocks/tip/height")?;
|
fn id(&self) -> Txid {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
Ok(url)
|
fn script(&self) -> Script {
|
||||||
|
self.1.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Client {
|
||||||
|
electrum: bdk::electrum_client::Client,
|
||||||
|
latest_block: BlockHeight,
|
||||||
|
last_ping: Instant,
|
||||||
|
interval: Duration,
|
||||||
|
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
fn new(electrum: bdk::electrum_client::Client, interval: Duration) -> Result<Self> {
|
||||||
|
let latest_block = electrum.block_headers_subscribe().map_err(|e| {
|
||||||
|
anyhow!(
|
||||||
|
"Electrum client failed to subscribe to header notifications: {:?}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
electrum,
|
||||||
|
latest_block: BlockHeight::try_from(latest_block)?,
|
||||||
|
last_ping: Instant::now(),
|
||||||
|
interval,
|
||||||
|
script_history: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ping the electrum server unless we already did within the set interval.
|
||||||
|
///
|
||||||
|
/// Returns a boolean indicating whether we actually pinged the server.
|
||||||
|
fn ping(&mut self) -> bool {
|
||||||
|
if self.last_ping.elapsed() <= self.interval {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.electrum.ping() {
|
||||||
|
Ok(()) => {
|
||||||
|
self.last_ping = Instant::now();
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::debug!(?error, "Failed to ping electrum server");
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_notifications(&mut self) -> Result<()> {
|
||||||
|
let pinged = self.ping();
|
||||||
|
|
||||||
|
if !pinged {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.drain_blockheight_notifications()?;
|
||||||
|
self.update_script_histories()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_of_script<T>(&mut self, tx: &T) -> Result<ScriptStatus>
|
||||||
|
where
|
||||||
|
T: Watchable,
|
||||||
|
{
|
||||||
|
let txid = tx.id();
|
||||||
|
let script = tx.script();
|
||||||
|
|
||||||
|
if !self.script_history.contains_key(&script) {
|
||||||
|
self.script_history.insert(script.clone(), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.drain_notifications()?;
|
||||||
|
|
||||||
|
let history = self.script_history.entry(script).or_default();
|
||||||
|
|
||||||
|
let history_of_tx = history
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.tx_hash == txid)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
match history_of_tx.as_slice() {
|
||||||
|
[] => Ok(ScriptStatus::Unseen),
|
||||||
|
[remaining @ .., last] => {
|
||||||
|
if !remaining.is_empty() {
|
||||||
|
tracing::warn!("Found more than a single history entry for script. This is highly unexpected and those history entries will be ignored.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if last.height <= 0 {
|
||||||
|
Ok(ScriptStatus::InMempool)
|
||||||
|
} else {
|
||||||
|
Ok(ScriptStatus::Confirmed(
|
||||||
|
Confirmed::from_inclusion_and_latest_block(
|
||||||
|
u32::try_from(last.height)?,
|
||||||
|
u32::from(self.latest_block),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_blockheight_notifications(&mut self) -> Result<()> {
|
||||||
|
let latest_block = std::iter::from_fn(|| self.electrum.block_headers_pop().transpose())
|
||||||
|
.last()
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| anyhow!("Failed to pop header notification: {:?}", e))?;
|
||||||
|
|
||||||
|
if let Some(new_block) = latest_block {
|
||||||
|
tracing::debug!(
|
||||||
|
"Got notification for new block at height {}",
|
||||||
|
new_block.height
|
||||||
|
);
|
||||||
|
self.latest_block = BlockHeight::try_from(new_block)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_script_histories(&mut self) -> Result<()> {
|
||||||
|
let histories = self
|
||||||
|
.electrum
|
||||||
|
.batch_script_get_history(self.script_history.keys())
|
||||||
|
.map_err(|e| anyhow!("Failed to get script histories {:?}", e))?;
|
||||||
|
|
||||||
|
if histories.len() != self.script_history.len() {
|
||||||
|
bail!(
|
||||||
|
"Expected {} history entries, received {}",
|
||||||
|
self.script_history.len(),
|
||||||
|
histories.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let scripts = self.script_history.keys().cloned();
|
||||||
|
let histories = histories.into_iter();
|
||||||
|
|
||||||
|
self.script_history = scripts.zip(histories).collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum ScriptStatus {
|
||||||
|
Unseen,
|
||||||
|
InMempool,
|
||||||
|
Confirmed(Confirmed),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptStatus {
|
||||||
|
pub fn from_confirmations(confirmations: u32) -> Self {
|
||||||
|
match confirmations {
|
||||||
|
0 => Self::InMempool,
|
||||||
|
confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Confirmed {
|
||||||
|
/// The depth of this transaction within the blockchain.
|
||||||
|
///
|
||||||
|
/// Will be zero if the transaction is included in the latest block.
|
||||||
|
depth: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Confirmed {
|
||||||
|
pub fn new(depth: u32) -> Self {
|
||||||
|
Self { depth }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the depth of a transaction based on its inclusion height and the
|
||||||
|
/// latest known block.
|
||||||
|
///
|
||||||
|
/// Our information about the latest block might be outdated. To avoid an
|
||||||
|
/// overflow, we make sure the depth is 0 in case the inclusion height
|
||||||
|
/// exceeds our latest known block,
|
||||||
|
pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self {
|
||||||
|
let depth = latest_block.saturating_sub(inclusion_height);
|
||||||
|
|
||||||
|
Self { depth }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn confirmations(&self) -> u32 {
|
||||||
|
self.depth + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meets_target<T>(&self, target: T) -> bool
|
||||||
|
where
|
||||||
|
u32: PartialOrd<T>,
|
||||||
|
{
|
||||||
|
self.confirmations() >= target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptStatus {
|
||||||
|
/// Check if the script has any confirmations.
|
||||||
|
pub fn is_confirmed(&self) -> bool {
|
||||||
|
matches!(self, ScriptStatus::Confirmed(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the script has met the given confirmation target.
|
||||||
|
pub fn is_confirmed_with<T>(&self, target: T) -> bool
|
||||||
|
where
|
||||||
|
u32: PartialOrd<T>,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_been_seen(&self) -> bool {
|
||||||
|
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ScriptStatus {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ScriptStatus::Unseen => write!(f, "unseen"),
|
||||||
|
ScriptStatus::InMempool => write!(f, "in mempool"),
|
||||||
|
ScriptStatus::Confirmed(inner) => {
|
||||||
|
write!(f, "confirmed with {} blocks", inner.confirmations())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::cli::command::DEFAULT_ELECTRUM_HTTP_URL;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_tx_status_url_from_default_base_url_success() {
|
fn given_depth_0_should_meet_confirmation_target_one() {
|
||||||
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
|
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
||||||
let txid = Txid::default;
|
|
||||||
|
|
||||||
let url = make_tx_status_url(&base_url, txid()).unwrap();
|
let confirmed = script.is_confirmed_with(1);
|
||||||
|
|
||||||
assert_eq!(url.as_str(), "https://blockstream.info/testnet/api/tx/0000000000000000000000000000000000000000000000000000000000000000/status");
|
assert!(confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_block_tip_height_url_from_default_base_url_success() {
|
fn given_confirmations_1_should_meet_confirmation_target_one() {
|
||||||
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
|
let script = ScriptStatus::from_confirmations(1);
|
||||||
|
|
||||||
let url = make_blocks_tip_height_url(&base_url).unwrap();
|
let confirmed = script.is_confirmed_with(1);
|
||||||
|
|
||||||
assert_eq!(
|
assert!(confirmed)
|
||||||
url.as_str(),
|
}
|
||||||
"https://blockstream.info/testnet/api/blocks/tip/height"
|
|
||||||
);
|
#[test]
|
||||||
|
fn given_inclusion_after_lastest_known_block_at_least_depth_0() {
|
||||||
|
let included_in = 10;
|
||||||
|
let latest_block = 9;
|
||||||
|
|
||||||
|
let confirmed = Confirmed::from_inclusion_and_latest_block(included_in, latest_block);
|
||||||
|
|
||||||
|
assert_eq!(confirmed.depth, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,11 @@ pub enum Command {
|
|||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
connect_params: AliceConnectParams,
|
connect_params: AliceConnectParams,
|
||||||
|
|
||||||
#[structopt(flatten)]
|
#[structopt(long = "electrum-rpc",
|
||||||
bitcoin_params: BitcoinParams,
|
help = "Provide the Bitcoin Electrum RPC URL",
|
||||||
|
default_value = DEFAULT_ELECTRUM_RPC_URL
|
||||||
|
)]
|
||||||
|
electrum_rpc_url: Url,
|
||||||
|
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
monero_params: MoneroParams,
|
monero_params: MoneroParams,
|
||||||
@ -59,8 +62,11 @@ pub enum Command {
|
|||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
connect_params: AliceConnectParams,
|
connect_params: AliceConnectParams,
|
||||||
|
|
||||||
#[structopt(flatten)]
|
#[structopt(long = "electrum-rpc",
|
||||||
bitcoin_params: BitcoinParams,
|
help = "Provide the Bitcoin Electrum RPC URL",
|
||||||
|
default_value = DEFAULT_ELECTRUM_RPC_URL
|
||||||
|
)]
|
||||||
|
electrum_rpc_url: Url,
|
||||||
|
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
monero_params: MoneroParams,
|
monero_params: MoneroParams,
|
||||||
@ -76,8 +82,11 @@ pub enum Command {
|
|||||||
#[structopt(short, long)]
|
#[structopt(short, long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
|
|
||||||
#[structopt(flatten)]
|
#[structopt(long = "electrum-rpc",
|
||||||
bitcoin_params: BitcoinParams,
|
help = "Provide the Bitcoin Electrum RPC URL",
|
||||||
|
default_value = DEFAULT_ELECTRUM_RPC_URL
|
||||||
|
)]
|
||||||
|
electrum_rpc_url: Url,
|
||||||
},
|
},
|
||||||
/// Try to cancel a swap and refund my BTC (expert users only)
|
/// Try to cancel a swap and refund my BTC (expert users only)
|
||||||
Refund {
|
Refund {
|
||||||
@ -90,8 +99,11 @@ pub enum Command {
|
|||||||
#[structopt(short, long)]
|
#[structopt(short, long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
|
|
||||||
#[structopt(flatten)]
|
#[structopt(long = "electrum-rpc",
|
||||||
bitcoin_params: BitcoinParams,
|
help = "Provide the Bitcoin Electrum RPC URL",
|
||||||
|
default_value = DEFAULT_ELECTRUM_RPC_URL
|
||||||
|
)]
|
||||||
|
electrum_rpc_url: Url,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,21 +140,6 @@ pub struct MoneroParams {
|
|||||||
pub monero_daemon_host: String,
|
pub monero_daemon_host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
|
||||||
pub struct BitcoinParams {
|
|
||||||
#[structopt(long = "electrum-http",
|
|
||||||
help = "Provide the Bitcoin Electrum HTTP URL",
|
|
||||||
default_value = DEFAULT_ELECTRUM_HTTP_URL
|
|
||||||
)]
|
|
||||||
pub electrum_http_url: Url,
|
|
||||||
|
|
||||||
#[structopt(long = "electrum-rpc",
|
|
||||||
help = "Provide the Bitcoin Electrum RPC URL",
|
|
||||||
default_value = DEFAULT_ELECTRUM_RPC_URL
|
|
||||||
)]
|
|
||||||
pub electrum_rpc_url: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Data(pub PathBuf);
|
pub struct Data(pub PathBuf);
|
||||||
|
|
||||||
|
1
swap/src/cli/config.rs
Normal file
1
swap/src/cli/config.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
use crate::bitcoin::{EncryptedSignature, TxCancel, TxRefund};
|
use crate::bitcoin::EncryptedSignature;
|
||||||
use crate::monero;
|
use crate::monero;
|
||||||
use crate::monero::monero_private_key;
|
use crate::monero::monero_private_key;
|
||||||
use crate::protocol::alice;
|
use crate::protocol::alice;
|
||||||
@ -177,37 +177,18 @@ impl From<Alice> for AliceState {
|
|||||||
Alice::BtcCancelled {
|
Alice::BtcCancelled {
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
state3,
|
state3,
|
||||||
} => {
|
} => AliceState::BtcCancelled {
|
||||||
let tx_cancel = TxCancel::new(
|
monero_wallet_restore_blockheight,
|
||||||
&state3.tx_lock,
|
state3: Box::new(state3),
|
||||||
state3.cancel_timelock,
|
},
|
||||||
state3.a.public(),
|
|
||||||
state3.B,
|
|
||||||
);
|
|
||||||
|
|
||||||
AliceState::BtcCancelled {
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
state3: Box::new(state3),
|
|
||||||
tx_cancel: Box::new(tx_cancel),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Alice::BtcPunishable {
|
Alice::BtcPunishable {
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
state3,
|
state3,
|
||||||
} => {
|
} => AliceState::BtcPunishable {
|
||||||
let tx_cancel = TxCancel::new(
|
monero_wallet_restore_blockheight,
|
||||||
&state3.tx_lock,
|
state3: Box::new(state3),
|
||||||
state3.cancel_timelock,
|
},
|
||||||
state3.a.public(),
|
|
||||||
state3.B,
|
|
||||||
);
|
|
||||||
let tx_refund = TxRefund::new(&tx_cancel, &state3.refund_address);
|
|
||||||
AliceState::BtcPunishable {
|
|
||||||
monero_wallet_restore_blockheight,
|
|
||||||
tx_refund: Box::new(tx_refund),
|
|
||||||
state3: Box::new(state3),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Alice::BtcRefunded {
|
Alice::BtcRefunded {
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
state3,
|
state3,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks,
|
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
|
||||||
PunishTimelock, TxCancel, TxRefund,
|
|
||||||
};
|
};
|
||||||
use crate::execution_params::ExecutionParams;
|
use crate::execution_params::ExecutionParams;
|
||||||
use crate::protocol::alice::{Message1, Message3};
|
use crate::protocol::alice::{Message1, Message3};
|
||||||
@ -37,7 +36,6 @@ pub enum AliceState {
|
|||||||
BtcRedeemed,
|
BtcRedeemed,
|
||||||
BtcCancelled {
|
BtcCancelled {
|
||||||
monero_wallet_restore_blockheight: BlockHeight,
|
monero_wallet_restore_blockheight: BlockHeight,
|
||||||
tx_cancel: Box<TxCancel>,
|
|
||||||
state3: Box<State3>,
|
state3: Box<State3>,
|
||||||
},
|
},
|
||||||
BtcRefunded {
|
BtcRefunded {
|
||||||
@ -47,7 +45,6 @@ pub enum AliceState {
|
|||||||
},
|
},
|
||||||
BtcPunishable {
|
BtcPunishable {
|
||||||
monero_wallet_restore_blockheight: BlockHeight,
|
monero_wallet_restore_blockheight: BlockHeight,
|
||||||
tx_refund: Box<TxRefund>,
|
|
||||||
state3: Box<State3>,
|
state3: Box<State3>,
|
||||||
},
|
},
|
||||||
XmrRefunded,
|
XmrRefunded,
|
||||||
@ -323,24 +320,45 @@ impl State3 {
|
|||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
wait_for_cancel_timelock_to_expire(
|
bitcoin_wallet
|
||||||
bitcoin_wallet,
|
.watch_until_status(&self.tx_lock, |status| {
|
||||||
self.cancel_timelock,
|
status.is_confirmed_with(self.cancel_timelock)
|
||||||
self.tx_lock.txid(),
|
})
|
||||||
)
|
.await?;
|
||||||
.await
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn expired_timelocks(
|
pub async fn expired_timelocks(
|
||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<ExpiredTimelocks> {
|
) -> Result<ExpiredTimelocks> {
|
||||||
current_epoch(
|
let tx_cancel = self.tx_cancel();
|
||||||
bitcoin_wallet,
|
|
||||||
|
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||||
|
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||||
|
|
||||||
|
Ok(current_epoch(
|
||||||
self.cancel_timelock,
|
self.cancel_timelock,
|
||||||
self.punish_timelock,
|
self.punish_timelock,
|
||||||
self.tx_lock.txid(),
|
tx_lock_status,
|
||||||
|
tx_cancel_status,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tx_cancel(&self) -> TxCancel {
|
||||||
|
TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tx_punish(&self) -> TxPunish {
|
||||||
|
bitcoin::TxPunish::new(
|
||||||
|
&self.tx_cancel(),
|
||||||
|
&self.punish_address,
|
||||||
|
self.punish_timelock,
|
||||||
)
|
)
|
||||||
.await
|
}
|
||||||
|
|
||||||
|
pub fn tx_refund(&self) -> TxRefund {
|
||||||
|
bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,13 @@
|
|||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature,
|
CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund,
|
||||||
PunishTimelock, TxCancel, TxLock, TxRefund,
|
|
||||||
};
|
};
|
||||||
use crate::execution_params::ExecutionParams;
|
|
||||||
use crate::protocol::alice;
|
use crate::protocol::alice;
|
||||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||||
use crate::protocol::alice::TransferProof;
|
use crate::protocol::alice::TransferProof;
|
||||||
use crate::{bitcoin, monero};
|
use crate::{bitcoin, monero};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
|
||||||
use ecdsa_fun::nonce::Deterministic;
|
|
||||||
use futures::future::{select, Either};
|
|
||||||
use futures::pin_mut;
|
use futures::pin_mut;
|
||||||
use libp2p::PeerId;
|
use libp2p::PeerId;
|
||||||
use sha2::Sha256;
|
|
||||||
use tokio::time::timeout;
|
|
||||||
|
|
||||||
// TODO(Franck): Use helper functions from xmr-btc instead of re-writing them
|
|
||||||
// here
|
|
||||||
pub async fn wait_for_locked_bitcoin(
|
|
||||||
lock_bitcoin_txid: bitcoin::Txid,
|
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
|
||||||
execution_params: ExecutionParams,
|
|
||||||
) -> Result<()> {
|
|
||||||
// We assume we will see Bob's transaction in the mempool first.
|
|
||||||
timeout(
|
|
||||||
execution_params.bob_time_to_act,
|
|
||||||
bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("Failed to find lock Bitcoin tx")??;
|
|
||||||
|
|
||||||
// // We saw the transaction in the mempool, waiting for it to be confirmed.
|
|
||||||
bitcoin_wallet
|
|
||||||
.wait_for_transaction_finality(lock_bitcoin_txid, execution_params)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn lock_xmr(
|
pub async fn lock_xmr(
|
||||||
bob_peer_id: PeerId,
|
bob_peer_id: PeerId,
|
||||||
@ -81,36 +51,6 @@ pub async fn wait_for_bitcoin_encrypted_signature(
|
|||||||
Ok(msg3.tx_redeem_encsig)
|
Ok(msg3.tx_redeem_encsig)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_bitcoin_redeem_transaction(
|
|
||||||
encrypted_signature: EncryptedSignature,
|
|
||||||
tx_lock: &TxLock,
|
|
||||||
a: bitcoin::SecretKey,
|
|
||||||
s_a: ecdsa_fun::fun::Scalar,
|
|
||||||
B: bitcoin::PublicKey,
|
|
||||||
redeem_address: &bitcoin::Address,
|
|
||||||
) -> Result<bitcoin::Transaction> {
|
|
||||||
let adaptor = Adaptor::<HashTranscript<Sha256>, Deterministic<Sha256>>::default();
|
|
||||||
|
|
||||||
let tx_redeem = bitcoin::TxRedeem::new(tx_lock, redeem_address);
|
|
||||||
|
|
||||||
bitcoin::verify_encsig(
|
|
||||||
B,
|
|
||||||
bitcoin::PublicKey::from(s_a.clone()),
|
|
||||||
&tx_redeem.digest(),
|
|
||||||
&encrypted_signature,
|
|
||||||
)
|
|
||||||
.context("Invalid encrypted signature received")?;
|
|
||||||
|
|
||||||
let sig_a = a.sign(tx_redeem.digest());
|
|
||||||
let sig_b = adaptor.decrypt_signature(&s_a, encrypted_signature);
|
|
||||||
|
|
||||||
let tx = tx_redeem
|
|
||||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
|
||||||
.context("Failed to sign Bitcoin redeem transaction")?;
|
|
||||||
|
|
||||||
Ok(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_cancel_transaction(
|
pub async fn publish_cancel_transaction(
|
||||||
tx_lock: TxLock,
|
tx_lock: TxLock,
|
||||||
a: bitcoin::SecretKey,
|
a: bitcoin::SecretKey,
|
||||||
@ -118,12 +58,10 @@ pub async fn publish_cancel_transaction(
|
|||||||
cancel_timelock: CancelTimelock,
|
cancel_timelock: CancelTimelock,
|
||||||
tx_cancel_sig_bob: bitcoin::Signature,
|
tx_cancel_sig_bob: bitcoin::Signature,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<bitcoin::TxCancel> {
|
) -> Result<()> {
|
||||||
// First wait for cancel timelock to expire
|
bitcoin_wallet
|
||||||
let tx_lock_height = bitcoin_wallet
|
.watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock))
|
||||||
.transaction_block_height(tx_lock.txid())
|
|
||||||
.await?;
|
.await?;
|
||||||
poll_until_block_height_is_gte(&bitcoin_wallet, tx_lock_height + cancel_timelock).await?;
|
|
||||||
|
|
||||||
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
|
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
|
||||||
|
|
||||||
@ -140,42 +78,52 @@ pub async fn publish_cancel_transaction(
|
|||||||
let sig_b = tx_cancel_sig_bob.clone();
|
let sig_b = tx_cancel_sig_bob.clone();
|
||||||
|
|
||||||
let tx_cancel = tx_cancel
|
let tx_cancel = tx_cancel
|
||||||
.clone()
|
|
||||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
.add_signatures((a.public(), sig_a), (B, sig_b))
|
||||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||||
|
|
||||||
// TODO(Franck): Error handling is delicate, why can't we broadcast?
|
// TODO(Franck): Error handling is delicate, why can't we broadcast?
|
||||||
bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
let (..) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
||||||
|
|
||||||
// TODO(Franck): Wait until transaction is mined and returned mined
|
// TODO(Franck): Wait until transaction is mined and returned mined
|
||||||
// block height
|
// block height
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(tx_cancel)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wait_for_bitcoin_refund(
|
pub async fn wait_for_bitcoin_refund(
|
||||||
tx_cancel: &TxCancel,
|
tx_cancel: &TxCancel,
|
||||||
cancel_tx_height: BlockHeight,
|
tx_refund: &TxRefund,
|
||||||
punish_timelock: PunishTimelock,
|
punish_timelock: PunishTimelock,
|
||||||
refund_address: &bitcoin::Address,
|
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)> {
|
) -> Result<Option<bitcoin::Transaction>> {
|
||||||
let punish_timelock_expired =
|
let refund_tx_id = tx_refund.txid();
|
||||||
poll_until_block_height_is_gte(bitcoin_wallet, cancel_tx_height + punish_timelock);
|
let seen_refund_tx =
|
||||||
|
bitcoin_wallet.watch_until_status(tx_refund, |status| status.has_been_seen());
|
||||||
|
|
||||||
let tx_refund = bitcoin::TxRefund::new(tx_cancel, refund_address);
|
let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| {
|
||||||
|
status.is_confirmed_with(punish_timelock)
|
||||||
// TODO(Franck): This only checks the mempool, need to cater for the case where
|
});
|
||||||
// the transaction goes directly in a block
|
|
||||||
let seen_refund_tx = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
|
|
||||||
|
|
||||||
pin_mut!(punish_timelock_expired);
|
pin_mut!(punish_timelock_expired);
|
||||||
pin_mut!(seen_refund_tx);
|
pin_mut!(seen_refund_tx);
|
||||||
|
|
||||||
match select(punish_timelock_expired, seen_refund_tx).await {
|
tokio::select! {
|
||||||
Either::Left(_) => Ok((tx_refund, None)),
|
seen_refund = seen_refund_tx => {
|
||||||
Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))),
|
match seen_refund {
|
||||||
|
Ok(()) => {
|
||||||
|
let published_refund_tx = bitcoin_wallet.get_raw_transaction(refund_tx_id).await?;
|
||||||
|
|
||||||
|
Ok(Some(published_refund_tx))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
bail!(e.context("Failed to monitor refund transaction"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = punish_timelock_expired => {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,25 +149,3 @@ pub fn extract_monero_private_key(
|
|||||||
|
|
||||||
Ok(spend_key)
|
Ok(spend_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_bitcoin_punish_transaction(
|
|
||||||
tx_lock: &TxLock,
|
|
||||||
cancel_timelock: CancelTimelock,
|
|
||||||
punish_address: &bitcoin::Address,
|
|
||||||
punish_timelock: PunishTimelock,
|
|
||||||
tx_punish_sig_bob: bitcoin::Signature,
|
|
||||||
a: bitcoin::SecretKey,
|
|
||||||
B: bitcoin::PublicKey,
|
|
||||||
) -> Result<bitcoin::Transaction> {
|
|
||||||
let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B);
|
|
||||||
let tx_punish = bitcoin::TxPunish::new(&tx_cancel, &punish_address, punish_timelock);
|
|
||||||
|
|
||||||
let sig_a = a.sign(tx_punish.digest());
|
|
||||||
let sig_b = tx_punish_sig_bob;
|
|
||||||
|
|
||||||
let signed_tx_punish = tx_punish
|
|
||||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
|
||||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
|
||||||
|
|
||||||
Ok(signed_tx_punish)
|
|
||||||
}
|
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
//! Run an XMR/BTC swap in the role of Alice.
|
//! Run an XMR/BTC swap in the role of Alice.
|
||||||
//! Alice holds XMR and wishes receive BTC.
|
//! Alice holds XMR and wishes receive BTC.
|
||||||
use crate::bitcoin::ExpiredTimelocks;
|
use crate::bitcoin::{ExpiredTimelocks, TxRedeem};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::execution_params::ExecutionParams;
|
use crate::execution_params::ExecutionParams;
|
||||||
use crate::monero_ext::ScalarExt;
|
use crate::monero_ext::ScalarExt;
|
||||||
use crate::protocol::alice;
|
use crate::protocol::alice;
|
||||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||||
use crate::protocol::alice::steps::{
|
use crate::protocol::alice::steps::{
|
||||||
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, extract_monero_private_key,
|
extract_monero_private_key, lock_xmr, publish_cancel_transaction,
|
||||||
lock_xmr, publish_cancel_transaction, wait_for_bitcoin_encrypted_signature,
|
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund,
|
||||||
wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
|
||||||
};
|
};
|
||||||
use crate::protocol::alice::AliceState;
|
use crate::protocol::alice::AliceState;
|
||||||
use crate::{bitcoin, database, monero};
|
use crate::{bitcoin, database, monero};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use futures::future::{select, Either};
|
use futures::future::{select, Either};
|
||||||
use futures::pin_mut;
|
use futures::pin_mut;
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::time::timeout;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -80,12 +80,19 @@ async fn run_until_internal(
|
|||||||
state3,
|
state3,
|
||||||
bob_peer_id,
|
bob_peer_id,
|
||||||
} => {
|
} => {
|
||||||
let _ = wait_for_locked_bitcoin(
|
timeout(
|
||||||
state3.tx_lock.txid(),
|
execution_params.bob_time_to_act,
|
||||||
&bitcoin_wallet,
|
bitcoin_wallet
|
||||||
execution_params,
|
.watch_until_status(&state3.tx_lock, |status| status.has_been_seen()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.context("Failed to find lock Bitcoin tx")??;
|
||||||
|
|
||||||
|
bitcoin_wallet
|
||||||
|
.watch_until_status(&state3.tx_lock, |status| {
|
||||||
|
status.is_confirmed_with(execution_params.bitcoin_finality_confirmations)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
let state = AliceState::BtcLocked {
|
let state = AliceState::BtcLocked {
|
||||||
bob_peer_id,
|
bob_peer_id,
|
||||||
@ -198,27 +205,19 @@ async fn run_until_internal(
|
|||||||
} => {
|
} => {
|
||||||
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||||
ExpiredTimelocks::None => {
|
ExpiredTimelocks::None => {
|
||||||
match build_bitcoin_redeem_transaction(
|
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
|
||||||
*encrypted_signature,
|
*encrypted_signature,
|
||||||
&state3.tx_lock,
|
|
||||||
state3.a.clone(),
|
state3.a.clone(),
|
||||||
state3.s_a.to_secpfun_scalar(),
|
state3.s_a.to_secpfun_scalar(),
|
||||||
state3.B,
|
state3.B,
|
||||||
&state3.redeem_address,
|
|
||||||
) {
|
) {
|
||||||
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
||||||
Ok(txid) => {
|
Ok((_, finality)) => match finality.await {
|
||||||
let publishded_redeem_tx = bitcoin_wallet
|
Ok(_) => AliceState::BtcRedeemed,
|
||||||
.wait_for_transaction_finality(txid, execution_params)
|
Err(e) => {
|
||||||
.await;
|
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
|
||||||
|
|
||||||
match publishded_redeem_tx {
|
|
||||||
Ok(_) => AliceState::BtcRedeemed,
|
|
||||||
Err(e) => {
|
|
||||||
bail!("Waiting for Bitcoin transaction finality failed with {}! The redeem transaction was published, but it is not ensured that the transaction was included! You're screwed.", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
|
error!("Publishing the redeem transaction failed with {}, attempting to wait for cancellation now. If you restart the application before the timelock is expired publishing the redeem transaction will be retried.", e);
|
||||||
state3
|
state3
|
||||||
@ -269,7 +268,7 @@ async fn run_until_internal(
|
|||||||
state3,
|
state3,
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
} => {
|
} => {
|
||||||
let tx_cancel = publish_cancel_transaction(
|
publish_cancel_transaction(
|
||||||
state3.tx_lock.clone(),
|
state3.tx_lock.clone(),
|
||||||
state3.a.clone(),
|
state3.a.clone(),
|
||||||
state3.B,
|
state3.B,
|
||||||
@ -281,7 +280,6 @@ async fn run_until_internal(
|
|||||||
|
|
||||||
let state = AliceState::BtcCancelled {
|
let state = AliceState::BtcCancelled {
|
||||||
state3,
|
state3,
|
||||||
tx_cancel: Box::new(tx_cancel),
|
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
};
|
};
|
||||||
let db_state = (&state).into();
|
let db_state = (&state).into();
|
||||||
@ -301,18 +299,12 @@ async fn run_until_internal(
|
|||||||
}
|
}
|
||||||
AliceState::BtcCancelled {
|
AliceState::BtcCancelled {
|
||||||
state3,
|
state3,
|
||||||
tx_cancel,
|
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
} => {
|
} => {
|
||||||
let tx_cancel_height = bitcoin_wallet
|
let published_refund_tx = wait_for_bitcoin_refund(
|
||||||
.transaction_block_height(tx_cancel.txid())
|
&state3.tx_cancel(),
|
||||||
.await?;
|
&state3.tx_refund(),
|
||||||
|
|
||||||
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
|
|
||||||
&tx_cancel,
|
|
||||||
tx_cancel_height,
|
|
||||||
state3.punish_timelock,
|
state3.punish_timelock,
|
||||||
&state3.refund_address,
|
|
||||||
&bitcoin_wallet,
|
&bitcoin_wallet,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -321,7 +313,6 @@ async fn run_until_internal(
|
|||||||
match published_refund_tx {
|
match published_refund_tx {
|
||||||
None => {
|
None => {
|
||||||
let state = AliceState::BtcPunishable {
|
let state = AliceState::BtcPunishable {
|
||||||
tx_refund: Box::new(tx_refund),
|
|
||||||
state3,
|
state3,
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
};
|
};
|
||||||
@ -344,7 +335,7 @@ async fn run_until_internal(
|
|||||||
Some(published_refund_tx) => {
|
Some(published_refund_tx) => {
|
||||||
let spend_key = extract_monero_private_key(
|
let spend_key = extract_monero_private_key(
|
||||||
published_refund_tx,
|
published_refund_tx,
|
||||||
&tx_refund,
|
&state3.tx_refund(),
|
||||||
state3.s_a,
|
state3.s_a,
|
||||||
state3.a.clone(),
|
state3.a.clone(),
|
||||||
state3.S_b_bitcoin,
|
state3.S_b_bitcoin,
|
||||||
@ -390,39 +381,38 @@ async fn run_until_internal(
|
|||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
AliceState::BtcPunishable {
|
AliceState::BtcPunishable {
|
||||||
tx_refund,
|
|
||||||
state3,
|
state3,
|
||||||
monero_wallet_restore_blockheight,
|
monero_wallet_restore_blockheight,
|
||||||
} => {
|
} => {
|
||||||
let signed_tx_punish = build_bitcoin_punish_transaction(
|
let signed_tx_punish = state3.tx_punish().complete(
|
||||||
&state3.tx_lock,
|
|
||||||
state3.cancel_timelock,
|
|
||||||
&state3.punish_address,
|
|
||||||
state3.punish_timelock,
|
|
||||||
state3.tx_punish_sig_bob.clone(),
|
state3.tx_punish_sig_bob.clone(),
|
||||||
state3.a.clone(),
|
state3.a.clone(),
|
||||||
state3.B,
|
state3.B,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let punish_tx_finalised = async {
|
let punish_tx_finalised = async {
|
||||||
let txid = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
|
let (txid, finality) =
|
||||||
|
bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?;
|
||||||
|
|
||||||
bitcoin_wallet
|
finality.await?;
|
||||||
.wait_for_transaction_finality(txid, execution_params)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Result::<_, anyhow::Error>::Ok(txid)
|
Result::<_, anyhow::Error>::Ok(txid)
|
||||||
};
|
};
|
||||||
|
|
||||||
let refund_tx_seen = bitcoin_wallet.watch_for_raw_transaction(tx_refund.txid());
|
let tx_refund = state3.tx_refund();
|
||||||
|
let refund_tx_seen =
|
||||||
|
bitcoin_wallet.watch_until_status(&tx_refund, |status| status.has_been_seen());
|
||||||
|
|
||||||
pin_mut!(punish_tx_finalised);
|
pin_mut!(punish_tx_finalised);
|
||||||
pin_mut!(refund_tx_seen);
|
pin_mut!(refund_tx_seen);
|
||||||
|
|
||||||
match select(refund_tx_seen, punish_tx_finalised).await {
|
match select(refund_tx_seen, punish_tx_finalised).await {
|
||||||
Either::Left((published_refund_tx, _)) => {
|
Either::Left((Ok(()), _)) => {
|
||||||
|
let published_refund_tx =
|
||||||
|
bitcoin_wallet.get_raw_transaction(tx_refund.txid()).await?;
|
||||||
|
|
||||||
let spend_key = extract_monero_private_key(
|
let spend_key = extract_monero_private_key(
|
||||||
published_refund_tx?,
|
published_refund_tx,
|
||||||
&tx_refund,
|
&tx_refund,
|
||||||
state3.s_a,
|
state3.s_a,
|
||||||
state3.a.clone(),
|
state3.a.clone(),
|
||||||
@ -448,6 +438,9 @@ async fn run_until_internal(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
Either::Left((Err(e), _)) => {
|
||||||
|
bail!(e.context("Failed to monitor refund transaction"))
|
||||||
|
}
|
||||||
Either::Right(_) => {
|
Either::Right(_) => {
|
||||||
let state = AliceState::BtcPunished;
|
let state = AliceState::BtcPunished;
|
||||||
let db_state = (&state).into();
|
let db_state = (&state).into();
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use crate::bitcoin::Wallet;
|
use crate::bitcoin::Wallet;
|
||||||
use crate::database::{Database, Swap};
|
use crate::database::{Database, Swap};
|
||||||
use crate::execution_params::ExecutionParams;
|
|
||||||
use crate::protocol::bob::BobState;
|
use crate::protocol::bob::BobState;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -13,7 +12,6 @@ pub struct SwapNotCancelledYet(Uuid);
|
|||||||
pub async fn refund(
|
pub async fn refund(
|
||||||
swap_id: Uuid,
|
swap_id: Uuid,
|
||||||
state: BobState,
|
state: BobState,
|
||||||
execution_params: ExecutionParams,
|
|
||||||
bitcoin_wallet: Arc<Wallet>,
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
db: Database,
|
db: Database,
|
||||||
force: bool,
|
force: bool,
|
||||||
@ -41,9 +39,7 @@ pub async fn refund(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
state4
|
state4.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||||
.refund_btc(bitcoin_wallet.as_ref(), execution_params)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let state = BobState::BtcRefunded(state4);
|
let state = BobState::BtcRefunded(state4);
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
use crate::bitcoin::{
|
use crate::bitcoin::{
|
||||||
self, current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks,
|
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||||
PunishTimelock, Transaction, TxCancel, Txid,
|
TxLock, Txid,
|
||||||
};
|
};
|
||||||
use crate::execution_params::ExecutionParams;
|
|
||||||
use crate::monero;
|
use crate::monero;
|
||||||
use crate::monero::{monero_private_key, InsufficientFunds, TransferProof};
|
use crate::monero::{monero_private_key, InsufficientFunds, TransferProof};
|
||||||
use crate::monero_ext::ScalarExt;
|
use crate::monero_ext::ScalarExt;
|
||||||
use crate::protocol::alice::{Message1, Message3};
|
use crate::protocol::alice::{Message1, Message3};
|
||||||
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
|
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
|
||||||
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
|
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||||
use ecdsa_fun::nonce::Deterministic;
|
use ecdsa_fun::nonce::Deterministic;
|
||||||
use ecdsa_fun::Signature;
|
use ecdsa_fun::Signature;
|
||||||
@ -262,31 +261,27 @@ impl State2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn lock_btc(self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State3> {
|
pub async fn lock_btc(self) -> Result<(State3, TxLock)> {
|
||||||
let signed_tx = bitcoin_wallet
|
Ok((
|
||||||
.sign_and_finalize(self.tx_lock.clone().into())
|
State3 {
|
||||||
.await
|
A: self.A,
|
||||||
.context("Failed to sign Bitcoin lock transaction")?;
|
b: self.b,
|
||||||
|
s_b: self.s_b,
|
||||||
let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
S_a_monero: self.S_a_monero,
|
||||||
|
S_a_bitcoin: self.S_a_bitcoin,
|
||||||
Ok(State3 {
|
v: self.v,
|
||||||
A: self.A,
|
xmr: self.xmr,
|
||||||
b: self.b,
|
cancel_timelock: self.cancel_timelock,
|
||||||
s_b: self.s_b,
|
punish_timelock: self.punish_timelock,
|
||||||
S_a_monero: self.S_a_monero,
|
refund_address: self.refund_address,
|
||||||
S_a_bitcoin: self.S_a_bitcoin,
|
redeem_address: self.redeem_address,
|
||||||
v: self.v,
|
tx_lock: self.tx_lock.clone(),
|
||||||
xmr: self.xmr,
|
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
||||||
cancel_timelock: self.cancel_timelock,
|
tx_refund_encsig: self.tx_refund_encsig,
|
||||||
punish_timelock: self.punish_timelock,
|
min_monero_confirmations: self.min_monero_confirmations,
|
||||||
refund_address: self.refund_address,
|
},
|
||||||
redeem_address: self.redeem_address,
|
self.tx_lock,
|
||||||
tx_lock: self.tx_lock,
|
))
|
||||||
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
|
||||||
tx_refund_encsig: self.tx_refund_encsig,
|
|
||||||
min_monero_confirmations: self.min_monero_confirmations,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,12 +349,12 @@ impl State3 {
|
|||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
wait_for_cancel_timelock_to_expire(
|
bitcoin_wallet
|
||||||
bitcoin_wallet,
|
.watch_until_status(&self.tx_lock, |status| {
|
||||||
self.cancel_timelock,
|
status.is_confirmed_with(self.cancel_timelock)
|
||||||
self.tx_lock.txid(),
|
})
|
||||||
)
|
.await?;
|
||||||
.await
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancel(&self) -> State4 {
|
pub fn cancel(&self) -> State4 {
|
||||||
@ -390,13 +385,17 @@ impl State3 {
|
|||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<ExpiredTimelocks> {
|
) -> Result<ExpiredTimelocks> {
|
||||||
current_epoch(
|
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||||
bitcoin_wallet,
|
|
||||||
|
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||||
|
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||||
|
|
||||||
|
Ok(current_epoch(
|
||||||
self.cancel_timelock,
|
self.cancel_timelock,
|
||||||
self.punish_timelock,
|
self.punish_timelock,
|
||||||
self.tx_lock.txid(),
|
tx_lock_status,
|
||||||
)
|
tx_cancel_status,
|
||||||
.await
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,10 +418,9 @@ pub struct State4 {
|
|||||||
|
|
||||||
impl State4 {
|
impl State4 {
|
||||||
pub fn next_message(&self) -> EncryptedSignature {
|
pub fn next_message(&self) -> EncryptedSignature {
|
||||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
EncryptedSignature {
|
||||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
tx_redeem_encsig: self.tx_redeem_encsig(),
|
||||||
|
}
|
||||||
EncryptedSignature { tx_redeem_encsig }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
|
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
|
||||||
@ -437,17 +435,6 @@ impl State4 {
|
|||||||
let tx_cancel =
|
let tx_cancel =
|
||||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||||
|
|
||||||
let sig_a = self.tx_cancel_sig_a.clone();
|
|
||||||
let sig_b = self.b.sign(tx_cancel.digest());
|
|
||||||
|
|
||||||
let tx_cancel = tx_cancel
|
|
||||||
.clone()
|
|
||||||
.add_signatures((self.A, sig_a), (self.b.public(), sig_b))
|
|
||||||
.expect(
|
|
||||||
"sig_{a,b} to be valid signatures for
|
|
||||||
tx_cancel",
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
let tx = bitcoin_wallet.get_raw_transaction(tx_cancel.txid()).await?;
|
||||||
|
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
@ -461,14 +448,13 @@ impl State4 {
|
|||||||
let sig_b = self.b.sign(tx_cancel.digest());
|
let sig_b = self.b.sign(tx_cancel.digest());
|
||||||
|
|
||||||
let tx_cancel = tx_cancel
|
let tx_cancel = tx_cancel
|
||||||
.clone()
|
|
||||||
.add_signatures((self.A, sig_a), (self.b.public(), sig_b))
|
.add_signatures((self.A, sig_a), (self.b.public(), sig_b))
|
||||||
.expect(
|
.expect(
|
||||||
"sig_{a,b} to be valid signatures for
|
"sig_{a,b} to be valid signatures for
|
||||||
tx_cancel",
|
tx_cancel",
|
||||||
);
|
);
|
||||||
|
|
||||||
let tx_id = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
||||||
|
|
||||||
Ok(tx_id)
|
Ok(tx_id)
|
||||||
}
|
}
|
||||||
@ -477,10 +463,12 @@ impl State4 {
|
|||||||
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address);
|
||||||
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
let tx_redeem_encsig = self.b.encsign(self.S_a_bitcoin, tx_redeem.digest());
|
||||||
|
|
||||||
let tx_redeem_candidate = bitcoin_wallet
|
bitcoin_wallet
|
||||||
.watch_for_raw_transaction(tx_redeem.txid())
|
.watch_until_status(&tx_redeem, |status| status.has_been_seen())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?;
|
||||||
|
|
||||||
let tx_redeem_sig =
|
let tx_redeem_sig =
|
||||||
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
|
tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?;
|
||||||
let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?;
|
||||||
@ -499,32 +487,33 @@ impl State4 {
|
|||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
wait_for_cancel_timelock_to_expire(
|
bitcoin_wallet
|
||||||
bitcoin_wallet,
|
.watch_until_status(&self.tx_lock, |status| {
|
||||||
self.cancel_timelock,
|
status.is_confirmed_with(self.cancel_timelock)
|
||||||
self.tx_lock.txid(),
|
})
|
||||||
)
|
.await?;
|
||||||
.await
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn expired_timelock(
|
pub async fn expired_timelock(
|
||||||
&self,
|
&self,
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
bitcoin_wallet: &bitcoin::Wallet,
|
||||||
) -> Result<ExpiredTimelocks> {
|
) -> Result<ExpiredTimelocks> {
|
||||||
current_epoch(
|
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||||
bitcoin_wallet,
|
|
||||||
|
let tx_lock_status = bitcoin_wallet.status_of_script(&self.tx_lock).await?;
|
||||||
|
let tx_cancel_status = bitcoin_wallet.status_of_script(&tx_cancel).await?;
|
||||||
|
|
||||||
|
Ok(current_epoch(
|
||||||
self.cancel_timelock,
|
self.cancel_timelock,
|
||||||
self.punish_timelock,
|
self.punish_timelock,
|
||||||
self.tx_lock.txid(),
|
tx_lock_status,
|
||||||
)
|
tx_cancel_status,
|
||||||
.await
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refund_btc(
|
pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
|
||||||
&self,
|
|
||||||
bitcoin_wallet: &bitcoin::Wallet,
|
|
||||||
execution_params: ExecutionParams,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tx_cancel =
|
let tx_cancel =
|
||||||
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
bitcoin::TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||||
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
let tx_refund = bitcoin::TxRefund::new(&tx_cancel, &self.refund_address);
|
||||||
@ -538,11 +527,9 @@ impl State4 {
|
|||||||
let signed_tx_refund =
|
let signed_tx_refund =
|
||||||
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
|
tx_refund.add_signatures((self.A, sig_a), (self.b.public(), sig_b))?;
|
||||||
|
|
||||||
let txid = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
|
let (_, finality) = bitcoin_wallet.broadcast(signed_tx_refund, "refund").await?;
|
||||||
|
|
||||||
bitcoin_wallet
|
finality.await?;
|
||||||
.wait_for_transaction_finality(txid, execution_params)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use crate::protocol::bob;
|
|||||||
use crate::protocol::bob::event_loop::EventLoopHandle;
|
use crate::protocol::bob::event_loop::EventLoopHandle;
|
||||||
use crate::protocol::bob::state::*;
|
use crate::protocol::bob::state::*;
|
||||||
use crate::{bitcoin, monero};
|
use crate::{bitcoin, monero};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -99,7 +99,16 @@ async fn run_until_internal(
|
|||||||
// Do not lock Bitcoin if not connected to Alice.
|
// Do not lock Bitcoin if not connected to Alice.
|
||||||
event_loop_handle.dial().await?;
|
event_loop_handle.dial().await?;
|
||||||
// Alice and Bob have exchanged info
|
// Alice and Bob have exchanged info
|
||||||
let state3 = state2.lock_btc(bitcoin_wallet.as_ref()).await?;
|
let (state3, tx_lock) = state2.lock_btc().await?;
|
||||||
|
let signed_tx = bitcoin_wallet
|
||||||
|
.sign_and_finalize(tx_lock.clone().into())
|
||||||
|
.await
|
||||||
|
.context("Failed to sign Bitcoin lock transaction")?;
|
||||||
|
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
||||||
|
|
||||||
|
bitcoin_wallet
|
||||||
|
.watch_until_status(&tx_lock, |status| status.is_confirmed())
|
||||||
|
.await?;
|
||||||
|
|
||||||
let state = BobState::BtcLocked(state3);
|
let state = BobState::BtcLocked(state3);
|
||||||
let db_state = state.clone().into();
|
let db_state = state.clone().into();
|
||||||
@ -371,9 +380,7 @@ async fn run_until_internal(
|
|||||||
bail!("Internal error: canceled state reached before cancel timelock was expired");
|
bail!("Internal error: canceled state reached before cancel timelock was expired");
|
||||||
}
|
}
|
||||||
ExpiredTimelocks::Cancel => {
|
ExpiredTimelocks::Cancel => {
|
||||||
state
|
state.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||||
.refund_btc(bitcoin_wallet.as_ref(), execution_params)
|
|
||||||
.await?;
|
|
||||||
BobState::BtcRefunded(state)
|
BobState::BtcRefunded(state)
|
||||||
}
|
}
|
||||||
ExpiredTimelocks::Punish => BobState::BtcPunished {
|
ExpiredTimelocks::Punish => BobState::BtcPunished {
|
||||||
|
@ -48,7 +48,6 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
|||||||
let bob_state = bob::refund(
|
let bob_state = bob::refund(
|
||||||
bob_swap.swap_id,
|
bob_swap.swap_id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.execution_params,
|
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
false,
|
false,
|
||||||
|
@ -42,7 +42,6 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
|||||||
bob::refund(
|
bob::refund(
|
||||||
bob_swap.swap_id,
|
bob_swap.swap_id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.execution_params,
|
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
false,
|
false,
|
||||||
|
@ -40,7 +40,6 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
|||||||
let is_error = bob::refund(
|
let is_error = bob::refund(
|
||||||
bob_swap.swap_id,
|
bob_swap.swap_id,
|
||||||
bob_swap.state,
|
bob_swap.state,
|
||||||
bob_swap.execution_params,
|
|
||||||
bob_swap.bitcoin_wallet,
|
bob_swap.bitcoin_wallet,
|
||||||
bob_swap.db,
|
bob_swap.db,
|
||||||
true,
|
true,
|
||||||
|
@ -339,10 +339,6 @@ where
|
|||||||
.electrs
|
.electrs
|
||||||
.get_host_port(testutils::electrs::RPC_PORT)
|
.get_host_port(testutils::electrs::RPC_PORT)
|
||||||
.expect("Could not map electrs rpc port");
|
.expect("Could not map electrs rpc port");
|
||||||
let electrs_http_port = containers
|
|
||||||
.electrs
|
|
||||||
.get_host_port(testutils::electrs::HTTP_PORT)
|
|
||||||
.expect("Could not map electrs http port");
|
|
||||||
|
|
||||||
let alice_seed = Seed::random().unwrap();
|
let alice_seed = Seed::random().unwrap();
|
||||||
let bob_seed = Seed::random().unwrap();
|
let bob_seed = Seed::random().unwrap();
|
||||||
@ -354,7 +350,6 @@ where
|
|||||||
alice_starting_balances.clone(),
|
alice_starting_balances.clone(),
|
||||||
tempdir().unwrap().path(),
|
tempdir().unwrap().path(),
|
||||||
electrs_rpc_port,
|
electrs_rpc_port,
|
||||||
electrs_http_port,
|
|
||||||
alice_seed,
|
alice_seed,
|
||||||
execution_params,
|
execution_params,
|
||||||
)
|
)
|
||||||
@ -377,7 +372,6 @@ where
|
|||||||
bob_starting_balances.clone(),
|
bob_starting_balances.clone(),
|
||||||
tempdir().unwrap().path(),
|
tempdir().unwrap().path(),
|
||||||
electrs_rpc_port,
|
electrs_rpc_port,
|
||||||
electrs_http_port,
|
|
||||||
bob_seed,
|
bob_seed,
|
||||||
execution_params,
|
execution_params,
|
||||||
)
|
)
|
||||||
@ -585,7 +579,6 @@ async fn init_test_wallets(
|
|||||||
starting_balances: StartingBalances,
|
starting_balances: StartingBalances,
|
||||||
datadir: &Path,
|
datadir: &Path,
|
||||||
electrum_rpc_port: u16,
|
electrum_rpc_port: u16,
|
||||||
electrum_http_port: u16,
|
|
||||||
seed: Seed,
|
seed: Seed,
|
||||||
execution_params: ExecutionParams,
|
execution_params: ExecutionParams,
|
||||||
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
||||||
@ -605,15 +598,11 @@ async fn init_test_wallets(
|
|||||||
let input = format!("tcp://@localhost:{}", electrum_rpc_port);
|
let input = format!("tcp://@localhost:{}", electrum_rpc_port);
|
||||||
Url::parse(&input).unwrap()
|
Url::parse(&input).unwrap()
|
||||||
};
|
};
|
||||||
let electrum_http_url = {
|
|
||||||
let input = format!("http://@localhost:{}", electrum_http_port);
|
|
||||||
Url::parse(&input).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let btc_wallet = swap::bitcoin::Wallet::new(
|
let btc_wallet = swap::bitcoin::Wallet::new(
|
||||||
electrum_rpc_url,
|
electrum_rpc_url,
|
||||||
electrum_http_url,
|
|
||||||
bitcoin::Network::Regtest,
|
bitcoin::Network::Regtest,
|
||||||
|
execution_params.bitcoin_finality_confirmations,
|
||||||
datadir,
|
datadir,
|
||||||
seed.derive_extended_private_key(bitcoin::Network::Regtest)
|
seed.derive_extended_private_key(bitcoin::Network::Regtest)
|
||||||
.expect("Could not create extended private key from seed"),
|
.expect("Could not create extended private key from seed"),
|
||||||
@ -675,26 +664,9 @@ pub fn init_tracing() -> DefaultGuard {
|
|||||||
// trouble when running multiple tests.
|
// trouble when running multiple tests.
|
||||||
let _ = LogTracer::init();
|
let _ = LogTracer::init();
|
||||||
|
|
||||||
let global_filter = tracing::Level::WARN;
|
|
||||||
let swap_filter = tracing::Level::DEBUG;
|
|
||||||
let xmr_btc_filter = tracing::Level::DEBUG;
|
|
||||||
let monero_rpc_filter = tracing::Level::DEBUG;
|
|
||||||
let monero_harness_filter = tracing::Level::DEBUG;
|
|
||||||
let bitcoin_harness_filter = tracing::Level::INFO;
|
|
||||||
let testcontainers_filter = tracing::Level::DEBUG;
|
|
||||||
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(format!(
|
.with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=info,bitcoin_harness=info,testcontainers=info")
|
||||||
"{},swap={},xmr_btc={},monero_harness={},monero_rpc={},bitcoin_harness={},testcontainers={}",
|
|
||||||
global_filter,
|
|
||||||
swap_filter,
|
|
||||||
xmr_btc_filter,
|
|
||||||
monero_harness_filter,
|
|
||||||
monero_rpc_filter,
|
|
||||||
bitcoin_harness_filter,
|
|
||||||
testcontainers_filter
|
|
||||||
))
|
|
||||||
.set_default()
|
.set_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user