From 18afbb57657a842452c3bbb314e37d8745f35475 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 3 Jul 2021 12:04:48 +0200 Subject: [PATCH] Add contracts RPC (#9261) * Add contracts RPC * cleanup * Make pretty --- Cargo.lock | 10 +- bin/node/cli/Cargo.toml | 1 + bin/node/cli/src/service.rs | 9 +- bin/node/rpc/Cargo.toml | 4 - bin/node/rpc/src/lib.rs | 33 +-- frame/contracts/rpc/Cargo.toml | 9 +- frame/contracts/rpc/src/lib.rs | 386 ++++++++++++++++----------------- 7 files changed, 204 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5564abd25f0f3..a40ddd44b8c92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4168,6 +4168,7 @@ dependencies = [ "pallet-authority-discovery", "pallet-balances", "pallet-contracts", + "pallet-contracts-rpc", "pallet-grandpa", "pallet-im-online", "pallet-indices", @@ -4304,16 +4305,12 @@ version = "2.0.0" dependencies = [ "jsonrpc-core", "node-primitives", - "pallet-contracts-rpc", "sc-client-api", "sc-consensus-babe", "sc-consensus-epochs", "sc-finality-grandpa", "sc-rpc", "sc-rpc-api", - "sp-api", - "sp-block-builder", - "sp-blockchain", "sp-keystore", ] @@ -4895,9 +4892,8 @@ dependencies = [ name = "pallet-contracts-rpc" version = "3.0.0" dependencies = [ - "jsonrpc-core", - "jsonrpc-core-client", - "jsonrpc-derive", + "jsonrpsee", + "jsonrpsee-types", "pallet-contracts-primitives", "pallet-contracts-rpc-runtime-api", "parity-scale-codec", diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index b1cc91cf2fa4a..97b1c409e5b89 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -95,6 +95,7 @@ pallet-grandpa = { version = "3.1.0", path = "../../../frame/grandpa" } pallet-transaction-payment-rpc = { version = "3.0.0", path = "../../../frame/transaction-payment/rpc/" } substrate-frame-rpc-system = { version = "3.0.0", path = "../../../utils/frame/rpc/system/" } pallet-mmr-rpc = { version = "3.0.0", path = "../../../frame/merkle-mountain-range/rpc/" } +pallet-contracts-rpc = { version = "3.0.0", path = "../../../frame/contracts/rpc/" } # node-specific dependencies node-runtime = { version = "2.0.0", path = "../runtime" } diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index 4653e9c40b708..4c0e02a2c29e3 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -42,6 +42,7 @@ use sc_sync_state_rpc::SyncStateRpc; use pallet_transaction_payment_rpc::TransactionPaymentRpc; use substrate_frame_rpc_system::{SystemRpc, SystemRpcBackendFull}; use pallet_mmr_rpc::MmrRpc; +use pallet_contracts_rpc::ContractsRpc; type FullClient = sc_service::TFullClient; type FullBackend = sc_service::TFullBackend; @@ -195,7 +196,8 @@ pub fn new_partial( let system_rpc = SystemRpc::new(Box::new(system_rpc_backend)).into_rpc_module().expect("TODO: error handling"); let mmr_rpc = MmrRpc::new(client2.clone()).into_rpc_module().expect("TODO: error handling"); - // TODO: add other rpc modules here + let contracts_rpc = ContractsRpc::new(client2.clone()).into_rpc_module().expect("TODO: error handling"); + let mut module = RpcModule::new(()); module.merge(grandpa_rpc).expect("TODO: error handling"); module.merge(babe_rpc).expect("TODO: error handling"); @@ -203,6 +205,7 @@ pub fn new_partial( module.merge(transaction_payment_rpc).expect("TODO: error handling"); module.merge(system_rpc).expect("TODO: error handling"); module.merge(mmr_rpc).expect("TODO: error handling"); + module.merge(contracts_rpc).expect("TODO: error handling"); module }; @@ -211,10 +214,8 @@ pub fn new_partial( // TODO: (dp) remove this when all APIs are ported. let (rpc_extensions_builder, rpc_setup) = { let rpc_setup = grandpa::SharedVoterState::empty(); - let client = client.clone(); let rpc_extensions_builder = move |_deny_unsafe, _subscription_executor| { - let deps = node_rpc::FullDeps { client: client.clone() }; - node_rpc::create_full(deps) + node_rpc::create_full() }; (rpc_extensions_builder, rpc_setup) diff --git a/bin/node/rpc/Cargo.toml b/bin/node/rpc/Cargo.toml index 9b1268557c62d..8533445bc7128 100644 --- a/bin/node/rpc/Cargo.toml +++ b/bin/node/rpc/Cargo.toml @@ -13,14 +13,10 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] jsonrpc-core = "15.1.0" node-primitives = { version = "2.0.0", path = "../primitives" } -pallet-contracts-rpc = { version = "3.0.0", path = "../../../frame/contracts/rpc/" } sc-client-api = { version = "3.0.0", path = "../../../client/api" } sc-consensus-babe = { version = "0.9.0", path = "../../../client/consensus/babe" } sc-consensus-epochs = { version = "0.9.0", path = "../../../client/consensus/epochs" } sc-finality-grandpa = { version = "0.9.0", path = "../../../client/finality-grandpa" } sc-rpc-api = { version = "0.9.0", path = "../../../client/rpc-api" } sc-rpc = { version = "3.0.0", path = "../../../client/rpc" } -sp-api = { version = "3.0.0", path = "../../../primitives/api" } -sp-block-builder = { version = "3.0.0", path = "../../../primitives/block-builder" } -sp-blockchain = { version = "3.0.0", path = "../../../primitives/blockchain" } sp-keystore = { version = "0.9.0", path = "../../../primitives/keystore" } diff --git a/bin/node/rpc/src/lib.rs b/bin/node/rpc/src/lib.rs index 6e5c96ed9e582..45485c59fa834 100644 --- a/bin/node/rpc/src/lib.rs +++ b/bin/node/rpc/src/lib.rs @@ -34,18 +34,14 @@ use std::sync::Arc; use sp_keystore::SyncCryptoStorePtr; -use node_primitives::{Block, BlockNumber, AccountId, Balance, Hash}; +use node_primitives::{Block, BlockNumber, Hash}; use sc_consensus_babe::{Config, Epoch}; use sc_consensus_epochs::SharedEpochChanges; use sc_finality_grandpa::{ SharedVoterState, SharedAuthoritySet, FinalityProofProvider, GrandpaJustificationStream }; pub use sc_rpc_api::DenyUnsafe; -use sp_api::ProvideRuntimeApi; -use sp_block_builder::BlockBuilder; -use sp_blockchain::{Error as BlockChainError, HeaderMetadata, HeaderBackend}; use sc_rpc::SubscriptionTaskExecutor; -use sc_client_api::AuxStore; /// Light client extra dependencies. pub struct LightDeps { @@ -83,35 +79,12 @@ pub struct GrandpaDeps { pub finality_provider: Arc>, } -/// Full client dependencies. -pub struct FullDeps { - /// The client instance to use. - pub client: Arc, -} - /// A IO handler that uses all Full RPC extensions. pub type IoHandler = jsonrpc_core::IoHandler<()>; /// Instantiate all Full RPC extensions. // TODO(niklasad1): replace these. -pub fn create_full( - deps: FullDeps, -) -> jsonrpc_core::IoHandler<()> where - C: ProvideRuntimeApi + HeaderBackend + AuxStore + - HeaderMetadata + Sync + Send + 'static, - C::Api: pallet_contracts_rpc::ContractsRuntimeApi, - C::Api: BlockBuilder, +pub fn create_full() -> jsonrpc_core::IoHandler<()> { - use pallet_contracts_rpc::{Contracts, ContractsApi}; - - let mut io = jsonrpc_core::IoHandler::default(); - let FullDeps { client } = deps; - - // Making synchronous calls in light client freezes the browser currently, - // more context: https://github.com/paritytech/substrate/pull/3480 - // These RPCs should use an asynchronous caller instead. - io.extend_with( - ContractsApi::to_delegate(Contracts::new(client.clone())) - ); - io + jsonrpc_core::IoHandler::default() } diff --git a/frame/contracts/rpc/Cargo.toml b/frame/contracts/rpc/Cargo.toml index dbd4356acc4a9..50e5ecbd39b2f 100644 --- a/frame/contracts/rpc/Cargo.toml +++ b/frame/contracts/rpc/Cargo.toml @@ -14,10 +14,10 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { package = "parity-scale-codec", version = "2" } -jsonrpc-core = "15" -jsonrpc-core-client = "15" -jsonrpc-derive = "15" +jsonrpsee = { git = "https://github.com/paritytech/jsonrpsee", branch = "master", features = ["server"] } +jsonrpsee-types = { git = "https://github.com/paritytech/jsonrpsee", branch = "master" } serde = { version = "1", features = ["derive"] } +serde_json = "1" # Substrate Dependencies pallet-contracts-primitives = { version = "3.0.0", path = "../common" } @@ -27,6 +27,3 @@ sp-blockchain = { version = "3.0.0", path = "../../../primitives/blockchain" } sp-core = { version = "3.0.0", path = "../../../primitives/core" } sp-rpc = { version = "3.0.0", path = "../../../primitives/rpc" } sp-runtime = { version = "3.0.0", path = "../../../primitives/runtime" } - -[dev-dependencies] -serde_json = "1" diff --git a/frame/contracts/rpc/src/lib.rs b/frame/contracts/rpc/src/lib.rs index 1250d3cb285e7..eaa223673a275 100644 --- a/frame/contracts/rpc/src/lib.rs +++ b/frame/contracts/rpc/src/lib.rs @@ -17,29 +17,33 @@ //! Node-specific RPC methods for interaction with contracts. -use std::sync::Arc; +#![warn(unused_crate_dependencies)] + +use std::{marker::PhantomData, sync::Arc}; use codec::Codec; -use jsonrpc_core::{Error, ErrorCode, Result}; -use jsonrpc_derive::rpc; -use pallet_contracts_primitives::RentProjection; +use jsonrpsee::RpcModule; +use jsonrpsee_types::error::{CallError, Error as JsonRpseeError}; +use pallet_contracts_primitives::{ + Code, ContractExecResult, ContractInstantiateResult, RentProjection, +}; use serde::{Deserialize, Serialize}; +use serde_json::value::to_raw_value; use sp_api::ProvideRuntimeApi; use sp_blockchain::HeaderBackend; use sp_core::{Bytes, H256}; use sp_rpc::number::NumberOrHex; use sp_runtime::{ generic::BlockId, - traits::{Block as BlockT, Header as HeaderT}, + traits::{self, Block as BlockT, Header as HeaderT}, }; use std::convert::{TryFrom, TryInto}; -use pallet_contracts_primitives::{Code, ContractExecResult, ContractInstantiateResult}; pub use pallet_contracts_rpc_runtime_api::ContractsApi as ContractsRuntimeApi; -const RUNTIME_ERROR: i64 = 1; -const CONTRACT_DOESNT_EXIST: i64 = 2; -const CONTRACT_IS_A_TOMBSTONE: i64 = 3; +const RUNTIME_ERROR: i32 = 1; +const CONTRACT_DOESNT_EXIST: i32 = 2; +const CONTRACT_IS_A_TOMBSTONE: i32 = 3; pub type Weight = u64; @@ -59,17 +63,17 @@ const GAS_LIMIT: Weight = 5 * GAS_PER_SECOND; /// A private newtype for converting `ContractAccessError` into an RPC error. struct ContractAccessError(pallet_contracts_primitives::ContractAccessError); -impl From for Error { - fn from(e: ContractAccessError) -> Error { +impl From for CallError { + fn from(e: ContractAccessError) -> CallError { use pallet_contracts_primitives::ContractAccessError::*; match e.0 { - DoesntExist => Error { - code: ErrorCode::ServerError(CONTRACT_DOESNT_EXIST), + DoesntExist => CallError::Custom { + code: CONTRACT_DOESNT_EXIST, message: "The specified contract doesn't exist.".into(), data: None, }, - IsTombstone => Error { - code: ErrorCode::ServerError(CONTRACT_IS_A_TOMBSTONE), + IsTombstone => CallError::Custom { + code: CONTRACT_IS_A_TOMBSTONE, message: "The contract is a tombstone and doesn't have any storage.".into(), data: None, }, @@ -103,217 +107,204 @@ pub struct InstantiateRequest { } /// Contracts RPC methods. -#[rpc] -pub trait ContractsApi { - /// Executes a call to a contract. - /// - /// This call is performed locally without submitting any transactions. Thus executing this - /// won't change any state. Nonetheless, the calling state-changing contracts is still possible. - /// - /// This method is useful for calling getter-like methods on contracts. - #[rpc(name = "contracts_call")] - fn call( - &self, - call_request: CallRequest, - at: Option, - ) -> Result; - - /// Instantiate a new contract. - /// - /// This call is performed locally without submitting any transactions. Thus the contract - /// is not actually created. - /// - /// This method is useful for UIs to dry-run contract instantiations. - #[rpc(name = "contracts_instantiate")] - fn instantiate( - &self, - instantiate_request: InstantiateRequest, - at: Option, - ) -> Result>; - - /// Returns the value under a specified storage `key` in a contract given by `address` param, - /// or `None` if it is not set. - #[rpc(name = "contracts_getStorage")] - fn get_storage( - &self, - address: AccountId, - key: H256, - at: Option, - ) -> Result>; - - /// Returns the projected time a given contract will be able to sustain paying its rent. - /// - /// The returned projection is relevant for the given block, i.e. it is as if the contract was - /// accessed at the beginning of that block. - /// - /// Returns `None` if the contract is exempted from rent. - #[rpc(name = "contracts_rentProjection")] - fn rent_projection( - &self, - address: AccountId, - at: Option, - ) -> Result>; -} - -/// An implementation of contract specific RPC methods. -pub struct Contracts { - client: Arc, - _marker: std::marker::PhantomData, +pub struct ContractsRpc { + client: Arc, + _block: PhantomData, + _account_id: PhantomData, + _balance: PhantomData, + _hash: PhantomData, } -impl Contracts { - /// Create new `Contracts` with the given reference to the client. - pub fn new(client: Arc) -> Self { - Contracts { - client, - _marker: Default::default(), - } - } -} -impl - ContractsApi< - ::Hash, - <::Header as HeaderT>::Number, - AccountId, - Balance, - Hash, - > for Contracts +impl ContractsRpc where Block: BlockT, - C: Send + Sync + 'static + ProvideRuntimeApi + HeaderBackend, - C::Api: ContractsRuntimeApi< + Client: Send + Sync + 'static + ProvideRuntimeApi + HeaderBackend, + Client::Api: ContractsRuntimeApi< Block, AccountId, Balance, <::Header as HeaderT>::Number, Hash, >, - AccountId: Codec, - Balance: Codec + TryFrom, - Hash: Codec, + AccountId: traits::MaybeSerializeDeserialize + Codec + Send + Sync + 'static, + Balance: Codec + TryFrom + Send + Sync + 'static, + Hash: traits::MaybeSerializeDeserialize + Codec + Send + Sync + 'static, { - fn call( - &self, - call_request: CallRequest, - at: Option<::Hash>, - ) -> Result { - let api = self.client.runtime_api(); - let at = BlockId::hash(at.unwrap_or_else(|| - // If the block hash is not supplied assume the best block. - self.client.info().best_hash)); - - let CallRequest { - origin, - dest, - value, - gas_limit, - input_data, - } = call_request; - - let value: Balance = decode_hex(value, "balance")?; - let gas_limit: Weight = decode_hex(gas_limit, "weight")?; - limit_gas(gas_limit)?; - - let exec_result = api - .call(&at, origin, dest, value, gas_limit, input_data.to_vec()) - .map_err(runtime_error_into_rpc_err)?; - - Ok(exec_result) + pub fn new(client: Arc) -> Self { + Self { + client, + _block: Default::default(), + _account_id: Default::default(), + _balance: Default::default(), + _hash: Default::default(), + } } - fn instantiate( - &self, - instantiate_request: InstantiateRequest, - at: Option<::Hash>, - ) -> Result::Header as HeaderT>::Number>> { - let api = self.client.runtime_api(); - let at = BlockId::hash(at.unwrap_or_else(|| - // If the block hash is not supplied assume the best block. - self.client.info().best_hash)); - - let InstantiateRequest { - origin, - endowment, - gas_limit, - code, - data, - salt, - } = instantiate_request; - - let endowment: Balance = decode_hex(endowment, "balance")?; - let gas_limit: Weight = decode_hex(gas_limit, "weight")?; - limit_gas(gas_limit)?; - - let exec_result = api - .instantiate(&at, origin, endowment, gas_limit, code, data.to_vec(), salt.to_vec()) - .map_err(runtime_error_into_rpc_err)?; - - Ok(exec_result) - } + /// Convert a [`ContractsRpc`] to an [`RpcModule`]. Registers all the RPC methods available with the RPC server. + pub fn into_rpc_module(self) -> Result, JsonRpseeError> { + let mut module = RpcModule::new(self); + + // Executes a call to a contract. + // + // This call is performed locally without submitting any transactions. Thus executing this + // won't change any state. Nonetheless, calling state-changing contracts is still possible. + // + // This method is useful for calling getter-like methods on contracts. + module.register_method( + "contracts_call", + |params, contracts| -> Result { + let (call_request, at): (CallRequest, Option<::Hash>) = + params.parse()?; + let api = contracts.client.runtime_api(); + let at = BlockId::hash(at.unwrap_or_else(|| contracts.client.info().best_hash)); + + let CallRequest { + origin, + dest, + value, + gas_limit, + input_data, + } = call_request; + + let value: Balance = decode_hex(value, "balance")?; + let gas_limit: Weight = decode_hex(gas_limit, "weight")?; + limit_gas(gas_limit)?; + + let exec_result = api + .call(&at, origin, dest, value, gas_limit, input_data.to_vec()) + .map_err(runtime_error_into_rpc_err)?; + + Ok(exec_result) + }, + )?; + + // Instantiate a new contract. + // + // This call is performed locally without submitting any transactions. Thus the contract + // is not actually created. + // + // This method is useful for UIs to dry-run contract instantiations. + module.register_method( + "contracts_instantiate", + |params, + contracts| + -> Result< + ContractInstantiateResult< + AccountId, + <::Header as HeaderT>::Number, + >, + CallError, + > { + let (instantiate_request, at): ( + InstantiateRequest, + Option<::Hash>, + ) = params.parse()?; + + let api = contracts.client.runtime_api(); + let at = BlockId::hash(at.unwrap_or_else(|| contracts.client.info().best_hash)); + let InstantiateRequest { + origin, + endowment, + gas_limit, + code, + data, + salt, + } = instantiate_request; + + let endowment: Balance = decode_hex(endowment, "balance")?; + let gas_limit: Weight = decode_hex(gas_limit, "weight")?; + limit_gas(gas_limit)?; + + let exec_result = api + .instantiate( + &at, + origin, + endowment, + gas_limit, + code, + data.to_vec(), + salt.to_vec(), + ) + .map_err(runtime_error_into_rpc_err)?; + + Ok(exec_result) + }, + )?; + + // Returns the value under a specified storage `key` in a contract given by `address` param, + // or `None` if it is not set. + module.register_method( + "contracts_getStorage", + |params, contracts| -> Result, CallError> { + let (address, key, at): (AccountId, H256, Option<::Hash>) = + params.parse()?; + + let api = contracts.client.runtime_api(); + let at = BlockId::hash(at.unwrap_or_else(|| contracts.client.info().best_hash)); + let result = api + .get_storage(&at, address, key.into()) + .map_err(runtime_error_into_rpc_err)? + .map_err(ContractAccessError)? + .map(Bytes); + + Ok(result) + }, + )?; + + // Returns the projected time a given contract will be able to sustain paying its rent. + // + // The returned projection is relevant for the given block, i.e. it is as if the contract was + // accessed at the beginning of that block. + // + // Returns `None` if the contract is exempted from rent. + module.register_method( + "contracts_rentProjection", + |params, contracts| -> Result::Header as HeaderT>::Number>, CallError> + { + let (address, at): (AccountId, Option<::Hash>) = params.parse()?; - fn get_storage( - &self, - address: AccountId, - key: H256, - at: Option<::Hash>, - ) -> Result> { - let api = self.client.runtime_api(); - let at = BlockId::hash(at.unwrap_or_else(|| - // If the block hash is not supplied assume the best block. - self.client.info().best_hash)); - - let result = api - .get_storage(&at, address, key.into()) - .map_err(runtime_error_into_rpc_err)? - .map_err(ContractAccessError)? - .map(Bytes); - - Ok(result) - } + let api = contracts.client.runtime_api(); + let at = BlockId::hash(at.unwrap_or_else(|| contracts.client.info().best_hash)); - fn rent_projection( - &self, - address: AccountId, - at: Option<::Hash>, - ) -> Result::Header as HeaderT>::Number>> { - let api = self.client.runtime_api(); - let at = BlockId::hash(at.unwrap_or_else(|| - // If the block hash is not supplied assume the best block. - self.client.info().best_hash)); - - let result = api - .rent_projection(&at, address) - .map_err(runtime_error_into_rpc_err)? - .map_err(ContractAccessError)?; - - Ok(match result { - RentProjection::NoEviction => None, - RentProjection::EvictionAt(block_num) => Some(block_num), - }) + let result = api + .rent_projection(&at, address) + .map_err(runtime_error_into_rpc_err)? + .map_err(ContractAccessError)?; + + Ok(match result { + RentProjection::NoEviction => None, + RentProjection::EvictionAt(block_num) => Some(block_num), + }) + })?; + + Ok(module) } } /// Converts a runtime trap into an RPC error. -fn runtime_error_into_rpc_err(err: impl std::fmt::Debug) -> Error { - Error { - code: ErrorCode::ServerError(RUNTIME_ERROR), +fn runtime_error_into_rpc_err(err: impl std::fmt::Debug) -> CallError { + CallError::Custom { + code: RUNTIME_ERROR, message: "Runtime error".into(), - data: Some(format!("{:?}", err).into()), + data: to_raw_value(&format!("{:?}", err)).ok(), } } -fn decode_hex>(from: H, name: &str) -> Result { - from.try_into().map_err(|_| Error { - code: ErrorCode::InvalidParams, +fn decode_hex>( + from: H, + name: &str, +) -> Result { + from.try_into().map_err(|_| CallError::Custom { + code: -32602, // TODO: was `ErrorCode::InvalidParams` message: format!("{:?} does not fit into the {} type", from, name), data: None, }) } -fn limit_gas(gas_limit: Weight) -> Result<()> { +fn limit_gas(gas_limit: Weight) -> Result<(), CallError> { if gas_limit > GAS_LIMIT { - Err(Error { - code: ErrorCode::InvalidParams, + Err(CallError::Custom { + code: -32602, // TODO: was `ErrorCode::InvalidParams,` message: format!( "Requested gas limit is greater than maximum allowed: {} > {}", gas_limit, GAS_LIMIT @@ -329,6 +320,7 @@ fn limit_gas(gas_limit: Weight) -> Result<()> { mod tests { use super::*; use sp_core::U256; + use pallet_contracts_primitives::{ContractExecResult, ContractInstantiateResult}; fn trim(json: &str) -> String { json.chars().filter(|c| !c.is_whitespace()).collect()