Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F12944891
D17335.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
31 KB
Subscribers
None
D17335.id.diff
View Options
diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -631,6 +631,23 @@
"hex-conservative",
]
+[[package]]
+name = "bitcoinsuite-chronik-client"
+version = "0.1.0"
+dependencies = [
+ "abc-rust-error",
+ "bitcoinsuite-core 0.1.0",
+ "bytes",
+ "chronik-proto",
+ "hex",
+ "pretty_assertions",
+ "prost",
+ "prost-build",
+ "reqwest",
+ "thiserror 1.0.69",
+ "tokio",
+]
+
[[package]]
name = "bitcoinsuite-chronik-client"
version = "0.1.0"
@@ -1412,7 +1429,7 @@
version = "0.1.0"
dependencies = [
"axum 0.5.17",
- "bitcoinsuite-chronik-client",
+ "bitcoinsuite-chronik-client 0.1.0 (git+https://github.com/LogosFoundation/bitcoinsuite?rev=6d2d946)",
"bitcoinsuite-error",
"explorer-server",
"futures",
@@ -1427,7 +1444,7 @@
"axum 0.5.17",
"base64 0.13.1",
"bitcoin",
- "bitcoinsuite-chronik-client",
+ "bitcoinsuite-chronik-client 0.1.0 (git+https://github.com/LogosFoundation/bitcoinsuite?rev=6d2d946)",
"bitcoinsuite-core 0.1.0 (git+https://github.com/LogosFoundation/bitcoinsuite?rev=6d2d946)",
"bitcoinsuite-error",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@
"chronik/chronik-plugin-impl",
"chronik/chronik-proto",
"chronik/chronik-util",
+ "modules/bitcoinsuite-chronik-client",
"modules/ecash-lib-wasm",
"modules/ecash-secp256k1",
"modules/ecash-secp256k1/ecash-secp256k1-sys",
diff --git a/contrib/teamcity/build-configurations.yml b/contrib/teamcity/build-configurations.yml
--- a/contrib/teamcity/build-configurations.yml
+++ b/contrib/teamcity/build-configurations.yml
@@ -350,6 +350,17 @@
"Bitcoin ABC Benchmark" \
"${BUILD_DIR}/src/bench/BitcoinABC_Bench.json"
+ build-bitcoinsuite-chronik-client:
+ runOnDiffRegex:
+ - modules/bitcoinsuite-chronik-client/
+ script: |
+ # Navigate to the bitcoinsuite-chronik-client directory
+ pushd "${TOPLEVEL}/modules/bitcoinsuite-chronik-client"
+ cargo build
+ cargo test
+ popd
+ timeout: 1200
+
build-chronik:
runOnDiffRegex:
- chronik/
diff --git a/modules/bitcoinsuite-chronik-client/Cargo.toml b/modules/bitcoinsuite-chronik-client/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/modules/bitcoinsuite-chronik-client/Cargo.toml
@@ -0,0 +1,38 @@
+# Copyright (c) 2024 The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+[package]
+name = "bitcoinsuite-chronik-client"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+abc-rust-error = { path = "../../chronik/abc-rust-error"}
+bitcoinsuite-core = { path = "../../chronik/bitcoinsuite-core" }
+chronik-proto = { path = "../../chronik/chronik-proto/"}
+
+# Error structs/enums
+thiserror = "1.0"
+
+# HTTP client
+reqwest = "0.11"
+
+# Async runtime and scheduler
+tokio = { version = "1.14", features = ["full"] }
+
+# Protobuf (de)serialization
+prost = "0.11"
+
+# Hex en-/decoding
+hex = "0.4"
+
+bytes = { version = "1.4", features = ["serde"] }
+
+[build-dependencies]
+# Build Protobuf structs
+prost-build = "0.11"
+
+[dev-dependencies]
+# Colorful diffs for assertions
+pretty_assertions = "1.0"
diff --git a/modules/bitcoinsuite-chronik-client/src/lib.rs b/modules/bitcoinsuite-chronik-client/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/modules/bitcoinsuite-chronik-client/src/lib.rs
@@ -0,0 +1,460 @@
+// Copyright (c) 2024 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+use std::fmt::Display;
+
+use abc_rust_error::{Result, WrapErr};
+use bitcoinsuite_core::hash::{Hashed, Sha256d};
+use bytes::Bytes;
+pub use chronik_proto::proto;
+use reqwest::{header::CONTENT_TYPE, StatusCode};
+use thiserror::Error;
+
+use crate::ChronikClientError::*;
+
+#[derive(Debug, Clone)]
+pub struct ChronikClient {
+ http_url: String,
+ ws_url: String,
+ client: reqwest::Client,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum ScriptType {
+ Other,
+ P2pk,
+ P2pkh,
+ P2sh,
+}
+
+#[derive(Debug, Clone)]
+pub struct ScriptEndpoint<'payload, 'client> {
+ script_type: ScriptType,
+ script_payload: &'payload [u8],
+ client: &'client ChronikClient,
+}
+
+#[derive(Debug, Clone)]
+pub struct PluginEndpoint<'payload, 'client> {
+ plugin_name: &'payload str,
+ payload: &'payload [u8],
+ client: &'client ChronikClient,
+}
+
+#[derive(Debug, Error, PartialEq)]
+pub enum ChronikClientError {
+ #[error("`url` cannot end with '/', got: {0}")]
+ CannotHaveTrailingSlashInUrl(String),
+
+ #[error("`url` must start with 'https://' or 'http://', got: {0}")]
+ InvalidUrlSchema(String),
+
+ #[error("HTTP request error")]
+ HttpRequestError,
+
+ #[error("Unexpected text message: {0}")]
+ UnexpectedWsTextMessage(String),
+
+ #[error("Chronik error ({status_code}): {error_msg}")]
+ ChronikError {
+ status_code: StatusCode,
+ error: proto::Error,
+ error_msg: String,
+ },
+
+ #[error("Invalid protobuf: {0}")]
+ InvalidProtobuf(String),
+}
+
+impl ChronikClient {
+ pub fn new(url: String) -> Result<Self> {
+ if url.ends_with('/') {
+ return Err(CannotHaveTrailingSlashInUrl(url).into());
+ }
+ let ws_url = if url.starts_with("https://") {
+ "wss://".to_string() + url.strip_prefix("https://").unwrap()
+ } else if url.starts_with("http://") {
+ "ws://".to_string() + url.strip_prefix("http://").unwrap()
+ } else {
+ return Err(InvalidUrlSchema(url).into());
+ };
+ let ws_url = ws_url + "/ws";
+ Ok(ChronikClient {
+ http_url: url,
+ ws_url,
+ client: reqwest::Client::new(),
+ })
+ }
+
+ pub fn ws_url(&self) -> &str {
+ &self.ws_url
+ }
+
+ pub async fn broadcast_tx(
+ &self,
+ raw_tx: Vec<u8>,
+ ) -> Result<proto::BroadcastTxResponse> {
+ let request = proto::BroadcastTxRequest {
+ raw_tx,
+ skip_token_checks: false,
+ };
+ self._post("/broadcast-tx", &request).await
+ }
+
+ pub async fn broadcast_txs(
+ &self,
+ raw_txs: Vec<Vec<u8>>,
+ ) -> Result<proto::BroadcastTxsResponse> {
+ let request = proto::BroadcastTxsRequest {
+ raw_txs,
+ skip_token_checks: false,
+ };
+ self._post("/broadcast-txs", &request).await
+ }
+
+ pub async fn blockchain_info(&self) -> Result<proto::BlockchainInfo> {
+ self._get("/blockchain-info").await
+ }
+
+ pub async fn block_by_height(&self, height: i32) -> Result<proto::Block> {
+ self._get(&format!("/block/{}", height)).await
+ }
+
+ pub async fn block_by_hash(&self, hash: &Sha256d) -> Result<proto::Block> {
+ self._get(&format!("/block/{}", hash.hex_be())).await
+ }
+
+ pub async fn blocks(
+ &self,
+ start_height: i32,
+ end_height: i32,
+ ) -> Result<Vec<proto::BlockInfo>> {
+ let blocks: proto::Blocks = self
+ ._get(&format!("/blocks/{}/{}", start_height, end_height))
+ .await?;
+ Ok(blocks.blocks)
+ }
+
+ pub async fn tx(&self, txid: &Sha256d) -> Result<proto::Tx> {
+ self._get(&format!("/tx/{}", txid.hex_be())).await
+ }
+
+ pub async fn raw_tx(&self, txid: &Sha256d) -> Result<Bytes> {
+ Ok(self
+ ._get::<proto::RawTx>(&format!("/raw-tx/{}", txid.hex_be()))
+ .await?
+ .raw_tx
+ .into())
+ }
+
+ pub async fn block_txs_by_height(
+ &self,
+ height: i32,
+ page: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self._get(&format!("/block-txs/{}?page={}", height, page))
+ .await
+ }
+
+ pub async fn block_txs_by_height_with_page_size(
+ &self,
+ height: i32,
+ page: usize,
+ page_size: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self._get(&format!(
+ "/block-txs/{}?page={}&page_size={}",
+ height, page, page_size
+ ))
+ .await
+ }
+
+ pub async fn block_txs_by_hash(
+ &self,
+ hash: &Sha256d,
+ page: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self._get(&format!("/block-txs/{}?page={}", hash.hex_be(), page))
+ .await
+ }
+
+ pub async fn block_txs_by_hash_with_page_size(
+ &self,
+ hash: &Sha256d,
+ page: usize,
+ page_size: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self._get(&format!(
+ "/block-txs/{}?page={}&page_size={}",
+ hash.hex_be(),
+ page,
+ page_size
+ ))
+ .await
+ }
+
+ pub async fn validate_tx(&self, raw_tx: Vec<u8>) -> Result<proto::Tx> {
+ self._post("/validate-tx", &proto::RawTx { raw_tx }).await
+ }
+
+ pub async fn token(&self, token_id: &Sha256d) -> Result<proto::TokenInfo> {
+ self._get(&format!("/token/{}", token_id.hex_be())).await
+ }
+
+ pub fn script<'payload, 'client>(
+ &'client self,
+ script_type: ScriptType,
+ script_payload: &'payload [u8],
+ ) -> ScriptEndpoint<'payload, 'client> {
+ ScriptEndpoint {
+ script_type,
+ script_payload,
+ client: self,
+ }
+ }
+
+ pub fn plugin<'payload, 'client>(
+ &'client self,
+ plugin_name: &'payload str,
+ payload: &'payload [u8],
+ ) -> PluginEndpoint<'payload, 'client> {
+ PluginEndpoint {
+ plugin_name,
+ payload,
+ client: self,
+ }
+ }
+
+ async fn _post<
+ MRequest: prost::Message,
+ MResponse: prost::Message + Default,
+ >(
+ &self,
+ url_suffix: &str,
+ request: &MRequest,
+ ) -> Result<MResponse> {
+ let response = self
+ .client
+ .post(format!("{}{}", self.http_url, url_suffix))
+ .header(CONTENT_TYPE, "application/x-protobuf")
+ .body(request.encode_to_vec())
+ .send()
+ .await
+ .wrap_err(HttpRequestError)?;
+ Self::_handle_response(response).await
+ }
+
+ async fn _get<MResponse: prost::Message + Default>(
+ &self,
+ url_suffix: &str,
+ ) -> Result<MResponse> {
+ let response = self
+ .client
+ .get(format!("{}{}", self.http_url, url_suffix))
+ .header(CONTENT_TYPE, "application/x-protobuf")
+ .send()
+ .await
+ .wrap_err(HttpRequestError)?;
+ Self::_handle_response(response).await
+ }
+
+ async fn _handle_response<MResponse: prost::Message + Default>(
+ response: reqwest::Response,
+ ) -> Result<MResponse> {
+ use prost::Message as _;
+ let status_code = response.status();
+ if status_code != StatusCode::OK {
+ let data = response.bytes().await?;
+ let error = proto::Error::decode(data.as_ref())
+ .wrap_err_with(|| InvalidProtobuf(hex::encode(&data)))?;
+ return Err(ChronikError {
+ status_code,
+ error_msg: error.msg.clone(),
+ error,
+ }
+ .into());
+ }
+ let bytes = response.bytes().await.wrap_err(HttpRequestError)?;
+ let response = MResponse::decode(bytes.as_ref())
+ .wrap_err_with(|| InvalidProtobuf(hex::encode(&bytes)))?;
+ Ok(response)
+ }
+}
+
+impl ScriptEndpoint<'_, '_> {
+ pub async fn history_with_page_size(
+ &self,
+ page: usize,
+ page_size: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/script/{}/{}/history?page={}&page_size={}",
+ self.script_type,
+ hex::encode(self.script_payload),
+ page,
+ page_size,
+ ))
+ .await
+ }
+
+ pub async fn history(&self, page: usize) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/script/{}/{}/history?page={}",
+ self.script_type,
+ hex::encode(self.script_payload),
+ page,
+ ))
+ .await
+ }
+
+ pub async fn confirmed_txs(
+ &self,
+ page: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/script/{}/{}/confirmed-txs?page={}",
+ self.script_type,
+ hex::encode(self.script_payload),
+ page,
+ ))
+ .await
+ }
+
+ pub async fn unconfirmed_txs(
+ &self,
+ page: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/script/{}/{}/unconfirmed-txs?page={}",
+ self.script_type,
+ hex::encode(self.script_payload),
+ page,
+ ))
+ .await
+ }
+
+ pub async fn utxos(&self) -> Result<Vec<proto::ScriptUtxo>> {
+ let utxos = self
+ .client
+ ._get::<proto::ScriptUtxos>(&format!(
+ "/script/{}/{}/utxos",
+ self.script_type,
+ hex::encode(self.script_payload),
+ ))
+ .await?;
+ Ok(utxos.utxos)
+ }
+}
+
+impl PluginEndpoint<'_, '_> {
+ pub async fn history_with_page_size(
+ &self,
+ page: usize,
+ page_size: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/plugin/{}/{}/history?page={}&page_size={}",
+ self.plugin_name,
+ hex::encode(self.payload),
+ page,
+ page_size,
+ ))
+ .await
+ }
+
+ pub async fn history(&self, page: usize) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/plugin/{}/{}/history?page={}",
+ self.plugin_name,
+ hex::encode(self.payload),
+ page,
+ ))
+ .await
+ }
+
+ pub async fn confirmed_txs(
+ &self,
+ page: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/plugin/{}/{}/confirmed-txs?page={}",
+ self.plugin_name,
+ hex::encode(self.payload),
+ page,
+ ))
+ .await
+ }
+
+ pub async fn unconfirmed_txs(
+ &self,
+ page: usize,
+ ) -> Result<proto::TxHistoryPage> {
+ self.client
+ ._get(&format!(
+ "/plugin/{}/{}/unconfirmed-txs?page={}",
+ self.plugin_name,
+ hex::encode(self.payload),
+ page,
+ ))
+ .await
+ }
+
+ pub async fn utxos(&self) -> Result<Vec<proto::ScriptUtxo>> {
+ let utxos = self
+ .client
+ ._get::<proto::ScriptUtxos>(&format!(
+ "/plugin/{}/{}/utxos",
+ self.plugin_name,
+ hex::encode(self.payload),
+ ))
+ .await?;
+ Ok(utxos.utxos)
+ }
+}
+
+impl Display for ScriptType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let text = match self {
+ ScriptType::Other => "other",
+ ScriptType::P2pk => "p2pk",
+ ScriptType::P2pkh => "p2pkh",
+ ScriptType::P2sh => "p2sh",
+ };
+ write!(f, "{}", text)?;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use abc_rust_error::Result;
+
+ use crate::{ChronikClient, ChronikClientError};
+
+ #[test]
+ fn test_constructor_trailing_slash() -> Result<()> {
+ let url = "https://chronik.be.cash/xec/".to_string();
+ let err = ChronikClient::new(url.clone())
+ .unwrap_err()
+ .downcast::<ChronikClientError>()?;
+ assert_eq!(err, ChronikClientError::CannotHaveTrailingSlashInUrl(url));
+ Ok(())
+ }
+
+ #[test]
+ fn test_constructor_invalid_schema() -> Result<()> {
+ let url = "soap://chronik.be.cash/xec".to_string();
+ let err = ChronikClient::new(url.clone())
+ .unwrap_err()
+ .downcast::<ChronikClientError>()?;
+ assert_eq!(err, ChronikClientError::InvalidUrlSchema(url));
+ Ok(())
+ }
+}
diff --git a/modules/bitcoinsuite-chronik-client/tests/test_chronik_client.rs b/modules/bitcoinsuite-chronik-client/tests/test_chronik_client.rs
new file mode 100644
--- /dev/null
+++ b/modules/bitcoinsuite-chronik-client/tests/test_chronik_client.rs
@@ -0,0 +1,436 @@
+// Copyright (c) 2024 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+use std::collections::HashMap;
+
+use abc_rust_error::Result;
+use bitcoinsuite_chronik_client::{
+ proto::{self, token_type},
+ ChronikClient, ChronikClientError, ScriptType,
+};
+use bitcoinsuite_core::hash::{Hashed, Sha256d};
+use pretty_assertions::assert_eq;
+use reqwest::StatusCode;
+
+const CHRONIK_URL: &str = "https://chronik.e.cash";
+const GENESIS_PK_HEX: &str = "04678afdb0fe5548271967f1a67130b7105cd6a828e03\
+ 909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e5\
+ 1ec112de5c384df7ba0b8d578a4c702b6bf11d5f";
+
+#[tokio::test]
+pub async fn test_broadcast_tx() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let response = client
+ .broadcast_tx(hex::decode("00000000")?)
+ .await
+ .unwrap_err()
+ .downcast::<ChronikClientError>()?;
+ let error_msg = "400: Parsing tx failed Invalid length, expected 1 bytes \
+ but got 0 bytes";
+ assert_eq!(
+ response,
+ ChronikClientError::ChronikError {
+ status_code: StatusCode::BAD_REQUEST,
+ error: proto::Error {
+ msg: error_msg.to_string(),
+ },
+ error_msg: error_msg.to_string(),
+ },
+ );
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_broadcast_txs() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let response = client
+ .broadcast_txs(vec![hex::decode("00000000")?])
+ .await
+ .unwrap_err()
+ .downcast::<ChronikClientError>()?;
+ let error_msg = "400: Parsing tx failed Invalid length, expected 1 bytes \
+ but got 0 bytes";
+ assert_eq!(
+ response,
+ ChronikClientError::ChronikError {
+ status_code: StatusCode::BAD_REQUEST,
+ error: proto::Error {
+ msg: error_msg.to_string(),
+ },
+ error_msg: error_msg.to_string(),
+ },
+ );
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_blockchain_info() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let blockchain_info = client.blockchain_info().await?;
+ assert!(blockchain_info.tip_height > 243892);
+ assert_eq!(blockchain_info.tip_hash[28..], [0; 4]);
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_block() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let block_hash = Sha256d::from_be_hex(
+ "00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee",
+ )?;
+ let prev_block_hash = Sha256d::from_be_hex(
+ "000000002a22cfee1f2c846adbd12b3e183d4f97683f85dad08a79780a84bd55",
+ )?;
+ let expected_height = 170;
+ let block = client.block_by_hash(&block_hash).await?;
+ assert_eq!(
+ block.block_info,
+ Some(proto::BlockInfo {
+ hash: block_hash.0.as_slice().to_vec(),
+ prev_hash: prev_block_hash.0.as_slice().to_vec(),
+ height: expected_height,
+ n_bits: 0x1d00ffff,
+ timestamp: 1231731025,
+ block_size: 490,
+ num_txs: 2,
+ num_inputs: 2,
+ num_outputs: 3,
+ sum_input_sats: 5000000000,
+ sum_coinbase_output_sats: 5000000000,
+ sum_normal_output_sats: 5000000000,
+ sum_burned_sats: 0,
+ is_final: true,
+ }),
+ );
+ assert_eq!(client.block_by_height(expected_height).await?, block);
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_blocks() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let blocks = client.blocks(129_113, 129_120).await?;
+ assert_eq!(blocks.len(), 8);
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_tx_missing() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let err = client
+ .tx(&Sha256d([0; 32]))
+ .await
+ .unwrap_err()
+ .downcast::<ChronikClientError>()?;
+ let error_msg =
+ "404: Transaction \
+ 0000000000000000000000000000000000000000000000000000000000000000 not \
+ found in the index";
+ assert_eq!(
+ err,
+ ChronikClientError::ChronikError {
+ status_code: StatusCode::NOT_FOUND,
+ error_msg: error_msg.to_string(),
+ error: proto::Error {
+ msg: error_msg.to_string(),
+ },
+ },
+ );
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_raw_tx_missing() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let err = client
+ .raw_tx(&Sha256d([0; 32]))
+ .await
+ .unwrap_err()
+ .downcast::<ChronikClientError>()?;
+ let error_msg =
+ "404: Transaction \
+ 0000000000000000000000000000000000000000000000000000000000000000 not \
+ found in the index";
+ assert_eq!(
+ err,
+ ChronikClientError::ChronikError {
+ status_code: StatusCode::NOT_FOUND,
+ error_msg: error_msg.to_string(),
+ error: proto::Error {
+ msg: error_msg.to_string(),
+ },
+ },
+ );
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_tx() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let block_hash = Sha256d::from_be_hex(
+ "00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee",
+ )?;
+ let txid = Sha256d::from_be_hex(
+ "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
+ )?;
+ let actual_tx = client.tx(&txid).await?;
+ let expected_tx = proto::Tx {
+ txid: txid.0.as_slice().to_vec(),
+ version: 1,
+ inputs: vec![proto::TxInput {
+ prev_out: Some(proto::OutPoint {
+ txid: Sha256d::from_be_hex(
+ "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106\
+ ee5a597c9",
+ )?
+ .0.as_slice()
+ .to_vec(),
+ out_idx: 0,
+ }),
+ input_script: hex::decode(
+ "47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c6\
+ 1548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbb\
+ ac4622082221a8768d1d0901",
+ )?,
+ output_script: hex::decode(
+ "410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148\
+ a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b864\
+ 3f656b412a3ac",
+ )?,
+ value: 5_000_000_000,
+ sequence_no: 0xffffffff,
+ token: None,
+ plugins: HashMap::new(),
+ }],
+ outputs: vec![
+ proto::TxOutput {
+ value: 1_000_000_000,
+ output_script: hex::decode(
+ "4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d7\
+ 1302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1bade\
+ d5c72a704f7e6cd84cac",
+ )?,
+ token: None,
+ spent_by: Some(proto::SpentBy {
+ txid: Sha256d::from_be_hex(
+ "ea44e97271691990157559d0bdd9959e02790c34db6c006d779e8\
+ 2fa5aee708e",
+ )?
+ .0.as_slice()
+ .to_vec(),
+ input_idx: 0,
+ }),
+ plugins: HashMap::new(),
+ },
+ proto::TxOutput {
+ value: 4_000_000_000,
+ output_script: hex::decode(
+ "410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b\
+ 148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f\
+ 999b8643f656b412a3ac",
+ )?,
+ token: None,
+ spent_by: Some(proto::SpentBy {
+ txid: Sha256d::from_be_hex(
+ "a16f3ce4dd5deb92d98ef5cf8afeaf0775ebca408f708b2146c4f\
+ b42b41e14be",
+ )?
+ .0.as_slice()
+ .to_vec(),
+ input_idx: 0,
+ }),
+ plugins: HashMap::new(),
+ },
+ ],
+ lock_time: 0,
+ token_entries: vec![],
+ token_failed_parsings: vec![],
+ token_status: proto::TokenStatus::NonToken as _,
+ block: Some(proto::BlockMetadata {
+ height: 170,
+ hash: block_hash.0.as_slice().to_vec(),
+ timestamp: 1231731025,
+ is_final: true,
+ }),
+ time_first_seen: 0,
+ size: 275,
+ is_coinbase: false,
+ is_final: true,
+ };
+ assert_eq!(actual_tx, expected_tx);
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_raw_tx() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let txid = Sha256d::from_be_hex(
+ "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
+ )?;
+ let actual_raw_tx = client.raw_tx(&txid).await?;
+ let expected_raw_tx =
+ "0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce258\
+ 57fcd3704000000004847304402204e45e16932b8af514961a1d3a1a25fdf3f4f\
+ 7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d8\
+ 31cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104\
+ ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e\
+ 7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac\
+ 00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b\
+ 1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c0\
+ 3f999b8643f656b412a3ac00000000";
+ assert_eq!(hex::encode(actual_raw_tx), expected_raw_tx);
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_block_txs() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let block_hash = Sha256d::from_be_hex(
+ "00000000000053807791091d70e691abff37fc4f8196df306ade8fd8fc40b9e8",
+ )?;
+ let block_height: i32 = 122740;
+ let block_txs_by_hash = client.block_txs_by_hash(&block_hash, 0).await?;
+ let block_txs_by_height =
+ client.block_txs_by_height(block_height, 0).await?;
+ assert_eq!(block_txs_by_hash, block_txs_by_height);
+
+ let num_txs_in_block: u32 = block_txs_by_hash.num_txs;
+ assert_eq!(block_txs_by_hash.num_txs, 64);
+
+ let num_txs_in_page: u32 = block_txs_by_hash.txs.len().try_into().unwrap();
+ assert_eq!(
+ block_txs_by_hash.num_pages,
+ num_txs_in_block / num_txs_in_page + 1
+ );
+
+ // Same page size gives the same result
+ let page_size: usize = num_txs_in_page.try_into().unwrap();
+ let block_txs_by_hash_with_page_size = client
+ .block_txs_by_hash_with_page_size(&block_hash, 0, page_size)
+ .await?;
+ let block_txs_by_height_with_page_size = client
+ .block_txs_by_height_with_page_size(block_height, 0, page_size)
+ .await?;
+
+ assert_eq!(
+ block_txs_by_hash_with_page_size,
+ block_txs_by_height_with_page_size
+ );
+ assert_eq!(block_txs_by_hash_with_page_size, block_txs_by_hash);
+
+ let block_txs_by_hash_with_max_page_size = client
+ .block_txs_by_hash_with_page_size(&block_hash, 0, 64)
+ .await?;
+ assert_eq!(block_txs_by_hash_with_max_page_size.num_pages, 1);
+ assert_eq!(block_txs_by_hash_with_max_page_size.num_txs, 64);
+ assert_eq!(block_txs_by_hash_with_max_page_size.txs.len(), 64);
+
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_slpv1_token() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let token_id = Sha256d::from_be_hex(
+ "0daf200e3418f2df1158efef36fbb507f12928f1fdcf3543703e64e75a4a9073",
+ )?;
+ let token = client.token(&token_id).await?;
+ let block_hash = Sha256d::from_be_hex(
+ "00000000000000002686aa5ffa8401c7ed67338fb9475561b2fa9817d6571da8",
+ )?;
+ assert_eq!(
+ token,
+ proto::TokenInfo {
+ token_id: token_id.hex_be(),
+ token_type: Some(proto::TokenType {
+ token_type: Some(token_type::TokenType::Slp(
+ proto::SlpTokenType::Fungible as _
+ ))
+ }),
+ genesis_info: Some(proto::GenesisInfo {
+ token_ticker: b"USDR".to_vec(),
+ token_name: b"RaiUSD".to_vec(),
+ mint_vault_scripthash: vec![],
+ url: b"https://www.raiusd.co/etoken".to_vec(),
+ hash: vec![],
+ data: vec![],
+ auth_pubkey: vec![],
+ decimals: 4,
+ }),
+ block: Some(proto::BlockMetadata {
+ hash: block_hash.0.as_slice().to_vec(),
+ height: 697721,
+ timestamp: 1627783243,
+ is_final: true,
+ }),
+ time_first_seen: token.time_first_seen,
+ },
+ );
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_slpv2_token() -> Result<()> {
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let token_id = Sha256d::from_be_hex(
+ "cdcdcdcdcdc9dda4c92bb1145aa84945c024346ea66fd4b699e344e45df2e145",
+ )?;
+ let token = client.token(&token_id).await?;
+ let block_hash = Sha256d::from_be_hex(
+ "00000000000000000b7e89959ee52ca1cd691e1fc3b4891c1888f84261c83e73",
+ )?;
+ assert_eq!(
+ token,
+ proto::TokenInfo {
+ token_id: token_id.hex_be(),
+ token_type: Some(proto::TokenType {
+ token_type: Some(token_type::TokenType::Alp(
+ proto::AlpTokenType::Standard as _
+ ))
+ }),
+ genesis_info: Some(proto::GenesisInfo {
+ token_ticker: b"CRD".to_vec(),
+ token_name: b"Credo In Unum Deo".to_vec(),
+ mint_vault_scripthash: vec![],
+ url: b"https://crd.network/token".to_vec(),
+ hash: vec![],
+ data: vec![],
+ auth_pubkey: hex::decode(
+ "0334b744e6338ad438c92900c0ed1869c3fd2c0f35a4a9b97a884\
+ 47b6e2b145f10"
+ )
+ .unwrap(),
+ decimals: 4,
+ }),
+ block: Some(proto::BlockMetadata {
+ hash: block_hash.0.as_slice().to_vec(),
+ height: 795680,
+ timestamp: 1686305735,
+ is_final: true,
+ }),
+ time_first_seen: token.time_first_seen,
+ },
+ );
+ Ok(())
+}
+
+#[tokio::test]
+pub async fn test_history() -> Result<()> {
+ let genesis_pk = hex::decode(GENESIS_PK_HEX)?;
+ let client = ChronikClient::new(CHRONIK_URL.to_string())?;
+ let history = client
+ .script(ScriptType::P2pk, &genesis_pk)
+ .confirmed_txs(0)
+ .await?;
+ assert_eq!(history.num_pages, 1);
+ assert_eq!(
+ history.txs[0].txid,
+ Sha256d::from_be_hex(
+ "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
+ )?
+ .0
+ .as_slice()
+ .to_vec(),
+ );
+ Ok(())
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Feb 6, 15:55 (16 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5082179
Default Alt Text
D17335.id.diff (31 KB)
Attached To
D17335: [Modules] Added `bitcoinsuite-chronik-client` to monorepo
Event Timeline
Log In to Comment