mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 14:37:57 +00:00
archive: Implement height, hashByHeight and call (#1582)
This PR implements: - `archive_unstable_finalized_height`: Get the height of the most recent finalized block - `archive_unstable_hash_by_height`: Get the hashes (possible empty) of blocks from the given height - `archive_unstable_call`: Call into the runtime of a block Builds on top of: https://github.com/paritytech/polkadot-sdk/pull/1560 ### Testing Done - unit tests for the methods with custom block tree for different heights / forks Closes: https://github.com/paritytech/polkadot-sdk/issues/1510 Closes: https://github.com/paritytech/polkadot-sdk/issues/1513 Closes: https://github.com/paritytech/polkadot-sdk/issues/1511 @paritytech/subxt-team --------- Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io> Co-authored-by: Sebastian Kunert <skunert49@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
//! API trait of the archive methods.
|
||||
|
||||
use crate::MethodResult;
|
||||
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
|
||||
|
||||
#[rpc(client, server)]
|
||||
@@ -53,4 +54,38 @@ pub trait ArchiveApi<Hash> {
|
||||
/// This method is unstable and subject to change in the future.
|
||||
#[method(name = "archive_unstable_header")]
|
||||
fn archive_unstable_header(&self, hash: Hash) -> RpcResult<Option<String>>;
|
||||
|
||||
/// Get the height of the current finalized block.
|
||||
///
|
||||
/// Returns an integer height of the current finalized block of the chain.
|
||||
///
|
||||
/// # Unstable
|
||||
///
|
||||
/// This method is unstable and subject to change in the future.
|
||||
#[method(name = "archive_unstable_finalizedHeight")]
|
||||
fn archive_unstable_finalized_height(&self) -> RpcResult<u64>;
|
||||
|
||||
/// Get the hashes of blocks from the given height.
|
||||
///
|
||||
/// Returns an array (possibly empty) of strings containing an hexadecimal-encoded hash of a
|
||||
/// block header.
|
||||
///
|
||||
/// # Unstable
|
||||
///
|
||||
/// This method is unstable and subject to change in the future.
|
||||
#[method(name = "archive_unstable_hashByHeight")]
|
||||
fn archive_unstable_hash_by_height(&self, height: u64) -> RpcResult<Vec<String>>;
|
||||
|
||||
/// Call into the Runtime API at a specified block's state.
|
||||
///
|
||||
/// # Unstable
|
||||
///
|
||||
/// This method is unstable and subject to change in the future.
|
||||
#[method(name = "archive_unstable_call")]
|
||||
fn archive_unstable_call(
|
||||
&self,
|
||||
hash: Hash,
|
||||
function: String,
|
||||
call_parameters: String,
|
||||
) -> RpcResult<MethodResult>;
|
||||
}
|
||||
|
||||
@@ -18,20 +18,34 @@
|
||||
|
||||
//! API implementation for `archive`.
|
||||
|
||||
use super::ArchiveApiServer;
|
||||
use crate::chain_head::hex_string;
|
||||
use crate::{
|
||||
archive::{error::Error as ArchiveError, ArchiveApiServer},
|
||||
chain_head::hex_string,
|
||||
MethodResult,
|
||||
};
|
||||
|
||||
use codec::Encode;
|
||||
use jsonrpsee::core::{async_trait, RpcResult};
|
||||
use sc_client_api::{Backend, BlockBackend, BlockchainEvents, ExecutorProvider, StorageProvider};
|
||||
use sp_api::CallApiAt;
|
||||
use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
|
||||
use sp_runtime::traits::Block as BlockT;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
use sc_client_api::{
|
||||
Backend, BlockBackend, BlockchainEvents, CallExecutor, ExecutorProvider, StorageProvider,
|
||||
};
|
||||
use sp_api::{CallApiAt, CallContext, NumberFor};
|
||||
use sp_blockchain::{
|
||||
Backend as BlockChainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata,
|
||||
};
|
||||
use sp_core::Bytes;
|
||||
use sp_runtime::{
|
||||
traits::{Block as BlockT, Header as HeaderT},
|
||||
SaturatedConversion,
|
||||
};
|
||||
use std::{collections::HashSet, marker::PhantomData, sync::Arc};
|
||||
|
||||
/// An API for archive RPC calls.
|
||||
pub struct Archive<BE: Backend<Block>, Block: BlockT, Client> {
|
||||
/// Substrate client.
|
||||
client: Arc<Client>,
|
||||
/// Backend of the chain.
|
||||
backend: Arc<BE>,
|
||||
/// The hexadecimal encoded hash of the genesis block.
|
||||
genesis_hash: String,
|
||||
/// Phantom member to pin the block type.
|
||||
@@ -40,17 +54,34 @@ pub struct Archive<BE: Backend<Block>, Block: BlockT, Client> {
|
||||
|
||||
impl<BE: Backend<Block>, Block: BlockT, Client> Archive<BE, Block, Client> {
|
||||
/// Create a new [`Archive`].
|
||||
pub fn new<GenesisHash: AsRef<[u8]>>(client: Arc<Client>, genesis_hash: GenesisHash) -> Self {
|
||||
pub fn new<GenesisHash: AsRef<[u8]>>(
|
||||
client: Arc<Client>,
|
||||
backend: Arc<BE>,
|
||||
genesis_hash: GenesisHash,
|
||||
) -> Self {
|
||||
let genesis_hash = hex_string(&genesis_hash.as_ref());
|
||||
Self { client, genesis_hash, _phantom: PhantomData }
|
||||
Self { client, backend, genesis_hash, _phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse hex-encoded string parameter as raw bytes.
|
||||
///
|
||||
/// If the parsing fails, returns an error propagated to the RPC method.
|
||||
fn parse_hex_param(param: String) -> Result<Vec<u8>, ArchiveError> {
|
||||
// Methods can accept empty parameters.
|
||||
if param.is_empty() {
|
||||
return Ok(Default::default())
|
||||
}
|
||||
|
||||
array_bytes::hex2bytes(¶m).map_err(|_| ArchiveError::InvalidParam(param))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<BE, Block, Client> ArchiveApiServer<Block::Hash> for Archive<BE, Block, Client>
|
||||
where
|
||||
Block: BlockT + 'static,
|
||||
Block::Header: Unpin,
|
||||
<<Block as BlockT>::Header as HeaderT>::Number: From<u64>,
|
||||
BE: Backend<Block> + 'static,
|
||||
Client: BlockBackend<Block>
|
||||
+ ExecutorProvider<Block>
|
||||
@@ -83,4 +114,75 @@ where
|
||||
|
||||
Ok(Some(hex_string(&header.encode())))
|
||||
}
|
||||
|
||||
fn archive_unstable_finalized_height(&self) -> RpcResult<u64> {
|
||||
Ok(self.client.info().finalized_number.saturated_into())
|
||||
}
|
||||
|
||||
fn archive_unstable_hash_by_height(&self, height: u64) -> RpcResult<Vec<String>> {
|
||||
let height: NumberFor<Block> = height.into();
|
||||
let finalized_num = self.client.info().finalized_number;
|
||||
|
||||
if finalized_num >= height {
|
||||
let Ok(Some(hash)) = self.client.block_hash(height.into()) else { return Ok(vec![]) };
|
||||
return Ok(vec![hex_string(&hash.as_ref())])
|
||||
}
|
||||
|
||||
let blockchain = self.backend.blockchain();
|
||||
// Fetch all the leaves of the blockchain that are on a higher or equal height.
|
||||
let mut headers: Vec<_> = blockchain
|
||||
.leaves()
|
||||
.map_err(|error| ArchiveError::FetchLeaves(error.to_string()))?
|
||||
.into_iter()
|
||||
.filter_map(|hash| {
|
||||
let Ok(Some(header)) = self.client.header(hash) else { return None };
|
||||
|
||||
if header.number() < &height {
|
||||
return None
|
||||
}
|
||||
|
||||
Some(header)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
while let Some(header) = headers.pop() {
|
||||
if header.number() == &height {
|
||||
result.push(hex_string(&header.hash().as_ref()));
|
||||
continue
|
||||
}
|
||||
|
||||
let parent_hash = *header.parent_hash();
|
||||
|
||||
// Continue the iteration for unique hashes.
|
||||
// Forks might intersect on a common chain that is not yet finalized.
|
||||
if visited.insert(parent_hash) {
|
||||
let Ok(Some(next_header)) = self.client.header(parent_hash) else { continue };
|
||||
headers.push(next_header);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn archive_unstable_call(
|
||||
&self,
|
||||
hash: Block::Hash,
|
||||
function: String,
|
||||
call_parameters: String,
|
||||
) -> RpcResult<MethodResult> {
|
||||
let call_parameters = Bytes::from(parse_hex_param(call_parameters)?);
|
||||
|
||||
let result =
|
||||
self.client
|
||||
.executor()
|
||||
.call(hash, &function, &call_parameters, CallContext::Offchain);
|
||||
|
||||
Ok(match result {
|
||||
Ok(result) => MethodResult::ok(hex_string(&result)),
|
||||
Err(error) => MethodResult::err(error.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Error helpers for `archive` RPC module.
|
||||
|
||||
use jsonrpsee::{
|
||||
core::Error as RpcError,
|
||||
types::error::{CallError, ErrorObject},
|
||||
};
|
||||
|
||||
/// ChainHead RPC errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Invalid parameter provided to the RPC method.
|
||||
#[error("Invalid parameter: {0}")]
|
||||
InvalidParam(String),
|
||||
/// Runtime call failed.
|
||||
#[error("Runtime call: {0}")]
|
||||
RuntimeCall(String),
|
||||
/// Failed to fetch leaves.
|
||||
#[error("Failed to fetch leaves of the chain: {0}")]
|
||||
FetchLeaves(String),
|
||||
}
|
||||
|
||||
// Base code for all `archive` errors.
|
||||
const BASE_ERROR: i32 = 3000;
|
||||
/// Invalid parameter error.
|
||||
const INVALID_PARAM_ERROR: i32 = BASE_ERROR + 1;
|
||||
/// Runtime call error.
|
||||
const RUNTIME_CALL_ERROR: i32 = BASE_ERROR + 2;
|
||||
/// Failed to fetch leaves.
|
||||
const FETCH_LEAVES_ERROR: i32 = BASE_ERROR + 3;
|
||||
|
||||
impl From<Error> for ErrorObject<'static> {
|
||||
fn from(e: Error) -> Self {
|
||||
let msg = e.to_string();
|
||||
|
||||
match e {
|
||||
Error::InvalidParam(_) => ErrorObject::owned(INVALID_PARAM_ERROR, msg, None::<()>),
|
||||
Error::RuntimeCall(_) => ErrorObject::owned(RUNTIME_CALL_ERROR, msg, None::<()>),
|
||||
Error::FetchLeaves(_) => ErrorObject::owned(FETCH_LEAVES_ERROR, msg, None::<()>),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for RpcError {
|
||||
fn from(e: Error) -> Self {
|
||||
CallError::Custom(e.into()).into()
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,6 @@ mod tests;
|
||||
|
||||
pub mod api;
|
||||
pub mod archive;
|
||||
pub mod error;
|
||||
|
||||
pub use api::ArchiveApiServer;
|
||||
|
||||
@@ -16,16 +16,23 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::chain_head::hex_string;
|
||||
use crate::{chain_head::hex_string, MethodResult};
|
||||
|
||||
use super::{archive::Archive, *};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use codec::{Decode, Encode};
|
||||
use jsonrpsee::{types::EmptyServerParams as EmptyParams, RpcModule};
|
||||
use jsonrpsee::{
|
||||
core::error::Error,
|
||||
types::{error::CallError, EmptyServerParams as EmptyParams},
|
||||
RpcModule,
|
||||
};
|
||||
use sc_block_builder::BlockBuilderProvider;
|
||||
|
||||
use sp_blockchain::HeaderBackend;
|
||||
use sp_consensus::BlockOrigin;
|
||||
use sp_runtime::SaturatedConversion;
|
||||
use std::sync::Arc;
|
||||
use substrate_test_runtime::Transfer;
|
||||
use substrate_test_runtime_client::{
|
||||
prelude::*, runtime, Backend, BlockBuilderExt, Client, ClientBlockImportExt,
|
||||
};
|
||||
@@ -38,9 +45,10 @@ type Block = substrate_test_runtime_client::runtime::Block;
|
||||
|
||||
fn setup_api() -> (Arc<Client<Backend>>, RpcModule<Archive<Backend, Block, Client<Backend>>>) {
|
||||
let builder = TestClientBuilder::new();
|
||||
let backend = builder.backend();
|
||||
let client = Arc::new(builder.build());
|
||||
|
||||
let api = Archive::new(client.clone(), CHAIN_GENESIS).into_rpc();
|
||||
let api = Archive::new(client.clone(), backend, CHAIN_GENESIS).into_rpc();
|
||||
|
||||
(client, api)
|
||||
}
|
||||
@@ -111,3 +119,140 @@ async fn archive_header() {
|
||||
let header: Header = Decode::decode(&mut &bytes[..]).unwrap();
|
||||
assert_eq!(header, block.header);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn archive_finalized_height() {
|
||||
let (client, api) = setup_api();
|
||||
|
||||
let client_height: u32 = client.info().finalized_number.saturated_into();
|
||||
|
||||
let height: u32 =
|
||||
api.call("archive_unstable_finalizedHeight", EmptyParams::new()).await.unwrap();
|
||||
|
||||
assert_eq!(client_height, height);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn archive_hash_by_height() {
|
||||
let (mut client, api) = setup_api();
|
||||
|
||||
// Genesis height.
|
||||
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [0]).await.unwrap();
|
||||
assert_eq!(hashes, vec![format!("{:?}", client.genesis_hash())]);
|
||||
|
||||
// Block tree:
|
||||
// genesis -> finalized -> block 1 -> block 2 -> block 3
|
||||
// -> block 1 -> block 4
|
||||
//
|
||||
// ^^^ h = N
|
||||
// ^^^ h = N + 1
|
||||
// ^^^ h = N + 2
|
||||
let finalized = client.new_block(Default::default()).unwrap().build().unwrap().block;
|
||||
let finalized_hash = finalized.header.hash();
|
||||
client.import(BlockOrigin::Own, finalized.clone()).await.unwrap();
|
||||
client.finalize_block(finalized_hash, None).unwrap();
|
||||
|
||||
let block_1 = client.new_block(Default::default()).unwrap().build().unwrap().block;
|
||||
let block_1_hash = block_1.header.hash();
|
||||
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
|
||||
|
||||
let block_2 = client.new_block(Default::default()).unwrap().build().unwrap().block;
|
||||
let block_2_hash = block_2.header.hash();
|
||||
client.import(BlockOrigin::Own, block_2.clone()).await.unwrap();
|
||||
let block_3 = client.new_block(Default::default()).unwrap().build().unwrap().block;
|
||||
let block_3_hash = block_3.header.hash();
|
||||
client.import(BlockOrigin::Own, block_3.clone()).await.unwrap();
|
||||
|
||||
// Import block 4 fork.
|
||||
let mut block_builder = client.new_block_at(block_1_hash, Default::default(), false).unwrap();
|
||||
// This push is required as otherwise block 3 has the same hash as block 1 and won't get
|
||||
// imported
|
||||
block_builder
|
||||
.push_transfer(Transfer {
|
||||
from: AccountKeyring::Alice.into(),
|
||||
to: AccountKeyring::Ferdie.into(),
|
||||
amount: 41,
|
||||
nonce: 0,
|
||||
})
|
||||
.unwrap();
|
||||
let block_4 = block_builder.build().unwrap().block;
|
||||
let block_4_hash = block_4.header.hash();
|
||||
client.import(BlockOrigin::Own, block_4.clone()).await.unwrap();
|
||||
|
||||
// Check finalized height.
|
||||
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [1]).await.unwrap();
|
||||
assert_eq!(hashes, vec![format!("{:?}", finalized_hash)]);
|
||||
|
||||
// Test nonfinalized heights.
|
||||
// Height N must include block 1.
|
||||
let mut height = block_1.header.number;
|
||||
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
|
||||
assert_eq!(hashes, vec![format!("{:?}", block_1_hash)]);
|
||||
|
||||
// Height (N + 1) must include block 2 and 4.
|
||||
height += 1;
|
||||
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
|
||||
assert_eq!(hashes, vec![format!("{:?}", block_4_hash), format!("{:?}", block_2_hash)]);
|
||||
|
||||
// Height (N + 2) must include block 3.
|
||||
height += 1;
|
||||
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
|
||||
assert_eq!(hashes, vec![format!("{:?}", block_3_hash)]);
|
||||
|
||||
// Height (N + 3) has no blocks.
|
||||
height += 1;
|
||||
let hashes: Vec<String> = api.call("archive_unstable_hashByHeight", [height]).await.unwrap();
|
||||
assert!(hashes.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn archive_call() {
|
||||
let (mut client, api) = setup_api();
|
||||
let invalid_hash = hex_string(&INVALID_HASH);
|
||||
|
||||
// Invalid parameter (non-hex).
|
||||
let err = api
|
||||
.call::<_, serde_json::Value>(
|
||||
"archive_unstable_call",
|
||||
[&invalid_hash, "BabeApi_current_epoch", "0x00X"],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_matches!(err, Error::Call(CallError::Custom(ref err)) if err.code() == 3001 && err.message().contains("Invalid parameter"));
|
||||
|
||||
// Pass an invalid parameters that cannot be decode.
|
||||
let err = api
|
||||
.call::<_, serde_json::Value>(
|
||||
"archive_unstable_call",
|
||||
// 0x0 is invalid.
|
||||
[&invalid_hash, "BabeApi_current_epoch", "0x0"],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_matches!(err, Error::Call(CallError::Custom(ref err)) if err.code() == 3001 && err.message().contains("Invalid parameter"));
|
||||
|
||||
// Invalid hash.
|
||||
let result: MethodResult = api
|
||||
.call("archive_unstable_call", [&invalid_hash, "BabeApi_current_epoch", "0x00"])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_matches!(result, MethodResult::Err(_));
|
||||
|
||||
let block_1 = client.new_block(Default::default()).unwrap().build().unwrap().block;
|
||||
let block_1_hash = block_1.header.hash();
|
||||
client.import(BlockOrigin::Own, block_1.clone()).await.unwrap();
|
||||
|
||||
// Valid call.
|
||||
let alice_id = AccountKeyring::Alice.to_account_id();
|
||||
// Hex encoded scale encoded bytes representing the call parameters.
|
||||
let call_parameters = hex_string(&alice_id.encode());
|
||||
let result: MethodResult = api
|
||||
.call(
|
||||
"archive_unstable_call",
|
||||
[&format!("{:?}", block_1_hash), "AccountNonceApi_account_nonce", &call_parameters],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let expected = MethodResult::ok("0x0000000000000000");
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unused_crate_dependencies)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod archive;
|
||||
pub mod chain_head;
|
||||
pub mod chain_spec;
|
||||
@@ -30,3 +32,74 @@ pub mod transaction;
|
||||
|
||||
/// Task executor that is being used by RPC subscriptions.
|
||||
pub type SubscriptionTaskExecutor = std::sync::Arc<dyn sp_core::traits::SpawnNamed>;
|
||||
|
||||
/// The result of an RPC method.
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum MethodResult {
|
||||
/// Method generated a result.
|
||||
Ok(MethodResultOk),
|
||||
/// Method ecountered an error.
|
||||
Err(MethodResultErr),
|
||||
}
|
||||
|
||||
impl MethodResult {
|
||||
/// Constructs a successful result.
|
||||
pub fn ok(result: impl Into<String>) -> MethodResult {
|
||||
MethodResult::Ok(MethodResultOk { success: true, result: result.into() })
|
||||
}
|
||||
|
||||
/// Constructs an error result.
|
||||
pub fn err(error: impl Into<String>) -> MethodResult {
|
||||
MethodResult::Err(MethodResultErr { success: false, error: error.into() })
|
||||
}
|
||||
}
|
||||
|
||||
/// The successful result of an RPC method.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MethodResultOk {
|
||||
/// Method was successful.
|
||||
success: bool,
|
||||
/// The result of the method.
|
||||
pub result: String,
|
||||
}
|
||||
|
||||
/// The error result of an RPC method.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MethodResultErr {
|
||||
/// Method encountered an error.
|
||||
success: bool,
|
||||
/// The error of the method.
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn method_result_ok() {
|
||||
let ok = MethodResult::ok("hello");
|
||||
|
||||
let ser = serde_json::to_string(&ok).unwrap();
|
||||
let exp = r#"{"success":true,"result":"hello"}"#;
|
||||
assert_eq!(ser, exp);
|
||||
|
||||
let ok_dec: MethodResult = serde_json::from_str(exp).unwrap();
|
||||
assert_eq!(ok_dec, ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_result_error() {
|
||||
let ok = MethodResult::err("hello");
|
||||
|
||||
let ser = serde_json::to_string(&ok).unwrap();
|
||||
let exp = r#"{"success":false,"error":"hello"}"#;
|
||||
assert_eq!(ser, exp);
|
||||
|
||||
let ok_dec: MethodResult = serde_json::from_str(exp).unwrap();
|
||||
assert_eq!(ok_dec, ok);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user