mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-02-17 13:14:21 -05:00
Merge #275
275: `main` fn and error message improvements r=thomaseizinger a=thomaseizinger Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
commit
ba9e92c4b9
@ -77,7 +77,7 @@ pub fn read_config(config_path: PathBuf) -> Result<Result<Config, ConfigNotIniti
|
|||||||
}
|
}
|
||||||
|
|
||||||
let file = Config::read(&config_path)
|
let file = Config::read(&config_path)
|
||||||
.with_context(|| format!("failed to read config file {}", config_path.display()))?;
|
.with_context(|| format!("Failed to read config file at {}", config_path.display()))?;
|
||||||
|
|
||||||
Ok(Ok(file))
|
Ok(Ok(file))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{bitcoin, monero};
|
use crate::{bitcoin, monero};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Context, Result};
|
||||||
use rust_decimal::prelude::ToPrimitive;
|
use rust_decimal::prelude::ToPrimitive;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
@ -30,20 +30,20 @@ impl Rate {
|
|||||||
let quote_in_sats = quote.as_sat();
|
let quote_in_sats = quote.as_sat();
|
||||||
let quote_in_btc = Decimal::from(quote_in_sats)
|
let quote_in_btc = Decimal::from(quote_in_sats)
|
||||||
.checked_div(Decimal::from(bitcoin::Amount::ONE_BTC.as_sat()))
|
.checked_div(Decimal::from(bitcoin::Amount::ONE_BTC.as_sat()))
|
||||||
.ok_or_else(|| anyhow!("division overflow"))?;
|
.context("Division overflow")?;
|
||||||
|
|
||||||
let rate_in_btc = Decimal::from(rate.as_sat())
|
let rate_in_btc = Decimal::from(rate.as_sat())
|
||||||
.checked_div(Decimal::from(bitcoin::Amount::ONE_BTC.as_sat()))
|
.checked_div(Decimal::from(bitcoin::Amount::ONE_BTC.as_sat()))
|
||||||
.ok_or_else(|| anyhow!("division overflow"))?;
|
.context("Division overflow")?;
|
||||||
|
|
||||||
let base_in_xmr = quote_in_btc
|
let base_in_xmr = quote_in_btc
|
||||||
.checked_div(rate_in_btc)
|
.checked_div(rate_in_btc)
|
||||||
.ok_or_else(|| anyhow!("division overflow"))?;
|
.context("Division overflow")?;
|
||||||
let base_in_piconero = base_in_xmr * Decimal::from(monero::Amount::ONE_XMR.as_piconero());
|
let base_in_piconero = base_in_xmr * Decimal::from(monero::Amount::ONE_XMR.as_piconero());
|
||||||
|
|
||||||
let base_in_piconero = base_in_piconero
|
let base_in_piconero = base_in_piconero
|
||||||
.to_u64()
|
.to_u64()
|
||||||
.ok_or_else(|| anyhow!("decimal cannot be represented as u64"))?;
|
.context("Failed to fit piconero amount into a u64")?;
|
||||||
|
|
||||||
Ok(monero::Amount::from_piconero(base_in_piconero))
|
Ok(monero::Amount::from_piconero(base_in_piconero))
|
||||||
}
|
}
|
||||||
|
@ -141,10 +141,7 @@ async fn init_wallets(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
bitcoin_wallet
|
bitcoin_wallet.sync().await?;
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync btc wallet");
|
|
||||||
|
|
||||||
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
||||||
info!(
|
info!(
|
||||||
|
@ -14,10 +14,8 @@
|
|||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use prettytable::{row, Table};
|
use prettytable::{row, Table};
|
||||||
use reqwest::Url;
|
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
@ -28,7 +26,6 @@ use swap::database::Database;
|
|||||||
use swap::execution_params::GetExecutionParams;
|
use swap::execution_params::GetExecutionParams;
|
||||||
use swap::network::quote::BidQuote;
|
use swap::network::quote::BidQuote;
|
||||||
use swap::protocol::bob;
|
use swap::protocol::bob;
|
||||||
use swap::protocol::bob::cancel::CancelError;
|
|
||||||
use swap::protocol::bob::{Builder, EventLoop};
|
use swap::protocol::bob::{Builder, EventLoop};
|
||||||
use swap::seed::Seed;
|
use swap::seed::Seed;
|
||||||
use swap::{bitcoin, execution_params, monero};
|
use swap::{bitcoin, execution_params, monero};
|
||||||
@ -39,8 +36,6 @@ use uuid::Uuid;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate prettytable;
|
extern crate prettytable;
|
||||||
|
|
||||||
const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet";
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = Arguments::from_args();
|
let args = Arguments::from_args();
|
||||||
@ -76,29 +71,17 @@ async fn main() -> Result<()> {
|
|||||||
None => Config::testnet(),
|
None => Config::testnet(),
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Database and seed will be stored in {}",
|
|
||||||
config.data.dir.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let db = Database::open(config.data.dir.join("database").as_path())
|
let db = Database::open(config.data.dir.join("database").as_path())
|
||||||
.context("Could not open database")?;
|
.context("Failed to open database")?;
|
||||||
|
|
||||||
let wallet_data_dir = config.data.dir.join("wallet");
|
|
||||||
let seed =
|
let seed =
|
||||||
Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed");
|
Seed::from_file_or_generate(&config.data.dir).context("Failed to read in seed file")?;
|
||||||
|
|
||||||
// hardcode to testnet/stagenet
|
// hardcode to testnet/stagenet
|
||||||
let bitcoin_network = bitcoin::Network::Testnet;
|
let bitcoin_network = bitcoin::Network::Testnet;
|
||||||
let monero_network = monero::Network::Stagenet;
|
let monero_network = monero::Network::Stagenet;
|
||||||
let execution_params = execution_params::Testnet::get_execution_params();
|
let execution_params = execution_params::Testnet::get_execution_params();
|
||||||
|
|
||||||
let monero_wallet_rpc = monero::WalletRpc::new(config.data.dir.join("monero")).await?;
|
|
||||||
|
|
||||||
let monero_wallet_rpc_process = monero_wallet_rpc
|
|
||||||
.run(monero_network, "monero-stagenet.exan.tech")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match args.cmd {
|
match args.cmd {
|
||||||
Command::BuyXmr {
|
Command::BuyXmr {
|
||||||
receive_monero_address,
|
receive_monero_address,
|
||||||
@ -113,10 +96,8 @@ async fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let bitcoin_wallet =
|
let bitcoin_wallet = init_bitcoin_wallet(bitcoin_network, &config, seed).await?;
|
||||||
init_bitcoin_wallet(config, bitcoin_network, &wallet_data_dir, seed).await?;
|
let (monero_wallet, _process) = init_monero_wallet(monero_network, &config).await?;
|
||||||
let monero_wallet =
|
|
||||||
init_monero_wallet(monero_network, monero_wallet_rpc_process.endpoint()).await?;
|
|
||||||
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
||||||
let (event_loop, mut event_loop_handle) = EventLoop::new(
|
let (event_loop, mut event_loop_handle) = EventLoop::new(
|
||||||
&seed.derive_libp2p_identity(),
|
&seed.derive_libp2p_identity(),
|
||||||
@ -132,7 +113,7 @@ async fn main() -> Result<()> {
|
|||||||
bitcoin_wallet.new_address(),
|
bitcoin_wallet.new_address(),
|
||||||
async {
|
async {
|
||||||
while bitcoin_wallet.balance().await? == Amount::ZERO {
|
while bitcoin_wallet.balance().await? == Amount::ZERO {
|
||||||
bitcoin_wallet.sync_wallet().await?;
|
bitcoin_wallet.sync().await?;
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
@ -187,10 +168,8 @@ async fn main() -> Result<()> {
|
|||||||
bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network)
|
bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, monero_network)
|
||||||
}
|
}
|
||||||
|
|
||||||
let bitcoin_wallet =
|
let bitcoin_wallet = init_bitcoin_wallet(bitcoin_network, &config, seed).await?;
|
||||||
init_bitcoin_wallet(config, bitcoin_network, &wallet_data_dir, seed).await?;
|
let (monero_wallet, _process) = init_monero_wallet(monero_network, &config).await?;
|
||||||
let monero_wallet =
|
|
||||||
init_monero_wallet(monero_network, monero_wallet_rpc_process.endpoint()).await?;
|
|
||||||
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
let bitcoin_wallet = Arc::new(bitcoin_wallet);
|
||||||
|
|
||||||
let (event_loop, event_loop_handle) = EventLoop::new(
|
let (event_loop, event_loop_handle) = EventLoop::new(
|
||||||
@ -223,8 +202,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Cancel { swap_id, force } => {
|
Command::Cancel { swap_id, force } => {
|
||||||
let bitcoin_wallet =
|
let bitcoin_wallet = init_bitcoin_wallet(bitcoin_network, &config, seed).await?;
|
||||||
init_bitcoin_wallet(config, bitcoin_network, &wallet_data_dir, seed).await?;
|
|
||||||
|
|
||||||
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
||||||
let cancel =
|
let cancel =
|
||||||
@ -234,18 +212,17 @@ async fn main() -> Result<()> {
|
|||||||
Ok((txid, _)) => {
|
Ok((txid, _)) => {
|
||||||
debug!("Cancel transaction successfully published with id {}", txid)
|
debug!("Cancel transaction successfully published with id {}", txid)
|
||||||
}
|
}
|
||||||
Err(CancelError::CancelTimelockNotExpiredYet) => error!(
|
Err(bob::cancel::Error::CancelTimelockNotExpiredYet) => error!(
|
||||||
"The Cancel Transaction cannot be published yet, \
|
"The Cancel Transaction cannot be published yet, \
|
||||||
because the timelock has not expired. Please try again later."
|
because the timelock has not expired. Please try again later."
|
||||||
),
|
),
|
||||||
Err(CancelError::CancelTxAlreadyPublished) => {
|
Err(bob::cancel::Error::CancelTxAlreadyPublished) => {
|
||||||
warn!("The Cancel Transaction has already been published.")
|
warn!("The Cancel Transaction has already been published.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Refund { swap_id, force } => {
|
Command::Refund { swap_id, force } => {
|
||||||
let bitcoin_wallet =
|
let bitcoin_wallet = init_bitcoin_wallet(bitcoin_network, &config, seed).await?;
|
||||||
init_bitcoin_wallet(config, bitcoin_network, &wallet_data_dir, seed).await?;
|
|
||||||
|
|
||||||
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
let resume_state = db.get_state(swap_id)?.try_into_bob()?.into();
|
||||||
|
|
||||||
@ -264,34 +241,41 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn init_bitcoin_wallet(
|
async fn init_bitcoin_wallet(
|
||||||
config: Config,
|
network: bitcoin::Network,
|
||||||
bitcoin_network: bitcoin::Network,
|
config: &Config,
|
||||||
bitcoin_wallet_data_dir: &Path,
|
|
||||||
seed: Seed,
|
seed: Seed,
|
||||||
) -> Result<bitcoin::Wallet> {
|
) -> Result<bitcoin::Wallet> {
|
||||||
let bitcoin_wallet = bitcoin::Wallet::new(
|
let wallet_dir = config.data.dir.join("wallet");
|
||||||
config.bitcoin.electrum_rpc_url,
|
|
||||||
config.bitcoin.electrum_http_url,
|
let wallet = bitcoin::Wallet::new(
|
||||||
bitcoin_network,
|
config.bitcoin.electrum_rpc_url.clone(),
|
||||||
bitcoin_wallet_data_dir,
|
config.bitcoin.electrum_http_url.clone(),
|
||||||
seed.derive_extended_private_key(bitcoin_network)?,
|
network,
|
||||||
|
&wallet_dir,
|
||||||
|
seed.derive_extended_private_key(network)?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.context("Failed to initialize Bitcoin wallet")?;
|
||||||
|
|
||||||
bitcoin_wallet
|
wallet.sync().await?;
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.context("failed to sync balance of bitcoin wallet")?;
|
|
||||||
|
|
||||||
Ok(bitcoin_wallet)
|
Ok(wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_monero_wallet(
|
async fn init_monero_wallet(
|
||||||
monero_network: monero::Network,
|
monero_network: monero::Network,
|
||||||
monero_wallet_rpc_url: Url,
|
config: &Config,
|
||||||
) -> Result<monero::Wallet> {
|
) -> Result<(monero::Wallet, monero::WalletRpcProcess)> {
|
||||||
|
const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet";
|
||||||
|
|
||||||
|
let monero_wallet_rpc = monero::WalletRpc::new(config.data.dir.join("monero")).await?;
|
||||||
|
|
||||||
|
let monero_wallet_rpc_process = monero_wallet_rpc
|
||||||
|
.run(monero_network, "monero-stagenet.exan.tech")
|
||||||
|
.await?;
|
||||||
|
|
||||||
let monero_wallet = monero::Wallet::new(
|
let monero_wallet = monero::Wallet::new(
|
||||||
monero_wallet_rpc_url.clone(),
|
monero_wallet_rpc_process.endpoint(),
|
||||||
monero_network,
|
monero_network,
|
||||||
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
|
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
|
||||||
);
|
);
|
||||||
@ -301,9 +285,9 @@ async fn init_monero_wallet(
|
|||||||
let _test_wallet_connection = monero_wallet
|
let _test_wallet_connection = monero_wallet
|
||||||
.block_height()
|
.block_height()
|
||||||
.await
|
.await
|
||||||
.context("failed to validate connection to monero-wallet-rpc")?;
|
.context("Failed to validate connection to monero-wallet-rpc")?;
|
||||||
|
|
||||||
Ok(monero_wallet)
|
Ok((monero_wallet, monero_wallet_rpc_process))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn determine_btc_to_swap(
|
async fn determine_btc_to_swap(
|
||||||
@ -315,7 +299,7 @@ async fn determine_btc_to_swap(
|
|||||||
) -> Result<bitcoin::Amount> {
|
) -> Result<bitcoin::Amount> {
|
||||||
debug!("Requesting quote");
|
debug!("Requesting quote");
|
||||||
|
|
||||||
let bid_quote = request_quote.await.context("failed to request quote")?;
|
let bid_quote = request_quote.await.context("Failed to request quote")?;
|
||||||
|
|
||||||
info!("Received quote: 1 XMR ~ {}", bid_quote.price);
|
info!("Received quote: 1 XMR ~ {}", bid_quote.price);
|
||||||
|
|
||||||
@ -329,14 +313,18 @@ async fn determine_btc_to_swap(
|
|||||||
bid_quote.max_quantity
|
bid_quote.max_quantity
|
||||||
);
|
);
|
||||||
|
|
||||||
let new_balance = wait_for_deposit.await?;
|
let new_balance = wait_for_deposit
|
||||||
|
.await
|
||||||
|
.context("Failed to wait for Bitcoin deposit")?;
|
||||||
|
|
||||||
info!("Received {}", new_balance);
|
info!("Received {}", new_balance);
|
||||||
} else {
|
} else {
|
||||||
info!("Found {} in wallet", initial_balance);
|
info!("Found {} in wallet", initial_balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_giveable = max_giveable.await?;
|
let max_giveable = max_giveable
|
||||||
|
.await
|
||||||
|
.context("Failed to compute max 'giveable' Bitcoin amount")?;
|
||||||
let max_accepted = bid_quote.max_quantity;
|
let max_accepted = bid_quote.max_quantity;
|
||||||
|
|
||||||
if max_giveable > max_accepted {
|
if max_giveable > max_accepted {
|
||||||
|
@ -23,7 +23,7 @@ pub use wallet::Wallet;
|
|||||||
use ::bitcoin::hashes::hex::ToHex;
|
use ::bitcoin::hashes::hex::ToHex;
|
||||||
use ::bitcoin::hashes::Hash;
|
use ::bitcoin::hashes::Hash;
|
||||||
use ::bitcoin::{secp256k1, SigHash};
|
use ::bitcoin::{secp256k1, SigHash};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
use ecdsa_fun::adaptor::{Adaptor, HashTranscript};
|
||||||
use ecdsa_fun::fun::Point;
|
use ecdsa_fun::fun::Point;
|
||||||
use ecdsa_fun::nonce::Deterministic;
|
use ecdsa_fun::nonce::Deterministic;
|
||||||
@ -204,7 +204,7 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu
|
|||||||
let s = adaptor
|
let s = adaptor
|
||||||
.recover_decryption_key(&S.0, &sig, &encsig)
|
.recover_decryption_key(&S.0, &sig, &encsig)
|
||||||
.map(SecretKey::from)
|
.map(SecretKey::from)
|
||||||
.ok_or_else(|| anyhow!("secret recovery failure"))?;
|
.context("Failed to recover secret from adaptor signature")?;
|
||||||
|
|
||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ impl TxRedeem {
|
|||||||
let sig = sigs
|
let sig = sigs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
|
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
|
||||||
.context("neither signature on witness stack verifies against B")?;
|
.context("Neither signature on witness stack verifies against B")?;
|
||||||
|
|
||||||
Ok(sig)
|
Ok(sig)
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,7 @@ impl TxRefund {
|
|||||||
let sig = sigs
|
let sig = sigs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
|
.find(|sig| verify_sig(&B, &self.digest(), &sig).is_ok())
|
||||||
.context("neither signature on witness stack verifies against B")?;
|
.context("Neither signature on witness stack verifies against B")?;
|
||||||
|
|
||||||
Ok(sig)
|
Ok(sig)
|
||||||
}
|
}
|
||||||
|
@ -74,12 +74,23 @@ impl Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn balance(&self) -> Result<Amount> {
|
pub async fn balance(&self) -> Result<Amount> {
|
||||||
let balance = self.inner.lock().await.get_balance()?;
|
let balance = self
|
||||||
|
.inner
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get_balance()
|
||||||
|
.context("Failed to calculate Bitcoin balance")?;
|
||||||
|
|
||||||
Ok(Amount::from_sat(balance))
|
Ok(Amount::from_sat(balance))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_address(&self) -> Result<Address> {
|
pub async fn new_address(&self) -> Result<Address> {
|
||||||
let address = self.inner.lock().await.get_new_address()?;
|
let address = self
|
||||||
|
.inner
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get_new_address()
|
||||||
|
.context("Failed to get new Bitcoin address")?;
|
||||||
|
|
||||||
Ok(address)
|
Ok(address)
|
||||||
}
|
}
|
||||||
@ -105,8 +116,13 @@ impl Wallet {
|
|||||||
Ok(Amount::from_sat(fees))
|
Ok(Amount::from_sat(fees))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sync_wallet(&self) -> Result<()> {
|
pub async fn sync(&self) -> Result<()> {
|
||||||
self.inner.lock().await.sync(noop_progress(), None)?;
|
self.inner
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.sync(noop_progress(), None)
|
||||||
|
.context("Failed to sync balance of Bitcoin wallet")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +155,7 @@ impl Wallet {
|
|||||||
tx_builder.set_single_recipient(dummy_script);
|
tx_builder.set_single_recipient(dummy_script);
|
||||||
tx_builder.drain_wallet();
|
tx_builder.drain_wallet();
|
||||||
tx_builder.fee_rate(self.select_feerate());
|
tx_builder.fee_rate(self.select_feerate());
|
||||||
let (_, details) = tx_builder.finish()?;
|
let (_, details) = tx_builder.finish().context("Failed to build transaction")?;
|
||||||
|
|
||||||
let max_giveable = details.sent - details.fees;
|
let max_giveable = details.sent - details.fees;
|
||||||
|
|
||||||
@ -160,7 +176,7 @@ impl Wallet {
|
|||||||
.await
|
.await
|
||||||
.broadcast(transaction)
|
.broadcast(transaction)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("failed to broadcast Bitcoin {} transaction {}", kind, txid)
|
format!("Failed to broadcast Bitcoin {} transaction {}", kind, txid)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
tracing::info!("Published Bitcoin {} transaction as {}", txid, kind);
|
tracing::info!("Published Bitcoin {} transaction as {}", txid, kind);
|
||||||
@ -203,7 +219,7 @@ impl Wallet {
|
|||||||
Result::<_, backoff::Error<Error>>::Ok(tx)
|
Result::<_, backoff::Error<Error>>::Ok(tx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.context("transient errors to be retried")?;
|
.context("Transient errors should be retried")?;
|
||||||
|
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
@ -224,7 +240,7 @@ impl Wallet {
|
|||||||
Result::<_, backoff::Error<Error>>::Ok(height)
|
Result::<_, backoff::Error<Error>>::Ok(height)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.context("transient errors to be retried")?;
|
.context("Transient errors should be retried")?;
|
||||||
|
|
||||||
Ok(BlockHeight::new(height))
|
Ok(BlockHeight::new(height))
|
||||||
}
|
}
|
||||||
@ -255,7 +271,7 @@ impl Wallet {
|
|||||||
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.context("transient errors to be retried")?;
|
.context("Transient errors should be retried")?;
|
||||||
|
|
||||||
Ok(BlockHeight::new(height))
|
Ok(BlockHeight::new(height))
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ pub fn read_config(config_path: PathBuf) -> Result<Result<Config, ConfigNotIniti
|
|||||||
}
|
}
|
||||||
|
|
||||||
let file = Config::read(&config_path)
|
let file = Config::read(&config_path)
|
||||||
.with_context(|| format!("failed to read config file {}", config_path.display()))?;
|
.with_context(|| format!("Failed to read config file at {}", config_path.display()))?;
|
||||||
|
|
||||||
Ok(Ok(file))
|
Ok(Ok(file))
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,8 @@ pub struct Database(sled::Db);
|
|||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
|
tracing::debug!("Opening database at {}", path.display());
|
||||||
|
|
||||||
let db =
|
let db =
|
||||||
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
|
sled::open(path).with_context(|| format!("Could not open the DB at {:?}", path))?;
|
||||||
|
|
||||||
@ -94,15 +96,15 @@ impl Database {
|
|||||||
.map(|item| match item {
|
.map(|item| match item {
|
||||||
Ok((key, value)) => {
|
Ok((key, value)) => {
|
||||||
let swap_id = deserialize::<Uuid>(&key);
|
let swap_id = deserialize::<Uuid>(&key);
|
||||||
let swap = deserialize::<Swap>(&value).context("failed to deserialize swap");
|
let swap = deserialize::<Swap>(&value).context("Failed to deserialize swap");
|
||||||
|
|
||||||
match (swap_id, swap) {
|
match (swap_id, swap) {
|
||||||
(Ok(swap_id), Ok(swap)) => Ok((swap_id, swap)),
|
(Ok(swap_id), Ok(swap)) => Ok((swap_id, swap)),
|
||||||
(Ok(_), Err(err)) => Err(err),
|
(Ok(_), Err(err)) => Err(err),
|
||||||
_ => bail!("failed to deserialize swap"),
|
_ => bail!("Failed to deserialize swap"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => Err(err).context("failed to retrieve swap from DB"),
|
Err(err) => Err(err).context("Failed to retrieve swap from DB"),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ impl WalletRpc {
|
|||||||
|
|
||||||
let content_length = response.headers()[CONTENT_LENGTH]
|
let content_length = response.headers()[CONTENT_LENGTH]
|
||||||
.to_str()
|
.to_str()
|
||||||
.context("failed to convert content-length to string")?
|
.context("Failed to convert content-length to string")?
|
||||||
.parse::<u64>()?;
|
.parse::<u64>()?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
@ -184,7 +184,7 @@ impl Behaviour {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.quote
|
self.quote
|
||||||
.send_response(channel, response)
|
.send_response(channel, response)
|
||||||
.map_err(|_| anyhow!("failed to respond with quote"))?;
|
.map_err(|_| anyhow!("Failed to respond with quote"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -196,7 +196,7 @@ impl Behaviour {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.spot_price
|
self.spot_price
|
||||||
.send_response(channel, response)
|
.send_response(channel, response)
|
||||||
.map_err(|_| anyhow!("failed to respond with spot price"))?;
|
.map_err(|_| anyhow!("Failed to respond with spot price"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -65,31 +65,31 @@ impl Behaviour {
|
|||||||
.do_protocol_listener(bob, move |mut substream| async move {
|
.do_protocol_listener(bob, move |mut substream| async move {
|
||||||
let message0 =
|
let message0 =
|
||||||
serde_cbor::from_slice::<Message0>(&substream.read_message(BUF_SIZE).await?)
|
serde_cbor::from_slice::<Message0>(&substream.read_message(BUF_SIZE).await?)
|
||||||
.context("failed to deserialize message0")?;
|
.context("Failed to deserialize message0")?;
|
||||||
let state1 = state0.receive(message0)?;
|
let state1 = state0.receive(message0)?;
|
||||||
|
|
||||||
substream
|
substream
|
||||||
.write_message(
|
.write_message(
|
||||||
&serde_cbor::to_vec(&state1.next_message())
|
&serde_cbor::to_vec(&state1.next_message())
|
||||||
.context("failed to serialize message1")?,
|
.context("Failed to serialize message1")?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let message2 =
|
let message2 =
|
||||||
serde_cbor::from_slice::<Message2>(&substream.read_message(BUF_SIZE).await?)
|
serde_cbor::from_slice::<Message2>(&substream.read_message(BUF_SIZE).await?)
|
||||||
.context("failed to deserialize message2")?;
|
.context("Failed to deserialize message2")?;
|
||||||
let state2 = state1.receive(message2);
|
let state2 = state1.receive(message2);
|
||||||
|
|
||||||
substream
|
substream
|
||||||
.write_message(
|
.write_message(
|
||||||
&serde_cbor::to_vec(&state2.next_message())
|
&serde_cbor::to_vec(&state2.next_message())
|
||||||
.context("failed to serialize message3")?,
|
.context("Failed to serialize message3")?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let message4 =
|
let message4 =
|
||||||
serde_cbor::from_slice::<Message4>(&substream.read_message(BUF_SIZE).await?)
|
serde_cbor::from_slice::<Message4>(&substream.read_message(BUF_SIZE).await?)
|
||||||
.context("failed to deserialize message4")?;
|
.context("Failed to deserialize message4")?;
|
||||||
let state3 = state2.receive(message4)?;
|
let state3 = state2.receive(message4)?;
|
||||||
|
|
||||||
Ok((bob, state3))
|
Ok((bob, state3))
|
||||||
|
@ -106,7 +106,7 @@ pub fn build_bitcoin_redeem_transaction(
|
|||||||
|
|
||||||
let tx = tx_redeem
|
let tx = tx_redeem
|
||||||
.add_signatures((a.public(), sig_a), (B, sig_b))
|
.add_signatures((a.public(), sig_a), (B, sig_b))
|
||||||
.context("sig_{a,b} are invalid for tx_redeem")?;
|
.context("Failed to sign Bitcoin redeem transaction")?;
|
||||||
|
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use std::sync::Arc;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, Clone, Copy)]
|
#[derive(Debug, thiserror::Error, Clone, Copy)]
|
||||||
pub enum CancelError {
|
pub enum Error {
|
||||||
#[error("The cancel timelock has not expired yet.")]
|
#[error("The cancel timelock has not expired yet.")]
|
||||||
CancelTimelockNotExpiredYet,
|
CancelTimelockNotExpiredYet,
|
||||||
#[error("The cancel transaction has already been published.")]
|
#[error("The cancel transaction has already been published.")]
|
||||||
@ -19,7 +19,7 @@ pub async fn cancel(
|
|||||||
bitcoin_wallet: Arc<Wallet>,
|
bitcoin_wallet: Arc<Wallet>,
|
||||||
db: Database,
|
db: Database,
|
||||||
force: bool,
|
force: bool,
|
||||||
) -> Result<Result<(Txid, BobState), CancelError>> {
|
) -> Result<Result<(Txid, BobState), Error>> {
|
||||||
let state4 = match state {
|
let state4 = match state {
|
||||||
BobState::BtcLocked(state3) => state3.cancel(),
|
BobState::BtcLocked(state3) => state3.cancel(),
|
||||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||||
@ -35,7 +35,7 @@ pub async fn cancel(
|
|||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
if let ExpiredTimelocks::None = state4.expired_timelock(bitcoin_wallet.as_ref()).await? {
|
||||||
return Ok(Err(CancelError::CancelTimelockNotExpiredYet));
|
return Ok(Err(Error::CancelTimelockNotExpiredYet));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state4
|
if state4
|
||||||
@ -47,7 +47,7 @@ pub async fn cancel(
|
|||||||
let db_state = state.into();
|
let db_state = state.into();
|
||||||
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
db.insert_latest_state(swap_id, Swap::Bob(db_state)).await?;
|
||||||
|
|
||||||
return Ok(Err(CancelError::CancelTxAlreadyPublished));
|
return Ok(Err(Error::CancelTxAlreadyPublished));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ impl EventLoop {
|
|||||||
let _ = self.conn_established.send(peer_id).await;
|
let _ = self.conn_established.send(peer_id).await;
|
||||||
} else {
|
} else {
|
||||||
debug!("Dialing alice at {}", peer_id);
|
debug!("Dialing alice at {}", peer_id);
|
||||||
libp2p::Swarm::dial(&mut self.swarm, &peer_id).context("failed to dial alice")?;
|
libp2p::Swarm::dial(&mut self.swarm, &peer_id).context("Failed to dial alice")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -73,31 +73,31 @@ impl Behaviour {
|
|||||||
substream
|
substream
|
||||||
.write_message(
|
.write_message(
|
||||||
&serde_cbor::to_vec(&state0.next_message())
|
&serde_cbor::to_vec(&state0.next_message())
|
||||||
.context("failed to serialize message0")?,
|
.context("Failed to serialize message0")?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let message1 =
|
let message1 =
|
||||||
serde_cbor::from_slice::<Message1>(&substream.read_message(BUF_SIZE).await?)
|
serde_cbor::from_slice::<Message1>(&substream.read_message(BUF_SIZE).await?)
|
||||||
.context("failed to deserialize message1")?;
|
.context("Failed to deserialize message1")?;
|
||||||
let state1 = state0.receive(bitcoin_wallet.as_ref(), message1).await?;
|
let state1 = state0.receive(bitcoin_wallet.as_ref(), message1).await?;
|
||||||
|
|
||||||
substream
|
substream
|
||||||
.write_message(
|
.write_message(
|
||||||
&serde_cbor::to_vec(&state1.next_message())
|
&serde_cbor::to_vec(&state1.next_message())
|
||||||
.context("failed to serialize message2")?,
|
.context("Failed to serialize message2")?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let message3 =
|
let message3 =
|
||||||
serde_cbor::from_slice::<Message3>(&substream.read_message(BUF_SIZE).await?)
|
serde_cbor::from_slice::<Message3>(&substream.read_message(BUF_SIZE).await?)
|
||||||
.context("failed to deserialize message3")?;
|
.context("Failed to deserialize message3")?;
|
||||||
let state2 = state1.receive(message3)?;
|
let state2 = state1.receive(message3)?;
|
||||||
|
|
||||||
substream
|
substream
|
||||||
.write_message(
|
.write_message(
|
||||||
&serde_cbor::to_vec(&state2.next_message())
|
&serde_cbor::to_vec(&state2.next_message())
|
||||||
.context("failed to serialize message4")?,
|
.context("Failed to serialize message4")?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -266,7 +266,7 @@ impl State2 {
|
|||||||
let signed_tx = bitcoin_wallet
|
let signed_tx = bitcoin_wallet
|
||||||
.sign_and_finalize(self.tx_lock.clone().into())
|
.sign_and_finalize(self.tx_lock.clone().into())
|
||||||
.await
|
.await
|
||||||
.context("failed to sign Bitcoin lock transaction")?;
|
.context("Failed to sign Bitcoin lock transaction")?;
|
||||||
|
|
||||||
let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
let _ = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::fs::ensure_directory_exists;
|
use crate::fs::ensure_directory_exists;
|
||||||
use ::bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
use ::bitcoin::secp256k1::constants::SECRET_KEY_SIZE;
|
||||||
use ::bitcoin::secp256k1::{self, SecretKey};
|
use ::bitcoin::secp256k1::{self, SecretKey};
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
||||||
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
use bitcoin::hashes::{sha256, Hash, HashEngine};
|
||||||
use libp2p::identity;
|
use libp2p::identity;
|
||||||
@ -34,7 +34,8 @@ impl Seed {
|
|||||||
network: bitcoin::Network,
|
network: bitcoin::Network,
|
||||||
) -> Result<ExtendedPrivKey> {
|
) -> Result<ExtendedPrivKey> {
|
||||||
let seed = self.derive(b"BITCOIN_EXTENDED_PRIVATE_KEY").bytes();
|
let seed = self.derive(b"BITCOIN_EXTENDED_PRIVATE_KEY").bytes();
|
||||||
let private_key = ExtendedPrivKey::new_master(network, &seed)?;
|
let private_key = ExtendedPrivKey::new_master(network, &seed)
|
||||||
|
.context("Failed to create new master extended private key")?;
|
||||||
|
|
||||||
Ok(private_key)
|
Ok(private_key)
|
||||||
}
|
}
|
||||||
@ -90,7 +91,7 @@ impl Seed {
|
|||||||
let contents = fs::read_to_string(file)?;
|
let contents = fs::read_to_string(file)?;
|
||||||
let pem = pem::parse(contents)?;
|
let pem = pem::parse(contents)?;
|
||||||
|
|
||||||
tracing::trace!("Read in seed from {}", file.display());
|
tracing::debug!("Reading in seed from {}", file.display());
|
||||||
|
|
||||||
Self::from_pem(pem)
|
Self::from_pem(pem)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
pub mod testutils;
|
pub mod testutils;
|
||||||
|
|
||||||
use bob::cancel::CancelError;
|
use bob::cancel::Error;
|
||||||
use swap::protocol::bob;
|
use swap::protocol::bob;
|
||||||
use swap::protocol::bob::BobState;
|
use swap::protocol::bob::BobState;
|
||||||
use testutils::bob_run_until::is_btc_locked;
|
use testutils::bob_run_until::is_btc_locked;
|
||||||
@ -30,7 +30,7 @@ async fn given_bob_manually_cancels_when_timelock_not_expired_errors() {
|
|||||||
.err()
|
.err()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(matches!(result, CancelError::CancelTimelockNotExpiredYet));
|
assert!(matches!(result, Error::CancelTimelockNotExpiredYet));
|
||||||
|
|
||||||
let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await;
|
let (bob_swap, bob_join_handle) = ctx.stop_and_resume_bob_from_db(bob_join_handle).await;
|
||||||
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
assert!(matches!(bob_swap.state, BobState::BtcLocked { .. }));
|
||||||
|
@ -151,10 +151,7 @@ impl TestContext {
|
|||||||
|
|
||||||
assert!(matches!(state, AliceState::BtcRedeemed));
|
assert!(matches!(state, AliceState::BtcRedeemed));
|
||||||
|
|
||||||
self.alice_bitcoin_wallet
|
self.alice_bitcoin_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync wallet");
|
|
||||||
|
|
||||||
let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap();
|
let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -184,10 +181,7 @@ impl TestContext {
|
|||||||
|
|
||||||
assert!(matches!(state, AliceState::XmrRefunded));
|
assert!(matches!(state, AliceState::XmrRefunded));
|
||||||
|
|
||||||
self.alice_bitcoin_wallet
|
self.alice_bitcoin_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync wallet");
|
|
||||||
|
|
||||||
let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap();
|
let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap();
|
||||||
assert_eq!(btc_balance_after_swap, self.alice_starting_balances.btc);
|
assert_eq!(btc_balance_after_swap, self.alice_starting_balances.btc);
|
||||||
@ -206,10 +200,7 @@ impl TestContext {
|
|||||||
pub async fn assert_alice_punished(&self, state: AliceState) {
|
pub async fn assert_alice_punished(&self, state: AliceState) {
|
||||||
assert!(matches!(state, AliceState::BtcPunished));
|
assert!(matches!(state, AliceState::BtcPunished));
|
||||||
|
|
||||||
self.alice_bitcoin_wallet
|
self.alice_bitcoin_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync wallet");
|
|
||||||
|
|
||||||
let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap();
|
let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -228,10 +219,7 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_bob_redeemed(&self, state: BobState) {
|
pub async fn assert_bob_redeemed(&self, state: BobState) {
|
||||||
self.bob_bitcoin_wallet
|
self.bob_bitcoin_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync wallet");
|
|
||||||
|
|
||||||
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
|
let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state {
|
||||||
tx_lock_id
|
tx_lock_id
|
||||||
@ -263,10 +251,7 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_bob_refunded(&self, state: BobState) {
|
pub async fn assert_bob_refunded(&self, state: BobState) {
|
||||||
self.bob_bitcoin_wallet
|
self.bob_bitcoin_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync wallet");
|
|
||||||
|
|
||||||
let lock_tx_id = if let BobState::BtcRefunded(state4) = state {
|
let lock_tx_id = if let BobState::BtcRefunded(state4) = state {
|
||||||
state4.tx_lock_id()
|
state4.tx_lock_id()
|
||||||
@ -300,10 +285,7 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn assert_bob_punished(&self, state: BobState) {
|
pub async fn assert_bob_punished(&self, state: BobState) {
|
||||||
self.bob_bitcoin_wallet
|
self.bob_bitcoin_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync wallet");
|
|
||||||
|
|
||||||
let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state {
|
let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state {
|
||||||
tx_lock_id
|
tx_lock_id
|
||||||
@ -654,10 +636,7 @@ async fn init_test_wallets(
|
|||||||
let max_retries = 30u8;
|
let max_retries = 30u8;
|
||||||
loop {
|
loop {
|
||||||
retries += 1;
|
retries += 1;
|
||||||
btc_wallet
|
btc_wallet.sync().await.unwrap();
|
||||||
.sync_wallet()
|
|
||||||
.await
|
|
||||||
.expect("Could not sync btc wallet");
|
|
||||||
|
|
||||||
let btc_balance = btc_wallet.balance().await.unwrap();
|
let btc_balance = btc_wallet.balance().await.unwrap();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user