Skip to content

Commit 9eef99f

Browse files
committed
wip(feat): add new AsyncAnonymizedClient using arti-hyper
- feat: add new async client, `AsyncAnonymizedClient`, that uses `arti-hyper`, and `arti-client` to connect and do requests over the Tor network. - feat+test: add all methods and tests for `get_tx_..`, `Transaction` related endpoints. - wip(feat+test): add all methods and tests for `get_block_...`, all `Block` related endpoints
1 parent ef1925e commit 9eef99f

File tree

3 files changed

+430
-7
lines changed

3 files changed

+430
-7
lines changed

Cargo.toml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@ path = "src/lib.rs"
1717

1818
[dependencies]
1919
serde = { version = "1.0", features = ["derive"] }
20+
serde_json = { version = "1.0" }
2021
bitcoin = { version = "0.30.0", features = ["serde", "std"], default-features = false }
2122
# Temporary dependency on internals until the rust-bitcoin devs release the hex-conservative crate.
2223
bitcoin-internals = { version = "0.1.0", features = ["alloc"] }
2324
log = "^0.4"
24-
ureq = { version = "2.5.0", features = ["json"], optional = true }
25+
ureq = { version = "2.5.0", optional = true, features = ["json"]}
2526
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
27+
hyper = { version = "0.14", optional = true, features = ["http1", "client", "runtime"], default-features = false }
28+
arti-client = { version = "0.12.0", optional = true }
29+
tor-rtcompat = { version = "0.9.6", optional = true, features = ["tokio"]}
30+
tls-api = { version = "0.9.0", optional = true }
31+
tls-api-native-tls = { version = "0.9.0", optional = true }
32+
arti-hyper = { version = "0.12.0", optional = true, features = ["default"] }
33+
34+
[target.'cfg(target_vendor="apple")'.dependencies]
35+
tls-api-openssl = { version = "0.9.0", optional = true }
2636

2737
[dev-dependencies]
28-
serde_json = "1.0"
2938
tokio = { version = "1.20.1", features = ["full"] }
3039
electrsd = { version = "0.24.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_22_0"] }
3140
electrum-client = "0.16.0"
@@ -36,10 +45,14 @@ zip = "=0.6.3"
3645
base64ct = "<1.6.0"
3746

3847
[features]
39-
default = ["blocking", "async", "async-https"]
48+
default = ["blocking", "async", "async-https", "async-arti-hyper"]
4049
blocking = ["ureq", "ureq/socks-proxy"]
4150
async = ["reqwest", "reqwest/socks"]
4251
async-https = ["async", "reqwest/default-tls"]
4352
async-https-native = ["async", "reqwest/native-tls"]
4453
async-https-rustls = ["async", "reqwest/rustls-tls"]
4554
async-https-rustls-manual-roots = ["async", "reqwest/rustls-tls-manual-roots"]
55+
# TODO: (@leonardo) Should I rename it to async-anonymized ?
56+
async-arti-hyper = ["hyper", "arti-client", "tor-rtcompat", "tls-api", "tls-api-native-tls", "tls-api-openssl", "arti-hyper"]
57+
async-arti-hyper-native = ["async-arti-hyper", "arti-hyper/native-tls"]
58+
async-arti-hyper-rustls = ["async-arti-hyper", "arti-hyper/rustls"]

src/async.rs

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
// You may not use this file except in accordance with one or both of these
1010
// licenses.
1111

12-
//! Esplora by way of `reqwest` HTTP client.
12+
//! Esplora by way of `reqwest`, and `arti-hyper` HTTP client.
1313
1414
use std::collections::HashMap;
1515
use std::str::FromStr;
1616

17+
use arti_client::{TorClient, TorClientConfig};
18+
19+
use arti_hyper::ArtiHttpConnector;
1720
use bitcoin::consensus::{deserialize, serialize};
1821
use bitcoin::hashes::hex::FromHex;
1922
use bitcoin::hashes::{sha256, Hash};
@@ -22,10 +25,17 @@ use bitcoin::{
2225
};
2326
use bitcoin_internals::hex::display::DisplayHex;
2427

28+
use hyper::{Body, Response, Uri};
2529
#[allow(unused_imports)]
2630
use log::{debug, error, info, trace};
2731

