2021-03-04 00:28:58 +00:00
|
|
|
use crate::bitcoin::timelocks::BlockHeight;
|
|
|
|
use crate::bitcoin::{Address, Amount, Transaction};
|
|
|
|
use crate::execution_params::ExecutionParams;
|
|
|
|
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
|
|
|
use ::bitcoin::Txid;
|
2021-02-17 00:09:09 +00:00
|
|
|
use anyhow::{anyhow, bail, Context, Result};
|
2021-03-04 00:28:58 +00:00
|
|
|
use backoff::backoff::Constant as ConstantBackoff;
|
|
|
|
use backoff::future::retry;
|
|
|
|
use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain};
|
|
|
|
use bdk::descriptor::Segwitv0;
|
|
|
|
use bdk::electrum_client::{self, Client, ElectrumApi};
|
|
|
|
use bdk::keys::DerivableKey;
|
|
|
|
use bdk::{FeeRate, KeychainKind};
|
2021-02-26 03:31:09 +00:00
|
|
|
use bitcoin::Script;
|
2021-02-01 23:39:34 +00:00
|
|
|
use reqwest::{Method, Url};
|
|
|
|
use serde::{Deserialize, Serialize};
|
2021-03-04 00:28:58 +00:00
|
|
|
use std::path::Path;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use std::time::Duration;
|
|
|
|
use tokio::sync::Mutex;
|
|
|
|
use tokio::time::interval;
|
2021-02-01 23:39:34 +00:00
|
|
|
|
|
|
|
const SLED_TREE_NAME: &str = "default_tree";
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-02-17 00:09:09 +00:00
|
|
|
#[derive(Debug, thiserror::Error)]
|
2021-02-15 05:13:29 +00:00
|
|
|
enum Error {
|
2021-02-17 00:09:09 +00:00
|
|
|
#[error("Sending the request failed")]
|
2021-02-15 05:13:29 +00:00
|
|
|
Io(reqwest::Error),
|
2021-02-17 00:09:09 +00:00
|
|
|
#[error("Conversion to Integer failed")]
|
2021-02-15 05:13:29 +00:00
|
|
|
Parse(std::num::ParseIntError),
|
2021-02-17 00:09:09 +00:00
|
|
|
#[error("The transaction is not minded yet")]
|
2021-02-15 05:13:29 +00:00
|
|
|
NotYetMined,
|
2021-02-17 00:09:09 +00:00
|
|
|
#[error("Deserialization failed")]
|
|
|
|
JsonDeserialization(reqwest::Error),
|
2021-02-17 00:32:41 +00:00
|
|
|
#[error("Electrum client error")]
|
|
|
|
ElectrumClient(electrum_client::Error),
|
2021-02-15 05:13:29 +00:00
|
|
|
}
|
|
|
|
|
2021-01-05 03:08:36 +00:00
|
|
|
pub struct Wallet {
|
2021-02-25 02:22:03 +00:00
|
|
|
inner: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
|
|
|
|
http_url: Url,
|
|
|
|
rpc_url: Url,
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Wallet {
|
2021-02-01 23:39:34 +00:00
|
|
|
pub async fn new(
|
|
|
|
electrum_rpc_url: Url,
|
|
|
|
electrum_http_url: Url,
|
|
|
|
network: bitcoin::Network,
|
2021-02-09 06:23:13 +00:00
|
|
|
wallet_dir: &Path,
|
2021-03-02 06:10:29 +00:00
|
|
|
key: impl DerivableKey<Segwitv0> + Clone,
|
2021-02-01 23:39:34 +00:00
|
|
|
) -> Result<Self> {
|
2021-02-15 05:04:51 +00:00
|
|
|
// Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47.
|
|
|
|
let config = electrum_client::ConfigBuilder::default().retry(2).build();
|
|
|
|
|
|
|
|
let client = Client::from_config(electrum_rpc_url.as_str(), config)
|
2021-02-01 23:39:34 +00:00
|
|
|
.map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?;
|
|
|
|
|
2021-02-09 06:23:13 +00:00
|
|
|
let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
2021-02-01 23:39:34 +00:00
|
|
|
|
|
|
|
let bdk_wallet = bdk::Wallet::new(
|
2021-03-02 06:10:29 +00:00
|
|
|
bdk::template::BIP84(key.clone(), KeychainKind::External),
|
|
|
|
Some(bdk::template::BIP84(key, KeychainKind::Internal)),
|
2021-02-01 23:39:34 +00:00
|
|
|
network,
|
|
|
|
db,
|
|
|
|
ElectrumBlockchain::from(client),
|
|
|
|
)?;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
|
|
|
Ok(Self {
|
2021-02-01 23:39:34 +00:00
|
|
|
inner: Arc::new(Mutex::new(bdk_wallet)),
|
|
|
|
http_url: electrum_http_url,
|
|
|
|
rpc_url: electrum_rpc_url,
|
2021-01-05 03:08:36 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn balance(&self) -> Result<Amount> {
|
2021-03-04 06:25:05 +00:00
|
|
|
let balance = self
|
|
|
|
.inner
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.get_balance()
|
|
|
|
.context("Failed to calculate Bitcoin balance")?;
|
2021-03-04 06:22:59 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
Ok(Amount::from_sat(balance))
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn new_address(&self) -> Result<Address> {
|
2021-03-04 06:25:05 +00:00
|
|
|
let address = self
|
|
|
|
.inner
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.get_new_address()
|
|
|
|
.context("Failed to get new Bitcoin address")?;
|
2021-02-25 01:51:30 +00:00
|
|
|
|
|
|
|
Ok(address)
|
2021-02-01 23:39:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_tx(&self, txid: Txid) -> Result<Option<Transaction>> {
|
|
|
|
let tx = self.inner.lock().await.client().get_tx(&txid)?;
|
|
|
|
Ok(tx)
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
2021-02-01 23:39:34 +00:00
|
|
|
let fees = self
|
2021-01-05 03:08:36 +00:00
|
|
|
.inner
|
2021-02-01 23:39:34 +00:00
|
|
|
.lock()
|
2021-01-05 03:08:36 +00:00
|
|
|
.await
|
2021-02-01 23:39:34 +00:00
|
|
|
.list_transactions(true)?
|
|
|
|
.iter()
|
|
|
|
.find(|tx| tx.txid == txid)
|
|
|
|
.ok_or_else(|| {
|
|
|
|
anyhow!("Could not find tx in bdk wallet when trying to determine fees")
|
2021-01-05 03:08:36 +00:00
|
|
|
})?
|
2021-02-01 23:39:34 +00:00
|
|
|
.fees;
|
|
|
|
|
|
|
|
Ok(Amount::from_sat(fees))
|
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-04 06:07:02 +00:00
|
|
|
pub async fn sync(&self) -> Result<()> {
|
2021-03-04 06:05:00 +00:00
|
|
|
self.inner
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.sync(noop_progress(), None)
|
2021-03-04 06:25:05 +00:00
|
|
|
.context("Failed to sync balance of Bitcoin wallet")?;
|
2021-03-04 06:05:00 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
Ok(())
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-02-25 23:18:47 +00:00
|
|
|
pub async fn send_to_address(
|
2021-01-05 03:08:36 +00:00
|
|
|
&self,
|
2021-02-25 23:18:47 +00:00
|
|
|
address: Address,
|
|
|
|
amount: Amount,
|
2021-01-05 03:08:36 +00:00
|
|
|
) -> Result<PartiallySignedTransaction> {
|
2021-02-18 02:33:50 +00:00
|
|
|
let wallet = self.inner.lock().await;
|
|
|
|
|
|
|
|
let mut tx_builder = wallet.build_tx();
|
2021-02-25 23:18:47 +00:00
|
|
|
tx_builder.add_recipient(address.script_pubkey(), amount.as_sat());
|
2021-02-26 03:31:09 +00:00
|
|
|
tx_builder.fee_rate(self.select_feerate());
|
2021-02-18 02:33:50 +00:00
|
|
|
let (psbt, _details) = tx_builder.finish()?;
|
2021-02-25 23:18:47 +00:00
|
|
|
|
2021-01-05 03:08:36 +00:00
|
|
|
Ok(psbt)
|
|
|
|
}
|
2021-02-25 23:14:43 +00:00
|
|
|
|
2021-02-26 03:31:09 +00:00
|
|
|
/// Calculates the maximum "giveable" amount of this wallet.
|
|
|
|
///
|
|
|
|
/// We define this as the maximum amount we can pay to a single output,
|
|
|
|
/// already accounting for the fees we need to spend to get the
|
|
|
|
/// transaction confirmed.
|
2021-03-01 04:35:45 +00:00
|
|
|
pub async fn max_giveable(&self, locking_script_size: usize) -> Result<Amount> {
|
2021-02-26 03:31:09 +00:00
|
|
|
let wallet = self.inner.lock().await;
|
|
|
|
|
|
|
|
let mut tx_builder = wallet.build_tx();
|
|
|
|
|
2021-03-01 04:35:45 +00:00
|
|
|
let dummy_script = Script::from(vec![0u8; locking_script_size]);
|
2021-02-26 03:31:09 +00:00
|
|
|
tx_builder.set_single_recipient(dummy_script);
|
|
|
|
tx_builder.drain_wallet();
|
|
|
|
tx_builder.fee_rate(self.select_feerate());
|
2021-03-04 06:39:17 +00:00
|
|
|
let (_, details) = tx_builder.finish().context("Failed to build transaction")?;
|
2021-02-26 03:31:09 +00:00
|
|
|
|
|
|
|
let max_giveable = details.sent - details.fees;
|
|
|
|
|
|
|
|
Ok(Amount::from_sat(max_giveable))
|
|
|
|
}
|
|
|
|
|
2021-02-25 23:14:43 +00:00
|
|
|
pub async fn get_network(&self) -> bitcoin::Network {
|
|
|
|
self.inner.lock().await.network()
|
|
|
|
}
|
2021-02-26 03:31:09 +00:00
|
|
|
|
2021-03-02 01:29:11 +00:00
|
|
|
/// Broadcast the given transaction to the network and emit a log statement
|
|
|
|
/// if done so successfully.
|
|
|
|
pub async fn broadcast(&self, transaction: Transaction, kind: &str) -> Result<Txid> {
|
2021-03-02 01:22:23 +00:00
|
|
|
let txid = transaction.txid();
|
|
|
|
|
|
|
|
self.inner
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.broadcast(transaction)
|
2021-03-02 01:29:11 +00:00
|
|
|
.with_context(|| {
|
2021-03-04 06:25:05 +00:00
|
|
|
format!("Failed to broadcast Bitcoin {} transaction {}", kind, txid)
|
2021-03-02 01:29:11 +00:00
|
|
|
})?;
|
|
|
|
|
2021-03-05 05:07:13 +00:00
|
|
|
tracing::info!(%txid, "Published Bitcoin {} transaction", kind);
|
2021-03-02 01:22:23 +00:00
|
|
|
|
|
|
|
Ok(txid)
|
2021-02-26 03:31:09 +00:00
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-02 01:53:40 +00:00
|
|
|
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
|
2021-02-01 23:39:34 +00:00
|
|
|
let (signed_psbt, finalized) = self.inner.lock().await.sign(psbt, None)?;
|
2021-03-02 01:53:40 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
if !finalized {
|
2021-03-02 01:53:40 +00:00
|
|
|
bail!("PSBT is not finalized")
|
2021-02-01 23:39:34 +00:00
|
|
|
}
|
2021-03-02 01:53:40 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
let tx = signed_psbt.extract_tx();
|
2021-03-02 01:53:40 +00:00
|
|
|
|
2021-01-05 03:08:36 +00:00
|
|
|
Ok(tx)
|
|
|
|
}
|
2021-02-26 06:01:36 +00:00
|
|
|
|
2021-03-02 01:22:23 +00:00
|
|
|
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
|
|
|
self.get_tx(txid)
|
|
|
|
.await?
|
|
|
|
.ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid))
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 01:22:23 +00:00
|
|
|
pub async fn watch_for_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
2021-02-01 23:39:34 +00:00
|
|
|
tracing::debug!("watching for tx: {}", txid);
|
2021-02-16 04:19:11 +00:00
|
|
|
let tx = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
|
|
|
let client = Client::new(self.rpc_url.as_ref())
|
2021-02-17 00:32:41 +00:00
|
|
|
.map_err(|err| backoff::Error::Permanent(Error::ElectrumClient(err)))?;
|
|
|
|
|
|
|
|
let tx = client.transaction_get(&txid).map_err(|err| match err {
|
|
|
|
electrum_client::Error::Protocol(err) => {
|
|
|
|
tracing::debug!("Received protocol error {} from Electrum, retrying...", err);
|
|
|
|
backoff::Error::Transient(Error::NotYetMined)
|
|
|
|
}
|
|
|
|
err => backoff::Error::Permanent(Error::ElectrumClient(err)),
|
|
|
|
})?;
|
2021-02-16 04:19:11 +00:00
|
|
|
|
|
|
|
Result::<_, backoff::Error<Error>>::Ok(tx)
|
2021-01-15 05:58:16 +00:00
|
|
|
})
|
|
|
|
.await
|
2021-03-04 06:25:05 +00:00
|
|
|
.context("Transient errors should be retried")?;
|
2021-02-16 04:19:11 +00:00
|
|
|
|
|
|
|
Ok(tx)
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 01:22:23 +00:00
|
|
|
pub async fn get_block_height(&self) -> Result<BlockHeight> {
|
2021-03-05 05:45:50 +00:00
|
|
|
let url = make_blocks_tip_height_url(&self.http_url)?;
|
2021-01-15 05:58:16 +00:00
|
|
|
let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
2021-02-01 23:39:34 +00:00
|
|
|
let height = reqwest::Client::new()
|
2021-02-15 05:13:29 +00:00
|
|
|
.request(Method::GET, url.clone())
|
2021-02-01 23:39:34 +00:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.map_err(Error::Io)?
|
|
|
|
.text()
|
|
|
|
.await
|
|
|
|
.map_err(Error::Io)?
|
|
|
|
.parse::<u32>()
|
2021-02-15 05:13:29 +00:00
|
|
|
.map_err(|err| backoff::Error::Permanent(Error::Parse(err)))?;
|
2021-02-01 23:39:34 +00:00
|
|
|
Result::<_, backoff::Error<Error>>::Ok(height)
|
2021-01-15 05:58:16 +00:00
|
|
|
})
|
|
|
|
.await
|
2021-03-04 06:25:05 +00:00
|
|
|
.context("Transient errors should be retried")?;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-02-15 05:13:29 +00:00
|
|
|
Ok(BlockHeight::new(height))
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 01:22:23 +00:00
|
|
|
pub async fn transaction_block_height(&self, txid: Txid) -> Result<BlockHeight> {
|
2021-03-05 05:45:50 +00:00
|
|
|
let url = make_tx_status_url(&self.http_url, txid)?;
|
2021-02-01 23:39:34 +00:00
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
|
|
struct TransactionStatus {
|
|
|
|
block_height: Option<u32>,
|
|
|
|
confirmed: bool,
|
|
|
|
}
|
2021-01-15 05:58:16 +00:00
|
|
|
let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async {
|
2021-02-01 23:39:34 +00:00
|
|
|
let resp = reqwest::Client::new()
|
2021-02-15 05:13:29 +00:00
|
|
|
.request(Method::GET, url.clone())
|
2021-02-01 23:39:34 +00:00
|
|
|
.send()
|
2021-01-05 03:08:36 +00:00
|
|
|
.await
|
2021-02-01 23:39:34 +00:00
|
|
|
.map_err(|err| backoff::Error::Transient(Error::Io(err)))?;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
let tx_status: TransactionStatus = resp
|
|
|
|
.json()
|
|
|
|
.await
|
2021-02-17 00:09:09 +00:00
|
|
|
.map_err(|err| backoff::Error::Permanent(Error::JsonDeserialization(err)))?;
|
2021-02-01 23:39:34 +00:00
|
|
|
|
|
|
|
let block_height = tx_status
|
|
|
|
.block_height
|
|
|
|
.ok_or(backoff::Error::Transient(Error::NotYetMined))?;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
|
|
|
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
|
|
|
})
|
|
|
|
.await
|
2021-03-04 06:25:05 +00:00
|
|
|
.context("Transient errors should be retried")?;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-02-15 05:13:29 +00:00
|
|
|
Ok(BlockHeight::new(height))
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 01:22:23 +00:00
|
|
|
pub async fn wait_for_transaction_finality(
|
2021-01-27 02:33:32 +00:00
|
|
|
&self,
|
|
|
|
txid: Txid,
|
|
|
|
execution_params: ExecutionParams,
|
|
|
|
) -> Result<()> {
|
2021-03-05 05:07:13 +00:00
|
|
|
let conf_target = execution_params.bitcoin_finality_confirmations;
|
|
|
|
|
|
|
|
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" });
|
|
|
|
|
2021-01-05 03:08:36 +00:00
|
|
|
// Divide by 4 to not check too often yet still be aware of the new block early
|
|
|
|
// on.
|
2021-01-27 02:33:32 +00:00
|
|
|
let mut interval = interval(execution_params.bitcoin_avg_block_time / 4);
|
2021-01-05 03:08:36 +00:00
|
|
|
|
|
|
|
loop {
|
2021-02-15 05:13:29 +00:00
|
|
|
let tx_block_height = self.transaction_block_height(txid).await?;
|
2021-02-01 23:39:34 +00:00
|
|
|
tracing::debug!("tx_block_height: {:?}", tx_block_height);
|
2021-03-05 05:07:13 +00:00
|
|
|
|
2021-02-15 05:13:29 +00:00
|
|
|
let block_height = self.get_block_height().await?;
|
2021-02-01 23:39:34 +00:00
|
|
|
tracing::debug!("latest_block_height: {:?}", block_height);
|
2021-03-05 05:07:13 +00:00
|
|
|
|
2021-02-19 06:09:53 +00:00
|
|
|
if let Some(confirmations) = block_height.checked_sub(
|
|
|
|
tx_block_height
|
|
|
|
.checked_sub(BlockHeight::new(1))
|
|
|
|
.expect("transaction must be included in block with height >= 1"),
|
|
|
|
) {
|
2021-03-05 05:07:13 +00:00
|
|
|
tracing::debug!(%txid, "confirmations: {:?}", confirmations);
|
|
|
|
if u32::from(confirmations) >= conf_target {
|
2021-01-05 03:08:36 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
interval.tick().await;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-03-02 01:22:23 +00:00
|
|
|
|
|
|
|
/// Selects an appropriate [`FeeRate`] to be used for getting transactions
|
|
|
|
/// confirmed within a reasonable amount of time.
|
|
|
|
fn select_feerate(&self) -> FeeRate {
|
|
|
|
// TODO: This should obviously not be a const :)
|
|
|
|
FeeRate::from_sat_per_vb(5.0)
|
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-05 05:45:50 +00:00
|
|
|
fn make_tx_status_url(base_url: &Url, txid: Txid) -> Result<Url> {
|
2021-02-16 00:48:46 +00:00
|
|
|
let url = base_url.join(&format!("tx/{}/status", txid))?;
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-02-16 00:48:46 +00:00
|
|
|
Ok(url)
|
|
|
|
}
|
|
|
|
|
2021-03-05 05:45:50 +00:00
|
|
|
fn make_blocks_tip_height_url(base_url: &Url) -> Result<Url> {
|
2021-02-16 00:48:46 +00:00
|
|
|
let url = base_url.join("blocks/tip/height")?;
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-02-16 00:48:46 +00:00
|
|
|
Ok(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-03-05 05:45:50 +00:00
|
|
|
use super::*;
|
2021-03-04 00:28:58 +00:00
|
|
|
use crate::cli::config::DEFAULT_ELECTRUM_HTTP_URL;
|
2021-02-16 00:48:46 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn create_tx_status_url_from_default_base_url_success() {
|
2021-03-05 05:45:50 +00:00
|
|
|
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
|
|
|
|
let txid = Txid::default;
|
|
|
|
|
|
|
|
let url = make_tx_status_url(&base_url, txid()).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(url.as_str(), "https://blockstream.info/testnet/api/tx/0000000000000000000000000000000000000000000000000000000000000000/status");
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn create_block_tip_height_url_from_default_base_url_success() {
|
2021-03-05 05:45:50 +00:00
|
|
|
let base_url = DEFAULT_ELECTRUM_HTTP_URL.parse().unwrap();
|
|
|
|
|
|
|
|
let url = make_blocks_tip_height_url(&base_url).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
url.as_str(),
|
|
|
|
"https://blockstream.info/testnet/api/blocks/tip/height"
|
|
|
|
);
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|
|
|
|
}
|