diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1b944157fe9c5..10b293acd8bac 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -196,25 +196,13 @@ check-web-wasm: - time cargo web build -p sr-io - time cargo web build -p sr-primitives - time cargo web build -p sr-std - - time cargo web build -p substrate-chain-spec - time cargo web build -p substrate-client - time cargo web build -p substrate-consensus-aura - time cargo web build -p substrate-consensus-babe - time cargo web build -p substrate-consensus-common - - time cargo web build -p substrate-keyring - - time cargo web build -p substrate-keystore - - time cargo web build -p substrate-executor - - time cargo web build -p substrate-network - - time cargo web build -p substrate-offchain - - time cargo web build -p substrate-panic-handler - - time cargo web build -p substrate-peerset - - time cargo web build -p substrate-primitives - - time cargo web build -p substrate-rpc-servers - - time cargo web build -p substrate-serializer - - time cargo web build -p substrate-state-db - - time cargo web build -p substrate-state-machine - time cargo web build -p substrate-telemetry - - time cargo web build -p substrate-trie + # Note: the command below is a bit weird because several Cargo issues prevent us from compiling the node in a more straight-forward way. + - time cargo build --manifest-path=node/cli/Cargo.toml --no-default-features --features "browser" --target=wasm32-unknown-unknown - sccache -s node-exits: diff --git a/Cargo.lock b/Cargo.lock index 75a1b797b07c3..5244444ecd26a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -468,6 +468,24 @@ dependencies = [ "cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "console_log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "const-random" version = "0.1.6" @@ -1153,6 +1171,7 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2377,18 +2396,25 @@ dependencies = [ name = "node-cli" version = "2.0.0" dependencies = [ + "clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "exit-future 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "futures-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", "hex-literal 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys 0.3.28 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 13.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "kvdb-memorydb 0.1.0 (git+https://github.com/paritytech/parity-common?rev=b0317f649ab2c665b7987b8475878fc4d2e1f81d)", + "libp2p 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "node-executor 2.0.0", "node-primitives 2.0.0", "node-rpc 2.0.0", "node-runtime 2.0.0", "parity-scale-codec 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", @@ -2428,6 +2454,8 @@ dependencies = [ "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "transaction-factory 0.0.1", "vergen 3.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.3.27 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3351,6 +3379,7 @@ dependencies = [ "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -6909,6 +6938,8 @@ dependencies = [ "checksum clear_on_drop 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" +"checksum console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48" "checksum const-random 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7b641a8c9867e341f3295564203b1c250eb8ce6cb6126e007941f78c4d2ed7fe" "checksum const-random-macro 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c750ec12b83377637110d5a57f5ae08e895b06c4b16e2bdbf1a94ef717428c59" "checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml index a2723484ad4c1..2298bba0b711f 100644 --- a/core/cli/Cargo.toml +++ b/core/cli/Cargo.toml @@ -28,7 +28,7 @@ header-metadata = { package = "substrate-header-metadata", path = "../../core/cl network = { package = "substrate-network", path = "../../core/network" } sr-primitives = { path = "../../core/sr-primitives" } primitives = { package = "substrate-primitives", path = "../../core/primitives" } -service = { package = "substrate-service", path = "../../core/service" } +service = { package = "substrate-service", path = "../../core/service", default-features = false } state-machine = { package = "substrate-state-machine", path = "../../core/state-machine" } substrate-telemetry = { path = "../../core/telemetry" } keyring = { package = "substrate-keyring", path = "../keyring" } diff --git a/core/service/Cargo.toml b/core/service/Cargo.toml index 0d6f27936244f..1a85ed8de0f70 100644 --- a/core/service/Cargo.toml +++ b/core/service/Cargo.toml @@ -4,6 +4,12 @@ version = "2.0.0" authors = ["Parity Technologies "] edition = "2018" +[features] +default = ["rocksdb"] +# The RocksDB feature activates the RocksDB database backend. If it is not activated, and you pass +# a path to a database, an error will be produced at runtime. +rocksdb = ["client_db/kvdb-rocksdb"] + [dependencies] derive_more = "0.15.0" futures = "0.1.29" @@ -29,7 +35,7 @@ consensus_common = { package = "substrate-consensus-common", path = "../../core/ network = { package = "substrate-network", path = "../../core/network" } chain-spec = { package = "substrate-chain-spec", path = "../chain-spec" } client = { package = "substrate-client", path = "../../core/client" } -client_db = { package = "substrate-client-db", path = "../../core/client/db", features = ["kvdb-rocksdb"] } +client_db = { package = "substrate-client-db", path = "../../core/client/db" } codec = { package = "parity-scale-codec", version = "1.0.0" } substrate-executor = { path = "../../core/executor" } transaction_pool = { package = "substrate-transaction-pool", path = "../../core/transaction-pool" } diff --git a/core/service/src/lib.rs b/core/service/src/lib.rs index 3057b8ce4dcd4..c46732d511b3e 100644 --- a/core/service/src/lib.rs +++ b/core/service/src/lib.rs @@ -541,15 +541,16 @@ fn start_rpc_servers rpc_servers::RpcHandler components::RpcHandler>( +fn start_rpc_servers rpc_servers::RpcHandler>( _: &Configuration, _: H -) -> Result, error::Error> { +) -> Result, error::Error> { Ok(Box::new(())) } /// An RPC session. Used to perform in-memory RPC queries (ie. RPC queries that don't go through /// the HTTP or WebSockets server). +#[derive(Clone)] pub struct RpcSession { metadata: rpc::Metadata, } diff --git a/core/service/test/Cargo.toml b/core/service/test/Cargo.toml index 872d63415f4e8..4415369a6a1d3 100644 --- a/core/service/test/Cargo.toml +++ b/core/service/test/Cargo.toml @@ -12,7 +12,7 @@ log = "0.4.8" env_logger = "0.7.0" fdlimit = "0.1.1" futures03 = { package = "futures-preview", version = "=0.3.0-alpha.19", features = ["compat"] } -service = { package = "substrate-service", path = "../../../core/service" } +service = { package = "substrate-service", path = "../../../core/service", default-features = false } network = { package = "substrate-network", path = "../../../core/network" } consensus = { package = "substrate-consensus-common", path = "../../../core/consensus/common" } client = { package = "substrate-client", path = "../../../core/client" } diff --git a/node/cli/Cargo.toml b/node/cli/Cargo.toml index b85014eeef06a..99a4c6aebfb85 100644 --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -16,14 +16,15 @@ is-it-maintained-open-issues = { repository = "paritytech/substrate" } [[bin]] name = "substrate" path = "bin/main.rs" +required-features = ["cli"] + +[lib] +crate-type = ["cdylib", "rlib"] [dependencies] log = "0.4.8" -tokio = "0.1.22" futures = "0.1.29" -exit-future = "0.1.4" jsonrpc-core = "13.2.0" -cli = { package = "substrate-cli", path = "../../core/cli" } codec = { package = "parity-scale-codec", version = "1.0.0" } sr-io = { path = "../../core/sr-io" } client = { package = "substrate-client", path = "../../core/client" } @@ -35,7 +36,7 @@ node-primitives = { path = "../primitives" } hex-literal = "0.2.1" substrate-rpc = { package = "substrate-rpc", path = "../../core/rpc" } substrate-basic-authorship = { path = "../../core/basic-authorship" } -substrate-service = { path = "../../core/service" } +substrate-service = { path = "../../core/service", default-features = false } chain-spec = { package = "substrate-chain-spec", path = "../../core/chain-spec" } transaction_pool = { package = "substrate-transaction-pool", path = "../../core/transaction-pool" } network = { package = "substrate-network", path = "../../core/network" } @@ -47,7 +48,6 @@ sr-primitives = { path = "../../core/sr-primitives" } node-executor = { path = "../executor" } substrate-telemetry = { package = "substrate-telemetry", path = "../../core/telemetry" } structopt = "0.3.3" -transaction-factory = { path = "../../test-utils/transaction-factory" } keyring = { package = "substrate-keyring", path = "../../core/keyring" } indices = { package = "srml-indices", path = "../../srml/indices" } timestamp = { package = "srml-timestamp", path = "../../srml/timestamp", default-features = false } @@ -60,9 +60,26 @@ transaction-payment = { package = "srml-transaction-payment", path = "../../srml support = { package = "srml-support", path = "../../srml/support", default-features = false } im_online = { package = "srml-im-online", path = "../../srml/im-online", default-features = false } serde = { version = "1.0.101", features = [ "derive" ] } -client_db = { package = "substrate-client-db", path = "../../core/client/db", features = ["kvdb-rocksdb"] } +client_db = { package = "substrate-client-db", path = "../../core/client/db", default-features = false } offchain = { package = "substrate-offchain", path = "../../core/offchain" } -ctrlc = { version = "3.1.3", features = ["termination"] } + +# CLI-specific dependencies +tokio = { version = "0.1.22", optional = true } +exit-future = { version = "0.1.4", optional = true } +substrate-cli = { path = "../../core/cli", optional = true } +transaction-factory = { path = "../../test-utils/transaction-factory", optional = true } +ctrlc = { version = "3.1.3", features = ["termination"], optional = true } + +# WASM-specific dependencies +libp2p = { version = "0.12.0", default-features = false, optional = true } +clear_on_drop = { version = "0.2.3", features = ["no_cc"], optional = true } # Imported just for the `no_cc` feature +console_error_panic_hook = { version = "0.1.1", optional = true } +console_log = { version = "0.1.2", optional = true } +js-sys = { version = "0.3.22", optional = true } +wasm-bindgen = { version = "0.2.45", optional = true } +wasm-bindgen-futures = { version = "0.3.22", optional = true } +kvdb-memorydb = { git = "https://github.com/paritytech/parity-common", rev="b0317f649ab2c665b7987b8475878fc4d2e1f81d", optional = true } +rand6 = { package = "rand", version = "0.6", features = ["wasm-bindgen"], optional = true } # Imported just for the `wasm-bindgen` feature [dev-dependencies] keystore = { package = "substrate-keystore", path = "../../core/keystore" } @@ -73,6 +90,29 @@ futures03 = { package = "futures-preview", version = "0.3.0-alpha.19" } tempfile = "3.1.0" [build-dependencies] -cli = { package = "substrate-cli", path = "../../core/cli" } +substrate-cli = { package = "substrate-cli", path = "../../core/cli" } structopt = "0.3.3" vergen = "3.0.4" + +[features] +default = ["cli"] +browser = [ + "clear_on_drop", + "console_error_panic_hook", + "console_log", + "js-sys", + "libp2p", + "wasm-bindgen", + "wasm-bindgen-futures", + "kvdb-memorydb", + "rand/wasm-bindgen", + "rand6" +] +cli = [ + "substrate-cli", + "transaction-factory", + "tokio", + "exit-future", + "ctrlc", + "substrate-service/rocksdb" +] diff --git a/node/cli/bin/main.rs b/node/cli/bin/main.rs index 4b07d8c8a4805..e4415a2a89e66 100644 --- a/node/cli/bin/main.rs +++ b/node/cli/bin/main.rs @@ -18,15 +18,15 @@ #![warn(missing_docs)] -use cli::VersionInfo; use futures::sync::oneshot; use futures::{future, Future}; +use substrate_cli::VersionInfo; use std::cell::RefCell; // handles ctrl-c struct Exit; -impl cli::IntoExit for Exit { +impl substrate_cli::IntoExit for Exit { type Exit = future::MapErr, fn(oneshot::Canceled) -> ()>; fn into_exit(self) -> Self::Exit { // can't use signal directly here because CtrlC takes only `Fn`. @@ -43,7 +43,7 @@ impl cli::IntoExit for Exit { } } -fn main() -> Result<(), cli::error::Error> { +fn main() -> Result<(), substrate_cli::error::Error> { let version = VersionInfo { name: "Substrate Node", commit: env!("VERGEN_SHA_SHORT"), diff --git a/node/cli/browser-demo/.gitignore b/node/cli/browser-demo/.gitignore new file mode 100644 index 0000000000000..0c6117d9fb83b --- /dev/null +++ b/node/cli/browser-demo/.gitignore @@ -0,0 +1 @@ +pkg \ No newline at end of file diff --git a/node/cli/browser-demo/README.md b/node/cli/browser-demo/README.md new file mode 100644 index 0000000000000..4faebcbc76df4 --- /dev/null +++ b/node/cli/browser-demo/README.md @@ -0,0 +1,10 @@ +# How to run this demo + +```sh +cargo install wasm-pack # If necessary + +# From the `node/cli` directory (parent from this README) +wasm-pack build --target web --out-dir ./demo/pkg --no-typescript --release -- --no-default-features --features "browser" + +xdg-open index.html +``` diff --git a/node/cli/browser-demo/build.sh b/node/cli/browser-demo/build.sh new file mode 100755 index 0000000000000..c16100794ad2e --- /dev/null +++ b/node/cli/browser-demo/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +wasm-pack build --target web --out-dir ./browser-demo/pkg --no-typescript --release ./.. -- --no-default-features --features "browser" +python -m SimpleHTTPServer 8000 diff --git a/node/cli/browser-demo/favicon.png b/node/cli/browser-demo/favicon.png new file mode 100644 index 0000000000000..8a4548ce34dfa Binary files /dev/null and b/node/cli/browser-demo/favicon.png differ diff --git a/node/cli/browser-demo/index.html b/node/cli/browser-demo/index.html new file mode 100644 index 0000000000000..cf107e64568f2 --- /dev/null +++ b/node/cli/browser-demo/index.html @@ -0,0 +1,39 @@ + + + + + Substrate node + + + + + diff --git a/node/cli/browser-demo/ws.js b/node/cli/browser-demo/ws.js new file mode 100644 index 0000000000000..fa7a499a8a7a3 --- /dev/null +++ b/node/cli/browser-demo/ws.js @@ -0,0 +1,148 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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. If not, see . + +export default () => { + return { + dial: dial, + listen_on: (addr) => { + let err = new Error("Listening on WebSockets is not possible from within a browser"); + err.name = "NotSupportedError"; + throw err; + }, + }; +} + +/// Turns a string multiaddress into a WebSockets string URL. +// TODO: support dns addresses as well +const multiaddr_to_ws = (addr) => { + let parsed = addr.match(/^\/(ip4|ip6|dns4|dns6)\/(.*?)\/tcp\/(.*?)\/(ws|wss|x-parity-ws\/(.*)|x-parity-wss\/(.*))$/); + let proto = 'wss'; + if (parsed[4] == 'ws' || parsed[4] == 'x-parity-ws') { + proto = 'ws'; + } + let url = decodeURIComponent(parsed[5] || parsed[6] || ''); + if (parsed != null) { + if (parsed[1] == 'ip6') { + return proto + "://[" + parsed[2] + "]:" + parsed[3] + url; + } else { + return proto + "://" + parsed[2] + ":" + parsed[3] + url; + } + } + + let err = new Error("Address not supported: " + addr); + err.name = "NotSupportedError"; + throw err; +} + +// Attempt to dial a multiaddress. +const dial = (addr) => { + let ws = new WebSocket(multiaddr_to_ws(addr)); + let reader = read_queue(); + + return new Promise((resolve, reject) => { + // TODO: handle ws.onerror properly after dialing has happened + ws.onerror = (ev) => reject(ev); + ws.onmessage = (ev) => reader.inject_blob(ev.data); + ws.onclose = () => reader.inject_eof(); + ws.onopen = () => resolve({ + read: (function*() { while(ws.readyState == 1) { yield reader.next(); } })(), + write: (data) => { + if (ws.readyState == 1) { + ws.send(data); + return promise_when_ws_finished(ws); + } else { + return Promise.reject("WebSocket is closed"); + } + }, + shutdown: () => {}, + close: () => ws.close() + }); + }); +} + +// Takes a WebSocket object and returns a Promise that resolves when bufferedAmount is 0. +const promise_when_ws_finished = (ws) => { + if (ws.bufferedAmount == 0) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + setTimeout(function check() { + if (ws.bufferedAmount == 0) { + resolve(); + } else { + setTimeout(check, 100); + } + }, 2); + }) +} + +// Creates a queue reading system. +const read_queue = () => { + // State of the queue. + let state = { + // Array of promises resolving to `ArrayBuffer`s, that haven't been transmitted back with + // `next` yet. + queue: new Array(), + // If `resolve` isn't null, it is a "resolve" function of a promise that has already been + // returned by `next`. It should be called with some data. + resolve: null, + }; + + return { + // Inserts a new Blob in the queue. + inject_blob: (blob) => { + if (state.resolve != null) { + var resolve = state.resolve; + state.resolve = null; + + var reader = new FileReader(); + reader.addEventListener("loadend", () => resolve(reader.result)); + reader.readAsArrayBuffer(blob); + } else { + state.queue.push(new Promise((resolve, reject) => { + var reader = new FileReader(); + reader.addEventListener("loadend", () => resolve(reader.result)); + reader.readAsArrayBuffer(blob); + })); + } + }, + + // Inserts an EOF message in the queue. + inject_eof: () => { + if (state.resolve != null) { + var resolve = state.resolve; + state.resolve = null; + resolve(null); + } else { + state.queue.push(Promise.resolve(null)); + } + }, + + // Returns a Promise that yields the next entry as an ArrayBuffer. + next: () => { + if (state.queue.length != 0) { + return state.queue.shift(0); + } else { + if (state.resolve !== null) + throw "Internal error: already have a pending promise"; + return new Promise((resolve, reject) => { + state.resolve = resolve; + }); + } + } + }; +}; diff --git a/node/cli/build.rs b/node/cli/build.rs index 48b57c4600e80..8ef644eed5d94 100644 --- a/node/cli/build.rs +++ b/node/cli/build.rs @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -use cli::{NoCustom, CoreParams}; use std::{fs, env, path::Path}; use structopt::{StructOpt, clap::Shell}; +use substrate_cli::{NoCustom, CoreParams}; use vergen::{ConstantsFlags, generate_cargo_keys}; fn main() { diff --git a/node/cli/src/browser.rs b/node/cli/src/browser.rs new file mode 100644 index 0000000000000..1c84efa107ea7 --- /dev/null +++ b/node/cli/src/browser.rs @@ -0,0 +1,159 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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. If not, see . + +use crate::ChainSpec; +use futures::{prelude::*, sync::oneshot, sync::mpsc}; +use libp2p::wasm_ext; +use log::{debug, info}; +use std::sync::Arc; +use substrate_service::{AbstractService, RpcSession, Roles as ServiceRoles, Configuration, config::DatabaseConfig}; +use wasm_bindgen::prelude::*; + +/// Starts the client. +/// +/// You must pass a libp2p transport that supports . +#[wasm_bindgen] +pub fn start_client(wasm_ext: wasm_ext::ffi::Transport) -> Result { + start_inner(wasm_ext) + .map_err(|err| JsValue::from_str(&err.to_string())) +} + +fn start_inner(wasm_ext: wasm_ext::ffi::Transport) -> Result> { + console_error_panic_hook::set_once(); + console_log::init_with_level(log::Level::Info); + + // Build the configuration to pass to the service. + let config = { + let wasm_ext = wasm_ext::ExtTransport::new(wasm_ext); + let chain_spec = ChainSpec::FlamingFir.load().map_err(|e| format!("{:?}", e))?; + let mut config = Configuration::<(), _, _>::default_with_spec(chain_spec); + config.network.transport = network::config::TransportConfig::Normal { + wasm_external_transport: Some(wasm_ext.clone()), + enable_mdns: false, + }; + config.telemetry_external_transport = Some(wasm_ext); + config.roles = ServiceRoles::LIGHT; + config.name = "Browser node".to_string(); + config.database = { + let db = Arc::new(kvdb_memorydb::create(10)); + DatabaseConfig::Custom(db) + }; + config + }; + + info!("Substrate browser node"); + info!(" version {}", config.full_version()); + info!(" by Parity Technologies, 2017-2019"); + info!("Chain specification: {}", config.chain_spec.name()); + info!("Node name: {}", config.name); + info!("Roles: {:?}", config.roles); + + // Create the service. This is the most heavy initialization step. + let mut service = crate::service::new_light(config).map_err(|e| format!("{:?}", e))?; + + // We now dispatch a background task responsible for processing the service. + // + // The main action performed by the code below consists in polling the service with + // `service.poll()`. + // The rest consists in handling RPC requests. + let (rpc_send_tx, mut rpc_send_rx) = mpsc::unbounded::(); + wasm_bindgen_futures::spawn_local(futures::future::poll_fn(move || { + loop { + match rpc_send_rx.poll() { + Ok(Async::Ready(Some(message))) => { + let fut = service.rpc_query(&message.session, &message.rpc_json); + let _ = message.send_back.send(Box::new(fut)); + }, + Ok(Async::NotReady) => break, + Err(_) | Ok(Async::Ready(None)) => return Ok(Async::Ready(())), + } + } + + loop { + match service.poll().map_err(|_| ())? { + Async::Ready(()) => return Ok(Async::Ready(())), + Async::NotReady => break + } + } + + Ok(Async::NotReady) + })); + + Ok(Client { + rpc_send_tx, + }) +} + +/// A running client. +#[wasm_bindgen] +pub struct Client { + rpc_send_tx: mpsc::UnboundedSender, +} + +struct RpcMessage { + rpc_json: String, + session: RpcSession, + send_back: oneshot::Sender, Error = ()>>>, +} + +#[wasm_bindgen] +impl Client { + /// Allows starting an RPC request. Returns a `Promise` containing the result of that request. + #[wasm_bindgen(js_name = "rpcSend")] + pub fn rpc_send(&mut self, rpc: &str) -> js_sys::Promise { + let rpc_session = RpcSession::new(mpsc::channel(1).0); + let (tx, rx) = oneshot::channel(); + let _ = self.rpc_send_tx.unbounded_send(RpcMessage { + rpc_json: rpc.to_owned(), + session: rpc_session, + send_back: tx, + }); + let fut = rx + .map_err(|_| ()) + .and_then(|fut| fut) + .map(|s| JsValue::from_str(&s.unwrap_or(String::new()))) + .map_err(|_| JsValue::NULL); + wasm_bindgen_futures::future_to_promise(fut) + } + + /// Subscribes to an RPC pubsub endpoint. + #[wasm_bindgen(js_name = "rpcSubscribe")] + pub fn rpc_subscribe(&mut self, rpc: &str, callback: js_sys::Function) { + let (tx, rx) = mpsc::channel(4); + let rpc_session = RpcSession::new(tx); + let (fut_tx, fut_rx) = oneshot::channel(); + let _ = self.rpc_send_tx.unbounded_send(RpcMessage { + rpc_json: rpc.to_owned(), + session: rpc_session.clone(), + send_back: fut_tx, + }); + let fut_rx = fut_rx + .map_err(|_| ()) + .and_then(|fut| fut); + wasm_bindgen_futures::spawn_local(fut_rx.then(|_| Ok(()))); + wasm_bindgen_futures::spawn_local(rx.for_each(move |s| { + match callback.call1(&callback, &JsValue::from_str(&s)) { + Ok(_) => Ok(()), + Err(_) => Err(()), + } + }).then(move |v| { + // We need to keep `rpc_session` alive. + debug!("RPC subscription has ended"); + drop(rpc_session); + v + })); + } +} diff --git a/node/cli/src/cli.rs b/node/cli/src/cli.rs new file mode 100644 index 0000000000000..2e5d27cec7a5f --- /dev/null +++ b/node/cli/src/cli.rs @@ -0,0 +1,210 @@ +// Copyright 2018-2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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. If not, see . + +pub use substrate_cli::error; +use tokio::prelude::Future; +use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; +pub use substrate_cli::{VersionInfo, IntoExit, NoCustom, SharedParams, ExecutionStrategyParam}; +use substrate_service::{AbstractService, Roles as ServiceRoles, Configuration}; +use log::info; +use structopt::{StructOpt, clap::App}; +use substrate_cli::{display_role, parse_and_prepare, AugmentClap, GetLogFilter, ParseAndPrepare}; +use crate::{service, ChainSpec, load_spec}; +use crate::factory_impl::FactoryState; +use transaction_factory::RuntimeAdapter; +use client::ExecutionStrategies; + +/// Custom subcommands. +#[derive(Clone, Debug, StructOpt)] +pub enum CustomSubcommands { + /// The custom factory subcommmand for manufacturing transactions. + #[structopt( + name = "factory", + about = "Manufactures num transactions from Alice to random accounts. \ + Only supported for development or local testnet." + )] + Factory(FactoryCmd), +} + +impl GetLogFilter for CustomSubcommands { + fn get_log_filter(&self) -> Option { + None + } +} + +/// The `factory` command used to generate transactions. +/// Please note: this command currently only works on an empty database! +#[derive(Debug, StructOpt, Clone)] +pub struct FactoryCmd { + /// How often to repeat. This option only has an effect in mode `MasterToNToM`. + #[structopt(long="rounds", default_value = "1")] + pub rounds: u64, + + /// MasterToN: Manufacture `num` transactions from the master account + /// to `num` randomly created accounts, one each. + /// + /// MasterTo1: Manufacture `num` transactions from the master account + /// to exactly one other randomly created account. + /// + /// MasterToNToM: Manufacture `num` transactions from the master account + /// to `num` randomly created accounts. + /// From each of these randomly created accounts manufacture + /// a transaction to another randomly created account. + /// Repeat this `rounds` times. If `rounds` = 1 the behavior + /// is the same as `MasterToN`.{n} + /// A -> B, A -> C, A -> D, ... x `num`{n} + /// B -> E, C -> F, D -> G, ...{n} + /// ... x `rounds` + /// + /// These three modes control manufacturing. + #[structopt(long="mode", default_value = "MasterToN")] + pub mode: transaction_factory::Mode, + + /// Number of transactions to generate. In mode `MasterNToNToM` this is + /// the number of transactions per round. + #[structopt(long="num", default_value = "8")] + pub num: u64, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + /// The means of execution used when calling into the runtime while importing blocks. + #[structopt( + long = "execution", + value_name = "STRATEGY", + possible_values = &ExecutionStrategyParam::variants(), + case_insensitive = true, + default_value = "NativeElseWasm" + )] + pub execution: ExecutionStrategyParam, +} + +impl AugmentClap for FactoryCmd { + fn augment_clap<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + FactoryCmd::augment_clap(app) + } +} + +/// Parse command line arguments into service configuration. +pub fn run(args: I, exit: E, version: substrate_cli::VersionInfo) -> error::Result<()> where + I: IntoIterator, + T: Into + Clone, + E: IntoExit, +{ + type Config = Configuration<(), A, B>; + + match parse_and_prepare::(&version, "substrate-node", args) { + ParseAndPrepare::Run(cmd) => cmd.run(load_spec, exit, + |exit, _cli_args, _custom_args, config: Config<_, _>| { + info!("{}", version.name); + info!(" version {}", config.full_version()); + info!(" by Parity Technologies, 2017-2019"); + info!("Chain specification: {}", config.chain_spec.name()); + info!("Node name: {}", config.name); + info!("Roles: {}", display_role(&config)); + let runtime = RuntimeBuilder::new().name_prefix("main-tokio-").build() + .map_err(|e| format!("{:?}", e))?; + match config.roles { + ServiceRoles::LIGHT => run_until_exit( + runtime, + service::new_light(config)?, + exit + ), + _ => run_until_exit( + runtime, + service::new_full(config)?, + exit + ), + } + }), + ParseAndPrepare::BuildSpec(cmd) => cmd.run(load_spec), + ParseAndPrepare::ExportBlocks(cmd) => cmd.run_with_builder(|config: Config<_, _>| + Ok(new_full_start!(config).0), load_spec, exit), + ParseAndPrepare::ImportBlocks(cmd) => cmd.run_with_builder(|config: Config<_, _>| + Ok(new_full_start!(config).0), load_spec, exit), + ParseAndPrepare::PurgeChain(cmd) => cmd.run(load_spec), + ParseAndPrepare::RevertChain(cmd) => cmd.run_with_builder(|config: Config<_, _>| + Ok(new_full_start!(config).0), load_spec), + ParseAndPrepare::CustomCommand(CustomSubcommands::Factory(cli_args)) => { + let mut config: Config<_, _> = substrate_cli::create_config_with_db_path( + load_spec, + &cli_args.shared_params, + &version, + )?; + config.execution_strategies = ExecutionStrategies { + importing: cli_args.execution.into(), + block_construction: cli_args.execution.into(), + other: cli_args.execution.into(), + ..Default::default() + }; + + match ChainSpec::from(config.chain_spec.id()) { + Some(ref c) if c == &ChainSpec::Development || c == &ChainSpec::LocalTestnet => {}, + _ => panic!("Factory is only supported for development and local testnet."), + } + + let factory_state = FactoryState::new( + cli_args.mode.clone(), + cli_args.num, + cli_args.rounds, + ); + + let service_builder = new_full_start!(config).0; + transaction_factory::factory::, _, _, _, _, _>( + factory_state, + service_builder.client(), + service_builder.select_chain() + .expect("The select_chain is always initialized by new_full_start!; QED") + ).map_err(|e| format!("Error in transaction factory: {}", e))?; + + Ok(()) + } + } +} + +fn run_until_exit( + mut runtime: Runtime, + service: T, + e: E, +) -> error::Result<()> +where + T: AbstractService, + E: IntoExit, +{ + let (exit_send, exit) = exit_future::signal(); + + let informant = substrate_cli::informant::build(&service); + runtime.executor().spawn(exit.until(informant).map(|_| ())); + + // we eagerly drop the service so that the internal exit future is fired, + // but we need to keep holding a reference to the global telemetry guard + let _telemetry = service.telemetry(); + + let service_res = { + let exit = e.into_exit().map_err(|_| error::Error::Other("Exit future failed.".into())); + let service = service.map_err(|err| error::Error::Service(err)); + let select = service.select(exit).map(|_| ()).map_err(|(err, _)| err); + runtime.block_on(select) + }; + + exit_send.fire(); + + // TODO [andre]: timeout this future #1318 + let _ = runtime.shutdown_on_idle().wait(); + + service_res +} diff --git a/node/cli/src/lib.rs b/node/cli/src/lib.rs index 5125ba216a2cc..78660ae92e3c5 100644 --- a/node/cli/src/lib.rs +++ b/node/cli/src/lib.rs @@ -15,26 +15,35 @@ // along with Substrate. If not, see . //! Substrate CLI library. +//! +//! This package has two Cargo features: +//! +//! - `cli` (default): exposes functions that parse command-line options, then start and run the +//! node as a CLI application. +//! +//! - `browser`: exposes the content of the `browser` module, which consists of exported symbols +//! that are meant to be passed through the `wasm-bindgen` utility and called from JavaScript. +//! Despite its name the produced WASM can theoretically also be used from NodeJS, although this +//! hasn't been tested. #![warn(missing_docs)] #![warn(unused_extern_crates)] -pub use cli::error; pub mod chain_spec; + #[macro_use] mod service; +#[cfg(feature = "browser")] +mod browser; +#[cfg(feature = "cli")] +mod cli; +#[cfg(feature = "cli")] mod factory_impl; -use tokio::prelude::Future; -use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; -pub use cli::{VersionInfo, IntoExit, NoCustom, SharedParams, ExecutionStrategyParam}; -use substrate_service::{AbstractService, Roles as ServiceRoles, Configuration}; -use log::info; -use structopt::{StructOpt, clap::App}; -use cli::{display_role, parse_and_prepare, AugmentClap, GetLogFilter, ParseAndPrepare}; -use crate::factory_impl::FactoryState; -use transaction_factory::RuntimeAdapter; -use client::ExecutionStrategies; +#[cfg(feature = "browser")] +pub use browser::*; +#[cfg(feature = "cli")] +pub use cli::*; /// The chain specification option. #[derive(Clone, Debug, PartialEq)] @@ -49,78 +58,6 @@ pub enum ChainSpec { StagingTestnet, } -/// Custom subcommands. -#[derive(Clone, Debug, StructOpt)] -pub enum CustomSubcommands { - /// The custom factory subcommmand for manufacturing transactions. - #[structopt( - name = "factory", - about = "Manufactures num transactions from Alice to random accounts. \ - Only supported for development or local testnet." - )] - Factory(FactoryCmd), -} - -impl GetLogFilter for CustomSubcommands { - fn get_log_filter(&self) -> Option { - None - } -} - -/// The `factory` command used to generate transactions. -/// Please note: this command currently only works on an empty database! -#[derive(Debug, StructOpt, Clone)] -pub struct FactoryCmd { - /// How often to repeat. This option only has an effect in mode `MasterToNToM`. - #[structopt(long="rounds", default_value = "1")] - pub rounds: u64, - - /// MasterToN: Manufacture `num` transactions from the master account - /// to `num` randomly created accounts, one each. - /// - /// MasterTo1: Manufacture `num` transactions from the master account - /// to exactly one other randomly created account. - /// - /// MasterToNToM: Manufacture `num` transactions from the master account - /// to `num` randomly created accounts. - /// From each of these randomly created accounts manufacture - /// a transaction to another randomly created account. - /// Repeat this `rounds` times. If `rounds` = 1 the behavior - /// is the same as `MasterToN`.{n} - /// A -> B, A -> C, A -> D, ... x `num`{n} - /// B -> E, C -> F, D -> G, ...{n} - /// ... x `rounds` - /// - /// These three modes control manufacturing. - #[structopt(long="mode", default_value = "MasterToN")] - pub mode: transaction_factory::Mode, - - /// Number of transactions to generate. In mode `MasterNToNToM` this is - /// the number of transactions per round. - #[structopt(long="num", default_value = "8")] - pub num: u64, - - #[allow(missing_docs)] - #[structopt(flatten)] - pub shared_params: SharedParams, - - /// The means of execution used when calling into the runtime while importing blocks. - #[structopt( - long = "execution", - value_name = "STRATEGY", - possible_values = &ExecutionStrategyParam::variants(), - case_insensitive = true, - default_value = "NativeElseWasm" - )] - pub execution: ExecutionStrategyParam, -} - -impl AugmentClap for FactoryCmd { - fn augment_clap<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { - FactoryCmd::augment_clap(app) - } -} - /// Get a chain config from a spec setting. impl ChainSpec { pub(crate) fn load(self) -> Result { @@ -149,113 +86,3 @@ fn load_spec(id: &str) -> Result, String> { None => None, }) } - -/// Parse command line arguments into service configuration. -pub fn run(args: I, exit: E, version: cli::VersionInfo) -> error::Result<()> where - I: IntoIterator, - T: Into + Clone, - E: IntoExit, -{ - type Config = Configuration<(), A, B>; - - match parse_and_prepare::(&version, "substrate-node", args) { - ParseAndPrepare::Run(cmd) => cmd.run(load_spec, exit, - |exit, _cli_args, _custom_args, config: Config<_, _>| { - info!("{}", version.name); - info!(" version {}", config.full_version()); - info!(" by Parity Technologies, 2017-2019"); - info!("Chain specification: {}", config.chain_spec.name()); - info!("Node name: {}", config.name); - info!("Roles: {}", display_role(&config)); - let runtime = RuntimeBuilder::new().name_prefix("main-tokio-").build() - .map_err(|e| format!("{:?}", e))?; - match config.roles { - ServiceRoles::LIGHT => run_until_exit( - runtime, - service::new_light(config)?, - exit - ), - _ => run_until_exit( - runtime, - service::new_full(config)?, - exit - ), - } - }), - ParseAndPrepare::BuildSpec(cmd) => cmd.run(load_spec), - ParseAndPrepare::ExportBlocks(cmd) => cmd.run_with_builder(|config: Config<_, _>| - Ok(new_full_start!(config).0), load_spec, exit), - ParseAndPrepare::ImportBlocks(cmd) => cmd.run_with_builder(|config: Config<_, _>| - Ok(new_full_start!(config).0), load_spec, exit), - ParseAndPrepare::PurgeChain(cmd) => cmd.run(load_spec), - ParseAndPrepare::RevertChain(cmd) => cmd.run_with_builder(|config: Config<_, _>| - Ok(new_full_start!(config).0), load_spec), - ParseAndPrepare::CustomCommand(CustomSubcommands::Factory(cli_args)) => { - let mut config: Config<_, _> = cli::create_config_with_db_path( - load_spec, - &cli_args.shared_params, - &version, - )?; - config.execution_strategies = ExecutionStrategies { - importing: cli_args.execution.into(), - block_construction: cli_args.execution.into(), - other: cli_args.execution.into(), - ..Default::default() - }; - - match ChainSpec::from(config.chain_spec.id()) { - Some(ref c) if c == &ChainSpec::Development || c == &ChainSpec::LocalTestnet => {}, - _ => panic!("Factory is only supported for development and local testnet."), - } - - let factory_state = FactoryState::new( - cli_args.mode.clone(), - cli_args.num, - cli_args.rounds, - ); - - let service_builder = new_full_start!(config).0; - transaction_factory::factory::, _, _, _, _, _>( - factory_state, - service_builder.client(), - service_builder.select_chain() - .expect("The select_chain is always initialized by new_full_start!; QED") - ).map_err(|e| format!("Error in transaction factory: {}", e))?; - - Ok(()) - } - } -} - -fn run_until_exit( - mut runtime: Runtime, - service: T, - e: E, -) -> error::Result<()> -where - T: AbstractService, - E: IntoExit, -{ - let (exit_send, exit) = exit_future::signal(); - - let informant = cli::informant::build(&service); - runtime.executor().spawn(exit.until(informant).map(|_| ())); - - // we eagerly drop the service so that the internal exit future is fired, - // but we need to keep holding a reference to the global telemetry guard - let _telemetry = service.telemetry(); - - let service_res = { - let exit = e.into_exit().map_err(|_| error::Error::Other("Exit future failed.".into())); - let service = service.map_err(|err| error::Error::Service(err)); - let select = service.select(exit).map(|_| ()).map_err(|(err, _)| err); - runtime.block_on(select) - }; - - exit_send.fire(); - - // TODO [andre]: timeout this future #1318 - let _ = runtime.shutdown_on_idle().wait(); - - service_res -}