-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
660 additions
and
639 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
use crate::lib_wallet::{FeeEstimatesResponse, Utxo}; | ||
use crate::ESPLORA_API_URL; | ||
use anyhow::{anyhow, bail, Context, Result}; | ||
use async_trait::async_trait; | ||
use elements::encode::{deserialize, serialize_hex}; | ||
use elements::{Address, Transaction, TxOut, Txid}; | ||
use futures::stream::FuturesUnordered; | ||
use futures::{StreamExt, TryStreamExt}; | ||
use reqwest::StatusCode; | ||
use wasm_bindgen::UnwrapThrowExt; | ||
|
||
pub async fn get_txouts<T: Send, FM: Fn(Utxo, TxOut) -> Result<Option<T>> + Copy + Send>( | ||
address: Address, | ||
filter_map: FM, | ||
) -> Result<Vec<T>> { | ||
let utxos = fetch_utxos(address).await?; | ||
|
||
let txouts = utxos | ||
.into_iter() | ||
.map(move |utxo| async move { | ||
let mut tx = fetch_transaction(utxo.txid).await?; | ||
let txout = tx.output.remove(utxo.vout as usize); | ||
|
||
filter_map(utxo, txout) | ||
}) | ||
.collect::<FuturesUnordered<_>>() | ||
.filter_map(|r| std::future::ready(r.transpose())) | ||
.try_collect::<Vec<_>>() | ||
.await?; | ||
|
||
Ok(txouts) | ||
} | ||
|
||
/// Fetches a transaction. | ||
/// | ||
/// This function makes use of the browsers local storage to avoid spamming the underlying source. | ||
/// Transaction never change after they've been mined, hence we can cache those indefinitely. | ||
pub async fn fetch_transaction(txid: Txid) -> Result<Transaction> { | ||
let esplora_url = { | ||
let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); | ||
guard.clone() | ||
}; | ||
|
||
//todo!(consider caching) | ||
|
||
let client = reqwest::Client::new(); | ||
let body = client | ||
.get(format!("{}tx/{}/hex", esplora_url, txid)) | ||
.send() | ||
.await?; | ||
let body_text = body | ||
.text() | ||
.await | ||
.with_context(|| "response is not a string")?; | ||
|
||
Ok(deserialize(&hex::decode(body_text)?)?) | ||
} | ||
|
||
/// Fetch the UTXOs of an address. | ||
/// | ||
/// UTXOs change over time and as such, this function never uses a cache. | ||
async fn fetch_utxos(address: Address) -> Result<Vec<Utxo>> { | ||
let esplora_url = { | ||
let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); | ||
guard.clone() | ||
}; | ||
|
||
let path = format!("address/{}/utxo", address); | ||
let esplora_url = esplora_url.join(path.as_str())?; | ||
let response = reqwest::get(esplora_url.clone()) | ||
.await | ||
.context("failed to fetch UTXOs")?; | ||
|
||
if response.status() == StatusCode::NOT_FOUND { | ||
log::debug!( | ||
"GET {} returned 404, defaulting to empty UTXO set", | ||
esplora_url | ||
); | ||
|
||
return Ok(Vec::new()); | ||
} | ||
|
||
if !response.status().is_success() { | ||
let error_body = response.text().await?; | ||
return Err(anyhow!( | ||
"failed to fetch utxos, esplora returned '{}'", | ||
error_body | ||
)); | ||
} | ||
|
||
response | ||
.json::<Vec<Utxo>>() | ||
.await | ||
.context("failed to deserialize response") | ||
} | ||
|
||
async fn get_fee_estimates() -> Result<FeeEstimatesResponse> { | ||
let esplora_url = { | ||
let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); | ||
guard.clone() | ||
}; | ||
let esplora_url = esplora_url.join("fee-estimates")?; | ||
|
||
let fee_estimates = reqwest::get(esplora_url.clone()) | ||
.await | ||
.with_context(|| format!("failed to GET {}", esplora_url))? | ||
.json() | ||
.await | ||
.context("failed to deserialize fee estimates")?; | ||
|
||
Ok(fee_estimates) | ||
} | ||
|
||
/// Fetch transaction history for the specified address. | ||
/// | ||
/// Returns up to 50 mempool transactions plus the first 25 confirmed | ||
/// transactions. See | ||
/// https://github.com/blockstream/esplora/blob/master/API.md#get-addressaddresstxs | ||
/// for more information. | ||
async fn fetch_transaction_history(address: &Address) -> Result<Vec<Txid>> { | ||
let esplora_url = { | ||
let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); | ||
guard.clone() | ||
}; | ||
let path = format!("address/{}/txs", address); | ||
let url = esplora_url.join(path.as_str())?; | ||
let response = reqwest::get(url.clone()) | ||
.await | ||
.context("failed to fetch transaction history")?; | ||
|
||
if !response.status().is_success() { | ||
let error_body = response.text().await?; | ||
return Err(anyhow!( | ||
"failed to fetch transaction history, esplora returned '{}' from '{}'", | ||
error_body, | ||
url | ||
)); | ||
} | ||
|
||
#[derive(serde::Deserialize)] | ||
struct HistoryElement { | ||
txid: Txid, | ||
} | ||
|
||
let response = response | ||
.json::<Vec<HistoryElement>>() | ||
.await | ||
.context("failed to deserialize response")?; | ||
|
||
Ok(response.iter().map(|elem| elem.txid).collect()) | ||
} | ||
|
||
pub async fn broadcast(tx: Transaction) -> Result<Txid> { | ||
let esplora_url = { | ||
let guard = ESPLORA_API_URL.lock().expect_throw("can get lock"); | ||
guard.clone() | ||
}; | ||
let esplora_url = esplora_url.join("tx")?; | ||
let client = reqwest::Client::new(); | ||
|
||
let response = client | ||
.post(esplora_url.clone()) | ||
.body(serialize_hex(&tx)) | ||
.send() | ||
.await?; | ||
|
||
let code = response.status(); | ||
|
||
if !code.is_success() { | ||
bail!("failed to successfully publish transaction"); | ||
} | ||
|
||
let txid = response | ||
.text() | ||
.await? | ||
.parse() | ||
.context("failed to parse response body as txid")?; | ||
|
||
Ok(txid) | ||
} |
Oops, something went wrong.