61: Prepare CLI for mainnet swap r=da-kami a=da-kami



Co-authored-by: Daniel Karzel <daniel@comit.network>
This commit is contained in:
bors[bot] 2020-12-11 03:48:01 +00:00 committed by GitHub
commit 9fecf7008e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 478 additions and 59 deletions

View file

@ -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(())
}

View file

@ -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<Self> {
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
Ok(Self(wallet))
Ok(Self {
inner: wallet,
network,
})
}
pub async fn balance(&self) -> Result<Amount> {
let balance = self.0.balance().await?;
let balance = self.inner.balance().await?;
Ok(balance)
}
pub async fn new_address(&self) -> Result<Address> {
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<Amount> {
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<PartiallySignedTransaction> {
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<Txid> {
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<Transaction> {
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
}
}

View file

@ -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<u16>,
},
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(long = "tor")]
tor: bool,
#[structopt(
short = "m",
long = "monerod",
default_value = "http://127.0.0.1:18083/json_rpc"
)]
monero_wallet_rpc_url: Url,
#[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<bitcoin::Amount> {
let amount = bitcoin::Amount::from_str_in(str, ::bitcoin::Denomination::Bitcoin)?;
Ok(amount)
}
fn parse_xmr(str: &str) -> anyhow::Result<xmr_btc::monero::Amount> {
let amount = xmr_btc::monero::Amount::parse_monero(str)?;
Ok(amount)
}

View file

@ -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;

View file

@ -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))
}
}

View file

@ -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?;

View file

@ -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();

View file

@ -241,7 +241,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(),
);
@ -316,13 +316,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