From fe72d1eda3e707fbc276bb2a841cc5e6c0e23b97 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 12:42:07 +0300 Subject: [PATCH 01/24] rpc/tx: Add transaction structures for serialization Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 1 + client/rpc-spec-v2/src/lib.rs | 1 + client/rpc-spec-v2/src/transaction/event.rs | 128 ++++++++++++++++++++ client/rpc-spec-v2/src/transaction/mod.rs | 28 +++++ 4 files changed, 158 insertions(+) create mode 100644 client/rpc-spec-v2/src/transaction/event.rs create mode 100644 client/rpc-spec-v2/src/transaction/mod.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 12dec7464e6d0..0924ffa5fb687 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -16,6 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"] jsonrpsee = { version = "0.15.1", features = ["server", "macros"] } # Internal chain structures for "chain_spec". sc-chain-spec = { version = "4.0.0-dev", path = "../chain-spec" } +serde = "1.0" hex = "0.4" [dev-dependencies] diff --git a/client/rpc-spec-v2/src/lib.rs b/client/rpc-spec-v2/src/lib.rs index 297fda13172d6..793422dd6eb18 100644 --- a/client/rpc-spec-v2/src/lib.rs +++ b/client/rpc-spec-v2/src/lib.rs @@ -24,3 +24,4 @@ #![deny(unused_crate_dependencies)] pub mod chain_spec; +pub mod transaction; diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs new file mode 100644 index 0000000000000..01722eaae1f27 --- /dev/null +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -0,0 +1,128 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The transaction's event returned as json compatible object. + +use serde::{Deserialize, Serialize}; + +/// The transaction was broadcasted to a number of peers. +/// +/// # Note +/// +/// The RPC does not guarantee that the peers have received the +/// transaction. +/// +/// When the number of peers is zero, the event guarantees that +/// shutting down the local node will lead to the transaction +/// not being included in the chain. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionBroadcasted { + /// The number of peers the transaction was broadcasted to. + pub num_peers: usize, +} + +/// The transaction was included in a block of the chain. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionBlock { + /// The hash of the block the transaction was included into. + pub hash: Hash, + /// The index (zero-based) of the transaction within the body of the block. + pub index: usize, +} + +/// The transaction could not be processed due to an error. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionError { + /// Reason of the error. + pub error: String, +} + +/// The transaction was dropped because of exceeding limits. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionDropped { + /// True if the transaction was broadcasted to other peers and + /// may still be included in the block. + pub broadcasted: bool, + /// Reason of the event. + pub error: String, +} + +/// Intermediate representation (IR) for the transaction events +/// that handles block events only. +/// +/// The block events require a JSON compatible interpretation similar to: +/// +/// ```sh +/// { event: "EVENT", block: { hash: "0xFF", index: 0 } } +/// ``` +/// +/// This IR is introduced to circumvent that the block events need to +/// be serialized/deserialized with "tag" and "content", while other +/// events only require "tag". +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event", content = "block")] +pub(crate) enum TransactionEventBlock { + /// The transaction was included in the best block of the chain. + BestChainBlockIncluded(Option>), + /// The transaction was included in a finalized block of the chain. + Finalized(TransactionBlock), +} + +/// Intermediate representation (IR) for the transaction events +/// that handles non-block events only. +/// +/// The non-block events require a JSON compatible interpretation similar to: +/// +/// ```sh +/// { event: "EVENT", num_peers: 0 } +/// ``` +/// +/// This IR is introduced to circumvent that the block events need to +/// be serialized/deserialized with "tag" and "content", while other +/// events only require "tag". +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "event")] +pub(crate) enum TransactionEventNonBlock { + Validated, + Broadcasted(TransactionBroadcasted), + Error(TransactionError), + Invalid(TransactionError), + Dropped(TransactionDropped), +} + +/// Intermediate representation (IR) used for serialization/deserialization of the +/// [`TransactionEvent`] in a JSON compatible format. +/// +/// Serde cannot mix `#[serde(tag = "event")]` with `#[serde(tag = "event", content = "block")]` +/// for specific enum variants. Therefore, this IR is introduced to circumvent this +/// restriction, while exposing a simplified [`TransactionEvent`] for users of the +/// rust ecosystem. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(bound(serialize = "Hash: Serialize", deserialize = "Hash: Deserialize<'de>"))] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub(crate) enum TransactionEventIR { + Block(TransactionEventBlock), + NonBlock(TransactionEventNonBlock), +} diff --git a/client/rpc-spec-v2/src/transaction/mod.rs b/client/rpc-spec-v2/src/transaction/mod.rs new file mode 100644 index 0000000000000..015f288babded --- /dev/null +++ b/client/rpc-spec-v2/src/transaction/mod.rs @@ -0,0 +1,28 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Substrate transaction API. +//! +//! The transaction methods allow submitting a transaction and subscribing to +//! its status updates generated by the chain. +//! +//! # Note +//! +//! Methods are prefixed by `transaction`. + +pub mod event; From bb9e92af423703ebad8f8388e9d97cbe2db4a02d Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 12:46:10 +0300 Subject: [PATCH 02/24] rpc/tx: Add public facing `TransactionEvent` To circumvent the fact that serde does not allow mixing `#[serde(tag = "event")]` with `#[serde(tag = "event", content = "block")]` the public facing subscription structure is serialized and deserialized to an intermmediate representation. Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 95 +++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 01722eaae1f27..09f706d2e567a 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -66,6 +66,60 @@ pub struct TransactionDropped { pub error: String, } +/// Possible transaction status events. +/// +/// The status events can be grouped based on their kinds as: +/// +/// 1. Runtime validated the transaction: +/// - `Validated` +/// +/// 2. Inside the `Ready` queue: +/// - `Broadcast` +/// +/// 3. Leaving the pool: +/// - `BestChainBlockIncluded` +/// - `Invalid` +/// +/// 4. Block finalized: +/// - `Finalized` +/// +/// 5. At any time: +/// - `Dropped` +/// - `Error` +/// +/// The subscription's stream is considered finished whenever the following events are +/// received: `Finalized`, `Error`, `Invalid` or `Dropped. However, the user is allowed +/// to unsubscribe at any moment. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +// We need to manually specify the trait bounds for the `Hash` trait to ensure `into` and +// `form` still work. +#[serde(bound( + serialize = "Hash: Serialize + Clone", + deserialize = "Hash: Deserialize<'de> + Clone" +))] +#[serde(into = "TransactionEventIR", from = "TransactionEventIR")] +pub enum TransactionEvent { + /// The transaction was validated by the runtime. + Validated, + /// The transaction was broadcasted to a number of peers. + Broadcasted(TransactionBroadcasted), + /// The transaction was included in a best block of the chain. + /// + /// # Note + /// + /// This may contain `None` if the block is no longer a best + /// block of the chain. + BestChainBlockIncluded(Option>), + /// The transaction was included in a finialized block. + Finalized(TransactionBlock), + /// The transaction could not be processed due to an error. + Error(TransactionError), + /// The transaction is marked as invalid. + Invalid(TransactionError), + /// The client was not capable of keeping track of this transaction. + Dropped(TransactionDropped), +} + /// Intermediate representation (IR) for the transaction events /// that handles block events only. /// @@ -126,3 +180,44 @@ pub(crate) enum TransactionEventIR { Block(TransactionEventBlock), NonBlock(TransactionEventNonBlock), } + +impl From> for TransactionEventIR { + fn from(value: TransactionEvent) -> Self { + match value { + TransactionEvent::Validated => + TransactionEventIR::NonBlock(TransactionEventNonBlock::Validated), + TransactionEvent::Broadcasted(event) => + TransactionEventIR::NonBlock(TransactionEventNonBlock::Broadcasted(event)), + TransactionEvent::BestChainBlockIncluded(event) => + TransactionEventIR::Block(TransactionEventBlock::BestChainBlockIncluded(event)), + TransactionEvent::Finalized(event) => + TransactionEventIR::Block(TransactionEventBlock::Finalized(event)), + TransactionEvent::Error(event) => + TransactionEventIR::NonBlock(TransactionEventNonBlock::Error(event)), + TransactionEvent::Invalid(event) => + TransactionEventIR::NonBlock(TransactionEventNonBlock::Invalid(event)), + TransactionEvent::Dropped(event) => + TransactionEventIR::NonBlock(TransactionEventNonBlock::Dropped(event)), + } + } +} + +impl From> for TransactionEvent { + fn from(value: TransactionEventIR) -> Self { + match value { + TransactionEventIR::NonBlock(status) => match status { + TransactionEventNonBlock::Validated => TransactionEvent::Validated, + TransactionEventNonBlock::Broadcasted(event) => + TransactionEvent::Broadcasted(event), + TransactionEventNonBlock::Error(event) => TransactionEvent::Error(event), + TransactionEventNonBlock::Invalid(event) => TransactionEvent::Invalid(event), + TransactionEventNonBlock::Dropped(event) => TransactionEvent::Dropped(event), + }, + TransactionEventIR::Block(block) => match block { + TransactionEventBlock::Finalized(event) => TransactionEvent::Finalized(event), + TransactionEventBlock::BestChainBlockIncluded(event) => + TransactionEvent::BestChainBlockIncluded(event), + }, + } + } +} From 8ff7cc6babca6cb1af0d16b808f0c71ce3a642df Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 12:51:40 +0300 Subject: [PATCH 03/24] rpc/tx: Add trait for the `transaction` API Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 2 ++ client/rpc-spec-v2/src/transaction/api.rs | 38 +++++++++++++++++++++++ client/rpc-spec-v2/src/transaction/mod.rs | 7 +++++ 3 files changed, 47 insertions(+) create mode 100644 client/rpc-spec-v2/src/transaction/api.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 0924ffa5fb687..6251dec771eb2 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -16,6 +16,8 @@ targets = ["x86_64-unknown-linux-gnu"] jsonrpsee = { version = "0.15.1", features = ["server", "macros"] } # Internal chain structures for "chain_spec". sc-chain-spec = { version = "4.0.0-dev", path = "../chain-spec" } +sp-core = { version = "6.0.0", path = "../../primitives/core" } + serde = "1.0" hex = "0.4" diff --git a/client/rpc-spec-v2/src/transaction/api.rs b/client/rpc-spec-v2/src/transaction/api.rs new file mode 100644 index 0000000000000..c081518b62f5f --- /dev/null +++ b/client/rpc-spec-v2/src/transaction/api.rs @@ -0,0 +1,38 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! API trait for transactions. + +use crate::transaction::event::TransactionEvent; +use jsonrpsee::proc_macros::rpc; +use sp_core::Bytes; + +#[rpc(client, server)] +// This trait bound is required by serde, see `event`. +pub trait TransactionApi { + /// Submit an extrinsic to watch. + /// + /// See [`TransactionEvent`](crate::transaction::event::TransactionEvent) for details on + /// transaction life cycle. + #[subscription( + name = "transaction_unstable_submitAndWatch" => "transaction_unstable_submitExtrinsic", + unsubscribe = "transaction_unstable_unwatch", + item = TransactionEvent, + )] + fn submit_and_watch(&self, bytes: Bytes); +} diff --git a/client/rpc-spec-v2/src/transaction/mod.rs b/client/rpc-spec-v2/src/transaction/mod.rs index 015f288babded..6fc1e117c760f 100644 --- a/client/rpc-spec-v2/src/transaction/mod.rs +++ b/client/rpc-spec-v2/src/transaction/mod.rs @@ -25,4 +25,11 @@ //! //! Methods are prefixed by `transaction`. +pub mod api; pub mod event; + +pub use api::TransactionApiServer; +pub use event::{ + TransactionBlock, TransactionBroadcasted, TransactionDropped, TransactionError, + TransactionEvent, +}; From 4b1431b6f412bb5a18d08ec49fe195aa32ef0b7b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 12:55:50 +0300 Subject: [PATCH 04/24] rpc/tx: Convert RPC errors to transaction events Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 4 + client/rpc-spec-v2/src/transaction/error.rs | 100 ++++++++++++++++++++ client/rpc-spec-v2/src/transaction/mod.rs | 1 + 3 files changed, 105 insertions(+) create mode 100644 client/rpc-spec-v2/src/transaction/error.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 6251dec771eb2..936f2a263ce2b 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -16,7 +16,11 @@ targets = ["x86_64-unknown-linux-gnu"] jsonrpsee = { version = "0.15.1", features = ["server", "macros"] } # Internal chain structures for "chain_spec". sc-chain-spec = { version = "4.0.0-dev", path = "../chain-spec" } +# Pool for submitting extrinsics required by "transaction" +sc-transaction-pool-api = { version = "4.0.0-dev", path = "../transaction-pool/api" } sp-core = { version = "6.0.0", path = "../../primitives/core" } +sp-runtime = { version = "6.0.0", path = "../../primitives/runtime" } +thiserror = "1.0" serde = "1.0" hex = "0.4" diff --git a/client/rpc-spec-v2/src/transaction/error.rs b/client/rpc-spec-v2/src/transaction/error.rs new file mode 100644 index 0000000000000..72a5959992f9e --- /dev/null +++ b/client/rpc-spec-v2/src/transaction/error.rs @@ -0,0 +1,100 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Transaction RPC errors. +//! +//! Errors are interpreted as transaction events for subscriptions. + +use crate::transaction::event::{TransactionError, TransactionEvent}; +use sc_transaction_pool_api::error::Error as PoolError; +use sp_runtime::transaction_validity::InvalidTransaction; + +/// Transaction RPC errors. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Transaction pool error. + #[error("Transaction pool error: {}", .0)] + Pool(#[from] PoolError), + /// Verification error. + #[error("Extrinsic verification error: {}", .0)] + Verification(Box), +} + +impl From for TransactionEvent { + fn from(e: Error) -> Self { + match e { + Error::Verification(e) => TransactionEvent::Invalid(TransactionError { + error: format!("Verification error: {}", e), + }), + Error::Pool(PoolError::InvalidTransaction(InvalidTransaction::Custom(e))) => + TransactionEvent::Invalid(TransactionError { + error: format!("Invalid transaction with custom error: {}", e), + }), + Error::Pool(PoolError::InvalidTransaction(e)) => { + let msg: &str = e.into(); + TransactionEvent::Invalid(TransactionError { + error: format!("Invalid transaction: {}", msg), + }) + }, + Error::Pool(PoolError::UnknownTransaction(e)) => { + let msg: &str = e.into(); + TransactionEvent::Invalid(TransactionError { + error: format!("Unknown transaction validity: {}", msg), + }) + }, + Error::Pool(PoolError::TemporarilyBanned) => + TransactionEvent::Invalid(TransactionError { + error: "Transaction is temporarily banned".into(), + }), + Error::Pool(PoolError::AlreadyImported(_)) => + TransactionEvent::Invalid(TransactionError { + error: "Transaction is already imported".into(), + }), + Error::Pool(PoolError::TooLowPriority { old, new }) => + TransactionEvent::Invalid(TransactionError { + error: format!( + "The priority of the transactin is too low (pool {} > current {})", + old, new + ), + }), + Error::Pool(PoolError::CycleDetected) => TransactionEvent::Invalid(TransactionError { + error: "The transaction contains a cyclic dependency".into(), + }), + Error::Pool(PoolError::ImmediatelyDropped) => + TransactionEvent::Invalid(TransactionError { + error: "The transaction could not enter the pool because of the limit".into(), + }), + Error::Pool(PoolError::Unactionable) => TransactionEvent::Invalid(TransactionError { + error: "Transaction cannot be propagated and the local node does not author blocks" + .into(), + }), + Error::Pool(PoolError::NoTagsProvided) => TransactionEvent::Invalid(TransactionError { + error: "Transaction does not provide any tags, so the pool cannot identify it" + .into(), + }), + Error::Pool(PoolError::InvalidBlockId(_)) => + TransactionEvent::Invalid(TransactionError { + error: "The provided block ID is not valid".into(), + }), + Error::Pool(PoolError::RejectedFutureTransaction) => + TransactionEvent::Invalid(TransactionError { + error: "The pool is not accepting future transactions".into(), + }), + } + } +} diff --git a/client/rpc-spec-v2/src/transaction/mod.rs b/client/rpc-spec-v2/src/transaction/mod.rs index 6fc1e117c760f..1dda377b86288 100644 --- a/client/rpc-spec-v2/src/transaction/mod.rs +++ b/client/rpc-spec-v2/src/transaction/mod.rs @@ -26,6 +26,7 @@ //! Methods are prefixed by `transaction`. pub mod api; +pub mod error; pub mod event; pub use api::TransactionApiServer; From e6680e4c34ee085618c796aec62448ffbdf31930 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 13:00:35 +0300 Subject: [PATCH 05/24] rpc/tx: Implement `transaction` RPC methods Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/Cargo.toml | 5 +- client/rpc-spec-v2/src/lib.rs | 3 + client/rpc-spec-v2/src/transaction/mod.rs | 2 + .../src/transaction/transaction.rs | 91 +++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 client/rpc-spec-v2/src/transaction/transaction.rs diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml index 936f2a263ce2b..885d415eb50d2 100644 --- a/client/rpc-spec-v2/Cargo.toml +++ b/client/rpc-spec-v2/Cargo.toml @@ -20,10 +20,13 @@ sc-chain-spec = { version = "4.0.0-dev", path = "../chain-spec" } sc-transaction-pool-api = { version = "4.0.0-dev", path = "../transaction-pool/api" } sp-core = { version = "6.0.0", path = "../../primitives/core" } sp-runtime = { version = "6.0.0", path = "../../primitives/runtime" } +sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } +sp-blockchain = { version = "4.0.0-dev", path = "../../primitives/blockchain" } +codec = { package = "parity-scale-codec", version = "3.0.0" } thiserror = "1.0" - serde = "1.0" hex = "0.4" +futures = "0.3.21" [dev-dependencies] serde_json = "1.0" diff --git a/client/rpc-spec-v2/src/lib.rs b/client/rpc-spec-v2/src/lib.rs index 793422dd6eb18..f4b9d2f95bf97 100644 --- a/client/rpc-spec-v2/src/lib.rs +++ b/client/rpc-spec-v2/src/lib.rs @@ -25,3 +25,6 @@ pub mod chain_spec; pub mod transaction; + +/// Task executor that is being used by RPC subscriptions. +pub type SubscriptionTaskExecutor = std::sync::Arc; diff --git a/client/rpc-spec-v2/src/transaction/mod.rs b/client/rpc-spec-v2/src/transaction/mod.rs index 1dda377b86288..bb983894a428c 100644 --- a/client/rpc-spec-v2/src/transaction/mod.rs +++ b/client/rpc-spec-v2/src/transaction/mod.rs @@ -28,9 +28,11 @@ pub mod api; pub mod error; pub mod event; +pub mod transaction; pub use api::TransactionApiServer; pub use event::{ TransactionBlock, TransactionBroadcasted, TransactionDropped, TransactionError, TransactionEvent, }; +pub use transaction::Transaction; diff --git a/client/rpc-spec-v2/src/transaction/transaction.rs b/client/rpc-spec-v2/src/transaction/transaction.rs new file mode 100644 index 0000000000000..9c3c29232320b --- /dev/null +++ b/client/rpc-spec-v2/src/transaction/transaction.rs @@ -0,0 +1,91 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! API implementation for submitting transactions. + +use crate::{ + transaction::{ + api::TransactionApiServer, + error::Error, + event::{TransactionBlock, TransactionBroadcasted, TransactionError, TransactionEvent}, + }, + SubscriptionTaskExecutor, +}; +use jsonrpsee::{core::async_trait, types::SubscriptionResult, SubscriptionSink}; +use sc_transaction_pool_api::{ + error::IntoPoolError, BlockHash, TransactionFor, TransactionPool, TransactionSource, + TransactionStatus, +}; +use std::sync::Arc; + +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_core::Bytes; +use sp_runtime::{generic, traits::Block as BlockT}; + +use codec::Decode; +use futures::{FutureExt, StreamExt, TryFutureExt}; +use jsonrpsee::types::error::CallError; + +/// An API for transaction RPC calls. +pub struct Transaction { + /// Substrate client. + client: Arc, + /// Transactions pool. + pool: Arc, + /// Executor to spawn subscriptions. + executor: SubscriptionTaskExecutor, +} + +impl Transaction { + /// Creates a new [`Transaction`]. + pub fn new(client: Arc, pool: Arc, executor: SubscriptionTaskExecutor) -> Self { + Transaction { client, pool, executor } + } +} + +/// Currently we treat all RPC transactions as externals. +/// +/// Possibly in the future we could allow opt-in for special treatment +/// of such transactions, so that the block authors can inject +/// some unique transactions via RPC and have them included in the pool. +const TX_SOURCE: TransactionSource = TransactionSource::External; + +#[async_trait] +impl TransactionApiServer> for Transaction +where + Pool: TransactionPool + Sync + Send + 'static, + Pool::Hash: Unpin, + ::Hash: Unpin, + Client: HeaderBackend + ProvideRuntimeApi + Send + Sync + 'static, +{ + fn submit_and_watch(&self, mut sink: SubscriptionSink, xt: Bytes) -> SubscriptionResult { + // This is the only place where the RPC server can return an error for this + // subscription. Other defects must be signaled as events to the sink. + let decoded_extrinsic = match TransactionFor::::decode(&mut &xt[..]) { + Ok(decoded_extrinsic) => decoded_extrinsic, + Err(e) => { + let err = CallError::Failed(e.into()); + let _ = sink.reject(err); + return Ok(()) + }, + }; + + Ok(()) + } +} From b0a1e4f134f70682eb36e08b90f8c7b02155c109 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 13:12:18 +0300 Subject: [PATCH 06/24] tx-pool: Propagate tx index to events Signed-off-by: Alexandru Vasile --- client/transaction-pool/api/src/lib.rs | 11 +++++++---- client/transaction-pool/src/graph/listener.rs | 13 +++++++++---- client/transaction-pool/src/graph/watcher.rs | 8 ++++---- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/client/transaction-pool/api/src/lib.rs b/client/transaction-pool/api/src/lib.rs index 0ebb8f9d4cd9c..9dce829c587b3 100644 --- a/client/transaction-pool/api/src/lib.rs +++ b/client/transaction-pool/api/src/lib.rs @@ -108,15 +108,16 @@ pub enum TransactionStatus { Ready, /// The transaction has been broadcast to the given peers. Broadcast(Vec), - /// Transaction has been included in block with given hash. - InBlock(BlockHash), + /// Transaction has been included in block with given hash + /// at the given position. + InBlock((BlockHash, TxIndex)), /// The block this transaction was included in has been retracted. Retracted(BlockHash), /// Maximum number of finality watchers has been reached, /// old watchers are being removed. FinalityTimeout(BlockHash), - /// Transaction has been finalized by a finality-gadget, e.g GRANDPA - Finalized(BlockHash), + /// Transaction has been finalized by a finality-gadget, e.g GRANDPA. + Finalized((BlockHash, TxIndex)), /// Transaction has been replaced in the pool, by another transaction /// that provides the same tags. (e.g. same (sender, nonce)). Usurped(Hash), @@ -143,6 +144,8 @@ pub type TransactionFor

= <

::Block as BlockT>::Extrinsi pub type TransactionStatusStreamFor

= TransactionStatusStream, BlockHash

>; /// Transaction type for a local pool. pub type LocalTransactionFor

= <

::Block as BlockT>::Extrinsic; +/// Transaction's index within the block in which it was included. +pub type TxIndex = usize; /// Typical future type used in transaction pool api. pub type PoolFuture = std::pin::Pin> + Send>>; diff --git a/client/transaction-pool/src/graph/listener.rs b/client/transaction-pool/src/graph/listener.rs index d4f42b32fdbb8..edb4a4c29bd9e 100644 --- a/client/transaction-pool/src/graph/listener.rs +++ b/client/transaction-pool/src/graph/listener.rs @@ -104,8 +104,13 @@ impl Listener { /// Transaction was pruned from the pool. pub fn pruned(&mut self, block_hash: BlockHash, tx: &H) { debug!(target: "txpool", "[{:?}] Pruned at {:?}", tx, block_hash); - self.fire(tx, |s| s.in_block(block_hash)); - self.finality_watchers.entry(block_hash).or_insert(vec![]).push(tx.clone()); + // Get the transactions included in the given block hash. + let txs = self.finality_watchers.entry(block_hash).or_insert(vec![]); + txs.push(tx.clone()); + // Current transaction is the last one included. + let tx_index = txs.len() - 1; + + self.fire(tx, |s| s.in_block(block_hash, tx_index)); while self.finality_watchers.len() > MAX_FINALITY_WATCHERS { if let Some((hash, txs)) = self.finality_watchers.pop_front() { @@ -128,9 +133,9 @@ impl Listener { /// Notify all watchers that transactions have been finalized pub fn finalized(&mut self, block_hash: BlockHash) { if let Some(hashes) = self.finality_watchers.remove(&block_hash) { - for hash in hashes { + for (tx_index, hash) in hashes.into_iter().enumerate() { log::debug!(target: "txpool", "[{:?}] Sent finalization event (block {:?})", hash, block_hash); - self.fire(&hash, |s| s.finalized(block_hash)) + self.fire(&hash, |s| s.finalized(block_hash, tx_index)) } } } diff --git a/client/transaction-pool/src/graph/watcher.rs b/client/transaction-pool/src/graph/watcher.rs index 8cd78cfc78240..0613300c8684b 100644 --- a/client/transaction-pool/src/graph/watcher.rs +++ b/client/transaction-pool/src/graph/watcher.rs @@ -84,13 +84,13 @@ impl Sender { } /// Extrinsic has been included in block with given hash. - pub fn in_block(&mut self, hash: BH) { - self.send(TransactionStatus::InBlock(hash)); + pub fn in_block(&mut self, hash: BH, index: usize) { + self.send(TransactionStatus::InBlock((hash, index))); } /// Extrinsic has been finalized by a finality gadget. - pub fn finalized(&mut self, hash: BH) { - self.send(TransactionStatus::Finalized(hash)); + pub fn finalized(&mut self, hash: BH, index: usize) { + self.send(TransactionStatus::Finalized((hash, index))); self.is_finalized = true; } From 83880e8a73f01f9eb85a0ebe8302c4a303e3f0b4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 20 Sep 2022 18:09:33 +0300 Subject: [PATCH 07/24] tx-pool: Adjust testing to reflect tx index in events Signed-off-by: Alexandru Vasile --- client/transaction-pool/src/graph/pool.rs | 4 +-- client/transaction-pool/tests/pool.rs | 41 ++++++++++++----------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/client/transaction-pool/src/graph/pool.rs b/client/transaction-pool/src/graph/pool.rs index 19acbddbe7843..108ae791e37b3 100644 --- a/client/transaction-pool/src/graph/pool.rs +++ b/client/transaction-pool/src/graph/pool.rs @@ -770,7 +770,7 @@ mod tests { assert_eq!(stream.next(), Some(TransactionStatus::Ready)); assert_eq!( stream.next(), - Some(TransactionStatus::InBlock(H256::from_low_u64_be(2).into())), + Some(TransactionStatus::InBlock((H256::from_low_u64_be(2).into(), 0))), ); } @@ -803,7 +803,7 @@ mod tests { assert_eq!(stream.next(), Some(TransactionStatus::Ready)); assert_eq!( stream.next(), - Some(TransactionStatus::InBlock(H256::from_low_u64_be(2).into())), + Some(TransactionStatus::InBlock((H256::from_low_u64_be(2).into(), 0))), ); } diff --git a/client/transaction-pool/tests/pool.rs b/client/transaction-pool/tests/pool.rs index f04a27cf81e1d..be75523c1230f 100644 --- a/client/transaction-pool/tests/pool.rs +++ b/client/transaction-pool/tests/pool.rs @@ -328,7 +328,7 @@ fn should_revalidate_across_many_blocks() { block_on( watcher1 - .take_while(|s| future::ready(*s != TransactionStatus::InBlock(block_hash))) + .take_while(|s| future::ready(*s != TransactionStatus::InBlock((block_hash, 0)))) .collect::>(), ); @@ -398,24 +398,24 @@ fn should_push_watchers_during_maintenance() { futures::executor::block_on_stream(watcher0).collect::>(), vec![ TransactionStatus::Ready, - TransactionStatus::InBlock(header_hash), - TransactionStatus::Finalized(header_hash) + TransactionStatus::InBlock((header_hash, 0)), + TransactionStatus::Finalized((header_hash, 0)) ], ); assert_eq!( futures::executor::block_on_stream(watcher1).collect::>(), vec![ TransactionStatus::Ready, - TransactionStatus::InBlock(header_hash), - TransactionStatus::Finalized(header_hash) + TransactionStatus::InBlock((header_hash, 1)), + TransactionStatus::Finalized((header_hash, 1)) ], ); assert_eq!( futures::executor::block_on_stream(watcher2).collect::>(), vec![ TransactionStatus::Ready, - TransactionStatus::InBlock(header_hash), - TransactionStatus::Finalized(header_hash) + TransactionStatus::InBlock((header_hash, 2)), + TransactionStatus::Finalized((header_hash, 2)) ], ); } @@ -450,8 +450,8 @@ fn finalization() { let mut stream = futures::executor::block_on_stream(watcher); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(header.hash()))); - assert_eq!(stream.next(), Some(TransactionStatus::Finalized(header.hash()))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((header.hash(), 0)))); + assert_eq!(stream.next(), Some(TransactionStatus::Finalized((header.hash(), 0)))); assert_eq!(stream.next(), None); } @@ -573,30 +573,31 @@ fn fork_aware_finalization() { for (canon_watcher, h) in canon_watchers { let mut stream = futures::executor::block_on_stream(canon_watcher); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(h))); - assert_eq!(stream.next(), Some(TransactionStatus::Finalized(h))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((h, 0)))); + assert_eq!(stream.next(), Some(TransactionStatus::Finalized((h, 0)))); assert_eq!(stream.next(), None); } { let mut stream = futures::executor::block_on_stream(from_dave_watcher); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(c2))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((c2, 0)))); assert_eq!(stream.next(), Some(TransactionStatus::Retracted(c2))); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(e1))); - assert_eq!(stream.next(), Some(TransactionStatus::Finalized(e1))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((e1, 0)))); + assert_eq!(stream.next(), Some(TransactionStatus::Finalized((e1, 0)))); assert_eq!(stream.next(), None); } { let mut stream = futures::executor::block_on_stream(from_bob_watcher); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(d2))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((d2, 0)))); assert_eq!(stream.next(), Some(TransactionStatus::Retracted(d2))); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(e1))); - assert_eq!(stream.next(), Some(TransactionStatus::Finalized(e1))); + // In block e1 we submitted: [dave, bob] xts in this order. + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((e1, 1)))); + assert_eq!(stream.next(), Some(TransactionStatus::Finalized((e1, 1)))); assert_eq!(stream.next(), None); } } @@ -646,10 +647,10 @@ fn prune_and_retract_tx_at_same_time() { { let mut stream = futures::executor::block_on_stream(watcher); assert_eq!(stream.next(), Some(TransactionStatus::Ready)); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(b1))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((b1, 0)))); assert_eq!(stream.next(), Some(TransactionStatus::Retracted(b1))); - assert_eq!(stream.next(), Some(TransactionStatus::InBlock(b2))); - assert_eq!(stream.next(), Some(TransactionStatus::Finalized(b2))); + assert_eq!(stream.next(), Some(TransactionStatus::InBlock((b2, 0)))); + assert_eq!(stream.next(), Some(TransactionStatus::Finalized((b2, 0)))); assert_eq!(stream.next(), None); } } From a67aa03c0899209a45d696900e1a4d471858deef Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 13:18:22 +0300 Subject: [PATCH 08/24] rpc/tx: Convert tx-pool events for the new RPC spec Signed-off-by: Alexandru Vasile --- Cargo.lock | 9 ++ .../src/transaction/transaction.rs | 96 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5172c3d02861d..24c0296021fc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8775,10 +8775,19 @@ dependencies = [ name = "sc-rpc-spec-v2" version = "0.10.0-dev" dependencies = [ + "futures", "hex", "jsonrpsee", + "parity-scale-codec", "sc-chain-spec", + "sc-transaction-pool-api", + "serde", "serde_json", + "sp-api", + "sp-blockchain", + "sp-core", + "sp-runtime", + "thiserror", "tokio", ] diff --git a/client/rpc-spec-v2/src/transaction/transaction.rs b/client/rpc-spec-v2/src/transaction/transaction.rs index 9c3c29232320b..d3a9d76f2adf4 100644 --- a/client/rpc-spec-v2/src/transaction/transaction.rs +++ b/client/rpc-spec-v2/src/transaction/transaction.rs @@ -86,6 +86,102 @@ where }, }; + let best_block_hash = self.client.info().best_hash; + + let submit = self + .pool + .submit_and_watch( + &generic::BlockId::hash(best_block_hash), + TX_SOURCE, + decoded_extrinsic, + ) + .map_err(|e| { + e.into_pool_error() + .map(Error::from) + .unwrap_or_else(|e| Error::Verification(Box::new(e))) + }); + + let fut = async move { + match submit.await { + Ok(stream) => { + let mut state = TransactionState::new(); + let stream = + stream.filter_map(|event| async move { state.handle_event(event) }); + sink.pipe_from_stream(stream.boxed()).await; + }, + Err(err) => { + // We have not created an `Watcher` for the tx. Make sure the + // error is still propagated as an event. + let event: TransactionEvent<::Hash> = err.into(); + sink.pipe_from_stream(futures::stream::iter(vec![event]).boxed()).await; + }, + }; + }; + + self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed()); Ok(()) } } + +/// The transaction's state that needs to be preserved between +/// multiple events generated by the transaction-pool. +/// +/// # Note +/// +/// In the future, the RPC server can submit only the last event when multiple +/// identical events happen in a row. +#[derive(Clone, Copy)] +struct TransactionState { + /// True if the transaction was previously broadcasted. + broadcasted: bool, +} + +impl TransactionState { + /// Construct a new [`TransactionState`]. + pub fn new() -> Self { + TransactionState { broadcasted: false } + } + + /// Handle events generated by the transaction-pool and convert them + /// to the new API expected state. + #[inline] + pub fn handle_event( + &mut self, + event: TransactionStatus, + ) -> Option> { + match event { + TransactionStatus::Ready | TransactionStatus::Future => + Some(TransactionEvent::::Validated), + TransactionStatus::Broadcast(peers) => { + // Set the broadcasted flag once if we submitted the transaction to + // at least one peer. + self.broadcasted = self.broadcasted || !peers.is_empty(); + + Some(TransactionEvent::Broadcasted(TransactionBroadcasted { + num_peers: peers.len(), + })) + }, + TransactionStatus::InBlock((hash, index)) => + Some(TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock { + hash, + index, + }))), + TransactionStatus::Retracted(_) => Some(TransactionEvent::BestChainBlockIncluded(None)), + TransactionStatus::FinalityTimeout(_) => + Some(TransactionEvent::Invalid(TransactionError { + error: "Maximum number of finality watchers has been reached".into(), + })), + TransactionStatus::Finalized((hash, index)) => + Some(TransactionEvent::Finalized(TransactionBlock { hash, index })), + TransactionStatus::Usurped(_) => Some(TransactionEvent::Invalid(TransactionError { + error: "Extrinsic was rendered invalid by another extrinsic".into(), + })), + TransactionStatus::Dropped => Some(TransactionEvent::Invalid(TransactionError { + error: "Extrinsic dropped from the pool due to exceeding limits".into(), + })), + TransactionStatus::Invalid => Some(TransactionEvent::Invalid(TransactionError { + error: "Extrinsic marked as invalid".into(), + })), + } + } +} From 9aa5dcb816f8acf994c65982967f198929f36400 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 13:48:05 +0300 Subject: [PATCH 09/24] rpc/tx: Convert tx-pool `FinalityTimeout` event to `Dropped` Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/transaction.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/transaction.rs b/client/rpc-spec-v2/src/transaction/transaction.rs index d3a9d76f2adf4..f59ae82da98cf 100644 --- a/client/rpc-spec-v2/src/transaction/transaction.rs +++ b/client/rpc-spec-v2/src/transaction/transaction.rs @@ -22,7 +22,10 @@ use crate::{ transaction::{ api::TransactionApiServer, error::Error, - event::{TransactionBlock, TransactionBroadcasted, TransactionError, TransactionEvent}, + event::{ + TransactionBlock, TransactionBroadcasted, TransactionDropped, TransactionError, + TransactionEvent, + }, }, SubscriptionTaskExecutor, }; @@ -168,7 +171,8 @@ impl TransactionState { }))), TransactionStatus::Retracted(_) => Some(TransactionEvent::BestChainBlockIncluded(None)), TransactionStatus::FinalityTimeout(_) => - Some(TransactionEvent::Invalid(TransactionError { + Some(TransactionEvent::Dropped(TransactionDropped { + broadcasted: self.broadcasted, error: "Maximum number of finality watchers has been reached".into(), })), TransactionStatus::Finalized((hash, index)) => From e5c3d29b297d1503250c411a7876cf686436f791 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 13:53:46 +0300 Subject: [PATCH 10/24] service: Enable the `transaction` API Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/service/Cargo.toml | 1 + client/service/src/builder.rs | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 24c0296021fc7..86bbdaa8434b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8839,6 +8839,7 @@ dependencies = [ "sc-offchain", "sc-rpc", "sc-rpc-server", + "sc-rpc-spec-v2", "sc-sysinfo", "sc-telemetry", "sc-tracing", diff --git a/client/service/Cargo.toml b/client/service/Cargo.toml index 3c574ef13c8e6..2b528d4ca1e0a 100644 --- a/client/service/Cargo.toml +++ b/client/service/Cargo.toml @@ -68,6 +68,7 @@ sc-transaction-pool-api = { version = "4.0.0-dev", path = "../transaction-pool/a sp-transaction-storage-proof = { version = "4.0.0-dev", path = "../../primitives/transaction-storage-proof" } sc-rpc-server = { version = "4.0.0-dev", path = "../rpc-servers" } sc-rpc = { version = "4.0.0-dev", path = "../rpc" } +sc-rpc-spec-v2 = { version = "0.10.0-dev", path = "../rpc-spec-v2" } sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" } sp-block-builder = { version = "4.0.0-dev", path = "../../primitives/block-builder" } sc-informant = { version = "0.10.0-dev", path = "../informant" } diff --git a/client/service/src/builder.rs b/client/service/src/builder.rs index 5a2f4cf978b41..7674c484095cd 100644 --- a/client/service/src/builder.rs +++ b/client/service/src/builder.rs @@ -56,6 +56,7 @@ use sc_rpc::{ system::SystemApiServer, DenyUnsafe, SubscriptionTaskExecutor, }; +use sc_rpc_spec_v2::transaction::TransactionApiServer; use sc_telemetry::{telemetry, ConnectionMessage, Telemetry, TelemetryHandle, SUBSTRATE_INFO}; use sc_transaction_pool_api::MaintainedTransactionPool; use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender}; @@ -665,6 +666,13 @@ where (chain, state, child_state) }; + let transaction = sc_rpc_spec_v2::transaction::Transaction::new( + client.clone(), + transaction_pool.clone(), + task_executor.clone(), + ) + .into_rpc(); + let author = sc_rpc::author::Author::new( client.clone(), transaction_pool, @@ -683,6 +691,7 @@ where } rpc_api.merge(chain).map_err(|e| Error::Application(e.into()))?; + rpc_api.merge(transaction).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(author).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(system).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(state).map_err(|e| Error::Application(e.into()))?; From 0ac07b25f877329ecd8e492d33ea9939f8136dd5 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 13:56:29 +0300 Subject: [PATCH 11/24] rpc/tx: Add tests for tx event encoding and decoding Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 106 ++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 09f706d2e567a..4ef24a8bf79b0 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -221,3 +221,109 @@ impl From> for TransactionEvent { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validated_event() { + let event: TransactionEvent<()> = TransactionEvent::Validated; + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"validated"}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn broadcasted_event() { + let event: TransactionEvent<()> = + TransactionEvent::Broadcasted(TransactionBroadcasted { num_peers: 2 }); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"broadcasted","numPeers":2}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn best_chain_event() { + let event: TransactionEvent<()> = TransactionEvent::BestChainBlockIncluded(None); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"bestChainBlockIncluded","block":null}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + + let event: TransactionEvent = + TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock { hash: 1, index: 2 })); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":1,"index":2}}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn finalized_event() { + let event: TransactionEvent = + TransactionEvent::Finalized(TransactionBlock { hash: 1, index: 2 }); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"finalized","block":{"hash":1,"index":2}}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn error_event() { + let event: TransactionEvent<()> = + TransactionEvent::Error(TransactionError { error: "abc".to_string() }); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"error","error":"abc"}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn invalid_event() { + let event: TransactionEvent<()> = + TransactionEvent::Invalid(TransactionError { error: "abc".to_string() }); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"invalid","error":"abc"}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } + + #[test] + fn dropped_event() { + let event: TransactionEvent<()> = TransactionEvent::Dropped(TransactionDropped { + broadcasted: true, + error: "abc".to_string(), + }); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"event":"dropped","broadcasted":true,"error":"abc"}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, event); + } +} From b8ea4cc8bb9626b127b0c3c82d65f0b4970fbd63 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 22 Sep 2022 14:30:36 +0300 Subject: [PATCH 12/24] tx: Add indentation for subscriptions Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/api.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/api.rs b/client/rpc-spec-v2/src/transaction/api.rs index c081518b62f5f..2c42275b11040 100644 --- a/client/rpc-spec-v2/src/transaction/api.rs +++ b/client/rpc-spec-v2/src/transaction/api.rs @@ -30,9 +30,9 @@ pub trait TransactionApi { /// See [`TransactionEvent`](crate::transaction::event::TransactionEvent) for details on /// transaction life cycle. #[subscription( - name = "transaction_unstable_submitAndWatch" => "transaction_unstable_submitExtrinsic", - unsubscribe = "transaction_unstable_unwatch", - item = TransactionEvent, + name = "transaction_unstable_submitAndWatch" => "transaction_unstable_submitExtrinsic", + unsubscribe = "transaction_unstable_unwatch", + item = TransactionEvent, )] fn submit_and_watch(&self, bytes: Bytes); } From 67effb6779d1ea3c1d5f954b2083e60b218109af Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 23 Sep 2022 16:23:45 +0300 Subject: [PATCH 13/24] rpc/tx: Fix documentation Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 4ef24a8bf79b0..daf87ed53137a 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -88,7 +88,7 @@ pub struct TransactionDropped { /// - `Error` /// /// The subscription's stream is considered finished whenever the following events are -/// received: `Finalized`, `Error`, `Invalid` or `Dropped. However, the user is allowed +/// received: `Finalized`, `Error`, `Invalid` or `Dropped`. However, the user is allowed /// to unsubscribe at any moment. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // We need to manually specify the trait bounds for the `Hash` trait to ensure `into` and @@ -110,7 +110,7 @@ pub enum TransactionEvent { /// This may contain `None` if the block is no longer a best /// block of the chain. BestChainBlockIncluded(Option>), - /// The transaction was included in a finialized block. + /// The transaction was included in a finalized block. Finalized(TransactionBlock), /// The transaction could not be processed due to an error. Error(TransactionError), @@ -125,7 +125,7 @@ pub enum TransactionEvent { /// /// The block events require a JSON compatible interpretation similar to: /// -/// ```sh +/// ```json /// { event: "EVENT", block: { hash: "0xFF", index: 0 } } /// ``` /// @@ -147,7 +147,7 @@ pub(crate) enum TransactionEventBlock { /// /// The non-block events require a JSON compatible interpretation similar to: /// -/// ```sh +/// ```json /// { event: "EVENT", num_peers: 0 } /// ``` /// From 5952d977c1a6d2b0954440903ae50637764783de Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 23 Sep 2022 19:41:33 +0300 Subject: [PATCH 14/24] rpc/tx: Serialize usize to hex Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 32 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index daf87ed53137a..613cc25875181 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -34,6 +34,7 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct TransactionBroadcasted { /// The number of peers the transaction was broadcasted to. + #[serde(with = "as_hex")] pub num_peers: usize, } @@ -44,6 +45,7 @@ pub struct TransactionBlock { /// The hash of the block the transaction was included into. pub hash: Hash, /// The index (zero-based) of the transaction within the body of the block. + #[serde(with = "as_hex")] pub index: usize, } @@ -222,6 +224,30 @@ impl From> for TransactionEvent { } } +/// Serialize and deserialize to hexadecimal representation. +mod as_hex { + use serde::{Deserializer, Serializer}; + use sp_core::U256; + + pub fn serialize(data: &T, serializer: S) -> Result + where + S: Serializer, + T: Copy + Into + TryFrom, + { + let u256: U256 = (*data).into(); + serde::Serialize::serialize(&u256, serializer) + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: Copy + Into + TryFrom, + { + let u256: U256 = serde::Deserialize::deserialize(deserializer)?; + TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed")) + } +} + #[cfg(test)] mod tests { use super::*; @@ -266,7 +292,7 @@ mod tests { TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock { hash: 1, index: 2 })); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":1,"index":2}}"#; + let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":1,"index":"0x2"}}"#; assert_eq!(ser, exp); let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); @@ -276,10 +302,10 @@ mod tests { #[test] fn finalized_event() { let event: TransactionEvent = - TransactionEvent::Finalized(TransactionBlock { hash: 1, index: 2 }); + TransactionEvent::Finalized(TransactionBlock { hash: 1, index: 10 }); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"finalized","block":{"hash":1,"index":2}}"#; + let exp = r#"{"event":"finalized","block":{"hash":1,"index":"0xa"}}"#; assert_eq!(ser, exp); let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); From 5e272e37d7a137118f447b3502f3682e3ad574ce Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 26 Sep 2022 11:31:34 +0300 Subject: [PATCH 15/24] tx-pool: Rename closure parameters Signed-off-by: Alexandru Vasile --- client/transaction-pool/src/graph/listener.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/transaction-pool/src/graph/listener.rs b/client/transaction-pool/src/graph/listener.rs index edb4a4c29bd9e..776749abf2d5d 100644 --- a/client/transaction-pool/src/graph/listener.rs +++ b/client/transaction-pool/src/graph/listener.rs @@ -110,12 +110,12 @@ impl Listener { // Current transaction is the last one included. let tx_index = txs.len() - 1; - self.fire(tx, |s| s.in_block(block_hash, tx_index)); + self.fire(tx, |watcher| watcher.in_block(block_hash, tx_index)); while self.finality_watchers.len() > MAX_FINALITY_WATCHERS { if let Some((hash, txs)) = self.finality_watchers.pop_front() { for tx in txs { - self.fire(&tx, |s| s.finality_timeout(hash)); + self.fire(&tx, |watcher| watcher.finality_timeout(hash)); } } } @@ -125,7 +125,7 @@ impl Listener { pub fn retracted(&mut self, block_hash: BlockHash) { if let Some(hashes) = self.finality_watchers.remove(&block_hash) { for hash in hashes { - self.fire(&hash, |s| s.retracted(block_hash)) + self.fire(&hash, |watcher| watcher.retracted(block_hash)) } } } @@ -135,7 +135,7 @@ impl Listener { if let Some(hashes) = self.finality_watchers.remove(&block_hash) { for (tx_index, hash) in hashes.into_iter().enumerate() { log::debug!(target: "txpool", "[{:?}] Sent finalization event (block {:?})", hash, block_hash); - self.fire(&hash, |s| s.finalized(block_hash, tx_index)) + self.fire(&hash, |watcher| watcher.finalized(block_hash, tx_index)) } } } From 6d6d23bb31ba3e8e0f0e3faaa566f84537f38e2e Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 26 Sep 2022 11:34:55 +0300 Subject: [PATCH 16/24] service: Separate RPC spec versions Signed-off-by: Alexandru Vasile --- client/service/src/builder.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/service/src/builder.rs b/client/service/src/builder.rs index 7674c484095cd..0e16cb144dc5e 100644 --- a/client/service/src/builder.rs +++ b/client/service/src/builder.rs @@ -666,7 +666,7 @@ where (chain, state, child_state) }; - let transaction = sc_rpc_spec_v2::transaction::Transaction::new( + let transaction_v2 = sc_rpc_spec_v2::transaction::Transaction::new( client.clone(), transaction_pool.clone(), task_executor.clone(), @@ -690,8 +690,11 @@ where rpc_api.merge(offchain).map_err(|e| Error::Application(e.into()))?; } + // Part of the RPC v2 spec. + rpc_api.merge(transaction_v2).map_err(|e| Error::Application(e.into()))?; + + // Part of the old RPC spec. rpc_api.merge(chain).map_err(|e| Error::Application(e.into()))?; - rpc_api.merge(transaction).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(author).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(system).map_err(|e| Error::Application(e.into()))?; rpc_api.merge(state).map_err(|e| Error::Application(e.into()))?; From f90e08a2c8ae4ad8e74027a8d2d0726e6036b015 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 26 Sep 2022 14:10:26 +0300 Subject: [PATCH 17/24] rpc/tx: Use `H256` for testing block's hash Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 24 +++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 613cc25875181..79c32c53d641a 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -251,6 +251,7 @@ mod as_hex { #[cfg(test)] mod tests { use super::*; + use sp_core::H256; #[test] fn validated_event() { @@ -270,7 +271,7 @@ mod tests { TransactionEvent::Broadcasted(TransactionBroadcasted { num_peers: 2 }); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"broadcasted","numPeers":2}"#; + let exp = r#"{"event":"broadcasted","numPeers":"0x2"}"#; assert_eq!(ser, exp); let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); @@ -288,27 +289,32 @@ mod tests { let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); assert_eq!(event_dec, event); - let event: TransactionEvent = - TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock { hash: 1, index: 2 })); + let event: TransactionEvent = + TransactionEvent::BestChainBlockIncluded(Some(TransactionBlock { + hash: H256::from_low_u64_be(1), + index: 2, + })); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":1,"index":"0x2"}}"#; + let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":"0x2"}}"#; assert_eq!(ser, exp); - let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); + let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); assert_eq!(event_dec, event); } #[test] fn finalized_event() { - let event: TransactionEvent = - TransactionEvent::Finalized(TransactionBlock { hash: 1, index: 10 }); + let event: TransactionEvent = TransactionEvent::Finalized(TransactionBlock { + hash: H256::from_low_u64_be(1), + index: 10, + }); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"finalized","block":{"hash":1,"index":"0xa"}}"#; + let exp = r#"{"event":"finalized","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":"0xa"}}"#; assert_eq!(ser, exp); - let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); + let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); assert_eq!(event_dec, event); } From bf120a9e72ac727fc3d6e78b791858c620a40511 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 26 Sep 2022 16:06:28 +0000 Subject: [PATCH 18/24] rpc/tx: Serialize numbers as string Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 38 ++++++++------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 79c32c53d641a..72a89b6d5f714 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -34,7 +34,7 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct TransactionBroadcasted { /// The number of peers the transaction was broadcasted to. - #[serde(with = "as_hex")] + #[serde(with = "as_string")] pub num_peers: usize, } @@ -45,7 +45,7 @@ pub struct TransactionBlock { /// The hash of the block the transaction was included into. pub hash: Hash, /// The index (zero-based) of the transaction within the body of the block. - #[serde(with = "as_hex")] + #[serde(with = "as_string")] pub index: usize, } @@ -224,27 +224,19 @@ impl From> for TransactionEvent { } } -/// Serialize and deserialize to hexadecimal representation. -mod as_hex { +/// Serialize and deserialize helper as string. +mod as_string { + use super::*; use serde::{Deserializer, Serializer}; - use sp_core::U256; - - pub fn serialize(data: &T, serializer: S) -> Result - where - S: Serializer, - T: Copy + Into + TryFrom, - { - let u256: U256 = (*data).into(); - serde::Serialize::serialize(&u256, serializer) + + pub fn serialize(data: &usize, serializer: S) -> Result { + data.to_string().serialize(serializer) } - pub fn deserialize<'de, D, T>(deserializer: D) -> Result - where - D: Deserializer<'de>, - T: Copy + Into + TryFrom, - { - let u256: U256 = serde::Deserialize::deserialize(deserializer)?; - TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed")) + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(|e| serde::de::Error::custom(format!("Parsing failed: {}", e))) } } @@ -271,7 +263,7 @@ mod tests { TransactionEvent::Broadcasted(TransactionBroadcasted { num_peers: 2 }); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"broadcasted","numPeers":"0x2"}"#; + let exp = r#"{"event":"broadcasted","numPeers":"2"}"#; assert_eq!(ser, exp); let event_dec: TransactionEvent<()> = serde_json::from_str(exp).unwrap(); @@ -296,7 +288,7 @@ mod tests { })); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":"0x2"}}"#; + let exp = r#"{"event":"bestChainBlockIncluded","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":"2"}}"#; assert_eq!(ser, exp); let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); @@ -311,7 +303,7 @@ mod tests { }); let ser = serde_json::to_string(&event).unwrap(); - let exp = r#"{"event":"finalized","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":"0xa"}}"#; + let exp = r#"{"event":"finalized","block":{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","index":"10"}}"#; assert_eq!(ser, exp); let event_dec: TransactionEvent = serde_json::from_str(exp).unwrap(); From 0f03b1ec5618e033f67a12159f3c85c7f84aa5d9 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 26 Sep 2022 16:58:55 +0000 Subject: [PATCH 19/24] tx-pool: Backward compatibility with RPC v1 Signed-off-by: Alexandru Vasile --- Cargo.lock | 1 + client/transaction-pool/api/Cargo.toml | 3 ++ client/transaction-pool/api/src/lib.rs | 51 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 86bbdaa8434b9..4a6a3a4ff780c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9060,6 +9060,7 @@ dependencies = [ "futures", "log", "serde", + "serde_json", "sp-blockchain", "sp-runtime", "thiserror", diff --git a/client/transaction-pool/api/Cargo.toml b/client/transaction-pool/api/Cargo.toml index d34ffe512b023..1ab0f32bc8bad 100644 --- a/client/transaction-pool/api/Cargo.toml +++ b/client/transaction-pool/api/Cargo.toml @@ -15,3 +15,6 @@ serde = { version = "1.0.136", features = ["derive"] } thiserror = "1.0.30" sp-blockchain = { version = "4.0.0-dev", path = "../../../primitives/blockchain" } sp-runtime = { version = "6.0.0", default-features = false, path = "../../../primitives/runtime" } + +[dev-dependencies] +serde_json = "1.0" diff --git a/client/transaction-pool/api/src/lib.rs b/client/transaction-pool/api/src/lib.rs index 9dce829c587b3..c0a94516ffc97 100644 --- a/client/transaction-pool/api/src/lib.rs +++ b/client/transaction-pool/api/src/lib.rs @@ -110,6 +110,7 @@ pub enum TransactionStatus { Broadcast(Vec), /// Transaction has been included in block with given hash /// at the given position. + #[serde(with = "v1_compatible")] InBlock((BlockHash, TxIndex)), /// The block this transaction was included in has been retracted. Retracted(BlockHash), @@ -117,6 +118,7 @@ pub enum TransactionStatus { /// old watchers are being removed. FinalityTimeout(BlockHash), /// Transaction has been finalized by a finality-gadget, e.g GRANDPA. + #[serde(with = "v1_compatible")] Finalized((BlockHash, TxIndex)), /// Transaction has been replaced in the pool, by another transaction /// that provides the same tags. (e.g. same (sender, nonce)). @@ -365,3 +367,52 @@ impl OffchainSubmitTransaction for TP }) } } + +/// Wrapper functions to keep the API backwards compatible over the wire for the old RPC spec. +mod v1_compatible { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(data: &(H, usize), serializer: S) -> Result + where + S: Serializer, + H: Serialize, + { + let (hash, _) = data; + serde::Serialize::serialize(&hash, serializer) + } + + pub fn deserialize<'de, D, H>(deserializer: D) -> Result<(H, usize), D::Error> + where + D: Deserializer<'de>, + H: Deserialize<'de>, + { + let hash: H = serde::Deserialize::deserialize(deserializer)?; + Ok((hash, 0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tx_status_compatibility() { + let event: TransactionStatus = TransactionStatus::InBlock((1, 2)); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"inBlock":1}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionStatus = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, TransactionStatus::InBlock((1, 0))); + + let event: TransactionStatus = TransactionStatus::Finalized((1, 2)); + let ser = serde_json::to_string(&event).unwrap(); + + let exp = r#"{"finalized":1}"#; + assert_eq!(ser, exp); + + let event_dec: TransactionStatus = serde_json::from_str(exp).unwrap(); + assert_eq!(event_dec, TransactionStatus::Finalized((1, 0))); + } +} From 795d110b342f3e2b0bf84a82840316d68fdc774c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Wed, 28 Sep 2022 15:17:30 +0300 Subject: [PATCH 20/24] Update client/rpc-spec-v2/src/transaction/transaction.rs Co-authored-by: Niklas Adolfsson --- client/rpc-spec-v2/src/transaction/transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/transaction/transaction.rs b/client/rpc-spec-v2/src/transaction/transaction.rs index f59ae82da98cf..027fbf19e4482 100644 --- a/client/rpc-spec-v2/src/transaction/transaction.rs +++ b/client/rpc-spec-v2/src/transaction/transaction.rs @@ -116,7 +116,7 @@ where // We have not created an `Watcher` for the tx. Make sure the // error is still propagated as an event. let event: TransactionEvent<::Hash> = err.into(); - sink.pipe_from_stream(futures::stream::iter(vec![event]).boxed()).await; + sink.pipe_from_stream(futures::stream::once(async { event }).boxed()).await; }, }; }; From b77cf978442a6f2ebd8b43da6c51d72764a6d1e1 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 28 Sep 2022 12:20:45 +0000 Subject: [PATCH 21/24] rpc/tx: Remove comment about serde clone Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/api.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/client/rpc-spec-v2/src/transaction/api.rs b/client/rpc-spec-v2/src/transaction/api.rs index 2c42275b11040..2f0c799f1cc19 100644 --- a/client/rpc-spec-v2/src/transaction/api.rs +++ b/client/rpc-spec-v2/src/transaction/api.rs @@ -23,7 +23,6 @@ use jsonrpsee::proc_macros::rpc; use sp_core::Bytes; #[rpc(client, server)] -// This trait bound is required by serde, see `event`. pub trait TransactionApi { /// Submit an extrinsic to watch. /// From aa1dde483065030e1263dfddc17e3851cdea3835 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 28 Sep 2022 14:51:19 +0000 Subject: [PATCH 22/24] rpc/tx: Use RPC custom error code for invalid tx format Signed-off-by: Alexandru Vasile --- .../src/transaction/transaction.rs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/transaction.rs b/client/rpc-spec-v2/src/transaction/transaction.rs index 027fbf19e4482..e2cf736dff17a 100644 --- a/client/rpc-spec-v2/src/transaction/transaction.rs +++ b/client/rpc-spec-v2/src/transaction/transaction.rs @@ -29,7 +29,14 @@ use crate::{ }, SubscriptionTaskExecutor, }; -use jsonrpsee::{core::async_trait, types::SubscriptionResult, SubscriptionSink}; +use jsonrpsee::{ + core::async_trait, + types::{ + error::{CallError, ErrorObject}, + SubscriptionResult, + }, + SubscriptionSink, +}; use sc_transaction_pool_api::{ error::IntoPoolError, BlockHash, TransactionFor, TransactionPool, TransactionSource, TransactionStatus, @@ -43,7 +50,6 @@ use sp_runtime::{generic, traits::Block as BlockT}; use codec::Decode; use futures::{FutureExt, StreamExt, TryFutureExt}; -use jsonrpsee::types::error::CallError; /// An API for transaction RPC calls. pub struct Transaction { @@ -69,6 +75,13 @@ impl Transaction { /// some unique transactions via RPC and have them included in the pool. const TX_SOURCE: TransactionSource = TransactionSource::External; +/// Extrinsic has an invalid format. +/// +/// # Note +/// +/// This is similar to the old `author` API error code. +const BAD_FORMAT: i32 = 1001; + #[async_trait] impl TransactionApiServer> for Transaction where @@ -83,7 +96,11 @@ where let decoded_extrinsic = match TransactionFor::::decode(&mut &xt[..]) { Ok(decoded_extrinsic) => decoded_extrinsic, Err(e) => { - let err = CallError::Failed(e.into()); + let err = CallError::Custom(ErrorObject::owned( + BAD_FORMAT, + format!("Extrinsic has invalid format: {}", e), + None::<()>, + )); let _ = sink.reject(err); return Ok(()) }, From 6459acdd990cc799bbe779e7beefe9fefdc45e2e Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:56:58 +0300 Subject: [PATCH 23/24] Update client/rpc-spec-v2/src/transaction/event.rs Co-authored-by: James Wilson --- client/rpc-spec-v2/src/transaction/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 72a89b6d5f714..7bdecd789d2c7 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -94,7 +94,7 @@ pub struct TransactionDropped { /// to unsubscribe at any moment. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // We need to manually specify the trait bounds for the `Hash` trait to ensure `into` and -// `form` still work. +// `from` still work. #[serde(bound( serialize = "Hash: Serialize + Clone", deserialize = "Hash: Deserialize<'de> + Clone" From 093c1a17fbdec0f14224a0bfd315325278b67e36 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 29 Sep 2022 09:28:20 +0000 Subject: [PATCH 24/24] rpc/tx: Adjust internal structures for serialization/deserialization Signed-off-by: Alexandru Vasile --- client/rpc-spec-v2/src/transaction/event.rs | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/client/rpc-spec-v2/src/transaction/event.rs b/client/rpc-spec-v2/src/transaction/event.rs index 7bdecd789d2c7..3c75eaff10fd4 100644 --- a/client/rpc-spec-v2/src/transaction/event.rs +++ b/client/rpc-spec-v2/src/transaction/event.rs @@ -137,7 +137,7 @@ pub enum TransactionEvent { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(tag = "event", content = "block")] -pub(crate) enum TransactionEventBlock { +enum TransactionEventBlockIR { /// The transaction was included in the best block of the chain. BestChainBlockIncluded(Option>), /// The transaction was included in a finalized block of the chain. @@ -159,7 +159,7 @@ pub(crate) enum TransactionEventBlock { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(tag = "event")] -pub(crate) enum TransactionEventNonBlock { +enum TransactionEventNonBlockIR { Validated, Broadcasted(TransactionBroadcasted), Error(TransactionError), @@ -178,28 +178,28 @@ pub(crate) enum TransactionEventNonBlock { #[serde(bound(serialize = "Hash: Serialize", deserialize = "Hash: Deserialize<'de>"))] #[serde(rename_all = "camelCase")] #[serde(untagged)] -pub(crate) enum TransactionEventIR { - Block(TransactionEventBlock), - NonBlock(TransactionEventNonBlock), +enum TransactionEventIR { + Block(TransactionEventBlockIR), + NonBlock(TransactionEventNonBlockIR), } impl From> for TransactionEventIR { fn from(value: TransactionEvent) -> Self { match value { TransactionEvent::Validated => - TransactionEventIR::NonBlock(TransactionEventNonBlock::Validated), + TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Validated), TransactionEvent::Broadcasted(event) => - TransactionEventIR::NonBlock(TransactionEventNonBlock::Broadcasted(event)), + TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Broadcasted(event)), TransactionEvent::BestChainBlockIncluded(event) => - TransactionEventIR::Block(TransactionEventBlock::BestChainBlockIncluded(event)), + TransactionEventIR::Block(TransactionEventBlockIR::BestChainBlockIncluded(event)), TransactionEvent::Finalized(event) => - TransactionEventIR::Block(TransactionEventBlock::Finalized(event)), + TransactionEventIR::Block(TransactionEventBlockIR::Finalized(event)), TransactionEvent::Error(event) => - TransactionEventIR::NonBlock(TransactionEventNonBlock::Error(event)), + TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Error(event)), TransactionEvent::Invalid(event) => - TransactionEventIR::NonBlock(TransactionEventNonBlock::Invalid(event)), + TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Invalid(event)), TransactionEvent::Dropped(event) => - TransactionEventIR::NonBlock(TransactionEventNonBlock::Dropped(event)), + TransactionEventIR::NonBlock(TransactionEventNonBlockIR::Dropped(event)), } } } @@ -208,16 +208,16 @@ impl From> for TransactionEvent { fn from(value: TransactionEventIR) -> Self { match value { TransactionEventIR::NonBlock(status) => match status { - TransactionEventNonBlock::Validated => TransactionEvent::Validated, - TransactionEventNonBlock::Broadcasted(event) => + TransactionEventNonBlockIR::Validated => TransactionEvent::Validated, + TransactionEventNonBlockIR::Broadcasted(event) => TransactionEvent::Broadcasted(event), - TransactionEventNonBlock::Error(event) => TransactionEvent::Error(event), - TransactionEventNonBlock::Invalid(event) => TransactionEvent::Invalid(event), - TransactionEventNonBlock::Dropped(event) => TransactionEvent::Dropped(event), + TransactionEventNonBlockIR::Error(event) => TransactionEvent::Error(event), + TransactionEventNonBlockIR::Invalid(event) => TransactionEvent::Invalid(event), + TransactionEventNonBlockIR::Dropped(event) => TransactionEvent::Dropped(event), }, TransactionEventIR::Block(block) => match block { - TransactionEventBlock::Finalized(event) => TransactionEvent::Finalized(event), - TransactionEventBlock::BestChainBlockIncluded(event) => + TransactionEventBlockIR::Finalized(event) => TransactionEvent::Finalized(event), + TransactionEventBlockIR::BestChainBlockIncluded(event) => TransactionEvent::BestChainBlockIncluded(event), }, }