mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-12-24 15:09:35 -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;
|
||||
|
||||
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_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)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Bitcoin {
|
||||
pub electrum_http_url: Url,
|
||||
pub electrum_rpc_url: Url,
|
||||
}
|
||||
|
||||
@ -120,12 +118,6 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
||||
.interact_text()?;
|
||||
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())
|
||||
.with_prompt("Enter Electrum RPC URL or hit return to use default")
|
||||
.default(DEFAULT_ELECTRUM_RPC_URL.to_owned())
|
||||
@ -144,10 +136,7 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
||||
network: Network {
|
||||
listen: listen_address,
|
||||
},
|
||||
bitcoin: Bitcoin {
|
||||
electrum_http_url,
|
||||
electrum_rpc_url,
|
||||
},
|
||||
bitcoin: Bitcoin { electrum_rpc_url },
|
||||
monero: Monero {
|
||||
wallet_rpc_url: monero_wallet_rpc_url,
|
||||
},
|
||||
@ -170,7 +159,6 @@ mod tests {
|
||||
dir: Default::default(),
|
||||
},
|
||||
bitcoin: Bitcoin {
|
||||
electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(),
|
||||
electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(),
|
||||
},
|
||||
network: Network {
|
||||
|
@ -152,8 +152,8 @@ async fn init_wallets(
|
||||
) -> Result<(bitcoin::Wallet, monero::Wallet)> {
|
||||
let bitcoin_wallet = bitcoin::Wallet::new(
|
||||
config.bitcoin.electrum_rpc_url,
|
||||
config.bitcoin.electrum_http_url,
|
||||
BITCOIN_NETWORK,
|
||||
execution_params.bitcoin_finality_confirmations,
|
||||
bitcoin_wallet_data_dir,
|
||||
key,
|
||||
)
|
||||
|
@ -21,9 +21,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use structopt::StructOpt;
|
||||
use swap::bitcoin::{Amount, TxLock};
|
||||
use swap::cli::command::{
|
||||
AliceConnectParams, Arguments, BitcoinParams, Command, Data, MoneroParams,
|
||||
};
|
||||
use swap::cli::command::{AliceConnectParams, Arguments, Command, Data, MoneroParams};
|
||||
use swap::database::Database;
|
||||
use swap::execution_params::{ExecutionParams, GetExecutionParams};
|
||||
use swap::network::quote::BidQuote;
|
||||
@ -95,11 +93,7 @@ async fn main() -> Result<()> {
|
||||
receive_monero_address,
|
||||
monero_daemon_host,
|
||||
},
|
||||
bitcoin_params:
|
||||
BitcoinParams {
|
||||
electrum_http_url,
|
||||
electrum_rpc_url,
|
||||
},
|
||||
electrum_rpc_url,
|
||||
} => {
|
||||
if receive_monero_address.network != monero_network {
|
||||
bail!(
|
||||
@ -112,9 +106,9 @@ async fn main() -> Result<()> {
|
||||
let bitcoin_wallet = init_bitcoin_wallet(
|
||||
bitcoin_network,
|
||||
electrum_rpc_url,
|
||||
electrum_http_url,
|
||||
seed,
|
||||
data_dir.clone(),
|
||||
execution_params,
|
||||
)
|
||||
.await?;
|
||||
let (monero_wallet, _process) = init_monero_wallet(
|
||||
@ -196,11 +190,7 @@ async fn main() -> Result<()> {
|
||||
receive_monero_address,
|
||||
monero_daemon_host,
|
||||
},
|
||||
bitcoin_params:
|
||||
BitcoinParams {
|
||||
electrum_http_url,
|
||||
electrum_rpc_url,
|
||||
},
|
||||
electrum_rpc_url,
|
||||
} => {
|
||||
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)
|
||||
@ -209,9 +199,9 @@ async fn main() -> Result<()> {
|
||||
let bitcoin_wallet = init_bitcoin_wallet(
|
||||
bitcoin_network,
|
||||
electrum_rpc_url,
|
||||
electrum_http_url,
|
||||
seed,
|
||||
data_dir.clone(),
|
||||
execution_params,
|
||||
)
|
||||
.await?;
|
||||
let (monero_wallet, _process) = init_monero_wallet(
|
||||
@ -255,18 +245,14 @@ async fn main() -> Result<()> {
|
||||
Command::Cancel {
|
||||
swap_id,
|
||||
force,
|
||||
bitcoin_params:
|
||||
BitcoinParams {
|
||||
electrum_http_url,
|
||||
electrum_rpc_url,
|
||||
},
|
||||
electrum_rpc_url,
|
||||
} => {
|
||||
let bitcoin_wallet = init_bitcoin_wallet(
|
||||
bitcoin_network,
|
||||
electrum_rpc_url,
|
||||
electrum_http_url,
|
||||
seed,
|
||||
data_dir,
|
||||
execution_params,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -290,32 +276,20 @@ async fn main() -> Result<()> {
|
||||
Command::Refund {
|
||||
swap_id,
|
||||
force,
|
||||
bitcoin_params:
|
||||
BitcoinParams {
|
||||
electrum_http_url,
|
||||
electrum_rpc_url,
|
||||
},
|
||||
electrum_rpc_url,
|
||||
} => {
|
||||
let bitcoin_wallet = init_bitcoin_wallet(
|
||||
bitcoin_network,
|
||||
electrum_rpc_url,
|
||||
electrum_http_url,
|
||||
seed,
|
||||
data_dir,
|
||||
execution_params,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
||||
|
||||
bob::refund(
|
||||
swap_id,
|
||||
resume_state,
|
||||
execution_params,
|
||||
Arc::new(bitcoin_wallet),
|
||||
db,
|
||||
force,
|
||||
)
|
||||
.await??;
|
||||
bob::refund(swap_id, resume_state, Arc::new(bitcoin_wallet), db, force).await??;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
@ -324,16 +298,16 @@ async fn main() -> Result<()> {
|
||||
async fn init_bitcoin_wallet(
|
||||
network: bitcoin::Network,
|
||||
electrum_rpc_url: Url,
|
||||
electrum_http_url: Url,
|
||||
seed: Seed,
|
||||
data_dir: PathBuf,
|
||||
execution_params: ExecutionParams,
|
||||
) -> Result<bitcoin::Wallet> {
|
||||
let wallet_dir = data_dir.join("wallet");
|
||||
|
||||
let wallet = bitcoin::Wallet::new(
|
||||
electrum_rpc_url.clone(),
|
||||
electrum_http_url.clone(),
|
||||
network,
|
||||
execution_params.bitcoin_finality_confirmations,
|
||||
&wallet_dir,
|
||||
seed.derive_extended_private_key(network)?,
|
||||
)
|
||||
|
@ -20,6 +20,7 @@ pub use ecdsa_fun::fun::Scalar;
|
||||
pub use ecdsa_fun::Signature;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
use crate::bitcoin::wallet::ScriptStatus;
|
||||
use ::bitcoin::hashes::hex::ToHex;
|
||||
use ::bitcoin::hashes::Hash;
|
||||
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 {
|
||||
fn from(p: Point) -> Self {
|
||||
Self(p)
|
||||
@ -209,46 +219,21 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub async fn poll_until_block_height_is_gte(
|
||||
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,
|
||||
pub fn current_epoch(
|
||||
cancel_timelock: CancelTimelock,
|
||||
punish_timelock: PunishTimelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
let current_block_height = bitcoin_wallet.get_block_height().await?;
|
||||
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
|
||||
let cancel_timelock_height = lock_tx_height + cancel_timelock;
|
||||
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),
|
||||
tx_lock_status: ScriptStatus,
|
||||
tx_cancel_status: ScriptStatus,
|
||||
) -> ExpiredTimelocks {
|
||||
if tx_cancel_status.is_confirmed_with(punish_timelock) {
|
||||
return ExpiredTimelocks::Punish;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet: &crate::bitcoin::Wallet,
|
||||
cancel_timelock: CancelTimelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> Result<()> {
|
||||
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?;
|
||||
if tx_lock_status.is_confirmed_with(cancel_timelock) {
|
||||
return ExpiredTimelocks::Cancel;
|
||||
}
|
||||
|
||||
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?;
|
||||
Ok(())
|
||||
ExpiredTimelocks::None
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
@ -266,3 +251,53 @@ pub struct EmptyWitnessStack;
|
||||
#[derive(Clone, Copy, thiserror::Error, Debug)]
|
||||
#[error("input has {0} witnesses, expected 3")]
|
||||
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::{
|
||||
build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock,
|
||||
TX_FEE,
|
||||
@ -5,9 +6,11 @@ use crate::bitcoin::{
|
||||
use ::bitcoin::util::bip143::SigHashCache;
|
||||
use ::bitcoin::{OutPoint, SigHash, SigHashType, TxIn, TxOut, Txid};
|
||||
use anyhow::Result;
|
||||
use bitcoin::Script;
|
||||
use ecdsa_fun::Signature;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
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
|
||||
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
|
||||
/// 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 {
|
||||
inner: Transaction,
|
||||
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::{
|
||||
build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, TX_FEE,
|
||||
};
|
||||
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
||||
use anyhow::Result;
|
||||
use bitcoin::Script;
|
||||
use ecdsa_fun::fun::Point;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
use rand::thread_rng;
|
||||
@ -55,6 +57,10 @@ impl TxLock {
|
||||
.len()
|
||||
}
|
||||
|
||||
pub fn script_pubkey(&self) -> Script {
|
||||
self.output_descriptor.script_pubkey()
|
||||
}
|
||||
|
||||
/// Retreive the index of the locked output in the transaction outputs
|
||||
/// vector
|
||||
fn lock_output_vout(&self) -> usize {
|
||||
@ -100,3 +106,13 @@ impl From<TxLock> for PartiallySignedTransaction {
|
||||
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::{SigHash, SigHashType};
|
||||
use anyhow::Result;
|
||||
use ecdsa_fun::Signature;
|
||||
use anyhow::{Context, Result};
|
||||
use bdk::bitcoin::Script;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@ -11,6 +12,7 @@ pub struct TxPunish {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||
watch_script: Script,
|
||||
}
|
||||
|
||||
impl TxPunish {
|
||||
@ -32,6 +34,7 @@ impl TxPunish {
|
||||
inner: tx_punish,
|
||||
digest,
|
||||
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
|
||||
watch_script: punish_address.script_pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,22 +42,20 @@ impl TxPunish {
|
||||
self.digest
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
pub fn complete(
|
||||
self,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
tx_punish_sig_bob: bitcoin::Signature,
|
||||
a: bitcoin::SecretKey,
|
||||
B: bitcoin::PublicKey,
|
||||
) -> Result<Transaction> {
|
||||
let sig_a = a.sign(self.digest());
|
||||
let sig_b = tx_punish_sig_bob;
|
||||
|
||||
let satisfier = {
|
||||
let mut satisfier = HashMap::with_capacity(2);
|
||||
|
||||
let A = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: A.0.into(),
|
||||
};
|
||||
let B = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: B.0.into(),
|
||||
};
|
||||
let A = a.public().into();
|
||||
let B = B.into();
|
||||
|
||||
// The order in which these are inserted doesn't matter
|
||||
satisfier.insert(A, (sig_a.into(), ::bitcoin::SigHashType::All));
|
||||
@ -65,8 +66,19 @@ impl TxPunish {
|
||||
|
||||
let mut tx_punish = self.inner;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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::{
|
||||
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
||||
Transaction, TxLock,
|
||||
verify_encsig, verify_sig, Address, EmptyWitnessStack, EncryptedSignature, NoInputs,
|
||||
NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock,
|
||||
};
|
||||
use ::bitcoin::util::bip143::SigHashCache;
|
||||
use ::bitcoin::{SigHash, SigHashType, Txid};
|
||||
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 miniscript::{Descriptor, DescriptorTrait};
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct TxRedeem {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
lock_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||
watch_script: Script,
|
||||
}
|
||||
|
||||
impl TxRedeem {
|
||||
@ -33,6 +40,7 @@ impl TxRedeem {
|
||||
inner: tx_redeem,
|
||||
digest,
|
||||
lock_output_descriptor: tx_lock.output_descriptor.clone(),
|
||||
watch_script: redeem_address.script_pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,17 +52,31 @@ impl TxRedeem {
|
||||
self.digest
|
||||
}
|
||||
|
||||
pub fn add_signatures(
|
||||
self,
|
||||
(A, sig_a): (PublicKey, Signature),
|
||||
(B, sig_b): (PublicKey, Signature),
|
||||
pub fn complete(
|
||||
mut self,
|
||||
encrypted_signature: EncryptedSignature,
|
||||
a: SecretKey,
|
||||
s_a: Scalar,
|
||||
B: PublicKey,
|
||||
) -> 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 mut satisfier = HashMap::with_capacity(2);
|
||||
|
||||
let A = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
key: A.0.into(),
|
||||
key: a.public.into(),
|
||||
};
|
||||
let B = ::bitcoin::PublicKey {
|
||||
compressed: true,
|
||||
@ -68,11 +90,11 @@ impl TxRedeem {
|
||||
satisfier
|
||||
};
|
||||
|
||||
let mut tx_redeem = self.inner;
|
||||
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(
|
||||
@ -112,3 +134,13 @@ impl TxRedeem {
|
||||
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::{
|
||||
verify_sig, Address, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs,
|
||||
Transaction, TxCancel,
|
||||
@ -5,6 +6,7 @@ use crate::bitcoin::{
|
||||
use ::bitcoin::util::bip143::SigHashCache;
|
||||
use ::bitcoin::{SigHash, SigHashType, Txid};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bitcoin::Script;
|
||||
use ecdsa_fun::Signature;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
use std::collections::HashMap;
|
||||
@ -14,6 +16,7 @@ pub struct TxRefund {
|
||||
inner: Transaction,
|
||||
digest: SigHash,
|
||||
cancel_output_descriptor: Descriptor<::bitcoin::PublicKey>,
|
||||
watch_script: Script,
|
||||
}
|
||||
|
||||
impl TxRefund {
|
||||
@ -31,6 +34,7 @@ impl TxRefund {
|
||||
inner: tx_punish,
|
||||
digest,
|
||||
cancel_output_descriptor: tx_cancel.output_descriptor.clone(),
|
||||
watch_script: refund_address.script_pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,3 +114,13 @@ impl TxRefund {
|
||||
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 std::convert::{TryFrom, TryInto};
|
||||
use std::ops::Add;
|
||||
|
||||
/// 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 {
|
||||
type Output = BlockHeight;
|
||||
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 {
|
||||
None,
|
||||
Cancel,
|
||||
|
@ -1,60 +1,46 @@
|
||||
use crate::bitcoin::timelocks::BlockHeight;
|
||||
use crate::bitcoin::{Address, Amount, Transaction};
|
||||
use crate::execution_params::ExecutionParams;
|
||||
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use ::bitcoin::Txid;
|
||||
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::descriptor::Segwitv0;
|
||||
use bdk::electrum_client::{self, Client, ElectrumApi};
|
||||
use bdk::electrum_client::{self, ElectrumApi, GetHistoryRes};
|
||||
use bdk::keys::DerivableKey;
|
||||
use bdk::{FeeRate, KeychainKind};
|
||||
use bitcoin::Script;
|
||||
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::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::interval;
|
||||
|
||||
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 {
|
||||
inner: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
|
||||
http_url: Url,
|
||||
rpc_url: Url,
|
||||
client: Arc<Mutex<Client>>,
|
||||
wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
|
||||
bitcoin_finality_confirmations: u32,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub async fn new(
|
||||
electrum_rpc_url: Url,
|
||||
electrum_http_url: Url,
|
||||
network: bitcoin::Network,
|
||||
bitcoin_finality_confirmations: u32,
|
||||
wallet_dir: &Path,
|
||||
key: impl DerivableKey<Segwitv0> + Clone,
|
||||
) -> Result<Self> {
|
||||
// Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47.
|
||||
let config = electrum_client::ConfigBuilder::default().retry(2).build();
|
||||
|
||||
let client = Client::from_config(electrum_rpc_url.as_str(), config)
|
||||
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?;
|
||||
let client =
|
||||
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)?;
|
||||
|
||||
@ -66,16 +52,21 @@ impl Wallet {
|
||||
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 {
|
||||
inner: Arc::new(Mutex::new(bdk_wallet)),
|
||||
http_url: electrum_http_url,
|
||||
rpc_url: electrum_rpc_url,
|
||||
wallet: Arc::new(Mutex::new(bdk_wallet)),
|
||||
client: Arc::new(Mutex::new(Client::new(electrum, interval)?)),
|
||||
bitcoin_finality_confirmations,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self
|
||||
.inner
|
||||
.wallet
|
||||
.lock()
|
||||
.await
|
||||
.get_balance()
|
||||
@ -86,7 +77,7 @@ impl Wallet {
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
let address = self
|
||||
.inner
|
||||
.wallet
|
||||
.lock()
|
||||
.await
|
||||
.get_new_address()
|
||||
@ -96,13 +87,14 @@ impl Wallet {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
let fees = self
|
||||
.inner
|
||||
.wallet
|
||||
.lock()
|
||||
.await
|
||||
.list_transactions(true)?
|
||||
@ -117,7 +109,7 @@ impl Wallet {
|
||||
}
|
||||
|
||||
pub async fn sync(&self) -> Result<()> {
|
||||
self.inner
|
||||
self.wallet
|
||||
.lock()
|
||||
.await
|
||||
.sync(noop_progress(), None)
|
||||
@ -131,7 +123,7 @@ impl Wallet {
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
let wallet = self.inner.lock().await;
|
||||
let wallet = self.wallet.lock().await;
|
||||
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
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
|
||||
/// transaction confirmed.
|
||||
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();
|
||||
|
||||
@ -163,15 +155,28 @@ impl Wallet {
|
||||
}
|
||||
|
||||
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
|
||||
/// 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();
|
||||
|
||||
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()
|
||||
.await
|
||||
.broadcast(transaction)
|
||||
@ -181,11 +186,11 @@ impl Wallet {
|
||||
|
||||
tracing::info!(%txid, "Published Bitcoin {} transaction", kind);
|
||||
|
||||
Ok(txid)
|
||||
Ok((txid, watcher))
|
||||
}
|
||||
|
||||
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 {
|
||||
bail!("PSBT is not finalized")
|
||||
@ -202,110 +207,72 @@ impl Wallet {
|
||||
.ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid))
|
||||
}
|
||||
|
||||
pub async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
tracing::debug!("watching for tx: {}", txid);
|
||||
let tx = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
||||
let client = Client::new(self.rpc_url.as_ref())
|
||||
.map_err(|err| backoff::Error::Permanent(Error::ElectrumClient(err)))?;
|
||||
|
||||
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 status_of_script<T>(&self, tx: &T) -> Result<ScriptStatus>
|
||||
where
|
||||
T: Watchable,
|
||||
{
|
||||
self.client.lock().await.status_of_script(tx)
|
||||
}
|
||||
|
||||
pub async fn get_block_height(&self) -> Result<BlockHeight> {
|
||||
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(
|
||||
pub async fn watch_until_status<T>(
|
||||
&self,
|
||||
txid: Txid,
|
||||
execution_params: ExecutionParams,
|
||||
) -> Result<()> {
|
||||
let conf_target = execution_params.bitcoin_finality_confirmations;
|
||||
tx: &T,
|
||||
mut status_fn: impl FnMut(ScriptStatus) -> bool,
|
||||
) -> Result<()>
|
||||
where
|
||||
T: Watchable,
|
||||
{
|
||||
let txid = tx.id();
|
||||
|
||||
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" });
|
||||
|
||||
// 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);
|
||||
let mut last_status = None;
|
||||
|
||||
loop {
|
||||
let tx_block_height = self.transaction_block_height(txid).await?;
|
||||
tracing::debug!("tx_block_height: {:?}", tx_block_height);
|
||||
let new_status = self.client.lock().await.status_of_script(tx)?;
|
||||
|
||||
let block_height = self.get_block_height().await?;
|
||||
tracing::debug!("latest_block_height: {:?}", block_height);
|
||||
|
||||
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;
|
||||
}
|
||||
if Some(new_status) != last_status {
|
||||
tracing::debug!(%txid, "Transaction is {}", new_status);
|
||||
}
|
||||
interval.tick().await;
|
||||
last_status = Some(new_status);
|
||||
|
||||
if status_fn(new_status) {
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
|
||||
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
|
||||
/// confirmed within a reasonable amount of time.
|
||||
fn select_feerate(&self) -> FeeRate {
|
||||
@ -314,42 +281,285 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result<Url> {
|
||||
let url = base_url.join(&format!("tx/{}/status", txid))?;
|
||||
|
||||
Ok(url)
|
||||
/// Defines a watchable transaction.
|
||||
///
|
||||
/// For a transaction to be watchable, we need to know two things: Its
|
||||
/// 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> {
|
||||
let url = base_url.join("blocks/tip/height")?;
|
||||
impl Watchable for (Txid, Script) {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cli::command::DEFAULT_ELECTRUM_HTTP_URL;
|
||||
|
||||
#[test]
|
||||
fn create_tx_status_url_from_default_base_url_success() {
|
||||
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
|
||||
let txid = Txid::default;
|
||||
fn given_depth_0_should_meet_confirmation_target_one() {
|
||||
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
||||
|
||||
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]
|
||||
fn create_block_tip_height_url_from_default_base_url_success() {
|
||||
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
|
||||
fn given_confirmations_1_should_meet_confirmation_target_one() {
|
||||
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!(
|
||||
url.as_str(),
|
||||
"https://blockstream.info/testnet/api/blocks/tip/height"
|
||||
);
|
||||
assert!(confirmed)
|
||||
}
|
||||
|
||||
#[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)]
|
||||
connect_params: AliceConnectParams,
|
||||
|
||||
#[structopt(flatten)]
|
||||
bitcoin_params: BitcoinParams,
|
||||
#[structopt(long = "electrum-rpc",
|
||||
help = "Provide the Bitcoin Electrum RPC URL",
|
||||
default_value = DEFAULT_ELECTRUM_RPC_URL
|
||||
)]
|
||||
electrum_rpc_url: Url,
|
||||
|
||||
#[structopt(flatten)]
|
||||
monero_params: MoneroParams,
|
||||
@ -59,8 +62,11 @@ pub enum Command {
|
||||
#[structopt(flatten)]
|
||||
connect_params: AliceConnectParams,
|
||||
|
||||
#[structopt(flatten)]
|
||||
bitcoin_params: BitcoinParams,
|
||||
#[structopt(long = "electrum-rpc",
|
||||
help = "Provide the Bitcoin Electrum RPC URL",
|
||||
default_value = DEFAULT_ELECTRUM_RPC_URL
|
||||
)]
|
||||
electrum_rpc_url: Url,
|
||||
|
||||
#[structopt(flatten)]
|
||||
monero_params: MoneroParams,
|
||||
@ -76,8 +82,11 @@ pub enum Command {
|
||||
#[structopt(short, long)]
|
||||
force: bool,
|
||||
|
||||
#[structopt(flatten)]
|
||||
bitcoin_params: BitcoinParams,
|
||||
#[structopt(long = "electrum-rpc",
|
||||
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)
|
||||
Refund {
|
||||
@ -90,8 +99,11 @@ pub enum Command {
|
||||
#[structopt(short, long)]
|
||||
force: bool,
|
||||
|
||||
#[structopt(flatten)]
|
||||
bitcoin_params: BitcoinParams,
|
||||
#[structopt(long = "electrum-rpc",
|
||||
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,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
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::monero_private_key;
|
||||
use crate::protocol::alice;
|
||||
@ -177,37 +177,18 @@ impl From<Alice> for AliceState {
|
||||
Alice::BtcCancelled {
|
||||
monero_wallet_restore_blockheight,
|
||||
state3,
|
||||
} => {
|
||||
let tx_cancel = TxCancel::new(
|
||||
&state3.tx_lock,
|
||||
state3.cancel_timelock,
|
||||
state3.a.public(),
|
||||
state3.B,
|
||||
);
|
||||
} => AliceState::BtcCancelled {
|
||||
monero_wallet_restore_blockheight,
|
||||
state3: Box::new(state3),
|
||||
},
|
||||
|
||||
AliceState::BtcCancelled {
|
||||
monero_wallet_restore_blockheight,
|
||||
state3: Box::new(state3),
|
||||
tx_cancel: Box::new(tx_cancel),
|
||||
}
|
||||
}
|
||||
Alice::BtcPunishable {
|
||||
monero_wallet_restore_blockheight,
|
||||
state3,
|
||||
} => {
|
||||
let tx_cancel = TxCancel::new(
|
||||
&state3.tx_lock,
|
||||
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),
|
||||
}
|
||||
}
|
||||
} => AliceState::BtcPunishable {
|
||||
monero_wallet_restore_blockheight,
|
||||
state3: Box::new(state3),
|
||||
},
|
||||
Alice::BtcRefunded {
|
||||
monero_wallet_restore_blockheight,
|
||||
state3,
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::bitcoin::{
|
||||
current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks,
|
||||
PunishTimelock, TxCancel, TxRefund,
|
||||
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
|
||||
};
|
||||
use crate::execution_params::ExecutionParams;
|
||||
use crate::protocol::alice::{Message1, Message3};
|
||||
@ -37,7 +36,6 @@ pub enum AliceState {
|
||||
BtcRedeemed,
|
||||
BtcCancelled {
|
||||
monero_wallet_restore_blockheight: BlockHeight,
|
||||
tx_cancel: Box<TxCancel>,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
BtcRefunded {
|
||||
@ -47,7 +45,6 @@ pub enum AliceState {
|
||||
},
|
||||
BtcPunishable {
|
||||
monero_wallet_restore_blockheight: BlockHeight,
|
||||
tx_refund: Box<TxRefund>,
|
||||
state3: Box<State3>,
|
||||
},
|
||||
XmrRefunded,
|
||||
@ -323,24 +320,45 @@ impl State3 {
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<()> {
|
||||
wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&self.tx_lock, |status| {
|
||||
status.is_confirmed_with(self.cancel_timelock)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn expired_timelocks(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
current_epoch(
|
||||
bitcoin_wallet,
|
||||
let tx_cancel = self.tx_cancel();
|
||||
|
||||
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.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::{
|
||||
poll_until_block_height_is_gte, BlockHeight, CancelTimelock, EncryptedSignature,
|
||||
PunishTimelock, TxCancel, TxLock, TxRefund,
|
||||
CancelTimelock, EncryptedSignature, PunishTimelock, TxCancel, TxLock, TxRefund,
|
||||
};
|
||||
use crate::execution_params::ExecutionParams;
|
||||
use crate::protocol::alice;
|
||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||
use crate::protocol::alice::TransferProof;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{Context, Result};
|
||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||
use ecdsa_fun::nonce::Deterministic;
|
||||
use futures::future::{select, Either};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use futures::pin_mut;
|
||||
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(
|
||||
bob_peer_id: PeerId,
|
||||
@ -81,36 +51,6 @@ pub async fn wait_for_bitcoin_encrypted_signature(
|
||||
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(
|
||||
tx_lock: TxLock,
|
||||
a: bitcoin::SecretKey,
|
||||
@ -118,12 +58,10 @@ pub async fn publish_cancel_transaction(
|
||||
cancel_timelock: CancelTimelock,
|
||||
tx_cancel_sig_bob: bitcoin::Signature,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<bitcoin::TxCancel> {
|
||||
// First wait for cancel timelock to expire
|
||||
let tx_lock_height = bitcoin_wallet
|
||||
.transaction_block_height(tx_lock.txid())
|
||||
) -> Result<()> {
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&tx_lock, |status| status.is_confirmed_with(cancel_timelock))
|
||||
.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);
|
||||
|
||||
@ -140,42 +78,52 @@ pub async fn publish_cancel_transaction(
|
||||
let sig_b = tx_cancel_sig_bob.clone();
|
||||
|
||||
let tx_cancel = tx_cancel
|
||||
.clone()
|
||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
||||
.expect("sig_{a,b} to be valid signatures for tx_cancel");
|
||||
|
||||
// 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
|
||||
// block height
|
||||
}
|
||||
|
||||
Ok(tx_cancel)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_for_bitcoin_refund(
|
||||
tx_cancel: &TxCancel,
|
||||
cancel_tx_height: BlockHeight,
|
||||
tx_refund: &TxRefund,
|
||||
punish_timelock: PunishTimelock,
|
||||
refund_address: &bitcoin::Address,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<(bitcoin::TxRefund, Option<bitcoin::Transaction>)> {
|
||||
let punish_timelock_expired =
|
||||
poll_until_block_height_is_gte(bitcoin_wallet, cancel_tx_height + punish_timelock);
|
||||
) -> Result<Option<bitcoin::Transaction>> {
|
||||
let refund_tx_id = tx_refund.txid();
|
||||
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);
|
||||
|
||||
// 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());
|
||||
let punish_timelock_expired = bitcoin_wallet.watch_until_status(tx_cancel, |status| {
|
||||
status.is_confirmed_with(punish_timelock)
|
||||
});
|
||||
|
||||
pin_mut!(punish_timelock_expired);
|
||||
pin_mut!(seen_refund_tx);
|
||||
|
||||
match select(punish_timelock_expired, seen_refund_tx).await {
|
||||
Either::Left(_) => Ok((tx_refund, None)),
|
||||
Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))),
|
||||
tokio::select! {
|
||||
seen_refund = seen_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)
|
||||
}
|
||||
|
||||
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.
|
||||
//! Alice holds XMR and wishes receive BTC.
|
||||
use crate::bitcoin::ExpiredTimelocks;
|
||||
use crate::bitcoin::{ExpiredTimelocks, TxRedeem};
|
||||
use crate::database::Database;
|
||||
use crate::execution_params::ExecutionParams;
|
||||
use crate::monero_ext::ScalarExt;
|
||||
use crate::protocol::alice;
|
||||
use crate::protocol::alice::event_loop::EventLoopHandle;
|
||||
use crate::protocol::alice::steps::{
|
||||
build_bitcoin_punish_transaction, build_bitcoin_redeem_transaction, extract_monero_private_key,
|
||||
lock_xmr, publish_cancel_transaction, wait_for_bitcoin_encrypted_signature,
|
||||
wait_for_bitcoin_refund, wait_for_locked_bitcoin,
|
||||
extract_monero_private_key, lock_xmr, publish_cancel_transaction,
|
||||
wait_for_bitcoin_encrypted_signature, wait_for_bitcoin_refund,
|
||||
};
|
||||
use crate::protocol::alice::AliceState;
|
||||
use crate::{bitcoin, database, monero};
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use futures::future::{select, Either};
|
||||
use futures::pin_mut;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::sync::Arc;
|
||||
use tokio::time::timeout;
|
||||
use tracing::{error, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -80,12 +80,19 @@ async fn run_until_internal(
|
||||
state3,
|
||||
bob_peer_id,
|
||||
} => {
|
||||
let _ = wait_for_locked_bitcoin(
|
||||
state3.tx_lock.txid(),
|
||||
&bitcoin_wallet,
|
||||
execution_params,
|
||||
timeout(
|
||||
execution_params.bob_time_to_act,
|
||||
bitcoin_wallet
|
||||
.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 {
|
||||
bob_peer_id,
|
||||
@ -198,27 +205,19 @@ async fn run_until_internal(
|
||||
} => {
|
||||
let state = match state3.expired_timelocks(bitcoin_wallet.as_ref()).await? {
|
||||
ExpiredTimelocks::None => {
|
||||
match build_bitcoin_redeem_transaction(
|
||||
match TxRedeem::new(&state3.tx_lock, &state3.redeem_address).complete(
|
||||
*encrypted_signature,
|
||||
&state3.tx_lock,
|
||||
state3.a.clone(),
|
||||
state3.s_a.to_secpfun_scalar(),
|
||||
state3.B,
|
||||
&state3.redeem_address,
|
||||
) {
|
||||
Ok(tx) => match bitcoin_wallet.broadcast(tx, "redeem").await {
|
||||
Ok(txid) => {
|
||||
let publishded_redeem_tx = bitcoin_wallet
|
||||
.wait_for_transaction_finality(txid, execution_params)
|
||||
.await;
|
||||
|
||||
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)
|
||||
}
|
||||
Ok((_, finality)) => match finality.await {
|
||||
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) => {
|
||||
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
|
||||
@ -269,7 +268,7 @@ async fn run_until_internal(
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let tx_cancel = publish_cancel_transaction(
|
||||
publish_cancel_transaction(
|
||||
state3.tx_lock.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B,
|
||||
@ -281,7 +280,6 @@ async fn run_until_internal(
|
||||
|
||||
let state = AliceState::BtcCancelled {
|
||||
state3,
|
||||
tx_cancel: Box::new(tx_cancel),
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
let db_state = (&state).into();
|
||||
@ -301,18 +299,12 @@ async fn run_until_internal(
|
||||
}
|
||||
AliceState::BtcCancelled {
|
||||
state3,
|
||||
tx_cancel,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let tx_cancel_height = bitcoin_wallet
|
||||
.transaction_block_height(tx_cancel.txid())
|
||||
.await?;
|
||||
|
||||
let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund(
|
||||
&tx_cancel,
|
||||
tx_cancel_height,
|
||||
let published_refund_tx = wait_for_bitcoin_refund(
|
||||
&state3.tx_cancel(),
|
||||
&state3.tx_refund(),
|
||||
state3.punish_timelock,
|
||||
&state3.refund_address,
|
||||
&bitcoin_wallet,
|
||||
)
|
||||
.await?;
|
||||
@ -321,7 +313,6 @@ async fn run_until_internal(
|
||||
match published_refund_tx {
|
||||
None => {
|
||||
let state = AliceState::BtcPunishable {
|
||||
tx_refund: Box::new(tx_refund),
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
};
|
||||
@ -344,7 +335,7 @@ async fn run_until_internal(
|
||||
Some(published_refund_tx) => {
|
||||
let spend_key = extract_monero_private_key(
|
||||
published_refund_tx,
|
||||
&tx_refund,
|
||||
&state3.tx_refund(),
|
||||
state3.s_a,
|
||||
state3.a.clone(),
|
||||
state3.S_b_bitcoin,
|
||||
@ -390,39 +381,38 @@ async fn run_until_internal(
|
||||
Ok(state)
|
||||
}
|
||||
AliceState::BtcPunishable {
|
||||
tx_refund,
|
||||
state3,
|
||||
monero_wallet_restore_blockheight,
|
||||
} => {
|
||||
let signed_tx_punish = build_bitcoin_punish_transaction(
|
||||
&state3.tx_lock,
|
||||
state3.cancel_timelock,
|
||||
&state3.punish_address,
|
||||
state3.punish_timelock,
|
||||
let signed_tx_punish = state3.tx_punish().complete(
|
||||
state3.tx_punish_sig_bob.clone(),
|
||||
state3.a.clone(),
|
||||
state3.B,
|
||||
)?;
|
||||
|
||||
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
|
||||
.wait_for_transaction_finality(txid, execution_params)
|
||||
.await?;
|
||||
finality.await?;
|
||||
|
||||
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!(refund_tx_seen);
|
||||
|
||||
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(
|
||||
published_refund_tx?,
|
||||
published_refund_tx,
|
||||
&tx_refund,
|
||||
state3.s_a,
|
||||
state3.a.clone(),
|
||||
@ -448,6 +438,9 @@ async fn run_until_internal(
|
||||
)
|
||||
.await
|
||||
}
|
||||
Either::Left((Err(e), _)) => {
|
||||
bail!(e.context("Failed to monitor refund transaction"))
|
||||
}
|
||||
Either::Right(_) => {
|
||||
let state = AliceState::BtcPunished;
|
||||
let db_state = (&state).into();
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::bitcoin::Wallet;
|
||||
use crate::database::{Database, Swap};
|
||||
use crate::execution_params::ExecutionParams;
|
||||
use crate::protocol::bob::BobState;
|
||||
use anyhow::{bail, Result};
|
||||
use std::sync::Arc;
|
||||
@ -13,7 +12,6 @@ pub struct SwapNotCancelledYet(Uuid);
|
||||
pub async fn refund(
|
||||
swap_id: Uuid,
|
||||
state: BobState,
|
||||
execution_params: ExecutionParams,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Database,
|
||||
force: bool,
|
||||
@ -41,9 +39,7 @@ pub async fn refund(
|
||||
}
|
||||
};
|
||||
|
||||
state4
|
||||
.refund_btc(bitcoin_wallet.as_ref(), execution_params)
|
||||
.await?;
|
||||
state4.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
let state = BobState::BtcRefunded(state4);
|
||||
let db_state = state.clone().into();
|
||||
|
@ -1,15 +1,14 @@
|
||||
use crate::bitcoin::{
|
||||
self, current_epoch, wait_for_cancel_timelock_to_expire, CancelTimelock, ExpiredTimelocks,
|
||||
PunishTimelock, Transaction, TxCancel, Txid,
|
||||
self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel,
|
||||
TxLock, Txid,
|
||||
};
|
||||
use crate::execution_params::ExecutionParams;
|
||||
use crate::monero;
|
||||
use crate::monero::{monero_private_key, InsufficientFunds, TransferProof};
|
||||
use crate::monero_ext::ScalarExt;
|
||||
use crate::protocol::alice::{Message1, Message3};
|
||||
use crate::protocol::bob::{EncryptedSignature, Message0, Message2, Message4};
|
||||
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::nonce::Deterministic;
|
||||
use ecdsa_fun::Signature;
|
||||
@ -262,31 +261,27 @@ impl State2 {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lock_btc(self, bitcoin_wallet: &bitcoin::Wallet) -> Result<State3> {
|
||||
let signed_tx = bitcoin_wallet
|
||||
.sign_and_finalize(self.tx_lock.clone().into())
|
||||
.await
|
||||
.context("Failed to sign Bitcoin lock transaction")?;
|
||||
|
||||
let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
||||
|
||||
Ok(State3 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
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,
|
||||
})
|
||||
pub async fn lock_btc(self) -> Result<(State3, TxLock)> {
|
||||
Ok((
|
||||
State3 {
|
||||
A: self.A,
|
||||
b: self.b,
|
||||
s_b: self.s_b,
|
||||
S_a_monero: self.S_a_monero,
|
||||
S_a_bitcoin: self.S_a_bitcoin,
|
||||
v: self.v,
|
||||
xmr: self.xmr,
|
||||
cancel_timelock: self.cancel_timelock,
|
||||
punish_timelock: self.punish_timelock,
|
||||
refund_address: self.refund_address,
|
||||
redeem_address: self.redeem_address,
|
||||
tx_lock: self.tx_lock.clone(),
|
||||
tx_cancel_sig_a: self.tx_cancel_sig_a,
|
||||
tx_refund_encsig: self.tx_refund_encsig,
|
||||
min_monero_confirmations: self.min_monero_confirmations,
|
||||
},
|
||||
self.tx_lock,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,12 +349,12 @@ impl State3 {
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<()> {
|
||||
wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&self.tx_lock, |status| {
|
||||
status.is_confirmed_with(self.cancel_timelock)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cancel(&self) -> State4 {
|
||||
@ -390,13 +385,17 @@ impl State3 {
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
current_epoch(
|
||||
bitcoin_wallet,
|
||||
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
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.punish_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -419,10 +418,9 @@ pub struct State4 {
|
||||
|
||||
impl State4 {
|
||||
pub fn next_message(&self) -> EncryptedSignature {
|
||||
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());
|
||||
|
||||
EncryptedSignature { tx_redeem_encsig }
|
||||
EncryptedSignature {
|
||||
tx_redeem_encsig: self.tx_redeem_encsig(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tx_redeem_encsig(&self) -> bitcoin::EncryptedSignature {
|
||||
@ -437,17 +435,6 @@ impl State4 {
|
||||
let tx_cancel =
|
||||
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?;
|
||||
|
||||
Ok(tx)
|
||||
@ -461,14 +448,13 @@ impl State4 {
|
||||
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_id = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
||||
let (tx_id, _) = bitcoin_wallet.broadcast(tx_cancel, "cancel").await?;
|
||||
|
||||
Ok(tx_id)
|
||||
}
|
||||
@ -477,10 +463,12 @@ impl State4 {
|
||||
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_candidate = bitcoin_wallet
|
||||
.watch_for_raw_transaction(tx_redeem.txid())
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&tx_redeem, |status| status.has_been_seen())
|
||||
.await?;
|
||||
|
||||
let tx_redeem_candidate = bitcoin_wallet.get_raw_transaction(tx_redeem.txid()).await?;
|
||||
|
||||
let tx_redeem_sig =
|
||||
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)?;
|
||||
@ -499,32 +487,33 @@ impl State4 {
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<()> {
|
||||
wait_for_cancel_timelock_to_expire(
|
||||
bitcoin_wallet,
|
||||
self.cancel_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
bitcoin_wallet
|
||||
.watch_until_status(&self.tx_lock, |status| {
|
||||
status.is_confirmed_with(self.cancel_timelock)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn expired_timelock(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
) -> Result<ExpiredTimelocks> {
|
||||
current_epoch(
|
||||
bitcoin_wallet,
|
||||
let tx_cancel = TxCancel::new(&self.tx_lock, self.cancel_timelock, self.A, self.b.public());
|
||||
|
||||
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.punish_timelock,
|
||||
self.tx_lock.txid(),
|
||||
)
|
||||
.await
|
||||
tx_lock_status,
|
||||
tx_cancel_status,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn refund_btc(
|
||||
&self,
|
||||
bitcoin_wallet: &bitcoin::Wallet,
|
||||
execution_params: ExecutionParams,
|
||||
) -> Result<()> {
|
||||
pub async fn refund_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result<()> {
|
||||
let tx_cancel =
|
||||
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);
|
||||
@ -538,11 +527,9 @@ impl State4 {
|
||||
let signed_tx_refund =
|
||||
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
|
||||
.wait_for_transaction_finality(txid, execution_params)
|
||||
.await?;
|
||||
finality.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use crate::protocol::bob;
|
||||
use crate::protocol::bob::event_loop::EventLoopHandle;
|
||||
use crate::protocol::bob::state::*;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use async_recursion::async_recursion;
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
@ -99,7 +99,16 @@ async fn run_until_internal(
|
||||
// Do not lock Bitcoin if not connected to Alice.
|
||||
event_loop_handle.dial().await?;
|
||||
// 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 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");
|
||||
}
|
||||
ExpiredTimelocks::Cancel => {
|
||||
state
|
||||
.refund_btc(bitcoin_wallet.as_ref(), execution_params)
|
||||
.await?;
|
||||
state.refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
BobState::BtcRefunded(state)
|
||||
}
|
||||
ExpiredTimelocks::Punish => BobState::BtcPunished {
|
||||
|
@ -48,7 +48,6 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
|
||||
let bob_state = bob::refund(
|
||||
bob_swap.swap_id,
|
||||
bob_swap.state,
|
||||
bob_swap.execution_params,
|
||||
bob_swap.bitcoin_wallet,
|
||||
bob_swap.db,
|
||||
false,
|
||||
|
@ -42,7 +42,6 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
||||
bob::refund(
|
||||
bob_swap.swap_id,
|
||||
bob_swap.state,
|
||||
bob_swap.execution_params,
|
||||
bob_swap.bitcoin_wallet,
|
||||
bob_swap.db,
|
||||
false,
|
||||
|
@ -40,7 +40,6 @@ async fn given_bob_manually_forces_cancel_when_timelock_not_expired_errors() {
|
||||
let is_error = bob::refund(
|
||||
bob_swap.swap_id,
|
||||
bob_swap.state,
|
||||
bob_swap.execution_params,
|
||||
bob_swap.bitcoin_wallet,
|
||||
bob_swap.db,
|
||||
true,
|
||||
|
@ -339,10 +339,6 @@ where
|
||||
.electrs
|
||||
.get_host_port(testutils::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 bob_seed = Seed::random().unwrap();
|
||||
@ -354,7 +350,6 @@ where
|
||||
alice_starting_balances.clone(),
|
||||
tempdir().unwrap().path(),
|
||||
electrs_rpc_port,
|
||||
electrs_http_port,
|
||||
alice_seed,
|
||||
execution_params,
|
||||
)
|
||||
@ -377,7 +372,6 @@ where
|
||||
bob_starting_balances.clone(),
|
||||
tempdir().unwrap().path(),
|
||||
electrs_rpc_port,
|
||||
electrs_http_port,
|
||||
bob_seed,
|
||||
execution_params,
|
||||
)
|
||||
@ -585,7 +579,6 @@ async fn init_test_wallets(
|
||||
starting_balances: StartingBalances,
|
||||
datadir: &Path,
|
||||
electrum_rpc_port: u16,
|
||||
electrum_http_port: u16,
|
||||
seed: Seed,
|
||||
execution_params: ExecutionParams,
|
||||
) -> (Arc<bitcoin::Wallet>, Arc<monero::Wallet>) {
|
||||
@ -605,15 +598,11 @@ async fn init_test_wallets(
|
||||
let input = format!("tcp://@localhost:{}", electrum_rpc_port);
|
||||
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(
|
||||
electrum_rpc_url,
|
||||
electrum_http_url,
|
||||
bitcoin::Network::Regtest,
|
||||
execution_params.bitcoin_finality_confirmations,
|
||||
datadir,
|
||||
seed.derive_extended_private_key(bitcoin::Network::Regtest)
|
||||
.expect("Could not create extended private key from seed"),
|
||||
@ -675,26 +664,9 @@ pub fn init_tracing() -> DefaultGuard {
|
||||
// trouble when running multiple tests.
|
||||
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 _;
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(format!(
|
||||
"{},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
|
||||
))
|
||||
.with_env_filter("warn,swap=debug,monero_harness=debug,monero_rpc=info,bitcoin_harness=info,testcontainers=info")
|
||||
.set_default()
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user