diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 48d6fd2823e..7f84d67bbc3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,7 +22,7 @@ jobs: run: cargo +nightly fmt --all -- --check - name: build - run: cargo build --verbose + run: cargo build --workspace --verbose - name: test - run: cargo test --verbose + run: cargo test --workspace --verbose diff --git a/Cargo.toml b/Cargo.toml index 9b98fb18e10..af3ef0a7209 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "proc-macro"] +members = [".", "light-client", "proc-macro"] [package] name = "substrate-subxt" @@ -16,6 +16,9 @@ description = "Submit extrinsics (transactions) to a substrate node via RPC" keywords = ["parity", "substrate", "blockchain"] include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] +[features] +light-client = ["substrate-subxt-light-client"] + [dependencies] log = "0.4" thiserror = "1.0" @@ -37,6 +40,7 @@ sp-rpc = { version = "2.0.0-rc2", package = "sp-rpc" } sp-core = { version = "2.0.0-rc2", package = "sp-core" } sc-rpc-api = { version = "0.8.0-rc2", package = "sc-rpc-api" } sp-transaction-pool = { version = "2.0.0-rc2", package = "sp-transaction-pool" } +substrate-subxt-light-client = { path = "light-client", optional = true } substrate-subxt-proc-macro = { version = "0.8.0", path = "proc-macro" } [dev-dependencies] @@ -46,3 +50,8 @@ wabt = "0.9" frame-system = { version = "2.0.0-rc2", package = "frame-system" } pallet-balances = { version = "2.0.0-rc2", package = "pallet-balances" } sp-keyring = { version = "2.0.0-rc2", package = "sp-keyring" } + +[patch.crates-io] +sc-informant = { path = "../substrate/client/informant" } +sc-network = { path = "../substrate/client/network" } +sc-service = { path = "../substrate/client/service" } diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml new file mode 100644 index 00000000000..41e43774ca8 --- /dev/null +++ b/light-client/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "substrate-subxt-light-client" +version = "0.1.0" +authors = ["David Craven "] +edition = "2018" + +[dependencies] +async-std = "1.6.0" +futures = { version = "0.3.5", features = ["compat"] } +futures01 = { package = "futures", version = "0.1.29" } +jsonrpsee = "0.1.0" +log = "0.4.8" +sc-informant = { version = "0.8.0-rc2", default-features = false } +sc-network = { version = "0.8.0-rc2", default-features = false } +sc-service = { version = "0.8.0-rc2", default-features = false } +serde_json = "1.0.53" +thiserror = "1.0.19" + +[dev-dependencies] +async-std = { version = "1.6.0", features = ["attributes"] } +env_logger = "0.7.1" +node-template = { path = "../../substrate/bin/node-template/node" } +sp-keyring = "2.0.0-rc2" +substrate-subxt = { path = ".." } diff --git a/light-client/src/lib.rs b/light-client/src/lib.rs new file mode 100644 index 00000000000..4c678cf1c58 --- /dev/null +++ b/light-client/src/lib.rs @@ -0,0 +1,276 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of substrate-subxt. +// +// subxt 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. +// +// subxt 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 substrate-subxt. If not, see . + +use async_std::task; +use futures::{ + compat::{ + Compat01As03, + Compat01As03Sink, + Sink01CompatExt, + Stream01CompatExt, + }, + future::poll_fn, + sink::SinkExt, + stream::{ + Stream, + StreamExt, + }, +}; +use futures01::sync::mpsc; +use jsonrpsee::{ + common::{ + Request, + Response, + }, + transport::TransportClient, +}; +use sc_network::config::TransportConfig; +pub use sc_service::{ + config::DatabaseConfig, + Error as ServiceError, +}; +use sc_service::{ + config::{ + KeystoreConfig, + NetworkConfiguration, + TaskType, + }, + AbstractService, + ChainSpec, + Configuration, + Role, + RpcSession, +}; +use std::{ + future::Future, + pin::Pin, + sync::Arc, + task::Poll, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LightClientError { + #[error("{0}")] + Json(#[from] serde_json::Error), + #[error("{0}")] + Mpsc(#[from] mpsc::SendError), +} + +#[derive(Clone)] +pub struct LightClientConfig { + pub impl_name: &'static str, + pub impl_version: &'static str, + pub author: &'static str, + pub copyright_start_year: i32, + pub db: DatabaseConfig, + pub builder: fn(Configuration) -> Result, + pub chain_spec: C, +} + +pub struct LightClient { + to_back: Compat01As03Sink, String>, + from_back: Compat01As03>, +} + +impl LightClient { + pub fn new( + config: LightClientConfig, + ) -> Result { + let (to_back, from_front) = mpsc::channel(4); + let (to_front, from_back) = mpsc::channel(4); + start_light_client(config, from_front, to_front)?; + Ok(LightClient { + to_back: to_back.sink_compat(), + from_back: from_back.compat(), + }) + } +} + +impl TransportClient for LightClient { + type Error = LightClientError; + + fn send_request<'a>( + &'a mut self, + request: Request, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let request = serde_json::to_string(&request)?; + self.to_back.send(request).await?; + Ok(()) + }) + } + + fn next_response<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let response = self + .from_back + .next() + .await + .expect("channel shouldn't close") + .unwrap(); + Ok(serde_json::from_str(&response)?) + }) + } +} + +impl From for jsonrpsee::Client { + fn from(client: LightClient) -> Self { + let client = jsonrpsee::raw::RawClient::new(client); + jsonrpsee::Client::new(client) + } +} + +fn start_light_client( + config: LightClientConfig, + from_front: mpsc::Receiver, + to_front: mpsc::Sender, +) -> Result<(), ServiceError> { + let mut network = NetworkConfiguration::new( + format!("{} (light client)", config.chain_spec.name()), + "unknown", + Default::default(), + None, + ); + network.boot_nodes = config.chain_spec.boot_nodes().to_vec(); + network.transport = TransportConfig::Normal { + enable_mdns: true, + allow_private_ipv4: true, + wasm_external_transport: None, + use_yamux_flow_control: true, + }; + let service_config = Configuration { + network, + impl_name: config.impl_name, + impl_version: config.impl_version, + chain_spec: Box::new(config.chain_spec), + role: Role::Light, + task_executor: Arc::new(move |fut, ty| { + match ty { + TaskType::Async => task::spawn(fut), + TaskType::Blocking => task::spawn_blocking(|| task::block_on(fut)), + }; + }), + database: config.db, + keystore: KeystoreConfig::InMemory, + max_runtime_instances: 8, + announce_block: true, + + telemetry_endpoints: Default::default(), + telemetry_external_transport: Default::default(), + default_heap_pages: Default::default(), + dev_key_seed: Default::default(), + disable_grandpa: Default::default(), + execution_strategies: Default::default(), + force_authoring: Default::default(), + offchain_worker: Default::default(), + prometheus_config: Default::default(), + pruning: Default::default(), + rpc_cors: Default::default(), + rpc_http: Default::default(), + rpc_ws: Default::default(), + rpc_ws_max_connections: Default::default(), + rpc_methods: Default::default(), + state_cache_child_ratio: Default::default(), + state_cache_size: Default::default(), + tracing_receiver: Default::default(), + tracing_targets: Default::default(), + transaction_pool: Default::default(), + wasm_method: Default::default(), + }; + + log::info!("{}", service_config.impl_name); + log::info!("✌️ version {}", service_config.impl_version); + log::info!("❤️ by {}, {}", config.author, config.copyright_start_year); + log::info!( + "📋 Chain specification: {}", + service_config.chain_spec.name() + ); + log::info!("🏷 Node name: {}", service_config.network.node_name); + log::info!("👤 Role: {:?}", service_config.role); + + // Create the service. This is the most heavy initialization step. + let mut service = (config.builder)(service_config)?; + + // Spawn informant. + task::spawn(sc_informant::build( + &service, + sc_informant::OutputFormat::Plain, + )); + + // Spawn background task. + let session = RpcSession::new(to_front.clone()); + let mut from_front = from_front.compat(); + task::spawn(poll_fn(move |cx| { + loop { + match Pin::new(&mut from_front).poll_next(cx) { + Poll::Ready(Some(message)) => { + let mut to_front = to_front.clone().sink_compat(); + let message = message.unwrap(); + let fut = service.rpc_query(&session, &message); + task::spawn(async move { + if let Some(response) = fut.await { + to_front.send(response).await.ok(); + } + }); + } + Poll::Pending => break, + Poll::Ready(None) => return Poll::Ready(()), + } + } + + loop { + match Pin::new(&mut service).poll(cx) { + Poll::Ready(Ok(())) => return Poll::Ready(()), + Poll::Pending => return Poll::Pending, + Poll::Ready(Err(e)) => log::error!("{}", e), + } + } + })); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use sp_keyring::AccountKeyring; + use substrate_subxt::{ClientBuilder, DefaultNodeRuntime, PairSigner, balances::TransferCallExt}; + + #[async_std::test] + async fn test_light_client() { + env_logger::try_init().ok(); + let light_client_config = LightClientConfig { + impl_name: "Substrate Node", + impl_version: "2.0.0-alpha.8-7e9a2ae78-x86_64-linux-gnu", + author: "author", + copyright_start_year: 2020, + db: DatabaseConfig::RocksDb { path: "/tmp/subxt-light-client".into(), cache_size: 64 }, + builder: node_template::service::new_light, + chain_spec: node_template::chain_spec::local_testnet_config(), + }; + let client = ClientBuilder::::new() + .set_client(LightClient::new(light_client_config).unwrap()) + .build() + .await + .unwrap(); + let signer = PairSigner::new(AccountKeyring::Eve.pair()); + let to = AccountKeyring::Bob.to_account_id().into(); + client.transfer_and_watch(&signer, &to, 10_000).await.unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9d38535627a..2f95db069e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,9 @@ #[macro_use] extern crate substrate_subxt_proc_macro; +#[cfg(feature = "light-client")] +pub use substrate_subxt_light_client as light_client; + pub use sp_core; pub use sp_runtime;