Page MenuHomePhabricator

D17335.id.diff
No OneTemporary

D17335.id.diff

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

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)

Event Timeline