diff --git a/Cargo.lock b/Cargo.lock
index a22cfa8ba8dd6..8cce9f50dd9e4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8534,18 +8534,25 @@ dependencies = [
name = "sc-rpc-spec-v2"
version = "0.10.0-dev"
dependencies = [
+ "array-bytes",
+ "assert_matches",
"futures",
"hex",
"jsonrpsee",
"parity-scale-codec",
+ "sc-block-builder",
"sc-chain-spec",
+ "sc-client-api",
"sc-transaction-pool-api",
"serde",
"serde_json",
"sp-api",
"sp-blockchain",
+ "sp-consensus",
"sp-core",
"sp-runtime",
+ "substrate-test-runtime",
+ "substrate-test-runtime-client",
"thiserror",
"tokio",
]
diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml
index 51f5516ecf9c8..d14de5b04d891 100644
--- a/client/rpc-spec-v2/Cargo.toml
+++ b/client/rpc-spec-v2/Cargo.toml
@@ -20,14 +20,21 @@ 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 = "7.0.0", path = "../../primitives/core" }
sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" }
+sc-client-api = { version = "4.0.0-dev", path = "../api" }
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"
+array-bytes = "4.1"
hex = "0.4"
futures = "0.3.21"
[dev-dependencies]
serde_json = "1.0"
-tokio = { version = "1.17.0", features = ["macros"] }
+tokio = { version = "1.17.0", features = ["full"] }
+substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" }
+substrate-test-runtime = { version = "2.0.0", path = "../../test-utils/runtime" }
+sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" }
+sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" }
+assert_matches = "1.3.0"
diff --git a/client/rpc-spec-v2/src/archive/api.rs b/client/rpc-spec-v2/src/archive/api.rs
new file mode 100644
index 0000000000000..bb8a86c715925
--- /dev/null
+++ b/client/rpc-spec-v2/src/archive/api.rs
@@ -0,0 +1,99 @@
+// 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 .
+
+#![allow(non_snake_case)]
+
+//! API trait of the archive functions.
+use crate::archive::event::{ArchiveEvent, NetworkConfig};
+use jsonrpsee::{core::RpcResult, proc_macros::rpc};
+
+#[rpc(client, server)]
+pub trait ArchiveApi {
+ /// Retrieves the body (list of transactions) of an archive block.
+ //
+ /// Use `chainHead_unstable_body` if instead you want to retrieve the body of a recent block.
+ ///
+ /// # Unstable
+ ///
+ /// This method is unstable and subject to change in the future.
+ #[subscription(
+ name = "archive_unstable_body",
+ unsubscribe = "archive_unstable_stopBody",
+ item = ArchiveEvent,
+ )]
+ fn archive_unstable_body(&self, hash: Hash, networkConfig: Option);
+
+ /// Get the chain's genesis hash.
+ ///
+ /// # Unstable
+ ///
+ /// This method is unstable and subject to change in the future.
+ #[method(name = "archive_unstable_genesisHash", blocking)]
+ fn archive_unstable_genesis_hash(&self) -> RpcResult;
+
+ /// Retrieves the hashes of the blocks that have the specified height.
+ ///
+ /// If the height parameter is less or equal to the latest finalized block
+ /// height, then only finalized blocks are fetched.
+ ///
+ /// # Unstable
+ ///
+ /// This method is unstable and subject to change in the future.
+ #[subscription(
+ name = "archive_unstable_hashByHeight",
+ unsubscribe = "archive_unstable_stopHashByHeight",
+ item = ArchiveEvent,
+ )]
+ fn archive_unstable_hash_by_height(&self, height: String, networkConfig: Option);
+
+ /// Retrieves the header of an archive block.
+ ///
+ /// Use `chainHead_unstable_header` if instead you want to retrieve the header of a
+ /// recent block.
+ ///
+ /// # Unstable
+ ///
+ /// This method is unstable and subject to change in the future.
+ #[subscription(
+ name = "archive_unstable_header",
+ unsubscribe = "archive_unstable_stopHeader",
+ item = ArchiveEvent,
+ )]
+ fn archive_unstable_header(&self, hash: Hash, networkConfig: Option);
+
+ /// Return a storage entry at a specific block's state.
+ ///
+ /// Use `chainHead_unstable_storage` if instead you want to retrieve the
+ /// storage of a recent block.
+ ///
+ /// # Unstable
+ ///
+ /// This method is unstable and subject to change in the future.
+ #[subscription(
+ name = "archive_unstable_storage",
+ unsubscribe = "archive_unstable_stopStorage",
+ item = ArchiveEvent,
+ )]
+ fn archive_unstable_storage(
+ &self,
+ hash: Hash,
+ key: String,
+ childKey: Option,
+ networkConfig: Option,
+ );
+}
diff --git a/client/rpc-spec-v2/src/archive/archive.rs b/client/rpc-spec-v2/src/archive/archive.rs
new file mode 100644
index 0000000000000..f912c78374e46
--- /dev/null
+++ b/client/rpc-spec-v2/src/archive/archive.rs
@@ -0,0 +1,304 @@
+// 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 `archive`.
+
+use crate::{
+ archive::{
+ error::Error as ArchiveRpcError,
+ event::{ArchiveEvent, ArchiveResult, ErrorEvent},
+ ArchiveApiServer, NetworkConfig,
+ },
+ SubscriptionTaskExecutor,
+};
+use codec::Encode;
+use futures::future::FutureExt;
+use jsonrpsee::{
+ core::{async_trait, RpcResult},
+ types::{SubscriptionEmptyError, SubscriptionResult},
+ SubscriptionSink,
+};
+use sc_client_api::{
+ Backend, BlockBackend, BlockchainEvents, ChildInfo, ExecutorProvider, StorageKey,
+ StorageProvider,
+};
+use sp_api::{BlockId, BlockT, NumberFor};
+use sp_blockchain::{
+ Backend as BlockchainBackend, Error as BlockChainError, HashAndNumber, HeaderBackend,
+ HeaderMetadata,
+};
+use sp_core::{hexdisplay::HexDisplay, storage::well_known_keys};
+use sp_runtime::{traits::One, Saturating};
+use std::{marker::PhantomData, sync::Arc};
+
+/// An API for archive RPC calls.
+pub struct Archive {
+ /// Substrate client.
+ client: Arc,
+ /// Backend of the chain.
+ backend: Arc,
+ /// Executor to spawn subscriptions.
+ executor: SubscriptionTaskExecutor,
+ /// The hexadecimal encoded hash of the genesis block.
+ genesis_hash: String,
+ /// Phantom member to pin the block type.
+ _phantom: PhantomData<(Block, BE)>,
+}
+
+impl Archive {
+ /// Create a new [`Archive`].
+ pub fn new>(
+ client: Arc,
+ backend: Arc,
+ executor: SubscriptionTaskExecutor,
+ genesis_hash: GenesisHash,
+ ) -> Self {
+ let genesis_hash = format!("0x{}", hex::encode(genesis_hash));
+
+ Self { client, backend, executor, genesis_hash, _phantom: PhantomData }
+ }
+}
+
+fn parse_hex_param(
+ sink: &mut SubscriptionSink,
+ param: String,
+) -> Result, SubscriptionEmptyError> {
+ match array_bytes::hex2bytes(¶m) {
+ Ok(bytes) => Ok(bytes),
+ Err(_) => {
+ let _ = sink.reject(ArchiveRpcError::InvalidParam(param));
+ Err(SubscriptionEmptyError)
+ },
+ }
+}
+
+fn get_blocks_by_height(
+ backend: &Arc,
+ parent: HashAndNumber,
+ target_height: NumberFor,
+) -> Vec
+where
+ Block: BlockT + 'static,
+ BE: Backend + 'static,
+{
+ let mut result = Vec::new();
+ let mut next_hash = Vec::new();
+ next_hash.push(parent);
+
+ while let Some(parent) = next_hash.pop() {
+ if parent.number == target_height {
+ result.push(parent.hash);
+ continue
+ }
+
+ let Ok(blocks) = backend.blockchain().children(parent.hash) else {
+ continue
+ };
+
+ let child_number = parent.number.saturating_add(One::one());
+ for child_hash in blocks {
+ next_hash.push(HashAndNumber { number: child_number, hash: child_hash });
+ }
+ }
+
+ result
+}
+
+#[async_trait]
+impl ArchiveApiServer for Archive
+where
+ Block: BlockT + 'static,
+ Block::Header: Unpin,
+ BE: Backend + 'static,
+ Client: BlockBackend
+ + ExecutorProvider
+ + HeaderBackend
+ + HeaderMetadata
+ + BlockchainEvents
+ + StorageProvider
+ + 'static,
+{
+ fn archive_unstable_body(
+ &self,
+ mut sink: SubscriptionSink,
+ hash: Block::Hash,
+ _network_config: Option,
+ ) -> SubscriptionResult {
+ let client = self.client.clone();
+
+ let fut = async move {
+ let event = match client.block(&BlockId::Hash(hash)) {
+ Ok(Some(signed_block)) => {
+ let extrinsics = signed_block.block.extrinsics();
+ let result = format!("0x{}", HexDisplay::from(&extrinsics.encode()));
+ ArchiveEvent::Done(ArchiveResult { result })
+ },
+ Ok(None) => ArchiveEvent::Inaccessible,
+ Err(error) => ArchiveEvent::Error(ErrorEvent { error: error.to_string() }),
+ };
+ let _ = sink.send(&event);
+ };
+
+ self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
+ Ok(())
+ }
+
+ fn archive_unstable_genesis_hash(&self) -> RpcResult {
+ Ok(self.genesis_hash.clone())
+ }
+
+ fn archive_unstable_hash_by_height(
+ &self,
+ mut sink: SubscriptionSink,
+ height: String,
+ _network_config: Option,
+ ) -> SubscriptionResult {
+ let height_str = height.trim_start_matches("0x");
+ let Ok(height_num) = u32::from_str_radix(&height_str, 16) else {
+ let _ = sink.reject(ArchiveRpcError::InvalidParam(height));
+ return Ok(())
+ };
+
+ let client = self.client.clone();
+ let backend = self.backend.clone();
+
+ let fut = async move {
+ let finalized_number = client.info().finalized_number;
+
+ // If the height has been finalized, return only the finalized block.
+ if finalized_number >= height_num.into() {
+ let result = if let Ok(Some(hash)) = client.block_hash(height_num.into()) {
+ vec![hash]
+ } else {
+ // The block hash should have existed in the database. However,
+ // it may be possible that it was pruned.
+ vec![]
+ };
+
+ let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result }));
+ return
+ }
+
+ let finalized_hash = client.info().finalized_hash;
+ let result = get_blocks_by_height(
+ &backend,
+ HashAndNumber { hash: finalized_hash, number: finalized_number },
+ height_num.into(),
+ );
+ let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result }));
+ };
+
+ self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
+ Ok(())
+ }
+
+ fn archive_unstable_header(
+ &self,
+ mut sink: SubscriptionSink,
+ hash: Block::Hash,
+ _network_config: Option,
+ ) -> SubscriptionResult {
+ let client = self.client.clone();
+
+ let fut = async move {
+ let event = match client.header(BlockId::Hash(hash)) {
+ Ok(Some(header)) => {
+ let result = format!("0x{}", HexDisplay::from(&header.encode()));
+ ArchiveEvent::Done(ArchiveResult { result })
+ },
+ Ok(None) => ArchiveEvent::Inaccessible,
+ Err(error) => ArchiveEvent::Error(ErrorEvent { error: error.to_string() }),
+ };
+ let _ = sink.send(&event);
+ };
+
+ self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
+ Ok(())
+ }
+
+ fn archive_unstable_storage(
+ &self,
+ mut sink: SubscriptionSink,
+ hash: Block::Hash,
+ key: String,
+ child_key: Option,
+ _network_config: Option,
+ ) -> SubscriptionResult {
+ let key = StorageKey(parse_hex_param(&mut sink, key)?);
+
+ let child_key = child_key
+ .map(|child_key| parse_hex_param(&mut sink, child_key))
+ .transpose()?
+ .map(ChildInfo::new_default_from_vec);
+
+ let client = self.client.clone();
+
+ let fut = async move {
+ // The child key is provided, use the key to query the child trie.
+ if let Some(child_key) = child_key {
+ // The child key must not be prefixed with ":child_storage:" nor
+ // ":child_storage:default:".
+ if well_known_keys::is_default_child_storage_key(child_key.storage_key()) ||
+ well_known_keys::is_child_storage_key(child_key.storage_key())
+ {
+ let _ =
+ sink.send(&ArchiveEvent::Done(ArchiveResult { result: None:: }));
+ return
+ }
+
+ let res = client
+ .child_storage(hash, &child_key, &key)
+ .map(|result| {
+ let result =
+ result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0)));
+ ArchiveEvent::Done(ArchiveResult { result })
+ })
+ .unwrap_or_else(|error| {
+ ArchiveEvent::Error(ErrorEvent { error: error.to_string() })
+ });
+ let _ = sink.send(&res);
+ return
+ }
+
+ // The main key must not be prefixed with b":child_storage:" nor
+ // b":child_storage:default:".
+ if well_known_keys::is_default_child_storage_key(&key.0) ||
+ well_known_keys::is_child_storage_key(&key.0)
+ {
+ let _ = sink.send(&ArchiveEvent::Done(ArchiveResult { result: None:: }));
+ return
+ }
+
+ // Main root trie storage query.
+ let res = client
+ .storage(hash, &key)
+ .map(|result| {
+ let result =
+ result.map(|storage| format!("0x{}", HexDisplay::from(&storage.0)));
+ ArchiveEvent::Done(ArchiveResult { result })
+ })
+ .unwrap_or_else(|error| {
+ ArchiveEvent::Error(ErrorEvent { error: error.to_string() })
+ });
+ let _ = sink.send(&res);
+ };
+
+ self.executor.spawn("substrate-rpc-subscription", Some("rpc"), fut.boxed());
+ Ok(())
+ }
+}
diff --git a/client/rpc-spec-v2/src/archive/error.rs b/client/rpc-spec-v2/src/archive/error.rs
new file mode 100644
index 0000000000000..fcda69f6cc67c
--- /dev/null
+++ b/client/rpc-spec-v2/src/archive/error.rs
@@ -0,0 +1,54 @@
+// 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 .
+
+//! Error helpers for `archive` RPC module.
+
+use jsonrpsee::{
+ core::Error as RpcError,
+ types::error::{CallError, ErrorObject},
+};
+
+/// Archive RPC errors.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// Invalid parameter provided to the RPC method.
+ #[error("Invalid parameter: {0}")]
+ InvalidParam(String),
+}
+
+// Base code for all `archive` errors.
+const BASE_ERROR: i32 = 3000;
+/// Invalid parameter error.
+const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 1;
+
+impl From for ErrorObject<'static> {
+ fn from(e: Error) -> Self {
+ let msg = e.to_string();
+
+ match e {
+ Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>),
+ }
+ .into()
+ }
+}
+
+impl From for RpcError {
+ fn from(e: Error) -> Self {
+ CallError::Custom(e.into()).into()
+ }
+}
diff --git a/client/rpc-spec-v2/src/archive/event.rs b/client/rpc-spec-v2/src/archive/event.rs
new file mode 100644
index 0000000000000..be1d6d7841d83
--- /dev/null
+++ b/client/rpc-spec-v2/src/archive/event.rs
@@ -0,0 +1,125 @@
+// 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 archive's event returned as json compatible object.
+
+use serde::{Deserialize, Serialize};
+
+/// The network config parameter is used when a function
+/// needs to request the information from its peers.
+///
+/// These values can be tweaked depending on the urgency of the JSON-RPC function call.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NetworkConfig {
+ /// The total number of peers from which the information is requested.
+ total_attempts: u64,
+ /// The maximum number of requests to perform in parallel.
+ ///
+ /// # Note
+ ///
+ /// A zero value is illegal.
+ max_parallel: u64,
+ /// The time, in milliseconds, after which a single requests towards one peer
+ /// is considered unsuccessful.
+ timeout_ms: u64,
+}
+
+/// The operation could not be processed due to an error.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ErrorEvent {
+ /// Reason of the error.
+ pub error: String,
+}
+
+/// The result of an archive method.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ArchiveResult {
+ /// Result of the method.
+ pub result: T,
+}
+
+/// The event of an archive method.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[serde(tag = "event")]
+pub enum ArchiveEvent {
+ /// The request completed successfully.
+ Done(ArchiveResult),
+ /// The resources requested are inaccessible.
+ ///
+ /// Resubmitting the request later might succeed.
+ Inaccessible,
+ /// An error occurred. This is definitive.
+ Error(ErrorEvent),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn archive_done_event() {
+ let event: ArchiveEvent = ArchiveEvent::Done(ArchiveResult { result: "A".into() });
+
+ let ser = serde_json::to_string(&event).unwrap();
+ let exp = r#"{"event":"done","result":"A"}"#;
+ assert_eq!(ser, exp);
+
+ let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap();
+ assert_eq!(event_dec, event);
+ }
+
+ #[test]
+ fn archive_inaccessible_event() {
+ let event: ArchiveEvent = ArchiveEvent::Inaccessible;
+
+ let ser = serde_json::to_string(&event).unwrap();
+ let exp = r#"{"event":"inaccessible"}"#;
+ assert_eq!(ser, exp);
+
+ let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap();
+ assert_eq!(event_dec, event);
+ }
+
+ #[test]
+ fn archive_error_event() {
+ let event: ArchiveEvent = ArchiveEvent::Error(ErrorEvent { error: "A".into() });
+
+ let ser = serde_json::to_string(&event).unwrap();
+ let exp = r#"{"event":"error","error":"A"}"#;
+ assert_eq!(ser, exp);
+
+ let event_dec: ArchiveEvent = serde_json::from_str(exp).unwrap();
+ assert_eq!(event_dec, event);
+ }
+
+ #[test]
+ fn archive_network_config() {
+ let conf = NetworkConfig { total_attempts: 1, max_parallel: 2, timeout_ms: 3 };
+
+ let ser = serde_json::to_string(&conf).unwrap();
+ let exp = r#"{"totalAttempts":1,"maxParallel":2,"timeoutMs":3}"#;
+ assert_eq!(ser, exp);
+
+ let conf_dec: NetworkConfig = serde_json::from_str(exp).unwrap();
+ assert_eq!(conf_dec, conf);
+ }
+}
diff --git a/client/rpc-spec-v2/src/archive/mod.rs b/client/rpc-spec-v2/src/archive/mod.rs
new file mode 100644
index 0000000000000..840d1023395c6
--- /dev/null
+++ b/client/rpc-spec-v2/src/archive/mod.rs
@@ -0,0 +1,41 @@
+// 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 archive specification API.
+//!
+//! The *archive* functions inspect the history of the chain.
+//!
+//! They can be used to access recent information as well,
+//! but JSON-RPC clients should keep in mind that the chainHead
+//! functions could be more appropriate.
+//!
+//! # Note
+//!
+//! Methods are prefixed by `archive`.
+
+#[cfg(test)]
+mod tests;
+
+pub mod api;
+pub mod archive;
+pub mod error;
+pub mod event;
+
+pub use api::ArchiveApiServer;
+pub use archive::Archive;
+pub use event::{ArchiveEvent, ArchiveResult, ErrorEvent, NetworkConfig};
diff --git a/client/rpc-spec-v2/src/archive/tests.rs b/client/rpc-spec-v2/src/archive/tests.rs
new file mode 100644
index 0000000000000..b25e817b44f6e
--- /dev/null
+++ b/client/rpc-spec-v2/src/archive/tests.rs
@@ -0,0 +1,255 @@
+use super::*;
+use assert_matches::assert_matches;
+use codec::Encode;
+use jsonrpsee::{
+ core::{server::rpc_module::Subscription as RpcSubscription, Error},
+ types::{error::CallError, EmptyParams},
+ RpcModule,
+};
+use sc_block_builder::BlockBuilderProvider;
+use sc_client_api::ChildInfo;
+use sp_api::{BlockId, HeaderT};
+use sp_consensus::BlockOrigin;
+use sp_core::{hexdisplay::HexDisplay, testing::TaskExecutor};
+use std::sync::Arc;
+use substrate_test_runtime::Transfer;
+use substrate_test_runtime_client::{prelude::*, runtime, Backend, Client, ClientBlockImportExt};
+
+type Block = substrate_test_runtime_client::runtime::Block;
+const CHAIN_GENESIS: [u8; 32] = [0; 32];
+const INVALID_HASH: [u8; 32] = [1; 32];
+const KEY: &[u8] = b":mock";
+const VALUE: &[u8] = b"hello world";
+const CHILD_STORAGE_KEY: &[u8] = b"child";
+const CHILD_VALUE: &[u8] = b"child value";
+
+async fn get_next_event(sub: &mut RpcSubscription) -> T {
+ let (event, _sub_id) = tokio::time::timeout(std::time::Duration::from_secs(1), sub.next())
+ .await
+ .unwrap()
+ .unwrap()
+ .unwrap();
+ event
+}
+
+async fn setup_api(
+) -> (Arc>, RpcModule>>, Block) {
+ let child_info = ChildInfo::new_default(CHILD_STORAGE_KEY);
+ let builder = TestClientBuilder::new().add_extra_child_storage(
+ &child_info,
+ KEY.to_vec(),
+ CHILD_VALUE.to_vec(),
+ );
+ let backend = builder.backend();
+ let mut client = Arc::new(builder.build());
+
+ let api =
+ Archive::new(client.clone(), backend, Arc::new(TaskExecutor::default()), CHAIN_GENESIS)
+ .into_rpc();
+
+ let block = client.new_block(Default::default()).unwrap().build().unwrap().block;
+ client.import(BlockOrigin::Own, block.clone()).await.unwrap();
+
+ (client, api, block)
+}
+
+#[tokio::test]
+async fn get_genesis() {
+ let (_client, api, _block) = setup_api().await;
+
+ let genesis: String =
+ api.call("archive_unstable_genesisHash", EmptyParams::new()).await.unwrap();
+ assert_eq!(genesis, format!("0x{}", HexDisplay::from(&CHAIN_GENESIS)));
+}
+
+#[tokio::test]
+async fn get_header() {
+ let (_client, api, block) = setup_api().await;
+ let block_hash = format!("{:?}", block.header.hash());
+ let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH));
+
+ // Invalid block hash.
+ let mut sub = api.subscribe("archive_unstable_header", [&invalid_hash]).await.unwrap();
+ let event: ArchiveEvent = get_next_event(&mut sub).await;
+ assert_eq!(event, ArchiveEvent::Inaccessible);
+
+ // Valid block hash.
+ let mut sub = api.subscribe("archive_unstable_header", [&block_hash]).await.unwrap();
+ let event: ArchiveEvent = get_next_event(&mut sub).await;
+ let expected = {
+ let result = format!("0x{}", HexDisplay::from(&block.header.encode()));
+ ArchiveEvent::Done(ArchiveResult { result })
+ };
+ assert_eq!(event, expected);
+}
+
+#[tokio::test]
+async fn get_body() {
+ let (mut client, api, block) = setup_api().await;
+ let block_hash = format!("{:?}", block.header.hash());
+ let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH));
+
+ // Invalid block hash.
+ let mut sub = api.subscribe("archive_unstable_body", [&invalid_hash]).await.unwrap();
+ let event: ArchiveEvent = get_next_event(&mut sub).await;
+ assert_eq!(event, ArchiveEvent::Inaccessible);
+
+ // Valid block hash with empty body.
+ let mut sub = api.subscribe("archive_unstable_body", [&block_hash]).await.unwrap();
+ let event: ArchiveEvent = get_next_event(&mut sub).await;
+ let expected = ArchiveEvent::Done(ArchiveResult { result: "0x00".into() });
+ assert_eq!(event, expected);
+
+ // Import a block with extrinsics.
+ let mut builder = client.new_block(Default::default()).unwrap();
+ builder
+ .push_transfer(runtime::Transfer {
+ from: AccountKeyring::Alice.into(),
+ to: AccountKeyring::Ferdie.into(),
+ amount: 42,
+ nonce: 0,
+ })
+ .unwrap();
+ let block = builder.build().unwrap().block;
+ let block_hash = format!("{:?}", block.header.hash());
+ client.import(BlockOrigin::Own, block.clone()).await.unwrap();
+
+ // Valid block hash with extrinsics.
+ let mut sub = api.subscribe("archive_unstable_body", [&block_hash]).await.unwrap();
+ let event: ArchiveEvent = get_next_event(&mut sub).await;
+ let expected = {
+ let result = format!("0x{}", HexDisplay::from(&block.extrinsics.encode()));
+ ArchiveEvent::Done(ArchiveResult { result })
+ };
+ assert_eq!(event, expected);
+}
+
+#[tokio::test]
+async fn get_storage() {
+ let (mut client, api, block) = setup_api().await;
+ let block_hash = format!("{:?}", block.header.hash());
+ let invalid_hash = format!("0x{:?}", HexDisplay::from(&INVALID_HASH));
+ let key = format!("0x{:?}", HexDisplay::from(&KEY));
+
+ // Invalid block hash.
+ let mut sub = api.subscribe("archive_unstable_storage", [&invalid_hash, &key]).await.unwrap();
+ let event: ArchiveEvent