From 25f7ba5e99726375140d0d6c84f067dd4d23b03d Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 1 Dec 2020 08:38:09 -0500 Subject: [PATCH] Add tx_search endpoint for RPC client (#701) * Add tx_search endpoint for RPC client Signed-off-by: Thane Thomson * Update CHANGELOG Signed-off-by: Thane Thomson * Fix tx_search Request docs Signed-off-by: Thane Thomson * Update proto/src/serializers/bytes.rs Co-authored-by: Greg Szabo <16846635+greg-szabo@users.noreply.github.com> * Replace sleep with Tokio 0.2.x-compatible method call Signed-off-by: Thane Thomson * Supply full tx to broadcast_tx helper method Signed-off-by: Thane Thomson * Fix tx_search query for integration test Signed-off-by: Thane Thomson Co-authored-by: Greg Szabo <16846635+greg-szabo@users.noreply.github.com> --- CHANGELOG.md | 2 + proto-compiler/src/constants.rs | 17 +- proto/src/prost/tendermint.crypto.rs | 5 + proto/src/prost/tendermint.types.rs | 3 + proto/src/serializers/bytes.rs | 33 ++ rpc-probe/src/kvstore.rs | 20 + rpc-probe/src/quick.rs | 2 + rpc/src/client.rs | 16 +- rpc/src/endpoint.rs | 1 + rpc/src/endpoint/tx_search.rs | 67 +++ rpc/src/event.rs | 6 +- rpc/src/lib.rs | 5 +- rpc/src/method.rs | 13 +- rpc/src/order.rs | 15 + rpc/tests/parse_response.rs | 43 ++ rpc/tests/support/tx_search_no_prove.json | 345 ++++++++++++++++ rpc/tests/support/tx_search_with_prove.json | 437 ++++++++++++++++++++ tendermint/src/abci/responses.rs | 4 +- tendermint/tests/integration.rs | 111 ++++- 19 files changed, 1110 insertions(+), 35 deletions(-) create mode 100644 rpc/src/endpoint/tx_search.rs create mode 100644 rpc/src/order.rs create mode 100644 rpc/tests/support/tx_search_no_prove.json create mode 100644 rpc/tests/support/tx_search_with_prove.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c73dc08..74b63962a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,13 @@ - `[light-client]` Only require Tokio when `rpc-client` feature is enabled ([#425]) - `[rpc]` The `WebSocketClient` now adds support for all remaining RPC requests by way of implementing the `Client` trait ([#646]) +- `[rpc]` Support for the `tx_search` RPC endpoint has been added ([#701]) - `[tendermint]` (Since v0.17.0-rc3) Bech32 encoding fix ([#690]) [#425]: https://github.com/informalsystems/tendermint-rs/issues/425 [#646]: https://github.com/informalsystems/tendermint-rs/pull/646 [#690]: https://github.com/informalsystems/tendermint-rs/issues/690 +[#701]: https://github.com/informalsystems/tendermint-rs/pull/701 ## v0.17.0-rc3 diff --git a/proto-compiler/src/constants.rs b/proto-compiler/src/constants.rs index 34b1bfd00..442601c90 100644 --- a/proto-compiler/src/constants.rs +++ b/proto-compiler/src/constants.rs @@ -18,6 +18,7 @@ const QUOTED: &str = r#"#[serde(with = "crate::serializers::from_str")]"#; const QUOTED_WITH_DEFAULT: &str = r#"#[serde(with = "crate::serializers::from_str", default)]"#; const HEXSTRING: &str = r#"#[serde(with = "crate::serializers::bytes::hexstring")]"#; const BASE64STRING: &str = r#"#[serde(with = "crate::serializers::bytes::base64string")]"#; +const VEC_BASE64STRING: &str = r#"#[serde(with = "crate::serializers::bytes::vec_base64string")]"#; const OPTIONAL: &str = r#"#[serde(with = "crate::serializers::optional")]"#; const VEC_SKIP_IF_EMPTY: &str = r#"#[serde(skip_serializing_if = "Vec::is_empty", with = "serde_bytes")]"#; @@ -25,7 +26,8 @@ const NULLABLEVECARRAY: &str = r#"#[serde(with = "crate::serializers::txs")]"#; const NULLABLE: &str = r#"#[serde(with = "crate::serializers::nullable")]"#; const ALIAS_POWER_QUOTED: &str = r#"#[serde(alias = "power", with = "crate::serializers::from_str")]"#; -const PART_SET_HEADER_TOTAL: &str = r#"#[serde(with = "crate::serializers::part_set_header_total")]"#; +const PART_SET_HEADER_TOTAL: &str = + r#"#[serde(with = "crate::serializers::part_set_header_total")]"#; const RENAME_PUBKEY: &str = r#"#[serde(rename = "tendermint/PubKeyEd25519", with = "crate::serializers::bytes::base64string")]"#; const RENAME_DUPLICATEVOTE: &str = r#"#[serde(rename = "tendermint/DuplicateVoteEvidence")]"#; const RENAME_LIGHTCLIENTATTACK: &str = @@ -69,6 +71,8 @@ pub static CUSTOM_TYPE_ATTRIBUTES: &[(&str, &str)] = &[ (".tendermint.types.CanonicalVote", SERIALIZED), (".tendermint.types.BlockMeta", SERIALIZED), (".tendermint.types.Evidence", EVIDENCE_VARIANT), + (".tendermint.types.TxProof", SERIALIZED), + (".tendermint.crypto.Proof", SERIALIZED), ]; /// Custom field attributes applied on top of protobuf fields in (a) struct(s) @@ -98,7 +102,10 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[ ".tendermint.types.CanonicalBlockID.part_set_header", ALIAS_PARTS, ), - (".tendermint.types.PartSetHeader.total", PART_SET_HEADER_TOTAL), + ( + ".tendermint.types.PartSetHeader.total", + PART_SET_HEADER_TOTAL, + ), (".tendermint.types.PartSetHeader.hash", HEXSTRING), (".tendermint.types.Header.height", QUOTED), (".tendermint.types.Header.time", OPTIONAL), @@ -144,4 +151,10 @@ pub static CUSTOM_FIELD_ATTRIBUTES: &[(&str, &str)] = &[ ".tendermint.types.Evidence.sum.light_client_attack_evidence", RENAME_LIGHTCLIENTATTACK, ), + (".tendermint.types.TxProof.data", BASE64STRING), + (".tendermint.types.TxProof.root_hash", HEXSTRING), + (".tendermint.crypto.Proof.index", QUOTED), + (".tendermint.crypto.Proof.total", QUOTED), + (".tendermint.crypto.Proof.aunts", VEC_BASE64STRING), + (".tendermint.crypto.Proof.leaf_hash", BASE64STRING), ]; diff --git a/proto/src/prost/tendermint.crypto.rs b/proto/src/prost/tendermint.crypto.rs index 4b3e8c2b0..9a0e9a6fe 100644 --- a/proto/src/prost/tendermint.crypto.rs +++ b/proto/src/prost/tendermint.crypto.rs @@ -1,12 +1,17 @@ #[derive(Clone, PartialEq, ::prost::Message)] +#[derive(::serde::Deserialize, ::serde::Serialize)] pub struct Proof { #[prost(int64, tag="1")] + #[serde(with = "crate::serializers::from_str")] pub total: i64, #[prost(int64, tag="2")] + #[serde(with = "crate::serializers::from_str")] pub index: i64, #[prost(bytes, tag="3")] + #[serde(with = "crate::serializers::bytes::base64string")] pub leaf_hash: std::vec::Vec, #[prost(bytes, repeated, tag="4")] + #[serde(with = "crate::serializers::bytes::vec_base64string")] pub aunts: ::std::vec::Vec>, } #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/prost/tendermint.types.rs b/proto/src/prost/tendermint.types.rs index a7d70e073..94642f6c3 100644 --- a/proto/src/prost/tendermint.types.rs +++ b/proto/src/prost/tendermint.types.rs @@ -253,10 +253,13 @@ pub struct BlockMeta { } /// TxProof represents a Merkle proof of the presence of a transaction in the Merkle tree. #[derive(Clone, PartialEq, ::prost::Message)] +#[derive(::serde::Deserialize, ::serde::Serialize)] pub struct TxProof { #[prost(bytes, tag="1")] + #[serde(with = "crate::serializers::bytes::hexstring")] pub root_hash: std::vec::Vec, #[prost(bytes, tag="2")] + #[serde(with = "crate::serializers::bytes::base64string")] pub data: std::vec::Vec, #[prost(message, optional, tag="3")] pub proof: ::std::option::Option, diff --git a/proto/src/serializers/bytes.rs b/proto/src/serializers/bytes.rs index 5d534f3f0..3561cf99f 100644 --- a/proto/src/serializers/bytes.rs +++ b/proto/src/serializers/bytes.rs @@ -54,6 +54,39 @@ pub mod base64string { } } +/// Serialize into Vec, deserialize from Vec +pub mod vec_base64string { + use serde::{Deserialize, Deserializer, Serializer}; + use subtle_encoding::base64; + + /// Deserialize array into Vec> + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + Option::>::deserialize(deserializer)? + .unwrap_or_default() + .into_iter() + .map(|s| base64::decode(&s).map_err(serde::de::Error::custom)) + .collect() + } + + /// Serialize from Vec into Vec + pub fn serialize(value: &[T], serializer: S) -> Result + where + S: Serializer, + T: AsRef<[u8]>, + { + let base64_strings = value + .iter() + .map(|v| { + String::from_utf8(base64::encode(v.as_ref())).map_err(serde::ser::Error::custom) + }) + .collect::, S::Error>>()?; + serializer.collect_seq(base64_strings) + } +} + /// Serialize into Option, deserialize from Option pub mod option_base64string { use serde::{Deserialize, Deserializer, Serializer}; diff --git a/rpc-probe/src/kvstore.rs b/rpc-probe/src/kvstore.rs index 3e2893dc7..0f9fd5664 100644 --- a/rpc-probe/src/kvstore.rs +++ b/rpc-probe/src/kvstore.rs @@ -86,3 +86,23 @@ pub fn status() -> PlannedInteraction { pub fn subscribe(query: &str) -> PlannedInteraction { PlannedSubscription::new(query).into() } + +pub fn tx_search( + query: &str, + prove: bool, + page: u32, + per_page: u8, + order_by: &str, +) -> PlannedInteraction { + Request::new( + "tx_search", + json!({ + "query": query, + "prove": prove, + "page": format!("{}", page), + "per_page": format!("{}", per_page), + "order_by": order_by, + }), + ) + .into() +} diff --git a/rpc-probe/src/quick.rs b/rpc-probe/src/quick.rs index 496f54292..67698780f 100644 --- a/rpc-probe/src/quick.rs +++ b/rpc-probe/src/quick.rs @@ -51,6 +51,8 @@ pub fn quick_probe_plan(output_path: &Path, request_wait: Duration) -> Result 1", false, 1, 10, "asc").with_name("tx_search_no_prove"), + tx_search("tx.height > 1", true, 1, 10, "asc").with_name("tx_search_with_prove"), ]), ], ) diff --git a/rpc/src/client.rs b/rpc/src/client.rs index d49828851..1dab44f63 100644 --- a/rpc/src/client.rs +++ b/rpc/src/client.rs @@ -13,7 +13,8 @@ pub use transport::http::HttpClient; pub use transport::websocket::{WebSocketClient, WebSocketClientDriver}; use crate::endpoint::*; -use crate::{Result, SimpleRequest}; +use crate::query::Query; +use crate::{Order, Result, SimpleRequest}; use async_trait::async_trait; use tendermint::abci::{self, Transaction}; use tendermint::block::Height; @@ -159,6 +160,19 @@ pub trait Client { self.perform(evidence::Request::new(e)).await } + /// `/tx_search`: search for transactions with their results. + async fn tx_search( + &self, + query: Query, + prove: bool, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.perform(tx_search::Request::new(query, prove, page, per_page, order)) + .await + } + /// Perform a request against the RPC endpoint async fn perform(&self, request: R) -> Result where diff --git a/rpc/src/endpoint.rs b/rpc/src/endpoint.rs index 6cd0055ab..ad4d0ab61 100644 --- a/rpc/src/endpoint.rs +++ b/rpc/src/endpoint.rs @@ -13,5 +13,6 @@ pub mod health; pub mod net_info; pub mod status; pub mod subscribe; +pub mod tx_search; pub mod unsubscribe; pub mod validators; diff --git a/rpc/src/endpoint/tx_search.rs b/rpc/src/endpoint/tx_search.rs new file mode 100644 index 000000000..b31a7a2f2 --- /dev/null +++ b/rpc/src/endpoint/tx_search.rs @@ -0,0 +1,67 @@ +//! `/tx_search` endpoint JSON-RPC wrapper + +use crate::{Method, Order}; +use serde::{Deserialize, Serialize}; +use tendermint::{abci, block}; +use tendermint_proto::types::TxProof; + +/// Request for searching for transactions with their results. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Request { + pub query: String, + pub prove: bool, + #[serde(with = "tendermint_proto::serializers::from_str")] + pub page: u32, + #[serde(with = "tendermint_proto::serializers::from_str")] + pub per_page: u8, + pub order_by: Order, +} + +impl Request { + /// Constructor. + pub fn new( + query: impl ToString, + prove: bool, + page: u32, + per_page: u8, + order_by: Order, + ) -> Self { + Self { + query: query.to_string(), + prove, + page, + per_page, + order_by, + } + } +} + +impl crate::Request for Request { + type Response = Response; + + fn method(&self) -> Method { + Method::TxSearch + } +} + +impl crate::SimpleRequest for Request {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Response { + pub txs: Vec, + #[serde(with = "tendermint_proto::serializers::from_str")] + pub total_count: u32, +} + +impl crate::Response for Response {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ResultTx { + pub hash: abci::transaction::Hash, + pub height: block::Height, + pub index: u32, + pub tx_result: abci::DeliverTx, + pub tx: abci::Transaction, + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, +} diff --git a/rpc/src/event.rs b/rpc/src/event.rs index b368005e6..4c12cbb6d 100644 --- a/rpc/src/event.rs +++ b/rpc/src/event.rs @@ -60,9 +60,11 @@ pub enum EventData { /// Transaction result info. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TxInfo { - pub height: String, + #[serde(with = "tendermint_proto::serializers::from_str")] + pub height: i64, pub index: Option, - pub tx: String, + #[serde(with = "tendermint_proto::serializers::bytes::base64string")] + pub tx: Vec, pub result: TxResult, } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 2b692992f..eff517e69 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -6,7 +6,7 @@ //! functionality and different client transports based on which features you //! select when using it. //! -//! Two features are provided at present. +//! Two features are provided at present: //! //! * `http-client` - Provides [`HttpClient`], which is a basic RPC client that //! interacts with remote Tendermint nodes via **JSON-RPC over HTTP**. This @@ -51,6 +51,7 @@ pub mod error; pub mod event; mod id; mod method; +mod order; pub mod query; pub mod request; pub mod response; @@ -59,6 +60,6 @@ mod utils; mod version; pub use self::{ - error::Error, id::Id, method::Method, request::Request, request::SimpleRequest, + error::Error, id::Id, method::Method, order::Order, request::Request, request::SimpleRequest, response::Response, result::Result, version::Version, }; diff --git a/rpc/src/method.rs b/rpc/src/method.rs index 37e5b42f1..c68958ef2 100644 --- a/rpc/src/method.rs +++ b/rpc/src/method.rs @@ -51,6 +51,9 @@ pub enum Method { /// Get node status Status, + /// Search for transactions with their results + TxSearch, + /// Get validator info for a block Validators, @@ -73,6 +76,7 @@ impl Method { Method::Block => "block", Method::BlockResults => "block_results", Method::Blockchain => "blockchain", + Method::BroadcastEvidence => "broadcast_evidence", Method::BroadcastTxAsync => "broadcast_tx_async", Method::BroadcastTxSync => "broadcast_tx_sync", Method::BroadcastTxCommit => "broadcast_tx_commit", @@ -81,10 +85,10 @@ impl Method { Method::Health => "health", Method::NetInfo => "net_info", Method::Status => "status", - Method::Validators => "validators", Method::Subscribe => "subscribe", - Method::BroadcastEvidence => "broadcast_evidence", + Method::TxSearch => "tx_search", Method::Unsubscribe => "unsubscribe", + Method::Validators => "validators", } } } @@ -99,6 +103,7 @@ impl FromStr for Method { "block" => Method::Block, "block_results" => Method::BlockResults, "blockchain" => Method::Blockchain, + "broadcast_evidence" => Method::BroadcastEvidence, "broadcast_tx_async" => Method::BroadcastTxAsync, "broadcast_tx_sync" => Method::BroadcastTxSync, "broadcast_tx_commit" => Method::BroadcastTxCommit, @@ -107,10 +112,10 @@ impl FromStr for Method { "health" => Method::Health, "net_info" => Method::NetInfo, "status" => Method::Status, - "validators" => Method::Validators, "subscribe" => Method::Subscribe, + "tx_search" => Method::TxSearch, "unsubscribe" => Method::Unsubscribe, - "broadcast_evidence" => Method::BroadcastEvidence, + "validators" => Method::Validators, other => return Err(Error::method_not_found(other)), }) } diff --git a/rpc/src/order.rs b/rpc/src/order.rs new file mode 100644 index 000000000..b9294464e --- /dev/null +++ b/rpc/src/order.rs @@ -0,0 +1,15 @@ +//! Ordering of paginated RPC responses. + +use serde::{Deserialize, Serialize}; + +/// Ordering of paginated RPC responses. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum Order { + /// Ascending order + #[serde(rename = "asc")] + Ascending, + + /// Descending order + #[serde(rename = "desc")] + Descending, +} diff --git a/rpc/tests/parse_response.rs b/rpc/tests/parse_response.rs index 7ce75fd39..7fc468c60 100644 --- a/rpc/tests/parse_response.rs +++ b/rpc/tests/parse_response.rs @@ -295,3 +295,46 @@ fn jsonrpc_error() { panic!("expected error, got {:?}", result) } } + +#[test] +fn tx_search_no_prove() { + let response = + endpoint::tx_search::Response::from_string(&read_json_fixture("tx_search_no_prove")) + .unwrap(); + + assert_eq!(8, response.total_count); + assert_eq!(8, response.txs.len()); + assert_eq!( + "9F28904F9C0F3AB74A81CBA48E39124DA1C680B47FBFCBA0126870DB722BCC30", + response.txs[0].hash.to_string() + ); + assert_eq!(11, response.txs[0].height.value()); + assert!(response.txs[0].proof.is_none()); +} + +#[test] +fn tx_search_with_prove() { + let response = + endpoint::tx_search::Response::from_string(&read_json_fixture("tx_search_with_prove")) + .unwrap(); + + assert_eq!(8, response.total_count); + assert_eq!(8, response.txs.len()); + assert_eq!( + "9F28904F9C0F3AB74A81CBA48E39124DA1C680B47FBFCBA0126870DB722BCC30", + response.txs[0].hash.to_string() + ); + assert_eq!(11, response.txs[0].height.value()); + let proof = response.txs[0].proof.as_ref().unwrap(); + assert_eq!( + vec![97, 115, 121, 110, 99, 45, 107, 101, 121, 61, 118, 97, 108, 117, 101], + proof.data + ); + assert_eq!( + vec![ + 245, 70, 67, 176, 5, 16, 101, 200, 125, 163, 26, 101, 69, 49, 182, 95, 155, 87, 56, 15, + 155, 243, 51, 47, 245, 188, 167, 88, 69, 103, 38, 140 + ], + proof.root_hash + ); +} diff --git a/rpc/tests/support/tx_search_no_prove.json b/rpc/tests/support/tx_search_no_prove.json new file mode 100644 index 000000000..272b57a2a --- /dev/null +++ b/rpc/tests/support/tx_search_no_prove.json @@ -0,0 +1,345 @@ +{ + "id": "60c1e528-8854-41ee-99ac-8144282f7e9f", + "jsonrpc": "2.0", + "result": { + "total_count": "8", + "txs": [ + { + "hash": "9F28904F9C0F3AB74A81CBA48E39124DA1C680B47FBFCBA0126870DB722BCC30", + "height": "11", + "index": 0, + "tx": "YXN5bmMta2V5PXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "YXN5bmMta2V5" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "57018296EE0919C9D351F2FFEA82A8D28DE223724D79965FC8D00A7477ED48BC", + "height": "11", + "index": 1, + "tx": "c3luYy1rZXk9dmFsdWU=", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "c3luYy1rZXk=" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "D63F9C23791E610410B576D8C27BB5AEAC93CC1A58522428A7B32A1276085860", + "height": "11", + "index": 2, + "tx": "Y29tbWl0LWtleT12YWx1ZQ==", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "Y29tbWl0LWtleQ==" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "FCB86F71C4EFF43E13C51FA12791F6DD1DDB8600A51131BE2289614D6882F6BE", + "height": "18", + "index": 0, + "tx": "dHgwPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgw" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "9F424A8E634AAF63CFA61151A306AA788C9CC792F16B370F7867ED0BD972476C", + "height": "19", + "index": 0, + "tx": "dHgxPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgx" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "C9D123E2CF19B9F0EC3CA1F64CD3BF0735397C84778B40B3EB5C49A752D53BF4", + "height": "19", + "index": 1, + "tx": "dHgyPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgy" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "73117D6A783E4A37C1D9AD48744AD9FCC0D094C48AB8322FA11CD901C5174CFD", + "height": "20", + "index": 0, + "tx": "dHgzPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgz" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "C349F213F04B4E8E749C6656E4C299E3BF22F4FAF141291A5C083336AD1A413B", + "height": "21", + "index": 0, + "tx": "dHg0PXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHg0" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + } + ] + } +} \ No newline at end of file diff --git a/rpc/tests/support/tx_search_with_prove.json b/rpc/tests/support/tx_search_with_prove.json new file mode 100644 index 000000000..0a2424b49 --- /dev/null +++ b/rpc/tests/support/tx_search_with_prove.json @@ -0,0 +1,437 @@ +{ + "id": "942ec9f5-1121-48f3-b3ea-d391a01429f7", + "jsonrpc": "2.0", + "result": { + "total_count": "8", + "txs": [ + { + "hash": "9F28904F9C0F3AB74A81CBA48E39124DA1C680B47FBFCBA0126870DB722BCC30", + "height": "11", + "index": 0, + "proof": { + "data": "YXN5bmMta2V5PXZhbHVl", + "proof": { + "aunts": [ + "oL+OYRo6LtD+lKo0W5A2kcPlbt4Of3c/VN57Ag54iEk=", + "wq4Wy/oF+/0xsH+eJq1SqY2BgYS2FVXbLAXNcCLkB74=" + ], + "index": "0", + "leaf_hash": "MIH5kVBA0TizrX+JVzLSdnwp6Ful2EOI0E4XpdgmK3o=", + "total": "3" + }, + "root_hash": "F54643B0051065C87DA31A654531B65F9B57380F9BF3332FF5BCA7584567268C" + }, + "tx": "YXN5bmMta2V5PXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "YXN5bmMta2V5" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "57018296EE0919C9D351F2FFEA82A8D28DE223724D79965FC8D00A7477ED48BC", + "height": "11", + "index": 1, + "proof": { + "data": "c3luYy1rZXk9dmFsdWU=", + "proof": { + "aunts": [ + "MIH5kVBA0TizrX+JVzLSdnwp6Ful2EOI0E4XpdgmK3o=", + "wq4Wy/oF+/0xsH+eJq1SqY2BgYS2FVXbLAXNcCLkB74=" + ], + "index": "1", + "leaf_hash": "oL+OYRo6LtD+lKo0W5A2kcPlbt4Of3c/VN57Ag54iEk=", + "total": "3" + }, + "root_hash": "F54643B0051065C87DA31A654531B65F9B57380F9BF3332FF5BCA7584567268C" + }, + "tx": "c3luYy1rZXk9dmFsdWU=", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "c3luYy1rZXk=" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "D63F9C23791E610410B576D8C27BB5AEAC93CC1A58522428A7B32A1276085860", + "height": "11", + "index": 2, + "proof": { + "data": "Y29tbWl0LWtleT12YWx1ZQ==", + "proof": { + "aunts": [ + "RaZ3Z52YXK7Rahqt14/2jlvLxqDpG0rmHt9ETIABLus=" + ], + "index": "2", + "leaf_hash": "wq4Wy/oF+/0xsH+eJq1SqY2BgYS2FVXbLAXNcCLkB74=", + "total": "3" + }, + "root_hash": "F54643B0051065C87DA31A654531B65F9B57380F9BF3332FF5BCA7584567268C" + }, + "tx": "Y29tbWl0LWtleT12YWx1ZQ==", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "Y29tbWl0LWtleQ==" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "FCB86F71C4EFF43E13C51FA12791F6DD1DDB8600A51131BE2289614D6882F6BE", + "height": "18", + "index": 0, + "proof": { + "data": "dHgwPXZhbHVl", + "proof": { + "aunts": [], + "index": "0", + "leaf_hash": "3UnhxGnCw+sKtov5uD2YZbCP79vHFwLMc1ZPTaVocKQ=", + "total": "1" + }, + "root_hash": "DD49E1C469C2C3EB0AB68BF9B83D9865B08FEFDBC71702CC73564F4DA56870A4" + }, + "tx": "dHgwPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgw" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "9F424A8E634AAF63CFA61151A306AA788C9CC792F16B370F7867ED0BD972476C", + "height": "19", + "index": 0, + "proof": { + "data": "dHgxPXZhbHVl", + "proof": { + "aunts": [ + "0eF/BEMF82Y/mIMIM0HX/isU2jI44Z2rAor5MA0PrKM=" + ], + "index": "0", + "leaf_hash": "QnwQk7ERU9NjeD1GlLa4H8lscIRFD3evj6SZulKp3T8=", + "total": "2" + }, + "root_hash": "05E17857792BBFE205D9F8C2498F6C05A3603D046EDF57C50D037D36804F35D1" + }, + "tx": "dHgxPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgx" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "C9D123E2CF19B9F0EC3CA1F64CD3BF0735397C84778B40B3EB5C49A752D53BF4", + "height": "19", + "index": 1, + "proof": { + "data": "dHgyPXZhbHVl", + "proof": { + "aunts": [ + "QnwQk7ERU9NjeD1GlLa4H8lscIRFD3evj6SZulKp3T8=" + ], + "index": "1", + "leaf_hash": "0eF/BEMF82Y/mIMIM0HX/isU2jI44Z2rAor5MA0PrKM=", + "total": "2" + }, + "root_hash": "05E17857792BBFE205D9F8C2498F6C05A3603D046EDF57C50D037D36804F35D1" + }, + "tx": "dHgyPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgy" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "73117D6A783E4A37C1D9AD48744AD9FCC0D094C48AB8322FA11CD901C5174CFD", + "height": "20", + "index": 0, + "proof": { + "data": "dHgzPXZhbHVl", + "proof": { + "aunts": [], + "index": "0", + "leaf_hash": "jt+3kmBnxAUZK7c0gytFbKegUaR38TB3aAlKsIZ0RNU=", + "total": "1" + }, + "root_hash": "8EDFB7926067C405192BB734832B456CA7A051A477F1307768094AB0867444D5" + }, + "tx": "dHgzPXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHgz" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + }, + { + "hash": "C349F213F04B4E8E749C6656E4C299E3BF22F4FAF141291A5C083336AD1A413B", + "height": "21", + "index": 0, + "proof": { + "data": "dHg0PXZhbHVl", + "proof": { + "aunts": [], + "index": "0", + "leaf_hash": "oZPOdbpVGhDcNY2OaPjybbnfL2SMaDmXm9ufOgruPCA=", + "total": "1" + }, + "root_hash": "A193CE75BA551A10DC358D8E68F8F26DB9DF2F648C6839979BDB9F3A0AEE3C20" + }, + "tx": "dHg0PXZhbHVl", + "tx_result": { + "code": 0, + "codespace": "", + "data": null, + "events": [ + { + "attributes": [ + { + "index": true, + "key": "Y3JlYXRvcg==", + "value": "Q29zbW9zaGkgTmV0b3dva28=" + }, + { + "index": true, + "key": "a2V5", + "value": "dHg0" + }, + { + "index": true, + "key": "aW5kZXhfa2V5", + "value": "aW5kZXggaXMgd29ya2luZw==" + }, + { + "index": false, + "key": "bm9pbmRleF9rZXk=", + "value": "aW5kZXggaXMgd29ya2luZw==" + } + ], + "type": "app" + } + ], + "gas_used": "0", + "gas_wanted": "0", + "info": "", + "log": "" + } + } + ] + } +} \ No newline at end of file diff --git a/tendermint/src/abci/responses.rs b/tendermint/src/abci/responses.rs index 4fe7f23f1..70764d578 100644 --- a/tendermint/src/abci/responses.rs +++ b/tendermint/src/abci/responses.rs @@ -60,11 +60,11 @@ pub struct DeliverTx { pub info: Info, /// Amount of gas wanted - #[serde(rename = "gasWanted")] + #[serde(default, rename = "gasWanted")] pub gas_wanted: Gas, /// Amount of gas used - #[serde(rename = "gasUsed")] + #[serde(default, rename = "gasUsed")] pub gas_used: Gas, /// Events diff --git a/tendermint/tests/integration.rs b/tendermint/tests/integration.rs index 3c12585c9..ab6a3681b 100644 --- a/tendermint/tests/integration.rs +++ b/tendermint/tests/integration.rs @@ -11,17 +11,17 @@ mod rpc { use std::cmp::min; - use tendermint_rpc::{Client, HttpClient, Id, SubscriptionClient, WebSocketClient}; + use tendermint_rpc::{Client, HttpClient, Id, Order, SubscriptionClient, WebSocketClient}; use futures::StreamExt; use std::convert::TryFrom; - use subtle_encoding::base64; use tendermint::abci::Log; use tendermint::abci::{Code, Transaction}; use tendermint::block::Height; use tendermint::merkle::simple_hash_from_byte_vectors; - use tendermint_rpc::event::{Event, EventData}; - use tendermint_rpc::query::EventType; + use tendermint_rpc::endpoint::tx_search::ResultTx; + use tendermint_rpc::event::{Event, EventData, TxInfo}; + use tendermint_rpc::query::{EventType, Query}; use tokio::time::Duration; /// Get the address of the local node @@ -216,6 +216,7 @@ mod rpc { // other and one of them will (incorrectly) fail. simple_transaction_subscription().await; concurrent_subscriptions().await; + tx_search().await; } async fn simple_transaction_subscription() { @@ -257,24 +258,21 @@ mod rpc { //println!("Got event: {:?}", ev); let next_val = expected_tx_values.remove(0); match ev.data { - EventData::Tx { tx_result } => match base64::decode(tx_result.tx) { - Ok(decoded_tx) => match String::from_utf8(decoded_tx) { - Ok(decoded_tx_str) => { - let decoded_tx_split = decoded_tx_str - .split('=') - .map(|s| s.to_string()) - .collect::>(); - assert_eq!(2, decoded_tx_split.len()); - - let key = decoded_tx_split.get(0).unwrap(); - let val = decoded_tx_split.get(1).unwrap(); - println!("Got tx: {}={}", key, val); - assert_eq!(format!("tx{}", cur_tx_id), *key); - assert_eq!(next_val, *val); - } - Err(e) => panic!("Failed to convert decoded tx to string: {}", e), - }, - Err(e) => panic!("Failed to base64 decode tx from event: {}", e), + EventData::Tx { tx_result } => match String::from_utf8(tx_result.tx) { + Ok(decoded_tx_str) => { + let decoded_tx_split = decoded_tx_str + .split('=') + .map(|s| s.to_string()) + .collect::>(); + assert_eq!(2, decoded_tx_split.len()); + + let key = decoded_tx_split.get(0).unwrap(); + let val = decoded_tx_split.get(1).unwrap(); + println!("Got tx: {}={}", key, val); + assert_eq!(format!("tx{}", cur_tx_id), *key); + assert_eq!(next_val, *val); + } + Err(e) => panic!("Failed to convert decoded tx to string: {}", e), }, _ => panic!("Unexpected event type: {:?}", ev), } @@ -348,4 +346,73 @@ mod rpc { client.close().unwrap(); let _ = driver_handle.await.unwrap(); } + + async fn tx_search() { + let rpc_client = localhost_rpc_client(); + let (mut subs_client, driver) = + WebSocketClient::new("tcp://127.0.0.1:26657".parse().unwrap()) + .await + .unwrap(); + let driver_handle = tokio::spawn(async move { driver.run().await }); + + let tx = "tx_search_key=tx_search_value".to_string(); + let tx_info = broadcast_tx( + &rpc_client, + &mut subs_client, + Transaction::from(tx.into_bytes()), + ) + .await + .unwrap(); + println!("Got tx_info: {:?}", tx_info); + + let res = rpc_client + .tx_search( + Query::eq("app.key", "tx_search_key"), + true, + 1, + 1, + Order::Ascending, + ) + .await + .unwrap(); + assert!(res.total_count > 0); + // We don't have more than 1 page of results + assert_eq!(res.total_count as usize, res.txs.len()); + // Find our transaction + let txs = res + .txs + .iter() + .filter(|tx| tx.height.value() == (tx_info.height as u64)) + .collect::>(); + assert_eq!(1, txs.len()); + assert_eq!(tx_info.tx, txs[0].tx.as_bytes()); + + subs_client.close().unwrap(); + driver_handle.await.unwrap().unwrap(); + } + + async fn broadcast_tx( + http_client: &HttpClient, + websocket_client: &mut WebSocketClient, + tx: Transaction, + ) -> Result { + let mut subs = websocket_client.subscribe(EventType::Tx.into()).await?; + let _ = http_client.broadcast_tx_async(tx.clone()).await?; + let mut timeout = tokio::time::delay_for(Duration::from_secs(3)); + tokio::select! { + Some(res) = subs.next() => { + let ev = res?; + match ev.data { + EventData::Tx { tx_result } => { + let tx_result_bytes: &[u8] = tx_result.tx.as_ref(); + // Make sure we have the right transaction here + assert_eq!(tx.as_bytes(), tx_result_bytes); + Ok(tx_result) + }, + _ => panic!("Unexpected event: {:?}", ev), + } + } + _ = &mut timeout => panic!("Timed out waiting for transaction"), + } + } }