diff --git a/Cargo.lock b/Cargo.lock index 40b1f224..363e9b51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2888,6 +2888,16 @@ dependencies = [ "crossbeam-utils 0.7.2", ] +[[package]] +name = "rust_decimal" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e81662973c7a8d9663e64a0de4cd642b89a21d64966e3d99606efdc5fb0cc6" +dependencies = [ + "num-traits", + "serde", +] + [[package]] name = "rustc-hex" version = "2.1.0" @@ -4182,6 +4192,7 @@ dependencies = [ "monero-harness", "rand 0.7.3", "reqwest", + "rust_decimal", "serde", "serde_cbor", "sha2 0.9.2", diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 9c2505c6..f8da52e4 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -13,8 +13,226 @@ #![forbid(unsafe_code)] use anyhow::Result; +use prettytable::{row, Table}; +use rand::rngs::OsRng; +use std::sync::Arc; +use structopt::StructOpt; +use swap::{ + alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, cli::Options, monero, + network::transport::build, recover::recover, storage::Database, trace::init_tracing, + SwapAmounts, +}; +use tracing::{info, log::LevelFilter}; +use uuid::Uuid; +use xmr_btc::{alice::State0, config::Config, cross_curve_dleq}; + +#[macro_use] +extern crate prettytable; #[tokio::main] async fn main() -> Result<()> { - unimplemented!() + init_tracing(LevelFilter::Trace).expect("initialize tracing"); + + let opt = Options::from_args(); + + // This currently creates the directory if it's not there in the first place + let db = Database::open(std::path::Path::new("./.swap-db/")).unwrap(); + let config = Config::mainnet(); + + match opt { + Options::SellXmr { + bitcoind_url, + bitcoin_wallet_name, + monero_wallet_rpc_url, + listen_addr, + send_monero, + receive_bitcoin, + } => { + info!("running swap node as Alice ..."); + + let bitcoin_wallet = bitcoin::Wallet::new( + bitcoin_wallet_name.as_str(), + bitcoind_url, + config.bitcoin_network, + ) + .await + .expect("failed to create bitcoin wallet"); + + let bitcoin_balance = bitcoin_wallet.balance().await?; + info!( + "Connection to Bitcoin wallet succeeded, balance: {}", + bitcoin_balance + ); + let bitcoin_wallet = Arc::new(bitcoin_wallet); + + let monero_wallet = monero::Wallet::new(monero_wallet_rpc_url); + let monero_balance = monero_wallet.get_balance().await?; + info!( + "Connection to Monero wallet succeeded, balance: {}", + monero_balance + ); + let monero_wallet = Arc::new(monero_wallet); + + let amounts = SwapAmounts { + btc: receive_bitcoin, + xmr: send_monero, + }; + + let (alice_state, alice_behaviour) = { + let rng = &mut OsRng; + let a = bitcoin::SecretKey::new_random(rng); + let s_a = cross_curve_dleq::Scalar::random(rng); + let v_a = xmr_btc::monero::PrivateViewKey::new_random(rng); + let redeem_address = bitcoin_wallet.as_ref().new_address().await.unwrap(); + let punish_address = redeem_address.clone(); + let state0 = State0::new( + a, + s_a, + v_a, + amounts.btc, + amounts.xmr, + config.bitcoin_refund_timelock, + config.bitcoin_punish_timelock, + redeem_address, + punish_address, + ); + + ( + AliceState::Started { + amounts, + state0: state0.clone(), + }, + alice::Behaviour::new(state0), + ) + }; + + let alice_peer_id = alice_behaviour.peer_id(); + info!( + "Alice Peer ID (to be used by Bob to dial her): {}", + alice_peer_id + ); + + let alice_transport = build(alice_behaviour.identity())?; + + let (mut event_loop, handle) = + alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?; + + let swap = alice::swap::swap( + alice_state, + handle, + bitcoin_wallet.clone(), + monero_wallet.clone(), + config, + ); + + let _event_loop = tokio::spawn(async move { event_loop.run().await }); + swap.await?; + } + Options::BuyXmr { + alice_addr, + alice_peer_id, + bitcoind_url, + bitcoin_wallet_name, + monero_wallet_rpc_url, + send_bitcoin, + receive_monero, + } => { + info!("running swap node as Bob ..."); + + let bob_behaviour = bob::Behaviour::default(); + let bob_transport = build(bob_behaviour.identity())?; + + let bitcoin_wallet = bitcoin::Wallet::new( + bitcoin_wallet_name.as_str(), + bitcoind_url, + config.bitcoin_network, + ) + .await + .expect("failed to create bitcoin wallet"); + let bitcoin_balance = bitcoin_wallet.balance().await?; + info!( + "Connection to Bitcoin wallet succeeded, balance: {}", + bitcoin_balance + ); + let bitcoin_wallet = Arc::new(bitcoin_wallet); + + let monero_wallet = monero::Wallet::new(monero_wallet_rpc_url); + let monero_balance = monero_wallet.get_balance().await?; + info!( + "Connection to Monero wallet succeeded, balance: {}", + monero_balance + ); + let monero_wallet = Arc::new(monero_wallet); + + let refund_address = bitcoin_wallet.new_address().await.unwrap(); + let state0 = xmr_btc::bob::State0::new( + &mut OsRng, + send_bitcoin, + receive_monero, + config.bitcoin_refund_timelock, + config.bitcoin_punish_timelock, + refund_address, + ); + + let amounts = SwapAmounts { + btc: send_bitcoin, + xmr: receive_monero, + }; + + let bob_state = BobState::Started { + state0, + amounts, + peer_id: alice_peer_id, + addr: alice_addr, + }; + + let (event_loop, handle) = + bob::event_loop::EventLoop::new(bob_transport, bob_behaviour).unwrap(); + + let swap = bob::swap::swap( + bob_state, + handle, + db, + bitcoin_wallet.clone(), + monero_wallet.clone(), + OsRng, + Uuid::new_v4(), + ); + + let _event_loop = tokio::spawn(async move { event_loop.run().await }); + swap.await?; + } + Options::History => { + let mut table = Table::new(); + + table.add_row(row!["SWAP ID", "STATE"]); + + for (swap_id, state) in db.all()? { + table.add_row(row![swap_id, state]); + } + + // Print the table to stdout + table.printstd(); + } + Options::Recover { + swap_id, + bitcoind_url, + monerod_url, + bitcoin_wallet_name, + } => { + let state = db.get_state(swap_id)?; + let bitcoin_wallet = bitcoin::Wallet::new( + bitcoin_wallet_name.as_ref(), + bitcoind_url, + config.bitcoin_network, + ) + .await + .expect("failed to create bitcoin wallet"); + let monero_wallet = monero::Wallet::new(monerod_url); + + recover(bitcoin_wallet, monero_wallet, state).await?; + } + } + + Ok(()) } diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 06017ba3..5dfc8edd 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -20,27 +20,33 @@ pub use xmr_btc::bitcoin::*; pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600; #[derive(Debug)] -pub struct Wallet(pub bitcoin_harness::Wallet); +pub struct Wallet { + pub inner: bitcoin_harness::Wallet, + pub network: bitcoin::Network, +} impl Wallet { - pub async fn new(name: &str, url: Url) -> Result { + pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result { let wallet = bitcoin_harness::Wallet::new(name, url).await?; - Ok(Self(wallet)) + Ok(Self { + inner: wallet, + network, + }) } pub async fn balance(&self) -> Result { - let balance = self.0.balance().await?; + let balance = self.inner.balance().await?; Ok(balance) } pub async fn new_address(&self) -> Result
{ - self.0.new_address().await.map_err(Into::into) + self.inner.new_address().await.map_err(Into::into) } pub async fn transaction_fee(&self, txid: Txid) -> Result { let fee = self - .0 + .inner .get_wallet_transaction(txid) .await .map(|res| { @@ -64,7 +70,7 @@ impl BuildTxLockPsbt for Wallet { output_address: Address, output_amount: Amount, ) -> Result { - let psbt = self.0.fund_psbt(output_address, output_amount).await?; + let psbt = self.inner.fund_psbt(output_address, output_amount).await?; let as_hex = base64::decode(psbt)?; let psbt = bitcoin::consensus::deserialize(&as_hex)?; @@ -81,7 +87,10 @@ impl SignTxLock for Wallet { let psbt = bitcoin::consensus::serialize(&psbt); let as_base64 = base64::encode(psbt); - let psbt = self.0.wallet_process_psbt(PsbtBase64(as_base64)).await?; + let psbt = self + .inner + .wallet_process_psbt(PsbtBase64(as_base64)) + .await?; let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt); let as_hex = base64::decode(signed_psbt)?; @@ -96,7 +105,9 @@ impl SignTxLock for Wallet { #[async_trait] impl BroadcastSignedTransaction for Wallet { async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { - Ok(self.0.send_raw_transaction(transaction).await?) + let txid = self.inner.send_raw_transaction(transaction).await?; + tracing::debug!("Bitcoin tx broadcasted! TXID = {}", txid); + Ok(txid) } } @@ -105,7 +116,7 @@ impl BroadcastSignedTransaction for Wallet { #[async_trait] impl WatchForRawTransaction for Wallet { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { - (|| async { Ok(self.0.get_raw_transaction(txid).await?) }) + (|| async { Ok(self.inner.get_raw_transaction(txid).await?) }) .retry(ConstantBackoff::new(Duration::from_secs(1))) .await .expect("transient errors to be retried") @@ -116,14 +127,14 @@ impl WatchForRawTransaction for Wallet { impl GetRawTransaction for Wallet { // todo: potentially replace with option async fn get_raw_transaction(&self, txid: Txid) -> Result { - Ok(self.0.get_raw_transaction(txid).await?) + Ok(self.inner.get_raw_transaction(txid).await?) } } #[async_trait] impl BlockHeight for Wallet { async fn block_height(&self) -> u32 { - (|| async { Ok(self.0.client.getblockcount().await?) }) + (|| async { Ok(self.inner.client.getblockcount().await?) }) .retry(ConstantBackoff::new(Duration::from_secs(1))) .await .expect("transient errors to be retried") @@ -141,7 +152,7 @@ impl TransactionBlockHeight for Wallet { (|| async { let block_height = self - .0 + .inner .transaction_block_height(txid) .await .map_err(|_| backoff::Error::Transient(Error::Io))?; @@ -167,7 +178,7 @@ impl WaitForTransactionFinality for Wallet { let mut interval = interval(config.bitcoin_avg_block_time / 4); loop { - let tx = self.0.client.get_raw_transaction_verbose(txid).await?; + let tx = self.inner.client.get_raw_transaction_verbose(txid).await?; if let Some(confirmations) = tx.confirmations { if confirmations >= config.bitcoin_finality_confirmations { break; @@ -179,3 +190,9 @@ impl WaitForTransactionFinality for Wallet { Ok(()) } } + +impl Network for Wallet { + fn get_network(&self) -> bitcoin::Network { + self.network + } +} diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 2d13fcb8..5005bd50 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -1,38 +1,70 @@ -use libp2p::core::Multiaddr; +use libp2p::{core::Multiaddr, PeerId}; use url::Url; use uuid::Uuid; #[derive(structopt::StructOpt, Debug)] #[structopt(name = "xmr-btc-swap", about = "Trustless XMR BTC swaps")] pub enum Options { - Alice { - #[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")] + SellXmr { + #[structopt( + short = "b", + long = "bitcoind", + default_value = "http://127.0.0.1:8332" + )] bitcoind_url: Url, - #[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")] - monerod_url: Url, + #[structopt(short = "n", long = "bitcoin-wallet-name")] + bitcoin_wallet_name: String, - #[structopt(default_value = "/ip4/127.0.0.1/tcp/9876", long = "listen-addr")] + #[structopt( + short = "m", + long = "monero-wallet-rpc", + default_value = "http://127.0.0.1:18083/json_rpc" + )] + monero_wallet_rpc_url: Url, + + #[structopt( + short = "a", + long = "listen-addr", + default_value = "/ip4/127.0.0.1/tcp/9876" + )] listen_addr: Multiaddr, - #[structopt(long = "tor-port")] - tor_port: Option, - }, - Bob { - #[structopt(long = "sats")] - satoshis: u64, + #[structopt(short = "s", long = "send-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))] + send_monero: xmr_btc::monero::Amount, - #[structopt(long = "alice-addr")] + #[structopt(short = "r", long = "receive-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] + receive_bitcoin: bitcoin::Amount, + }, + BuyXmr { + #[structopt(short = "a", long = "connect-addr")] alice_addr: Multiaddr, - #[structopt(default_value = "http://127.0.0.1:8332", long = "bitcoind")] + #[structopt(short = "p", long = "connect-peer-id")] + alice_peer_id: PeerId, + + #[structopt( + short = "b", + long = "bitcoind", + default_value = "http://127.0.0.1:8332" + )] bitcoind_url: Url, - #[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")] - monerod_url: Url, + #[structopt(short = "n", long = "bitcoin-wallet-name")] + bitcoin_wallet_name: String, + + #[structopt( + short = "m", + long = "monerod", + default_value = "http://127.0.0.1:18083/json_rpc" + )] + monero_wallet_rpc_url: Url, - #[structopt(long = "tor")] - tor: bool, + #[structopt(short = "s", long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] + send_bitcoin: bitcoin::Amount, + + #[structopt(short = "r", long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))] + receive_monero: xmr_btc::monero::Amount, }, History, Recover { @@ -44,5 +76,18 @@ pub enum Options { #[structopt(default_value = "http://127.0.0.1:18083/json_rpc", long = "monerod")] monerod_url: Url, + + #[structopt(short = "n", long = "bitcoin-wallet-name")] + bitcoin_wallet_name: String, }, } + +fn parse_btc(str: &str) -> anyhow::Result { + let amount = bitcoin::Amount::from_str_in(str, ::bitcoin::Denomination::Bitcoin)?; + Ok(amount) +} + +fn parse_xmr(str: &str) -> anyhow::Result { + let amount = xmr_btc::monero::Amount::parse_monero(str)?; + Ok(amount) +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 80b8d48d..3b31a652 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -13,6 +13,7 @@ pub mod recover; pub mod state; pub mod storage; pub mod tor; +pub mod trace; pub type Never = std::convert::Infallible; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 7d252b69..e97e9d64 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -39,11 +39,15 @@ impl Transfer for Wallet { .await?; let tx_hash = TxHash(res.tx_hash); + tracing::debug!("Monero tx broadcasted!, tx hash: {:?}", tx_hash); let tx_key = PrivateKey::from_str(&res.tx_key)?; let fee = Amount::from_piconero(res.fee); - Ok((TransferProof::new(tx_hash, tx_key), fee)) + let transfer_proof = TransferProof::new(tx_hash, tx_key); + tracing::debug!(" Transfer proof: {:?}", transfer_proof); + + Ok((transfer_proof, fee)) } } diff --git a/swap/src/recover.rs b/swap/src/recover.rs index bd8d40ac..f67ef7a6 100644 --- a/swap/src/recover.rs +++ b/swap/src/recover.rs @@ -61,7 +61,7 @@ pub async fn alice_recover( info!("Checking if the Bitcoin cancel transaction has been published"); if bitcoin_wallet - .0 + .inner .get_raw_transaction(tx_cancel.txid()) .await .is_err() @@ -164,7 +164,7 @@ pub async fn alice_recover( .transaction_block_height(state.tx_lock.txid()) .await; - let block_height = bitcoin_wallet.0.client.getblockcount().await?; + let block_height = bitcoin_wallet.inner.client.getblockcount().await?; let refund_absolute_expiry = tx_lock_height + state.refund_timelock; info!("Checking refund timelock"); @@ -187,7 +187,7 @@ pub async fn alice_recover( info!("Checking if the Bitcoin cancel transaction has been published"); if bitcoin_wallet - .0 + .inner .get_raw_transaction(tx_cancel.txid()) .await .is_err() @@ -290,7 +290,11 @@ pub async fn alice_recover( // TODO: Protect against transient errors so that we can correctly decide if the // bitcoin has been refunded - match bitcoin_wallet.0.get_raw_transaction(tx_refund.txid()).await { + match bitcoin_wallet + .inner + .get_raw_transaction(tx_refund.txid()) + .await + { Ok(tx_refund_published) => { info!("Bitcoin already refunded"); @@ -375,7 +379,7 @@ pub async fn bob_recover( info!("Checking if the Bitcoin cancel transaction has been published"); if bitcoin_wallet - .0 + .inner .get_raw_transaction(tx_cancel.txid()) .await .is_err() @@ -431,7 +435,7 @@ pub async fn bob_recover( let tx_redeem = bitcoin::TxRedeem::new(&state.tx_lock, &state.redeem_address); let tx_redeem_published = bitcoin_wallet - .0 + .inner .get_raw_transaction(tx_redeem.txid()) .await?; diff --git a/swap/src/trace.rs b/swap/src/trace.rs index c8f82e89..a8a46eba 100644 --- a/swap/src/trace.rs +++ b/swap/src/trace.rs @@ -1,4 +1,4 @@ -use atty::{self, Stream}; +use atty::{self}; use log::LevelFilter; use tracing::{info, subscriber}; use tracing_log::LogTracer; @@ -9,15 +9,16 @@ pub fn init_tracing(level: log::LevelFilter) -> anyhow::Result<()> { return Ok(()); } - // Upstream log filter. - LogTracer::init_with_filter(LevelFilter::Debug)?; + // We want upstream library log messages, just only at Info level. + LogTracer::init_with_filter(LevelFilter::Info)?; - let is_terminal = atty::is(Stream::Stdout); + let is_terminal = atty::is(atty::Stream::Stderr); let subscriber = FmtSubscriber::builder() .with_env_filter(format!( - "swap={},xmr_btc={},monero_harness={}", - level, level, level + "swap={},xmr-btc={},http=warn,warp=warn", + level, level )) + .with_writer(std::io::stderr) .with_ansi(is_terminal) .finish(); diff --git a/swap/tests/e2e.rs b/swap/tests/e2e.rs index efce4319..a66b4568 100644 --- a/swap/tests/e2e.rs +++ b/swap/tests/e2e.rs @@ -247,7 +247,7 @@ async fn init_alice( )); let alice_btc_wallet = Arc::new( - swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone()) + swap::bitcoin::Wallet::new("alice", bitcoind.node_url.clone(), config.bitcoin_network) .await .unwrap(), ); @@ -322,13 +322,13 @@ async fn init_bob( Database, ) { let bob_btc_wallet = Arc::new( - swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone()) + swap::bitcoin::Wallet::new("bob", bitcoind.node_url.clone(), config.bitcoin_network) .await .unwrap(), ); bitcoind .mint( - bob_btc_wallet.0.new_address().await.unwrap(), + bob_btc_wallet.inner.new_address().await.unwrap(), btc_starting_balance, ) .await diff --git a/xmr-btc/Cargo.toml b/xmr-btc/Cargo.toml index bac95877..bfaf5f9e 100644 --- a/xmr-btc/Cargo.toml +++ b/xmr-btc/Cargo.toml @@ -20,6 +20,7 @@ genawaiter = "0.99.1" miniscript = { version = "4", features = ["serde"] } monero = { version = "0.9", features = ["serde_support"] } rand = "0.7" +rust_decimal = "1.8" serde = { version = "1", features = ["derive"] } sha2 = "0.9" thiserror = "1" diff --git a/xmr-btc/src/bitcoin.rs b/xmr-btc/src/bitcoin.rs index d0dab9ea..562b262f 100644 --- a/xmr-btc/src/bitcoin.rs +++ b/xmr-btc/src/bitcoin.rs @@ -15,7 +15,14 @@ pub use bitcoin::{util::psbt::PartiallySignedTransaction, *}; pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature}; pub use transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund}; -pub const TX_FEE: u64 = 10_000; +// TODO: Configurable tx-fee (note: parties have to agree prior to swapping) +// Current reasoning: +// tx with largest weight (as determined by get_weight() upon broadcast in e2e +// test) = 609 assuming segwit and 60 sat/vB: +// (609 / 4) * 60 (sat/vB) = 9135 sats +// Recommended: Overpay a bit to ensure we don't have to wait too long for test +// runs. +pub const TX_FEE: u64 = 15_000; #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct SecretKey { @@ -212,6 +219,11 @@ pub trait GetRawTransaction { async fn get_raw_transaction(&self, txid: Txid) -> Result; } +#[async_trait] +pub trait Network { + fn get_network(&self) -> bitcoin::Network; +} + pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { let adaptor = Adaptor::>::default(); diff --git a/xmr-btc/src/bitcoin/transactions.rs b/xmr-btc/src/bitcoin/transactions.rs index aec6bb86..e9b789ad 100644 --- a/xmr-btc/src/bitcoin/transactions.rs +++ b/xmr-btc/src/bitcoin/transactions.rs @@ -1,10 +1,11 @@ use crate::bitcoin::{ - build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, OutPoint, PublicKey, Txid, TX_FEE, + build_shared_output_descriptor, verify_sig, BuildTxLockPsbt, Network, OutPoint, PublicKey, + Txid, TX_FEE, }; use anyhow::{bail, Context, Result}; use bitcoin::{ util::{bip143::SigHashCache, psbt::PartiallySignedTransaction}, - Address, Amount, Network, SigHash, SigHashType, Transaction, TxIn, TxOut, + Address, Amount, SigHash, SigHashType, Transaction, TxIn, TxOut, }; use ecdsa_fun::Signature; use miniscript::{Descriptor, NullCtx}; @@ -20,11 +21,11 @@ pub struct TxLock { impl TxLock { pub async fn new(wallet: &W, amount: Amount, A: PublicKey, B: PublicKey) -> Result where - W: BuildTxLockPsbt, + W: BuildTxLockPsbt + Network, { let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); let address = lock_output_descriptor - .address(Network::Regtest, NullCtx) + .address(wallet.get_network(), NullCtx) .expect("can derive address from descriptor"); // We construct a psbt for convenience diff --git a/xmr-btc/src/bob.rs b/xmr-btc/src/bob.rs index 8f52b788..dbc6b53c 100644 --- a/xmr-btc/src/bob.rs +++ b/xmr-btc/src/bob.rs @@ -33,7 +33,7 @@ use tracing::error; pub mod message; use crate::{ - bitcoin::{BlockHeight, GetRawTransaction, TransactionBlockHeight}, + bitcoin::{BlockHeight, GetRawTransaction, Network, TransactionBlockHeight}, monero::{CreateWalletForOutput, WatchForTransfer}, }; use ::bitcoin::{Transaction, Txid}; @@ -267,7 +267,7 @@ where // send to one receive in the correct order. pub async fn next_state< R: RngCore + CryptoRng, - B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction, + B: WatchForRawTransaction + SignTxLock + BuildTxLockPsbt + BroadcastSignedTransaction + Network, M: CreateWalletForOutput + WatchForTransfer, T: SendMessage + ReceiveMessage, >( @@ -401,7 +401,7 @@ impl State0 { pub async fn receive(self, wallet: &W, msg: alice::Message0) -> anyhow::Result where - W: BuildTxLockPsbt, + W: BuildTxLockPsbt + Network, { msg.dleq_proof_s_a.verify( msg.S_a_bitcoin.clone().into(), diff --git a/xmr-btc/src/config.rs b/xmr-btc/src/config.rs index 40b08b87..9f86a7bf 100644 --- a/xmr-btc/src/config.rs +++ b/xmr-btc/src/config.rs @@ -9,6 +9,7 @@ pub struct Config { pub monero_max_finality_time: Duration, pub bitcoin_refund_timelock: u32, pub bitcoin_punish_timelock: u32, + pub bitcoin_network: ::bitcoin::Network, } impl Config { @@ -23,6 +24,7 @@ impl Config { * mainnet::MONERO_FINALITY_CONFIRMATIONS, bitcoin_refund_timelock: mainnet::BITCOIN_REFUND_TIMELOCK, bitcoin_punish_timelock: mainnet::BITCOIN_PUNISH_TIMELOCK, + bitcoin_network: ::bitcoin::Network::Bitcoin, } } @@ -37,6 +39,7 @@ impl Config { * regtest::MONERO_FINALITY_CONFIRMATIONS, bitcoin_refund_timelock: regtest::BITCOIN_REFUND_TIMELOCK, bitcoin_punish_timelock: regtest::BITCOIN_PUNISH_TIMELOCK, + bitcoin_network: ::bitcoin::Network::Regtest, } } } diff --git a/xmr-btc/src/monero.rs b/xmr-btc/src/monero.rs index e4215853..859da380 100644 --- a/xmr-btc/src/monero.rs +++ b/xmr-btc/src/monero.rs @@ -5,10 +5,17 @@ use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use std::ops::{Add, Mul, Sub}; +use bitcoin::hashes::core::fmt::Formatter; pub use curve25519_dalek::scalar::Scalar; pub use monero::*; +use rust_decimal::{ + prelude::{FromPrimitive, ToPrimitive}, + Decimal, +}; +use std::{fmt::Display, str::FromStr}; pub const MIN_CONFIRMATIONS: u32 = 10; +pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; pub fn random_private_key(rng: &mut R) -> PrivateKey { let scalar = Scalar::random(rng); @@ -76,9 +83,20 @@ impl Amount { pub fn from_piconero(amount: u64) -> Self { Amount(amount) } + pub fn as_piconero(&self) -> u64 { self.0 } + + pub fn parse_monero(amount: &str) -> Result { + let decimal = Decimal::from_str(amount)?; + let piconeros_dec = + decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); + let piconeros = piconeros_dec + .to_u64() + .ok_or_else(|| OverflowError(amount.to_owned()))?; + Ok(Amount(piconeros)) + } } impl Add for Amount { @@ -111,6 +129,16 @@ impl From for u64 { } } +impl Display for Amount { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut decimal = Decimal::from(self.0); + decimal + .set_scale(12) + .expect("12 is smaller than max precision of 28"); + write!(f, "{} XMR", decimal) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TransferProof { tx_hash: TxHash, @@ -177,3 +205,70 @@ pub trait CreateWalletForOutput { private_view_key: PrivateViewKey, ) -> anyhow::Result<()>; } + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +#[error("Overflow, cannot convert {0} to u64")] +pub struct OverflowError(pub String); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_monero_min() { + let min_pics = 1; + let amount = Amount::from_piconero(min_pics); + let monero = amount.to_string(); + assert_eq!("0.000000000001 XMR", monero); + } + + #[test] + fn display_monero_one() { + let min_pics = 1000000000000; + let amount = Amount::from_piconero(min_pics); + let monero = amount.to_string(); + assert_eq!("1.000000000000 XMR", monero); + } + + #[test] + fn display_monero_max() { + let max_pics = 18_446_744_073_709_551_615; + let amount = Amount::from_piconero(max_pics); + let monero = amount.to_string(); + assert_eq!("18446744.073709551615 XMR", monero); + } + + #[test] + fn parse_monero_min() { + let monero_min = "0.000000000001"; + let amount = Amount::parse_monero(monero_min).unwrap(); + let pics = amount.0; + assert_eq!(1, pics); + } + + #[test] + fn parse_monero() { + let monero = "123"; + let amount = Amount::parse_monero(monero).unwrap(); + let pics = amount.0; + assert_eq!(123000000000000, pics); + } + + #[test] + fn parse_monero_max() { + let monero = "18446744.073709551615"; + let amount = Amount::parse_monero(monero).unwrap(); + let pics = amount.0; + assert_eq!(18446744073709551615, pics); + } + + #[test] + fn parse_monero_overflows() { + let overflow_pics = "18446744.073709551616"; + let error = Amount::parse_monero(overflow_pics).unwrap_err(); + assert_eq!( + error.downcast_ref::().unwrap(), + &OverflowError(overflow_pics.to_owned()) + ); + } +} diff --git a/xmr-btc/tests/harness/wallet/bitcoin.rs b/xmr-btc/tests/harness/wallet/bitcoin.rs index e2ac5faa..897322c5 100644 --- a/xmr-btc/tests/harness/wallet/bitcoin.rs +++ b/xmr-btc/tests/harness/wallet/bitcoin.rs @@ -7,8 +7,8 @@ use reqwest::Url; use std::time::Duration; use tokio::time; use xmr_btc::bitcoin::{ - BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight, - TxLock, WatchForRawTransaction, + BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, Network, SignTxLock, + TransactionBlockHeight, TxLock, WatchForRawTransaction, }; #[derive(Debug)] @@ -162,3 +162,9 @@ impl TransactionBlockHeight for Wallet { .expect("transient errors to be retried") } } + +impl Network for Wallet { + fn get_network(&self) -> bitcoin::Network { + bitcoin::Network::Regtest + } +}