mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-07-28 17:24:23 -04:00
Test on-chain protocol happy path
This commit is contained in:
parent
d3a7689059
commit
5395303a99
9 changed files with 457 additions and 245 deletions
|
@ -19,12 +19,13 @@ monero = "0.9"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
tokio = { version = "0.2", features = ["time"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
backoff = { version = "0.2", features = ["tokio"] }
|
backoff = { version = "0.2", features = ["tokio"] }
|
||||||
base64 = "0.12"
|
base64 = "0.12"
|
||||||
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "d402b36d3d6406150e3bfb71492ff4a0a7cb290e" }
|
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "7ff30a559ab57cc3aa71189e71433ef6b2a6c3a2" }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
monero-harness = { path = "../monero-harness" }
|
monero-harness = { path = "../monero-harness" }
|
||||||
reqwest = { version = "0.10", default-features = false }
|
reqwest = { version = "0.10", default-features = false }
|
||||||
|
|
|
@ -50,13 +50,15 @@ pub async fn next_state<
|
||||||
|
|
||||||
let message1 = transport.receive_message().await?.try_into()?;
|
let message1 = transport.receive_message().await?.try_into()?;
|
||||||
let state2 = state1.receive(message1)?;
|
let state2 = state1.receive(message1)?;
|
||||||
|
|
||||||
|
let message2 = state2.next_message();
|
||||||
|
transport.send_message(message2.into()).await?;
|
||||||
Ok(state2.into())
|
Ok(state2.into())
|
||||||
}
|
}
|
||||||
State::State2(state2) => {
|
State::State2(state2) => {
|
||||||
let message2 = state2.next_message();
|
|
||||||
let state3 = state2.lock_btc(bitcoin_wallet).await?;
|
let state3 = state2.lock_btc(bitcoin_wallet).await?;
|
||||||
tracing::info!("bob has locked btc");
|
tracing::info!("bob has locked btc");
|
||||||
transport.send_message(message2.into()).await?;
|
|
||||||
Ok(state3.into())
|
Ok(state3.into())
|
||||||
}
|
}
|
||||||
State::State3(state3) => {
|
State::State3(state3) => {
|
||||||
|
|
|
@ -59,6 +59,14 @@ use futures::{
|
||||||
};
|
};
|
||||||
use genawaiter::sync::{Gen, GenBoxed};
|
use genawaiter::sync::{Gen, GenBoxed};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
// TODO: Replace this with something configurable, such as an function argument.
|
||||||
|
/// Time that Bob has to publish the Bitcoin lock transaction before both
|
||||||
|
/// parties will abort, in seconds.
|
||||||
|
const SECS_TO_ACT_BOB: u64 = 60;
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -80,8 +88,13 @@ pub trait ReceiveTransferProof {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MedianTime {
|
pub trait BlockHeight {
|
||||||
async fn median_time(&self) -> u32;
|
async fn block_height(&self) -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TransactionBlockHeight {
|
||||||
|
async fn transaction_block_height(&self, txid: bitcoin::Txid) -> u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform the on-chain protocol to swap monero and bitcoin as Bob.
|
/// Perform the on-chain protocol to swap monero and bitcoin as Bob.
|
||||||
|
@ -89,9 +102,9 @@ pub trait MedianTime {
|
||||||
/// This is called post handshake, after all the keys, addresses and most of the
|
/// This is called post handshake, after all the keys, addresses and most of the
|
||||||
/// signatures have been exchanged.
|
/// signatures have been exchanged.
|
||||||
pub fn action_generator_bob<N, M, B>(
|
pub fn action_generator_bob<N, M, B>(
|
||||||
network: &'static mut N,
|
mut network: N,
|
||||||
monero_client: &'static M,
|
monero_client: Arc<M>,
|
||||||
bitcoin_client: &'static B,
|
bitcoin_client: Arc<B>,
|
||||||
// TODO: Replace this with a new, slimmer struct?
|
// TODO: Replace this with a new, slimmer struct?
|
||||||
bob::State2 {
|
bob::State2 {
|
||||||
A,
|
A,
|
||||||
|
@ -111,10 +124,16 @@ pub fn action_generator_bob<N, M, B>(
|
||||||
}: bob::State2,
|
}: bob::State2,
|
||||||
) -> GenBoxed<BobAction, (), ()>
|
) -> GenBoxed<BobAction, (), ()>
|
||||||
where
|
where
|
||||||
N: ReceiveTransferProof + Send + Sync,
|
N: ReceiveTransferProof + Send + Sync + 'static,
|
||||||
M: monero::WatchForTransfer + Send + Sync,
|
M: monero::WatchForTransfer + Send + Sync + 'static,
|
||||||
B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync,
|
B: BlockHeight
|
||||||
|
+ TransactionBlockHeight
|
||||||
|
+ bitcoin::WatchForRawTransaction
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static,
|
||||||
{
|
{
|
||||||
|
#[derive(Debug)]
|
||||||
enum SwapFailed {
|
enum SwapFailed {
|
||||||
BeforeBtcLock,
|
BeforeBtcLock,
|
||||||
AfterBtcLock(Reason),
|
AfterBtcLock(Reason),
|
||||||
|
@ -122,6 +141,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reason why the swap has failed.
|
/// Reason why the swap has failed.
|
||||||
|
#[derive(Debug)]
|
||||||
enum Reason {
|
enum Reason {
|
||||||
/// The refund timelock has been reached.
|
/// The refund timelock has been reached.
|
||||||
BtcExpired,
|
BtcExpired,
|
||||||
|
@ -140,37 +160,40 @@ where
|
||||||
if condition_future.clone().await {
|
if condition_future.clone().await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn bitcoin_time_is_gte<B>(bitcoin_client: &B, timestamp: u32) -> bool
|
async fn bitcoin_block_height_is_gte<B>(bitcoin_client: &B, n_blocks: u32) -> bool
|
||||||
where
|
where
|
||||||
B: MedianTime,
|
B: BlockHeight,
|
||||||
{
|
{
|
||||||
bitcoin_client.median_time().await >= timestamp
|
bitcoin_client.block_height().await >= n_blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
Gen::new_boxed(|co| async move {
|
Gen::new_boxed(|co| async move {
|
||||||
let swap_result: Result<(), SwapFailed> = async {
|
let swap_result: Result<(), SwapFailed> = async {
|
||||||
let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared();
|
|
||||||
let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared();
|
|
||||||
futures::pin_mut!(poll_until_btc_has_expired);
|
|
||||||
|
|
||||||
if btc_has_expired.clone().await {
|
|
||||||
return Err(SwapFailed::BeforeBtcLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await;
|
co.yield_(BobAction::LockBitcoin(tx_lock.clone())).await;
|
||||||
|
|
||||||
match select(
|
timeout(
|
||||||
|
Duration::from_secs(SECS_TO_ACT_BOB),
|
||||||
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
|
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
|
||||||
poll_until_btc_has_expired.clone(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
.map(|tx| tx.txid())
|
||||||
Either::Left(_) => {}
|
.map_err(|_| SwapFailed::BeforeBtcLock)?;
|
||||||
Either::Right(_) => return Err(SwapFailed::BeforeBtcLock),
|
|
||||||
}
|
let tx_lock_height = bitcoin_client
|
||||||
|
.transaction_block_height(tx_lock.txid())
|
||||||
|
.await;
|
||||||
|
let btc_has_expired = bitcoin_block_height_is_gte(
|
||||||
|
bitcoin_client.as_ref(),
|
||||||
|
tx_lock_height + refund_timelock,
|
||||||
|
)
|
||||||
|
.shared();
|
||||||
|
let poll_until_btc_has_expired = poll_until(btc_has_expired).shared();
|
||||||
|
futures::pin_mut!(poll_until_btc_has_expired);
|
||||||
|
|
||||||
let transfer_proof = match select(
|
let transfer_proof = match select(
|
||||||
network.receive_transfer_proof(),
|
network.receive_transfer_proof(),
|
||||||
|
@ -245,7 +268,9 @@ where
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(SwapFailed::AfterBtcLock(_)) = swap_result {
|
if let Err(err @ SwapFailed::AfterBtcLock(_)) = swap_result {
|
||||||
|
error!("Swap failed, reason: {:?}", err);
|
||||||
|
|
||||||
let tx_cancel =
|
let tx_cancel =
|
||||||
bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public());
|
bitcoin::TxCancel::new(&tx_lock, refund_timelock, A.clone(), b.public());
|
||||||
let tx_cancel_txid = tx_cancel.txid();
|
let tx_cancel_txid = tx_cancel.txid();
|
||||||
|
@ -316,10 +341,9 @@ pub trait ReceiveBitcoinRedeemEncsig {
|
||||||
///
|
///
|
||||||
/// This is called post handshake, after all the keys, addresses and most of the
|
/// This is called post handshake, after all the keys, addresses and most of the
|
||||||
/// signatures have been exchanged.
|
/// signatures have been exchanged.
|
||||||
pub fn action_generator_alice<N, M, B>(
|
pub fn action_generator_alice<N, B>(
|
||||||
network: &'static mut N,
|
mut network: N,
|
||||||
_monero_client: &'static M,
|
bitcoin_client: Arc<B>,
|
||||||
bitcoin_client: &'static B,
|
|
||||||
// TODO: Replace this with a new, slimmer struct?
|
// TODO: Replace this with a new, slimmer struct?
|
||||||
alice::State3 {
|
alice::State3 {
|
||||||
a,
|
a,
|
||||||
|
@ -341,16 +365,22 @@ pub fn action_generator_alice<N, M, B>(
|
||||||
}: alice::State3,
|
}: alice::State3,
|
||||||
) -> GenBoxed<AliceAction, (), ()>
|
) -> GenBoxed<AliceAction, (), ()>
|
||||||
where
|
where
|
||||||
N: ReceiveBitcoinRedeemEncsig + Send + Sync,
|
N: ReceiveBitcoinRedeemEncsig + Send + Sync + 'static,
|
||||||
M: Send + Sync,
|
B: BlockHeight
|
||||||
B: MedianTime + bitcoin::WatchForRawTransaction + Send + Sync,
|
+ TransactionBlockHeight
|
||||||
|
+ bitcoin::WatchForRawTransaction
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static,
|
||||||
{
|
{
|
||||||
|
#[derive(Debug)]
|
||||||
enum SwapFailed {
|
enum SwapFailed {
|
||||||
BeforeBtcLock,
|
BeforeBtcLock,
|
||||||
AfterXmrLock(Reason),
|
AfterXmrLock(Reason),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reason why the swap has failed.
|
/// Reason why the swap has failed.
|
||||||
|
#[derive(Debug)]
|
||||||
enum Reason {
|
enum Reason {
|
||||||
/// The refund timelock has been reached.
|
/// The refund timelock has been reached.
|
||||||
BtcExpired,
|
BtcExpired,
|
||||||
|
@ -373,31 +403,37 @@ where
|
||||||
if condition_future.clone().await {
|
if condition_future.clone().await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn bitcoin_time_is_gte<B>(bitcoin_client: &B, timestamp: u32) -> bool
|
async fn bitcoin_block_height_is_gte<B>(bitcoin_client: &B, n_blocks: u32) -> bool
|
||||||
where
|
where
|
||||||
B: MedianTime,
|
B: BlockHeight,
|
||||||
{
|
{
|
||||||
bitcoin_client.median_time().await >= timestamp
|
bitcoin_client.block_height().await >= n_blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
Gen::new_boxed(|co| async move {
|
Gen::new_boxed(|co| async move {
|
||||||
let swap_result: Result<(), SwapFailed> = async {
|
let swap_result: Result<(), SwapFailed> = async {
|
||||||
let btc_has_expired = bitcoin_time_is_gte(bitcoin_client, refund_timelock).shared();
|
timeout(
|
||||||
let poll_until_btc_has_expired = poll_until(btc_has_expired.clone()).shared();
|
Duration::from_secs(SECS_TO_ACT_BOB),
|
||||||
futures::pin_mut!(poll_until_btc_has_expired);
|
|
||||||
|
|
||||||
match select(
|
|
||||||
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
|
bitcoin_client.watch_for_raw_transaction(tx_lock.txid()),
|
||||||
poll_until_btc_has_expired.clone(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|_| SwapFailed::BeforeBtcLock)?;
|
||||||
Either::Left(_) => {}
|
|
||||||
Either::Right(_) => return Err(SwapFailed::BeforeBtcLock),
|
let tx_lock_height = bitcoin_client
|
||||||
}
|
.transaction_block_height(tx_lock.txid())
|
||||||
|
.await;
|
||||||
|
let btc_has_expired = bitcoin_block_height_is_gte(
|
||||||
|
bitcoin_client.as_ref(),
|
||||||
|
tx_lock_height + refund_timelock,
|
||||||
|
)
|
||||||
|
.shared();
|
||||||
|
let poll_until_btc_has_expired = poll_until(btc_has_expired).shared();
|
||||||
|
futures::pin_mut!(poll_until_btc_has_expired);
|
||||||
|
|
||||||
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey {
|
||||||
scalar: s_a.into_ed25519(),
|
scalar: s_a.into_ed25519(),
|
||||||
|
@ -459,7 +495,7 @@ where
|
||||||
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
|
if let Err(SwapFailed::AfterXmrLock(Reason::BtcExpired)) = swap_result {
|
||||||
let refund_result: Result<(), RefundFailed> = async {
|
let refund_result: Result<(), RefundFailed> = async {
|
||||||
let bob_can_be_punished =
|
let bob_can_be_punished =
|
||||||
bitcoin_time_is_gte(bitcoin_client, punish_timelock).shared();
|
bitcoin_block_height_is_gte(bitcoin_client.as_ref(), punish_timelock).shared();
|
||||||
let poll_until_bob_can_be_punished = poll_until(bob_can_be_punished).shared();
|
let poll_until_bob_can_be_punished = poll_until(bob_can_be_punished).shared();
|
||||||
futures::pin_mut!(poll_until_bob_can_be_punished);
|
futures::pin_mut!(poll_until_bob_can_be_punished);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||||
pub use curve25519_dalek::scalar::Scalar;
|
pub use curve25519_dalek::scalar::Scalar;
|
||||||
pub use monero::{Address, PrivateKey, PublicKey};
|
pub use monero::{Address, PrivateKey, PublicKey};
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
use std::ops::Add;
|
use std::ops::{Add, Sub};
|
||||||
|
|
||||||
pub const MIN_CONFIRMATIONS: u32 = 10;
|
pub const MIN_CONFIRMATIONS: u32 = 10;
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ impl From<PublicViewKey> for PublicKey {
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct PublicViewKey(PublicKey);
|
pub struct PublicViewKey(PublicKey);
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
pub struct Amount(u64);
|
pub struct Amount(u64);
|
||||||
|
|
||||||
impl Amount {
|
impl Amount {
|
||||||
|
@ -67,6 +67,22 @@ impl Amount {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Add for Amount {
|
||||||
|
type Output = Amount;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
Self(self.0 + rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Amount {
|
||||||
|
type Output = Amount;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
Self(self.0 - rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Amount> for u64 {
|
impl From<Amount> for u64 {
|
||||||
fn from(from: Amount) -> u64 {
|
fn from(from: Amount) -> u64 {
|
||||||
from.0
|
from.0
|
||||||
|
|
|
@ -1,156 +1,14 @@
|
||||||
pub mod harness;
|
pub mod harness;
|
||||||
|
|
||||||
use crate::harness::wallet;
|
|
||||||
use bitcoin_harness::Bitcoind;
|
|
||||||
use harness::{
|
|
||||||
node::{AliceNode, BobNode},
|
|
||||||
transport::Transport,
|
|
||||||
};
|
|
||||||
use monero_harness::Monero;
|
|
||||||
use rand::rngs::OsRng;
|
|
||||||
use testcontainers::clients::Cli;
|
|
||||||
use tokio::sync::{
|
|
||||||
mpsc,
|
|
||||||
mpsc::{Receiver, Sender},
|
|
||||||
};
|
|
||||||
use xmr_btc::{alice, bitcoin, bob, monero};
|
|
||||||
|
|
||||||
const TEN_XMR: u64 = 10_000_000_000_000;
|
|
||||||
const RELATIVE_REFUND_TIMELOCK: u32 = 1;
|
|
||||||
const RELATIVE_PUNISH_TIMELOCK: u32 = 1;
|
|
||||||
|
|
||||||
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
|
|
||||||
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
|
|
||||||
let _ = bitcoind.init(5).await;
|
|
||||||
|
|
||||||
bitcoind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct InitialBalances {
|
|
||||||
alice_xmr: u64,
|
|
||||||
alice_btc: bitcoin::Amount,
|
|
||||||
bob_xmr: u64,
|
|
||||||
bob_btc: bitcoin::Amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SwapAmounts {
|
|
||||||
xmr: monero::Amount,
|
|
||||||
btc: bitcoin::Amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_alice_and_bob_transports() -> (
|
|
||||||
Transport<alice::Message, bob::Message>,
|
|
||||||
Transport<bob::Message, alice::Message>,
|
|
||||||
) {
|
|
||||||
let (a_sender, b_receiver): (Sender<alice::Message>, Receiver<alice::Message>) =
|
|
||||||
mpsc::channel(5);
|
|
||||||
let (b_sender, a_receiver): (Sender<bob::Message>, Receiver<bob::Message>) = mpsc::channel(5);
|
|
||||||
|
|
||||||
let a_transport = Transport {
|
|
||||||
sender: a_sender,
|
|
||||||
receiver: a_receiver,
|
|
||||||
};
|
|
||||||
|
|
||||||
let b_transport = Transport {
|
|
||||||
sender: b_sender,
|
|
||||||
receiver: b_receiver,
|
|
||||||
};
|
|
||||||
|
|
||||||
(a_transport, b_transport)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_test(
|
|
||||||
monero: &Monero,
|
|
||||||
bitcoind: &Bitcoind<'_>,
|
|
||||||
) -> (
|
|
||||||
alice::State0,
|
|
||||||
bob::State0,
|
|
||||||
AliceNode,
|
|
||||||
BobNode,
|
|
||||||
InitialBalances,
|
|
||||||
SwapAmounts,
|
|
||||||
) {
|
|
||||||
// must be bigger than our hardcoded fee of 10_000
|
|
||||||
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
|
|
||||||
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
|
|
||||||
|
|
||||||
let swap_amounts = SwapAmounts {
|
|
||||||
xmr: xmr_amount,
|
|
||||||
btc: btc_amount,
|
|
||||||
};
|
|
||||||
|
|
||||||
let fund_alice = TEN_XMR;
|
|
||||||
let fund_bob = 0;
|
|
||||||
monero.init(fund_alice, fund_bob).await.unwrap();
|
|
||||||
|
|
||||||
let alice_monero_wallet = wallet::monero::Wallet(monero.alice_wallet_rpc_client());
|
|
||||||
let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client());
|
|
||||||
|
|
||||||
let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (alice_transport, bob_transport) = init_alice_and_bob_transports();
|
|
||||||
let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet);
|
|
||||||
|
|
||||||
let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet);
|
|
||||||
|
|
||||||
let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap();
|
|
||||||
let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap();
|
|
||||||
|
|
||||||
let alice_initial_xmr_balance = alice.monero_wallet.0.get_balance(0).await.unwrap();
|
|
||||||
let bob_initial_xmr_balance = bob.monero_wallet.0.get_balance(0).await.unwrap();
|
|
||||||
|
|
||||||
let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap();
|
|
||||||
let punish_address = redeem_address.clone();
|
|
||||||
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
|
|
||||||
|
|
||||||
let alice_state0 = alice::State0::new(
|
|
||||||
&mut OsRng,
|
|
||||||
btc_amount,
|
|
||||||
xmr_amount,
|
|
||||||
RELATIVE_REFUND_TIMELOCK,
|
|
||||||
RELATIVE_PUNISH_TIMELOCK,
|
|
||||||
redeem_address.clone(),
|
|
||||||
punish_address.clone(),
|
|
||||||
);
|
|
||||||
let bob_state0 = bob::State0::new(
|
|
||||||
&mut OsRng,
|
|
||||||
btc_amount,
|
|
||||||
xmr_amount,
|
|
||||||
RELATIVE_REFUND_TIMELOCK,
|
|
||||||
RELATIVE_PUNISH_TIMELOCK,
|
|
||||||
refund_address,
|
|
||||||
);
|
|
||||||
let initial_balances = InitialBalances {
|
|
||||||
alice_xmr: alice_initial_xmr_balance,
|
|
||||||
alice_btc: alice_initial_btc_balance,
|
|
||||||
bob_xmr: bob_initial_xmr_balance,
|
|
||||||
bob_btc: bob_initial_btc_balance,
|
|
||||||
};
|
|
||||||
(
|
|
||||||
alice_state0,
|
|
||||||
bob_state0,
|
|
||||||
alice,
|
|
||||||
bob,
|
|
||||||
initial_balances,
|
|
||||||
swap_amounts,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod tests {
|
mod tests {
|
||||||
// NOTE: For some reason running these tests overflows the stack. In order to
|
// NOTE: For some reason running these tests overflows the stack. In order to
|
||||||
// mitigate this run them with:
|
// mitigate this run them with:
|
||||||
//
|
//
|
||||||
// RUST_MIN_STACK=100000000 cargo test
|
// RUST_MIN_STACK=100000000 cargo test
|
||||||
|
|
||||||
use crate::{
|
use crate::harness::{
|
||||||
harness,
|
self, init_bitcoind, init_test,
|
||||||
harness::node::{run_alice_until, run_bob_until},
|
node::{run_alice_until, run_bob_until},
|
||||||
init_bitcoind, init_test,
|
|
||||||
};
|
};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use monero_harness::Monero;
|
use monero_harness::Monero;
|
||||||
|
@ -181,7 +39,7 @@ mod tests {
|
||||||
mut bob_node,
|
mut bob_node,
|
||||||
initial_balances,
|
initial_balances,
|
||||||
swap_amounts,
|
swap_amounts,
|
||||||
) = init_test(&monero, &bitcoind).await;
|
) = init_test(&monero, &bitcoind, None, None).await;
|
||||||
|
|
||||||
let (alice_state, bob_state) = future::try_join(
|
let (alice_state, bob_state) = future::try_join(
|
||||||
run_alice_until(
|
run_alice_until(
|
||||||
|
@ -212,11 +70,11 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap();
|
let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
monero.wait_for_bob_wallet_block_height().await.unwrap();
|
monero.wait_for_bob_wallet_block_height().await.unwrap();
|
||||||
|
|
||||||
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap();
|
let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
alice_final_btc_balance,
|
alice_final_btc_balance,
|
||||||
|
@ -230,13 +88,11 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
alice_final_xmr_balance,
|
alice_final_xmr_balance,
|
||||||
initial_balances.alice_xmr
|
initial_balances.alice_xmr - swap_amounts.xmr - alice_state6.lock_xmr_fee()
|
||||||
- u64::from(swap_amounts.xmr)
|
|
||||||
- u64::from(alice_state6.lock_xmr_fee())
|
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bob_final_xmr_balance,
|
bob_final_xmr_balance,
|
||||||
initial_balances.bob_xmr + u64::from(swap_amounts.xmr)
|
initial_balances.bob_xmr + swap_amounts.xmr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +113,7 @@ mod tests {
|
||||||
mut bob_node,
|
mut bob_node,
|
||||||
initial_balances,
|
initial_balances,
|
||||||
swap_amounts,
|
swap_amounts,
|
||||||
) = init_test(&monero, &bitcoind).await;
|
) = init_test(&monero, &bitcoind, None, None).await;
|
||||||
|
|
||||||
let (alice_state, bob_state) = future::try_join(
|
let (alice_state, bob_state) = future::try_join(
|
||||||
run_alice_until(
|
run_alice_until(
|
||||||
|
@ -300,8 +156,8 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
monero.wait_for_alice_wallet_block_height().await.unwrap();
|
monero.wait_for_alice_wallet_block_height().await.unwrap();
|
||||||
let alice_final_xmr_balance = alice_node.monero_wallet.0.get_balance(0).await.unwrap();
|
let alice_final_xmr_balance = alice_node.monero_wallet.get_balance().await.unwrap();
|
||||||
let bob_final_xmr_balance = bob_node.monero_wallet.0.get_balance(0).await.unwrap();
|
let bob_final_xmr_balance = bob_node.monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
assert_eq!(alice_final_btc_balance, initial_balances.alice_btc);
|
assert_eq!(alice_final_btc_balance, initial_balances.alice_btc);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -312,7 +168,7 @@ mod tests {
|
||||||
|
|
||||||
// Because we create a new wallet when claiming Monero, we can only assert on
|
// Because we create a new wallet when claiming Monero, we can only assert on
|
||||||
// this new wallet owning all of `xmr_amount` after refund
|
// this new wallet owning all of `xmr_amount` after refund
|
||||||
assert_eq!(alice_final_xmr_balance, u64::from(swap_amounts.xmr));
|
assert_eq!(alice_final_xmr_balance, swap_amounts.xmr);
|
||||||
assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr);
|
assert_eq!(bob_final_xmr_balance, initial_balances.bob_xmr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +189,7 @@ mod tests {
|
||||||
mut bob_node,
|
mut bob_node,
|
||||||
initial_balances,
|
initial_balances,
|
||||||
swap_amounts,
|
swap_amounts,
|
||||||
) = init_test(&monero, &bitcoind).await;
|
) = init_test(&monero, &bitcoind, None, None).await;
|
||||||
|
|
||||||
let (alice_state, bob_state) = future::try_join(
|
let (alice_state, bob_state) = future::try_join(
|
||||||
run_alice_until(
|
run_alice_until(
|
||||||
|
|
|
@ -5,6 +5,10 @@ pub mod wallet;
|
||||||
pub mod bob {
|
pub mod bob {
|
||||||
use xmr_btc::bob::State;
|
use xmr_btc::bob::State;
|
||||||
|
|
||||||
|
pub fn is_state2(state: &State) -> bool {
|
||||||
|
matches!(state, State::State2 { .. })
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: use macro or generics
|
// TODO: use macro or generics
|
||||||
pub fn is_state5(state: &State) -> bool {
|
pub fn is_state5(state: &State) -> bool {
|
||||||
matches!(state, State::State5 { .. })
|
matches!(state, State::State5 { .. })
|
||||||
|
@ -19,6 +23,10 @@ pub mod bob {
|
||||||
pub mod alice {
|
pub mod alice {
|
||||||
use xmr_btc::alice::State;
|
use xmr_btc::alice::State;
|
||||||
|
|
||||||
|
pub fn is_state3(state: &State) -> bool {
|
||||||
|
matches!(state, State::State3 { .. })
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: use macro or generics
|
// TODO: use macro or generics
|
||||||
pub fn is_state4(state: &State) -> bool {
|
pub fn is_state4(state: &State) -> bool {
|
||||||
matches!(state, State::State4 { .. })
|
matches!(state, State::State4 { .. })
|
||||||
|
@ -34,3 +42,148 @@ pub mod alice {
|
||||||
matches!(state, State::State6 { .. })
|
matches!(state, State::State6 { .. })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use bitcoin_harness::Bitcoind;
|
||||||
|
use monero_harness::Monero;
|
||||||
|
use node::{AliceNode, BobNode};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use testcontainers::clients::Cli;
|
||||||
|
use tokio::sync::{
|
||||||
|
mpsc,
|
||||||
|
mpsc::{Receiver, Sender},
|
||||||
|
};
|
||||||
|
use transport::Transport;
|
||||||
|
use xmr_btc::{bitcoin, monero};
|
||||||
|
|
||||||
|
const TEN_XMR: u64 = 10_000_000_000_000;
|
||||||
|
const RELATIVE_REFUND_TIMELOCK: u32 = 1;
|
||||||
|
const RELATIVE_PUNISH_TIMELOCK: u32 = 1;
|
||||||
|
|
||||||
|
pub async fn init_bitcoind(tc_client: &Cli) -> Bitcoind<'_> {
|
||||||
|
let bitcoind = Bitcoind::new(tc_client, "0.19.1").expect("failed to create bitcoind");
|
||||||
|
let _ = bitcoind.init(5).await;
|
||||||
|
|
||||||
|
bitcoind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InitialBalances {
|
||||||
|
pub alice_xmr: monero::Amount,
|
||||||
|
pub alice_btc: bitcoin::Amount,
|
||||||
|
pub bob_xmr: monero::Amount,
|
||||||
|
pub bob_btc: bitcoin::Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SwapAmounts {
|
||||||
|
pub xmr: monero::Amount,
|
||||||
|
pub btc: bitcoin::Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_alice_and_bob_transports() -> (
|
||||||
|
Transport<xmr_btc::alice::Message, xmr_btc::bob::Message>,
|
||||||
|
Transport<xmr_btc::bob::Message, xmr_btc::alice::Message>,
|
||||||
|
) {
|
||||||
|
let (a_sender, b_receiver): (
|
||||||
|
Sender<xmr_btc::alice::Message>,
|
||||||
|
Receiver<xmr_btc::alice::Message>,
|
||||||
|
) = mpsc::channel(5);
|
||||||
|
let (b_sender, a_receiver): (
|
||||||
|
Sender<xmr_btc::bob::Message>,
|
||||||
|
Receiver<xmr_btc::bob::Message>,
|
||||||
|
) = mpsc::channel(5);
|
||||||
|
|
||||||
|
let a_transport = Transport {
|
||||||
|
sender: a_sender,
|
||||||
|
receiver: a_receiver,
|
||||||
|
};
|
||||||
|
|
||||||
|
let b_transport = Transport {
|
||||||
|
sender: b_sender,
|
||||||
|
receiver: b_receiver,
|
||||||
|
};
|
||||||
|
|
||||||
|
(a_transport, b_transport)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_test(
|
||||||
|
monero: &Monero,
|
||||||
|
bitcoind: &Bitcoind<'_>,
|
||||||
|
refund_timelock: Option<u32>,
|
||||||
|
punish_timelock: Option<u32>,
|
||||||
|
) -> (
|
||||||
|
xmr_btc::alice::State0,
|
||||||
|
xmr_btc::bob::State0,
|
||||||
|
AliceNode,
|
||||||
|
BobNode,
|
||||||
|
InitialBalances,
|
||||||
|
SwapAmounts,
|
||||||
|
) {
|
||||||
|
// must be bigger than our hardcoded fee of 10_000
|
||||||
|
let btc_amount = bitcoin::Amount::from_sat(10_000_000);
|
||||||
|
let xmr_amount = monero::Amount::from_piconero(1_000_000_000_000);
|
||||||
|
|
||||||
|
let swap_amounts = SwapAmounts {
|
||||||
|
xmr: xmr_amount,
|
||||||
|
btc: btc_amount,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fund_alice = TEN_XMR;
|
||||||
|
let fund_bob = 0;
|
||||||
|
monero.init(fund_alice, fund_bob).await.unwrap();
|
||||||
|
|
||||||
|
let alice_monero_wallet = wallet::monero::Wallet(monero.alice_wallet_rpc_client());
|
||||||
|
let bob_monero_wallet = wallet::monero::Wallet(monero.bob_wallet_rpc_client());
|
||||||
|
|
||||||
|
let alice_btc_wallet = wallet::bitcoin::Wallet::new("alice", &bitcoind.node_url)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let bob_btc_wallet = wallet::bitcoin::make_wallet("bob", &bitcoind, btc_amount)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (alice_transport, bob_transport) = init_alice_and_bob_transports();
|
||||||
|
let alice = AliceNode::new(alice_transport, alice_btc_wallet, alice_monero_wallet);
|
||||||
|
|
||||||
|
let bob = BobNode::new(bob_transport, bob_btc_wallet, bob_monero_wallet);
|
||||||
|
|
||||||
|
let alice_initial_btc_balance = alice.bitcoin_wallet.balance().await.unwrap();
|
||||||
|
let bob_initial_btc_balance = bob.bitcoin_wallet.balance().await.unwrap();
|
||||||
|
|
||||||
|
let alice_initial_xmr_balance = alice.monero_wallet.get_balance().await.unwrap();
|
||||||
|
let bob_initial_xmr_balance = bob.monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
|
let redeem_address = alice.bitcoin_wallet.new_address().await.unwrap();
|
||||||
|
let punish_address = redeem_address.clone();
|
||||||
|
let refund_address = bob.bitcoin_wallet.new_address().await.unwrap();
|
||||||
|
|
||||||
|
let alice_state0 = xmr_btc::alice::State0::new(
|
||||||
|
&mut OsRng,
|
||||||
|
btc_amount,
|
||||||
|
xmr_amount,
|
||||||
|
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
|
||||||
|
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
|
||||||
|
redeem_address.clone(),
|
||||||
|
punish_address.clone(),
|
||||||
|
);
|
||||||
|
let bob_state0 = xmr_btc::bob::State0::new(
|
||||||
|
&mut OsRng,
|
||||||
|
btc_amount,
|
||||||
|
xmr_amount,
|
||||||
|
refund_timelock.unwrap_or(RELATIVE_REFUND_TIMELOCK),
|
||||||
|
punish_timelock.unwrap_or(RELATIVE_PUNISH_TIMELOCK),
|
||||||
|
refund_address,
|
||||||
|
);
|
||||||
|
let initial_balances = InitialBalances {
|
||||||
|
alice_xmr: alice_initial_xmr_balance,
|
||||||
|
alice_btc: alice_initial_btc_balance,
|
||||||
|
bob_xmr: bob_initial_xmr_balance,
|
||||||
|
bob_btc: bob_initial_btc_balance,
|
||||||
|
};
|
||||||
|
(
|
||||||
|
alice_state0,
|
||||||
|
bob_state0,
|
||||||
|
alice,
|
||||||
|
bob,
|
||||||
|
initial_balances,
|
||||||
|
swap_amounts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use backoff::{future::FutureOperation as _, ExponentialBackoff};
|
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||||
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
|
use bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
|
||||||
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
|
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, Bitcoind};
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
@ -10,7 +10,7 @@ use xmr_btc::{
|
||||||
bitcoin::{
|
bitcoin::{
|
||||||
BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction,
|
BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TxLock, WatchForRawTransaction,
|
||||||
},
|
},
|
||||||
MedianTime,
|
BlockHeight, TransactionBlockHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -114,23 +114,44 @@ impl BroadcastSignedTransaction for Wallet {
|
||||||
impl WatchForRawTransaction for Wallet {
|
impl WatchForRawTransaction for Wallet {
|
||||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
||||||
(|| async { Ok(self.0.get_raw_transaction(txid).await?) })
|
(|| async { Ok(self.0.get_raw_transaction(txid).await?) })
|
||||||
.retry(ExponentialBackoff {
|
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||||
max_elapsed_time: None,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.expect("transient errors to be retried")
|
.expect("transient errors to be retried")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl MedianTime for Wallet {
|
impl BlockHeight for Wallet {
|
||||||
async fn median_time(&self) -> u32 {
|
async fn block_height(&self) -> u32 {
|
||||||
(|| async { Ok(self.0.median_time().await?) })
|
(|| async { Ok(self.0.block_height().await?) })
|
||||||
.retry(ExponentialBackoff {
|
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||||
max_elapsed_time: None,
|
.await
|
||||||
..Default::default()
|
.expect("transient errors to be retried")
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TransactionBlockHeight for Wallet {
|
||||||
|
async fn transaction_block_height(&self, txid: Txid) -> u32 {
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Error {
|
||||||
|
Io,
|
||||||
|
NotYetMined,
|
||||||
|
}
|
||||||
|
|
||||||
|
(|| async {
|
||||||
|
let block_height = self
|
||||||
|
.0
|
||||||
|
.transaction_block_height(txid)
|
||||||
|
.await
|
||||||
|
.map_err(|_| backoff::Error::Transient(Error::Io))?;
|
||||||
|
|
||||||
|
let block_height =
|
||||||
|
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
|
||||||
|
|
||||||
|
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
||||||
|
})
|
||||||
|
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||||
.await
|
.await
|
||||||
.expect("transient errors to be retried")
|
.expect("transient errors to be retried")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use backoff::{future::FutureOperation as _, ExponentialBackoff};
|
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||||
use monero::{Address, Network, PrivateKey};
|
use monero::{Address, Network, PrivateKey};
|
||||||
use monero_harness::rpc::wallet;
|
use monero_harness::rpc::wallet;
|
||||||
use std::str::FromStr;
|
use std::{str::FromStr, time::Duration};
|
||||||
use xmr_btc::monero::{
|
use xmr_btc::monero::{
|
||||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
|
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicKey, PublicViewKey,
|
||||||
Transfer, TransferProof, TxHash, WatchForTransfer,
|
Transfer, TransferProof, TxHash, WatchForTransfer,
|
||||||
|
@ -11,6 +11,15 @@ use xmr_btc::monero::{
|
||||||
|
|
||||||
pub struct Wallet(pub wallet::Client);
|
pub struct Wallet(pub wallet::Client);
|
||||||
|
|
||||||
|
impl Wallet {
|
||||||
|
/// Get the balance of the primary account.
|
||||||
|
pub async fn get_balance(&self) -> Result<Amount> {
|
||||||
|
let amount = self.0.get_balance(0).await?;
|
||||||
|
|
||||||
|
Ok(Amount::from_piconero(amount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Transfer for Wallet {
|
impl Transfer for Wallet {
|
||||||
async fn transfer(
|
async fn transfer(
|
||||||
|
@ -106,10 +115,7 @@ impl WatchForTransfer for Wallet {
|
||||||
|
|
||||||
Ok(proof)
|
Ok(proof)
|
||||||
})
|
})
|
||||||
.retry(ExponentialBackoff {
|
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||||
max_elapsed_time: None,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
pub mod harness;
|
pub mod harness;
|
||||||
|
|
||||||
|
use std::{convert::TryInto, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc::{Receiver, Sender},
|
channel::mpsc::{channel, Receiver, Sender},
|
||||||
|
future::try_join,
|
||||||
SinkExt, StreamExt,
|
SinkExt, StreamExt,
|
||||||
};
|
};
|
||||||
use genawaiter::GeneratorState;
|
use genawaiter::GeneratorState;
|
||||||
use harness::wallet::{bitcoin, monero};
|
use harness::{
|
||||||
|
init_bitcoind, init_test,
|
||||||
|
node::{run_alice_until, run_bob_until},
|
||||||
|
};
|
||||||
|
use monero_harness::Monero;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use testcontainers::clients::Cli;
|
||||||
|
use tracing::info;
|
||||||
use xmr_btc::{
|
use xmr_btc::{
|
||||||
action_generator_alice, action_generator_bob, alice,
|
action_generator_alice, action_generator_bob, alice,
|
||||||
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
bitcoin::{BroadcastSignedTransaction, EncryptedSignature, SignTxLock},
|
||||||
|
@ -20,10 +30,18 @@ type AliceNetwork = Network<EncryptedSignature>;
|
||||||
type BobNetwork = Network<TransferProof>;
|
type BobNetwork = Network<TransferProof>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Network<RecvMsg> {
|
struct Network<M> {
|
||||||
// TODO: It is weird to use mpsc's in a situation where only one message is expected, but the
|
// TODO: It is weird to use mpsc's in a situation where only one message is expected, but the
|
||||||
// ownership rules of Rust are making this painful
|
// ownership rules of Rust are making this painful
|
||||||
pub receiver: Receiver<RecvMsg>,
|
pub receiver: Receiver<M>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M> Network<M> {
|
||||||
|
pub fn new() -> (Network<M>, Sender<M>) {
|
||||||
|
let (sender, receiver) = channel(1);
|
||||||
|
|
||||||
|
(Self { receiver }, sender)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -41,19 +59,22 @@ impl ReceiveBitcoinRedeemEncsig for AliceNetwork {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn swap_as_alice(
|
async fn swap_as_alice(
|
||||||
network: &'static mut AliceNetwork,
|
network: AliceNetwork,
|
||||||
// FIXME: It would be more intuitive to have a single network/transport struct instead of
|
// FIXME: It would be more intuitive to have a single network/transport struct instead of
|
||||||
// splitting into two, but Rust ownership rules make this tedious
|
// splitting into two, but Rust ownership rules make this tedious
|
||||||
mut sender: Sender<TransferProof>,
|
mut sender: Sender<TransferProof>,
|
||||||
monero_wallet: &'static monero::Wallet,
|
monero_wallet: &harness::wallet::monero::Wallet,
|
||||||
bitcoin_wallet: &'static bitcoin::Wallet,
|
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
|
||||||
state: alice::State3,
|
state: alice::State3,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut action_generator =
|
let mut action_generator = action_generator_alice(network, bitcoin_wallet.clone(), state);
|
||||||
action_generator_alice(network, monero_wallet, bitcoin_wallet, state);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match action_generator.async_resume().await {
|
let state = action_generator.async_resume().await;
|
||||||
|
|
||||||
|
info!("resumed execution of generator, got: {:?}", state);
|
||||||
|
|
||||||
|
match state {
|
||||||
GeneratorState::Yielded(AliceAction::LockXmr {
|
GeneratorState::Yielded(AliceAction::LockXmr {
|
||||||
amount,
|
amount,
|
||||||
public_spend_key,
|
public_spend_key,
|
||||||
|
@ -84,16 +105,25 @@ async fn swap_as_alice(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn swap_as_bob(
|
async fn swap_as_bob(
|
||||||
network: &'static mut BobNetwork,
|
network: BobNetwork,
|
||||||
mut sender: Sender<EncryptedSignature>,
|
mut sender: Sender<EncryptedSignature>,
|
||||||
monero_wallet: &'static monero::Wallet,
|
monero_wallet: Arc<harness::wallet::monero::Wallet>,
|
||||||
bitcoin_wallet: &'static bitcoin::Wallet,
|
bitcoin_wallet: Arc<harness::wallet::bitcoin::Wallet>,
|
||||||
state: bob::State2,
|
state: bob::State2,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut action_generator = action_generator_bob(network, monero_wallet, bitcoin_wallet, state);
|
let mut action_generator = action_generator_bob(
|
||||||
|
network,
|
||||||
|
monero_wallet.clone(),
|
||||||
|
bitcoin_wallet.clone(),
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match action_generator.async_resume().await {
|
let state = action_generator.async_resume().await;
|
||||||
|
|
||||||
|
info!("resumed execution of generator, got: {:?}", state);
|
||||||
|
|
||||||
|
match state {
|
||||||
GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => {
|
GeneratorState::Yielded(BobAction::LockBitcoin(tx_lock)) => {
|
||||||
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
let signed_tx_lock = bitcoin_wallet.sign_tx_lock(tx_lock).await?;
|
||||||
let _ = bitcoin_wallet
|
let _ = bitcoin_wallet
|
||||||
|
@ -126,5 +156,96 @@ async fn swap_as_bob(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
// NOTE: For some reason running these tests overflows the stack. In order to
|
||||||
fn on_chain_happy_path() {}
|
// mitigate this run them with:
|
||||||
|
//
|
||||||
|
// RUST_MIN_STACK=100000000 cargo test
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn on_chain_happy_path() {
|
||||||
|
let cli = Cli::default();
|
||||||
|
let (monero, _container) = Monero::new(&cli);
|
||||||
|
let bitcoind = init_bitcoind(&cli).await;
|
||||||
|
|
||||||
|
let (alice_state0, bob_state0, mut alice_node, mut bob_node, initial_balances, swap_amounts) =
|
||||||
|
init_test(&monero, &bitcoind, Some(100), Some(100)).await;
|
||||||
|
|
||||||
|
// run the handshake as part of the setup
|
||||||
|
let (alice_state, bob_state) = try_join(
|
||||||
|
run_alice_until(
|
||||||
|
&mut alice_node,
|
||||||
|
alice_state0.into(),
|
||||||
|
harness::alice::is_state3,
|
||||||
|
&mut OsRng,
|
||||||
|
),
|
||||||
|
run_bob_until(
|
||||||
|
&mut bob_node,
|
||||||
|
bob_state0.into(),
|
||||||
|
harness::bob::is_state2,
|
||||||
|
&mut OsRng,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let alice: alice::State3 = alice_state.try_into().unwrap();
|
||||||
|
let bob: bob::State2 = bob_state.try_into().unwrap();
|
||||||
|
let tx_lock_txid = bob.tx_lock.txid();
|
||||||
|
|
||||||
|
let alice_bitcoin_wallet = Arc::new(alice_node.bitcoin_wallet);
|
||||||
|
let bob_bitcoin_wallet = Arc::new(bob_node.bitcoin_wallet);
|
||||||
|
let alice_monero_wallet = Arc::new(alice_node.monero_wallet);
|
||||||
|
let bob_monero_wallet = Arc::new(bob_node.monero_wallet);
|
||||||
|
|
||||||
|
let (alice_network, bob_sender) = Network::<EncryptedSignature>::new();
|
||||||
|
let (bob_network, alice_sender) = Network::<TransferProof>::new();
|
||||||
|
|
||||||
|
try_join(
|
||||||
|
swap_as_alice(
|
||||||
|
alice_network,
|
||||||
|
alice_sender,
|
||||||
|
&alice_monero_wallet.clone(),
|
||||||
|
alice_bitcoin_wallet.clone(),
|
||||||
|
alice,
|
||||||
|
),
|
||||||
|
swap_as_bob(
|
||||||
|
bob_network,
|
||||||
|
bob_sender,
|
||||||
|
bob_monero_wallet.clone(),
|
||||||
|
bob_bitcoin_wallet.clone(),
|
||||||
|
bob,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice_final_btc_balance = alice_bitcoin_wallet.balance().await.unwrap();
|
||||||
|
let bob_final_btc_balance = bob_bitcoin_wallet.balance().await.unwrap();
|
||||||
|
|
||||||
|
let lock_tx_bitcoin_fee = bob_bitcoin_wallet
|
||||||
|
.transaction_fee(tx_lock_txid)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice_final_xmr_balance = alice_monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
|
monero.wait_for_bob_wallet_block_height().await.unwrap();
|
||||||
|
let bob_final_xmr_balance = bob_monero_wallet.get_balance().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
alice_final_btc_balance,
|
||||||
|
initial_balances.alice_btc + swap_amounts.btc
|
||||||
|
- bitcoin::Amount::from_sat(xmr_btc::bitcoin::TX_FEE)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bob_final_btc_balance,
|
||||||
|
initial_balances.bob_btc - swap_amounts.btc - lock_tx_bitcoin_fee
|
||||||
|
);
|
||||||
|
|
||||||
|
// Getting the Monero LockTx fee is tricky in a clean way, I think checking this
|
||||||
|
// condition is sufficient
|
||||||
|
assert!(alice_final_xmr_balance <= initial_balances.alice_xmr - swap_amounts.xmr,);
|
||||||
|
assert_eq!(
|
||||||
|
bob_final_xmr_balance,
|
||||||
|
initial_balances.bob_xmr + swap_amounts.xmr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue