From b70beaf7a49632325b3260a88f50a9295b746e63 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 2 Jun 2023 19:16:52 +0300 Subject: [PATCH] Backup platform impl Signed-off-by: Alexandru Vasile --- Cargo.lock | 56 ++-- Cargo.toml | 9 +- .../unstable_light_client_tx_basic.rs | 55 ++-- subxt/Cargo.toml | 12 + subxt/src/rpc/lightclient/client.rs | 271 +++++++++++++++++- subxt/src/rpc/lightclient/mod.rs | 1 + subxt/src/rpc/lightclient/platform.rs | 263 +++++++++++++++++ testing/wasm-tests/Cargo.lock | 16 +- testing/wasm-tests/Cargo.toml | 2 +- testing/wasm-tests/tests/wasm.rs | 11 + 10 files changed, 636 insertions(+), 60 deletions(-) create mode 100644 subxt/src/rpc/lightclient/platform.rs diff --git a/Cargo.lock b/Cargo.lock index 41a7a13d07..8c54e1b9a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3195,12 +3195,12 @@ checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "ruzstd" -version = "0.4.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3ffab8f9715a0d455df4bbb9d21e91135aab3cd3ca187af0cd0c3c3f868fdc" +checksum = "9a15e661f0f9dac21f3494fe5d23a6338c0ac116a2d22c2b63010acd89467ffe" dependencies = [ "byteorder", - "thiserror-core", + "thiserror", "twox-hash", ] @@ -3594,7 +3594,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smoldot" -version = "0.7.0" +version = "0.6.0" dependencies = [ "arrayvec 0.7.2", "async-lock", @@ -3646,7 +3646,7 @@ dependencies = [ [[package]] name = "smoldot-light" -version = "0.5.0" +version = "0.4.0" dependencies = [ "async-lock", "async-std", @@ -3666,11 +3666,30 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "siphasher", "slab", "smoldot", ] +[[package]] +name = "smoldot-light-wasm" +version = "1.0.6" +dependencies = [ + "async-executor", + "async-task", + "event-listener", + "fnv", + "futures-util", + "hashbrown 0.13.2", + "lazy_static", + "log", + "nom", + "pin-project", + "rand 0.8.5", + "slab", + "smoldot", + "smoldot-light", +] + [[package]] name = "snow" version = "0.9.2" @@ -4201,8 +4220,10 @@ dependencies = [ "either", "frame-metadata", "futures", + "futures-timer", "futures-util", "getrandom 0.2.9", + "gloo-net", "hex", "impl-serde", "jsonrpsee", @@ -4215,7 +4236,9 @@ dependencies = [ "scale-value", "serde", "serde_json", + "smoldot", "smoldot-light", + "smoldot-light-wasm", "sp-core", "sp-core-hashing", "sp-keyring", @@ -4227,6 +4250,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "wasm-bindgen-futures", ] [[package]] @@ -4379,26 +4403,6 @@ dependencies = [ "thiserror-impl", ] -[[package]] -name = "thiserror-core" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d97345f6437bb2004cd58819d8a9ef8e36cdd7661c2abc4bbde0a7c40d9f497" -dependencies = [ - "thiserror-core-impl", -] - -[[package]] -name = "thiserror-core-impl" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ac1c5050e43014d16b2f94d0d2ce79e65ffdd8b38d8048f9c8f6a8a6da62ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "thiserror-impl" version = "1.0.40" diff --git a/Cargo.toml b/Cargo.toml index b48c2a280e..ffb67b32d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,14 @@ wasm-bindgen-test = "0.3.24" which = "4.4.0" # Light client support: -smoldot-light = { path = "/home/lexnv/workspace/smoldot/light-base", default-features = false, opt-level = 2 } +smoldot = { path = "/home/lexnv/workspace/smoldot/lib", default-features = false } +smoldot-light = { path = "/home/lexnv/workspace/smoldot/light-base", default-features = false } +smoldot-light-wasm = { path = "/home/lexnv/workspace/smoldot/wasm-node/rust", default-features = false } +wasm-bindgen-futures = { version = "0.4.19" } +futures-timer = { version = "3" } +gloo-net = { version = "0.2.6", default-features = false, features = ["json", "websocket"] } + + tokio-stream = "0.1.14" futures-util = "0.3.28" diff --git a/examples/examples/unstable_light_client_tx_basic.rs b/examples/examples/unstable_light_client_tx_basic.rs index 94d44f00ef..82b34e0225 100644 --- a/examples/examples/unstable_light_client_tx_basic.rs +++ b/examples/examples/unstable_light_client_tx_basic.rs @@ -1,10 +1,11 @@ +use futures::StreamExt; use sp_keyring::AccountKeyring; use std::sync::Arc; use subxt::{rpc::LightClient, tx::PairSigner, OnlineClient, PolkadotConfig}; -// Generate an interface that we can use from the node's metadata. -#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")] -pub mod polkadot {} +// // Generate an interface that we can use from the node's metadata. +// #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")] +// pub mod polkadot {} #[tokio::main] async fn main() -> Result<(), Box> { @@ -48,25 +49,41 @@ async fn main() -> Result<(), Box> { let light_client = LightClient::new(include_str!("../../artifacts/dev_spec.json"))?; let api = OnlineClient::::from_rpc_client(Arc::new(light_client)).await?; - // Build a balance transfer extrinsic. - let dest = AccountKeyring::Bob.to_account_id().into(); - let balance_transfer_tx = polkadot::tx().balances().transfer(dest, 10_000); + { + println!("Subscribe to latest finalized blocks: "); - // Submit the balance transfer extrinsic from Alice, and wait for it to be successful - // and in a finalized block. We get back the extrinsic events if all is well. - let from = PairSigner::new(AccountKeyring::Alice.pair()); - let events = api - .tx() - .sign_and_submit_then_watch_default(&balance_transfer_tx, &from) - .await? - .wait_for_finalized_success() - .await?; + let mut blocks_sub = api.blocks().subscribe_finalized().await?.take(3); + // For each block, print a bunch of information about it: + while let Some(block) = blocks_sub.next().await { + let block = block?; - // Find a Transfer event and print it. - let transfer_event = events.find_first::()?; - if let Some(event) = transfer_event { - println!("Balance transfer success: {event:?}"); + let block_number = block.header().number; + let block_hash = block.hash(); + + println!("Block #{block_number}:"); + println!(" Hash: {block_hash}"); + } } + // Build a balance transfer extrinsic. + // let dest = AccountKeyring::Bob.to_account_id().into(); + // let balance_transfer_tx = polkadot::tx().balances().transfer(dest, 10_000); + + // // Submit the balance transfer extrinsic from Alice, and wait for it to be successful + // // and in a finalized block. We get back the extrinsic events if all is well. + // let from = PairSigner::new(AccountKeyring::Alice.pair()); + // let events = api + // .tx() + // .sign_and_submit_then_watch_default(&balance_transfer_tx, &from) + // .await? + // .wait_for_finalized_success() + // .await?; + + // // Find a Transfer event and print it. + // let transfer_event = events.find_first::()?; + // if let Some(event) = transfer_event { + // println!("Balance transfer success: {event:?}"); + // } + Ok(()) } diff --git a/subxt/Cargo.toml b/subxt/Cargo.toml index a62128eb87..1d0b2ebdcf 100644 --- a/subxt/Cargo.toml +++ b/subxt/Cargo.toml @@ -43,11 +43,16 @@ unstable-metadata = [] # Activate this to expose the Light Client functionality. # Note that this feature is experimental and things may break or not work as expected. unstable-light-client = [ + "smoldot", "smoldot-light/std", + "smoldot-light-wasm", "tokio-stream", "tokio/sync", "tokio/rt", "futures-util", + "wasm-bindgen-futures", + "futures-timer/wasm-bindgen", + "gloo-net", ] [dependencies] @@ -88,7 +93,14 @@ subxt-macro = { workspace = true } subxt-metadata = { workspace = true } # Light client support: +smoldot = { workspace = true, optional = true } smoldot-light = { workspace = true, optional = true } +smoldot-light-wasm = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } +futures-timer = { workspace = true, optional = true } +gloo-net = { workspace = true, optional = true } + + tokio = { workspace = true, optional = true } tokio-stream = { workspace = true, optional = true } futures-util = { workspace = true, optional = true } diff --git a/subxt/src/rpc/lightclient/client.rs b/subxt/src/rpc/lightclient/client.rs index 7ae526d18c..cd8b8894d0 100644 --- a/subxt/src/rpc/lightclient/client.rs +++ b/subxt/src/rpc/lightclient/client.rs @@ -32,6 +32,8 @@ use std::{ use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; +use smoldot_light_wasm::platform::Platform as WasmPlatform; + const LOG_TARGET: &str = "light-client"; /// Inner structure to work with light clients. @@ -39,7 +41,7 @@ struct LightClientInner { /// Smoldot light client implementation that leverages the `AsyncStdTcpWebSocket`. /// /// Note: `AsyncStdTcpWebSocket` is not wasm compatible. - client: smoldot_light::Client, + client: smoldot_light::Client, /// The ID of the chain used to identify the chain protocol (ie. substrate). /// /// Note: A single chain is supported for a client. This aligns with the subxt's @@ -129,6 +131,261 @@ impl LightClientInner { } } +/// Must stop the execution immediately. The message is a UTF-8 string found in the memory of +/// the WebAssembly at offset `message_ptr` and with length `message_len`. +/// +/// > **Note**: This function is typically implemented using `throw`. +/// +/// After this function has been called, no further Wasm functions must be called again on +/// this Wasm virtual machine. Explanation below. +/// +/// # About throwing and safety +/// +/// Rust programs can be configured in two panicking modes: `abort`, or `unwind`. Safe or +/// unsafe Rust code must be written by keeping in mind that the execution of a function can +/// be suddenly interrupted by a panic, but can rely on the fact that this panic will either +/// completely abort the program, or unwind the stack. In the latter case, they can rely on +/// the fact that `std::panic::catch_unwind` will catch this unwinding and let them perform +/// some additional clean-ups. +/// +/// This function is typically implemented using `throw`. However, "just" throwing a JavaScript +/// exception from within the implementation of this function is neither `abort`, because the +/// JavaScript could call into the Wasm again later, nor `unwind`, because it isn't caught by +/// `std::panic::catch_unwind`. By being neither of the two, it breaks the assumptions that +/// some Rust codes might rely on for either correctness or safety. +/// In order to solve this problem, we enforce that `panic` must behave like `abort`, and +/// forbid calling into the Wasm virtual machine again. +/// +/// Beyond the `panic` function itself, any other FFI function that throws must similarly +/// behave like `abort` and prevent any further execution. +#[no_mangle] +pub extern "C" fn panic(message_ptr: u32, message_len: u32) { + let slice = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + if let Ok(message) = std::str::from_utf8(slice) { + panic!("{message}"); + } +} + +/// Copies the entire content of the buffer with the given index to the memory of the +/// WebAssembly at offset `target_pointer`. +/// +/// In situations where a buffer must be provided from the JavaScript to the Rust code, the +/// JavaScript must (prior to calling the Rust function that requires the buffer) assign a +/// "buffer index" to the buffer it wants to provide. The Rust code then calls the +/// [`buffer_size`] and [`buffer_copy`] functions in order to obtain the length and content +/// of the buffer. +#[no_mangle] +pub extern "C" fn buffer_copy(buffer_index: u32, target_pointer: u32) {} + +/// Returns the size (in bytes) of the buffer with the given index. +/// +/// See the documentation of [`buffer_copy`] for context. +#[no_mangle] + +pub extern "C" fn buffer_size(buffer_index: u32) -> u32 { + 0 +} + +/// The queue of JSON-RPC responses of the given chain is no longer empty. +/// +/// This function is only ever called after [`json_rpc_responses_peek`] has returned a `len` +/// of 0. +/// +/// This function might be called spuriously, however this behavior must not be relied upon. +#[no_mangle] +pub extern "C" fn json_rpc_responses_non_empty(chain_id: u32) {} + +/// Client is emitting a log entry. +/// +/// Each log entry is made of a log level (`1 = Error, 2 = Warn, 3 = Info, 4 = Debug, +/// 5 = Trace`), a log target (e.g. "network"), and a log message. +/// +/// The log target and message is a UTF-8 string found in the memory of the WebAssembly +/// virtual machine at offset `ptr` and with length `len`. +#[no_mangle] +pub extern "C" fn log( + level: u32, + target_ptr: u32, + target_len: u32, + message_ptr: u32, + message_len: u32, +) { + let target_slice = + unsafe { std::slice::from_raw_parts(target_ptr as *const u8, target_len as usize) }; + let target = std::str::from_utf8(target_slice).unwrap_or_else(|_| "cannot decode target"); + + let msg_slice = + unsafe { std::slice::from_raw_parts(message_ptr as *const u8, message_len as usize) }; + let message = std::str::from_utf8(msg_slice).unwrap_or_else(|_| "cannot decode message"); + + println!("log level={level} target={target} message={message}"); +} + +/// Called when [`advance_execution`] should be executed again. +/// +/// This function might be called from within [`advance_execution`], in which case +/// [`advance_execution`] should be called again immediately after it returns. +#[no_mangle] +pub extern "C" fn advance_execution_ready() {} + +/// After at least `milliseconds` milliseconds have passed, [`timer_finished`] must be called. +/// +/// It is not a logic error to call [`timer_finished`] *before* `milliseconds` milliseconds +/// have passed, and this will likely cause smoldot to restart a new timer for the remainder +/// of the duration. +/// +/// When [`timer_finished`] is called, the value of the monotonic clock (in the WASI bindings) +/// must have increased by at least the given number of `milliseconds`. +/// +/// If `milliseconds` is 0, [`timer_finished`] should be called as soon as possible. +/// +/// `milliseconds` never contains a negative number, `NaN` or infinite. +#[no_mangle] +pub extern "C" fn start_timer(milliseconds: f64) {} + +/// Must initialize a new connection that tries to connect to the given multiaddress. +/// +/// The multiaddress is a UTF-8 string found in the WebAssembly memory at offset `addr_ptr` +/// and with `addr_len` bytes. The string is a multiaddress such as `/ip4/1.2.3.4/tcp/5/ws`. +/// +/// The `id` parameter is an identifier for this connection, as chosen by the Rust code. It +/// must be passed on every interaction with this connection. +/// +/// Returns 0 to indicate success, or 1 to indicate that an error happened. If an error is +/// returned, the `id` doesn't correspond to anything. +/// +/// > **Note**: If you implement this function using for example `new WebSocket()`, please +/// > keep in mind that exceptions should be caught and turned into an error code. +/// +/// If an error happened, assign a so-called "buffer index" (a `u32`) representing the buffer +/// containing the UTF-8 error message, then write this buffer index as little-endian to the +/// memory of the WebAssembly indicated by `error_buffer_index_ptr`. The Rust code will call +/// [`buffer_size`] and [`buffer_copy`] in order to obtain the content of this buffer. The +/// buffer index should remain assigned and buffer alive until the next time the JavaScript +/// code retains control. Then, write at location `error_buffer_index_ptr + 4` a `1` if the +/// error is caused by the address being forbidden or unsupported, and `0` otherwise. If no +/// error happens, nothing should be written to `error_buffer_index_ptr`. +/// +/// At any time, a connection can be in one of the three following states: +/// +/// - `Opening` (initial state) +/// - `Open` +/// - `Reset` +/// +/// When in the `Opening` or `Open` state, the connection can transition to the `Reset` state +/// if the remote closes the connection or refuses the connection altogether. When that +/// happens, [`connection_reset`] must be called. Once in the `Reset` state, the connection +/// cannot transition back to another state. +/// +/// Initially in the `Opening` state, the connection can transition to the `Open` state if the +/// remote accepts the connection. When that happens, [`connection_open_single_stream`] or +/// [`connection_open_multi_stream`] must be called. +/// +/// There exists two kind of connections: single-stream and multi-stream. Single-stream +/// connections are assumed to have a single stream open at all time and the encryption and +/// multiplexing are handled internally by smoldot. Multi-stream connections open and close +/// streams over time using [`connection_stream_opened`] and [`stream_reset`], and the +/// encryption and multiplexing are handled by the user of these bindings. +#[no_mangle] +pub extern "C" fn connection_new( + id: u32, + addr_ptr: u32, + addr_len: u32, + error_buffer_index_ptr: u32, +) -> u32 { + 0 +} + +/// Abruptly close a connection previously initialized with [`connection_new`]. +/// +/// This destroys the identifier passed as parameter. This identifier must never be passed +/// through the FFI boundary, unless the same identifier is later allocated again with +/// [`connection_new`]. +/// +/// Must never be called if [`connection_reset`] has been called on that object in the past. +/// +/// The connection must be closed in the background. The Rust code isn't interested in incoming +/// messages from this connection anymore. +/// +/// > **Note**: In JavaScript, remember to unregister event handlers before calling for +/// > example `WebSocket.close()`. +#[no_mangle] +pub extern "C" fn reset_connection(id: u32) {} + +/// Queues a new outbound substream opening. The [`connection_stream_opened`] function must +/// later be called when the substream has been successfully opened. +/// +/// This function will only be called for multi-stream connections. The connection must +/// currently be in the `Open` state. See the documentation of [`connection_new`] for details. +/// +/// > **Note**: No mechanism exists in this API to handle the situation where a substream fails +/// > to open, as this is not supposed to happen. If you need to handle such a +/// > situation, either try again opening a substream again or reset the entire +/// > connection. +#[no_mangle] +pub extern "C" fn connection_stream_open(connection_id: u32) {} + +/// Abruptly closes an existing substream of a multi-stream connection. The substream must +/// currently be in the `Open` state. +/// +/// Must never be called if [`stream_reset`] has been called on that object in the past. +/// +/// This function will only be called for multi-stream connections. The connection must +/// currently be in the `Open` state. See the documentation of [`connection_new`] for details. +#[no_mangle] +pub extern "C" fn connection_stream_reset(connection_id: u32, stream_id: u32) {} + +/// Queues data on the given stream. The data is found in the memory of the WebAssembly +/// virtual machine, at the given pointer. +/// +/// If `connection_id` is a single-stream connection, then the value of `stream_id` should +/// be ignored. If `connection_id` is a multi-stream connection, then the value of `stream_id` +/// contains the identifier of the stream on which to send the data, as was provided to +/// [`connection_stream_opened`]. +/// +/// The connection associated with that stream (and, in the case of a multi-stream connection, +/// the stream itself must currently be in the `Open` state. See the documentation of +/// [`connection_new`] for details. +/// +/// The size of the buffer must not exceed the number of writable bytes of the given stream. +/// Use [`stream_writable_bytes`] to notify that more data can be sent on the stream. +#[no_mangle] +pub extern "C" fn stream_send(connection_id: u32, stream_id: u32, ptr: u32, len: u32) {} + +/// Close the sending side of the given stream of the given connection. +/// +/// Never called for connection types where this isn't possible to implement (i.e. WebSocket +/// and WebRTC at the moment). +/// +/// If `connection_id` is a single-stream connection, then the value of `stream_id` should +/// be ignored. If `connection_id` is a multi-stream connection, then the value of `stream_id` +/// contains the identifier of the stream whose sending side should be closed, as was provided +/// to [`connection_stream_opened`]. +/// +/// The connection associated with that stream (and, in the case of a multi-stream connection, +/// the stream itself must currently be in the `Open` state. See the documentation of +/// [`connection_new`] for details. +#[no_mangle] +pub extern "C" fn stream_send_close(connection_id: u32, stream_id: u32) {} + +/// Called when the Wasm execution enters the context of a certain task. This is useful for +/// debugging purposes. +/// +/// Only one task can be currently executing at any time. +/// +/// The name of the task is a UTF-8 string found in the memory of the WebAssembly virtual +/// machine at offset `ptr` and with length `len`. +#[no_mangle] +pub extern "C" fn current_task_entered(ptr: u32, len: u32) {} + +/// Called when the Wasm execution leave the context of a certain task. This is useful for +/// debugging purposes. +/// +/// Only one task can be currently executing at any time. +#[no_mangle] +pub extern "C" fn current_task_exit() {} + /// The LightClient RPC offers a slightly different RPC methods than the /// substrate based chains. This is because the light client only exposes /// a small subset of the RPCs needed for basic functionality. @@ -181,10 +438,14 @@ impl LightClient { pub fn new(chain_spec: &str) -> Result { tracing::trace!(target: LOG_TARGET, "Create light client"); - let mut client = smoldot_light::Client::new(AsyncStdTcpWebSocket::new( - env!("CARGO_PKG_NAME").into(), - env!("CARGO_PKG_VERSION").into(), - )); + // let platform = AsyncStdTcpWebSocket::new( + // env!("CARGO_PKG_NAME").into(), + // env!("CARGO_PKG_VERSION").into(), + // ); + + let platform = WasmPlatform::new(); + + let mut client = smoldot_light::Client::new(platform); let smoldot_light::AddChainSuccess { chain_id, diff --git a/subxt/src/rpc/lightclient/mod.rs b/subxt/src/rpc/lightclient/mod.rs index cdb620ee24..40d3aa931e 100644 --- a/subxt/src/rpc/lightclient/mod.rs +++ b/subxt/src/rpc/lightclient/mod.rs @@ -1,5 +1,6 @@ mod background; mod client; +mod platform; pub use client::LightClient; diff --git a/subxt/src/rpc/lightclient/platform.rs b/subxt/src/rpc/lightclient/platform.rs new file mode 100644 index 0000000000..8a8b927fb3 --- /dev/null +++ b/subxt/src/rpc/lightclient/platform.rs @@ -0,0 +1,263 @@ +use futures_timer::Delay; +use futures_util::{future, FutureExt}; + +use gloo_net::websocket::{futures::WebSocket, Message, WebSocketError}; +use smoldot::libp2p::multiaddr::{Multiaddr, ProtocolRef}; + +use futures_util::stream::{SplitSink, SplitStream, StreamExt}; + +use futures_util::lock::Mutex; +use std::sync::Arc; +use std::{ + io::IoSlice, + net::{IpAddr, SocketAddr}, +}; + +use tokio::sync::{mpsc, oneshot}; + +#[derive(Clone)] +pub struct Platform {} + +impl Platform { + pub const fn new() -> Self { + Self {} + } +} + +impl smoldot_light::platform::PlatformRef for Platform { + type Delay = future::BoxFuture<'static, ()>; + + // No-op yielding. + type Yield = future::Ready<()>; + + type Instant = std::time::Instant; + + type Connection = std::convert::Infallible; + + type Stream = ConnectionSocket; + + type ConnectFuture = future::BoxFuture<'static, Result>; + + type StreamUpdateFuture<'a> = future::BoxFuture<'a, ()>; + + type NextSubstreamFuture<'a> = future::Pending<()>; + + fn now_from_unix_epoch(&self) -> std::time::Duration { + std::time::UNIX_EPOCH + .elapsed() + .expect("Invalid systime cannot be configured earlier than `UNIX_EPOCH`") + } + + fn now(&self) -> Self::Instant { + std::time::Instant::now() + } + + fn sleep(&self, duration: std::time::Duration) -> Self::Delay { + futures_timer::Delay::new(duration).boxed() + } + + fn sleep_until(&self, when: Self::Instant) -> Self::Delay { + self.sleep(when.saturating_duration_since(self.now())) + } + + fn spawn_task( + &self, + task_name: std::borrow::Cow, + task: futures_util::future::BoxFuture<'static, ()>, + ) { + println!("Spawning {task_name}"); + wasm_bindgen_futures::spawn_local(task) + } + + fn client_name(&self) -> std::borrow::Cow { + "subxt".into() + } + + fn client_version(&self) -> std::borrow::Cow { + env!("CARGO_PKG_VERSION").into() + } + + fn yield_after_cpu_intensive(&self) -> Self::Yield { + future::ready(()) + } + + fn connect(&self, url: &str) -> Self::ConnectFuture { + Box::pin(async move { + let multiaddr = url.parse::().map_err(|_| ConnectError { + message: format!("Address {url} is not a valid multiaddress"), + is_bad_addr: true, + })?; + + // First two protocals must be valid, the third one is optional. + let mut proto_iter = multiaddr.iter().fuse(); + + let addr = match ( + proto_iter.next().ok_or(ConnectError { + message: format!("Unknown protocol combination"), + is_bad_addr: true, + })?, + proto_iter.next().ok_or(ConnectError { + message: format!("Unknown protocol combination"), + is_bad_addr: true, + })?, + proto_iter.next(), + ) { + (ProtocolRef::Ip4(ip), ProtocolRef::Tcp(port), None) => { + SocketAddr::new(IpAddr::V4((ip).into()), port) + } + (ProtocolRef::Ip6(ip), ProtocolRef::Tcp(port), None) => { + SocketAddr::new(IpAddr::V6((ip).into()), port) + } + (ProtocolRef::Ip4(ip), ProtocolRef::Tcp(port), Some(ProtocolRef::Ws)) => { + SocketAddr::new(IpAddr::V4((ip).into()), port) + } + (ProtocolRef::Ip6(ip), ProtocolRef::Tcp(port), Some(ProtocolRef::Ws)) => { + SocketAddr::new(IpAddr::V6((ip).into()), port) + } + // // TODO: we don't care about the differences between Dns, Dns4, and Dns6 + // ( + // ProtocolRef::Dns(addr) | ProtocolRef::Dns4(addr) | ProtocolRef::Dns6(addr), + // ProtocolRef::Tcp(port), + // None, + // ) => (either::Right((addr.to_string(), *port)), None), + // ( + // ProtocolRef::Dns(addr) | ProtocolRef::Dns4(addr) | ProtocolRef::Dns6(addr), + // ProtocolRef::Tcp(port), + // Some(ProtocolRef::Ws), + // ) => ( + // either::Right((addr.to_string(), *port)), + // Some(format!("{}:{}", addr, *port)), + // ), + _ => { + return Err(ConnectError { + is_bad_addr: true, + message: "Unknown protocols combination".to_string(), + }) + } + }; + + // TODO: use `addr` instead. + let websocket = WebSocket::open(url.as_ref()).map_err(|e| ConnectError { + is_bad_addr: false, + message: "Cannot stablish WebSocket connection".to_string(), + })?; + + let (to_sender, from_sender) = mpsc::channel(1024); + let (to_receiver, from_receiver) = mpsc::channel(1024); + + // Note: WebSocket is not `Send`, work around that with a spawned task. + + wasm_bindgen_futures::spawn_local(async move { + let backend_event = tokio_stream::wrappers::ReceiverStream::new(from_sender); + + let rpc_responses_event = + futures_util::stream::unfold(rpc_responses, |mut rpc_responses| async { + rpc_responses + .next() + .await + .map(|result| (result, rpc_responses)) + }); + + tokio::pin!(backend_event, rpc_responses_event); + + let mut backend_event_fut = backend_event.next(); + let mut rpc_responses_fut = rpc_responses_event.next(); + + loop { + match future::select(backend_event_fut, rpc_responses_fut).await { + // Message received from the backend: user registered. + Either::Left((backend_value, previous_fut)) => { + let Some(message) = backend_value else { + tracing::trace!(target: LOG_TARGET, "Frontend channel closed"); + break; + }; + tracing::trace!( + target: LOG_TARGET, + "Received register message {:?}", + message + ); + + self.handle_register(message).await; + + backend_event_fut = backend_event.next(); + rpc_responses_fut = previous_fut; + } + // Message received from rpc handler: lightclient response. + Either::Right((response, previous_fut)) => { + // Smoldot returns `None` if the chain has been removed (which subxt does not remove). + let Some(response) = response else { + tracing::trace!(target: LOG_TARGET, "Smoldot RPC responses channel closed"); + break; + }; + tracing::trace!( + target: LOG_TARGET, + "Received smoldot RPC result {:?}", + response + ); + + self.handle_rpc_response(response).await; + + // Advance backend, save frontend. + backend_event_fut = previous_fut; + rpc_responses_fut = rpc_responses_event.next(); + } + } + } + }); + + let (sender, receiver) = websocket.split(); + Ok(ConnectionSocket { sender, receiver }) + }) + } + + fn open_out_substream(&self, _connection: &mut Self::Connection) { + // Called from MultiStream connections that are not supported. + } + + fn next_substream<'a>( + &self, + connection: &'a mut Self::Connection, + ) -> Self::NextSubstreamFuture<'a> { + // Called from MultiStream connections that are not supported. + } + + fn update_stream<'a>(&self, stream: &'a mut Self::Stream) -> Self::StreamUpdateFuture<'a> { + todo!() + } + + fn read_buffer<'a>( + &self, + stream: &'a mut Self::Stream, + ) -> smoldot_light::platform::ReadBuffer<'a> { + todo!() + } + + fn advance_read_cursor(&self, stream: &mut Self::Stream, bytes: usize) { + todo!() + } + + fn writable_bytes(&self, stream: &mut Self::Stream) -> usize { + todo!() + } + + fn send(&self, stream: &mut Self::Stream, data: &[u8]) { + todo!() + } + + fn close_send(&self, stream: &mut Self::Stream) { + todo!() + } +} + +/// Error potentially returned by [`PlatformRef::connect`]. +pub struct ConnectError { + /// Human-readable error message. + pub message: String, + /// `true` if the error is caused by the address to connect to being forbidden or unsupported. + pub is_bad_addr: bool, +} + +pub struct ConnectionSocket { + sender: SplitSink, + receiver: SSplitStream, +} diff --git a/testing/wasm-tests/Cargo.lock b/testing/wasm-tests/Cargo.lock index d39a8bc8f7..578113e7b7 100644 --- a/testing/wasm-tests/Cargo.lock +++ b/testing/wasm-tests/Cargo.lock @@ -988,18 +988,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -1379,9 +1379,9 @@ dependencies = [ [[package]] name = "sp-core-hashing" -version = "8.0.0" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27449abdfbe41b473e625bce8113745e81d65777dd1d5a8462cf24137930dad8" +checksum = "2ee599a8399448e65197f9a6cee338ad192e9023e35e31f22382964c3c174c68" dependencies = [ "blake2b_simd", "byteorder", @@ -1394,9 +1394,9 @@ dependencies = [ [[package]] name = "sp-std" -version = "7.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de8eef39962b5b97478719c493bed2926cf70cb621005bbf68ebe58252ff986" +checksum = "53458e3c57df53698b3401ec0934bea8e8cfce034816873c0b0abbd83d7bac0d" [[package]] name = "spin" diff --git a/testing/wasm-tests/Cargo.toml b/testing/wasm-tests/Cargo.toml index ac1a04d963..9d5a5e34be 100644 --- a/testing/wasm-tests/Cargo.toml +++ b/testing/wasm-tests/Cargo.toml @@ -12,4 +12,4 @@ wasm-bindgen-test = "0.3.24" tracing-wasm = "0.2.1" console_error_panic_hook = "0.1.7" serde_json = "1" -subxt = { path = "../../subxt", default-features = false, features = ["jsonrpsee-web"] } +subxt = { path = "../../subxt", default-features = false, features = ["jsonrpsee-web", "unstable-light-client"] } diff --git a/testing/wasm-tests/tests/wasm.rs b/testing/wasm-tests/tests/wasm.rs index ccfac7e763..025bc1d086 100644 --- a/testing/wasm-tests/tests/wasm.rs +++ b/testing/wasm-tests/tests/wasm.rs @@ -1,7 +1,9 @@ #![cfg(target_arch = "wasm32")] use subxt::config::PolkadotConfig; +use subxt::rpc::LightClient; use wasm_bindgen_test::*; +use std::sync::Arc; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -16,3 +18,12 @@ async fn wasm_ws_transport_works() { let chain = client.rpc().system_chain().await.unwrap(); assert_eq!(&chain, "Development"); } + +#[wasm_bindgen_test] +async fn light_client_transport_works() { + let light_client = LightClient::new(include_str!("../../artifacts/dev_spec.json")).unwrap(); + let client = subxt::client::OnlineClient::::from_rpc_client(Arc::new(light_client)).await.unwrap(); + + let chain = client.rpc().system_chain().await.unwrap(); + assert_eq!(&chain, "Development"); +}