2832
use reqwest::{Client, StatusCode};
33+
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
34+
#[cfg(not(target_vendor = "apple"))]
35+
use tls_api_native_tls::TlsConnector;
36+
#[cfg(target_vendor = "apple")]
37+
use tls_api_openssl::TlsConnector;
38+
use tor_rtcompat::PreferredRuntime;
2939

3040
use crate::{BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus};
3141

@@ -429,3 +439,204 @@ impl AsyncClient {
429439
&self.client
430440
}
431441
}
442+
443+
#[derive(Debug, Clone)]
444+
pub struct AsyncAnonymizedClient {
445+
url: String,
446+
client: hyper::Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>>,
447+
}
448+
449+
impl AsyncAnonymizedClient {
450+
/// build an async [`TorClient`] with default Tor configuration
451+
async fn create_tor_client() -> Result<TorClient<PreferredRuntime>, arti_client::Error> {
452+
let config = TorClientConfig::default();
453+
TorClient::create_bootstrapped(config).await
454+
}
455+
456+
/// build an [`AsyncAnonymizedClient`] from a [`Builder`]
457+
pub async fn from_builder(builder: Builder) -> Result<Self, Error> {
458+
let tor_client = Self::create_tor_client().await?.isolated_client();
459+
460+
let tls_conn: TlsConnector = TlsConnector::builder()
461+
.map_err(|_| Error::TlsConnector)?
462+
.build()
463+
.map_err(|_| Error::TlsConnector)?;
464+
465+
let connector = ArtiHttpConnector::new(tor_client, tls_conn);
466+
467+
// TODO: (@leonardo) how to handle/pass the timeout option ?
468+
let client = hyper::Client::builder().build::<_, Body>(connector);
469+
Ok(Self::from_client(builder.base_url, client))
470+
}
471+
472+
/// build an async client from the base url and [`Client`]
473+
pub fn from_client(
474+
url: String,
475+
client: hyper::Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>>,
476+
) -> Self {
477+
AsyncAnonymizedClient { url, client }
478+
}
479+
480+
/// Get a [`Option<Transaction>`] given its [`Txid`]
481+
pub async fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
482+
let path = format!("{}/tx/{}/raw", self.url, txid);
483+
let uri = Uri::from_str(&path).map_err(|_| Error::InvalidUri)?;
484+
485+
let resp = self.client.get(uri).await?;
486+
487+
if let StatusCode::NOT_FOUND = resp.status() {
488+
return Ok(None);
489+
}
490+
491+
if resp.status().is_server_error() || resp.status().is_client_error() {
492+
Err(Error::HttpResponse {
493+
status: resp.status().as_u16(),
494+
message: Self::text(resp).await?,
495+
})
496+
} else {
497+
let body = resp.into_body();
498+
let bytes = hyper::body::to_bytes(body).await?;
499+
Ok(Some(deserialize(&bytes)?))
500+
}
501+
}
502+
503+
/// Get a [`Transaction`] given its [`Txid`].
504+
pub async fn get_tx_no_opt(&self, txid: &Txid) -> Result<Transaction, Error> {
505+
match self.get_tx(txid).await {
506+
Ok(Some(tx)) => Ok(tx),
507+
Ok(None) => Err(Error::TransactionNotFound(*txid)),
508+
Err(e) => Err(e),
509+
}
510+
}
511+
512+
/// Get a [`Txid`] of a transaction given its index in a block with a given hash.
513+
pub async fn get_txid_at_block_index(
514+
&self,
515+
block_hash: &BlockHash,
516+
index: usize,
517+
) -> Result<Option<Txid>, Error> {
518+
let path = format!("{}/block/{}/txid/{}", self.url, block_hash, index);
519+
let uri = Uri::from_str(&path).map_err(|_| Error::InvalidUri)?;
520+
521+
let resp = self.client.get(uri).await?;
522+
523+
if let StatusCode::NOT_FOUND = resp.status() {
524+
return Ok(None);
525+
}
526+
527+
if resp.status().is_server_error() || resp.status().is_client_error() {
528+
Err(Error::HttpResponse {
529+
status: resp.status().as_u16(),
530+
message: Self::text(resp).await?,
531+
})
532+
} else {
533+
let text = Self::text(resp).await?;
534+
let txid = Txid::from_str(&text)?;
535+
Ok(Some(txid))
536+
}
537+
}
538+
539+
/// Get the status of a [`Transaction`] given its [`Txid`].
540+
pub async fn get_tx_status(&self, txid: &Txid) -> Result<TxStatus, Error> {
541+
let path = format!("{}/tx/{}/status", self.url, txid);
542+
let uri = Uri::from_str(&path).map_err(|_| Error::InvalidUri)?;
543+
544+
let resp = self.client.get(uri).await?;
545+
546+
if resp.status().is_server_error() || resp.status().is_client_error() {
547+
Err(Error::HttpResponse {
548+
status: resp.status().as_u16(),
549+
message: Self::text(resp).await?,
550+
})
551+
} else {
552+
let body = resp.into_body();
553+
let bytes = hyper::body::to_bytes(body).await?;
554+
let tx_status =
555+
serde_json::from_slice::<TxStatus>(&bytes).map_err(|_| Error::ResponseDecoding)?;
556+
Ok(tx_status)
557+
}
558+
}
559+
560+
/// Get a [`BlockHeader`] given a particular block hash.
561+
pub async fn get_header_by_hash(&self, block_hash: &BlockHash) -> Result<BlockHeader, Error> {
562+
let path = format!("{}/block/{}/header", self.url, block_hash);
563+
let uri = Uri::from_str(&path).map_err(|_| Error::InvalidUri)?;
564+
565+
let resp = self.client.get(uri).await?;
566+
567+
if resp.status().is_server_error() || resp.status().is_client_error() {
568+
Err(Error::HttpResponse {
569+
status: resp.status().as_u16(),
570+
message: Self::text(resp).await?,
571+
})
572+
} else {
573+
let text = Self::text(resp).await?;
574+
let block_header = deserialize(&Vec::from_hex(&text)?)?;
575+
Ok(block_header)
576+
}
577+
}
578+
579+
/// Get the [`BlockStatus`] given a particular [`BlockHash`].
580+
pub async fn get_block_status(&self, block_hash: &BlockHash) -> Result<BlockStatus, Error> {
581+
let path = &format!("{}/block/{}/status", self.url, block_hash);
582+
let uri = Uri::from_str(path).map_err(|_| Error::InvalidUri)?;
583+
let resp = self.client.get(uri).await?;
584+
585+
if resp.status().is_server_error() || resp.status().is_client_error() {
586+
Err(Error::HttpResponse {
587+
status: resp.status().as_u16(),
588+
message: Self::text(resp).await?,
589+
})
590+
} else {
591+
let body = resp.into_body();
592+
let bytes = hyper::body::to_bytes(body).await?;
593+
594+
let block_status = serde_json::from_slice::<BlockStatus>(&bytes)
595+
.map_err(|_| Error::ResponseDecoding)?;
596+
Ok(block_status)
597+
}
598+
}
599+
600+
/// Get a [`Block`] given a particular [`BlockHash`].
601+
pub async fn get_block_by_hash(&self, block_hash: &BlockHash) -> Result<Option<Block>, Error> {
602+
let path = format!("{}/block/{}/raw", self.url, block_hash);
603+
let uri = Uri::from_str(&path).map_err(|_| Error::InvalidUri)?;
604+
let resp = self.client.get(uri).await?;
605+
606+
if let StatusCode::NOT_FOUND = resp.status() {
607+
return Ok(None);
608+
}
609+
610+
if resp.status().is_server_error() || resp.status().is_client_error() {
611+
Err(Error::HttpResponse {
612+
status: resp.status().as_u16(),
613+
message: Self::text(resp).await?,
614+
})
615+
} else {
616+
let body = resp.into_body();
617+
let bytes = hyper::body::to_bytes(body).await?;
618+
Ok(Some(deserialize(&bytes)?))
619+
}
620+
}
621+
622+
/// Get the underlying base URL.
623+
pub fn url(&self) -> &str {
624+
&self.url
625+
}
626+
627+
/// Get the underlying [`hyper::Client`].
628+
pub fn client(&self) -> &hyper::Client<ArtiHttpConnector<PreferredRuntime, TlsConnector>> {
629+
&self.client
630+
}
631+
632+
/// Get the given [`Response<Body>`] as [`String`].
633+
async fn text(response: Response<Body>) -> Result<String, Error> {
634+
let body = response.into_body();
635+
let bytes = hyper::body::to_bytes(body).await?;
636+
637+
match std::str::from_utf8(&bytes) {
638+
Ok(text) => Ok(text.to_string()),
639+
Err(_) => Err(Error::ResponseDecoding),
640+
}
641+
}
642+
}

0 commit comments

Comments
 (0)