-
Notifications
You must be signed in to change notification settings - Fork 245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Yew Subxt WASM examples #968
Changes from 5 commits
cb618ab
2e57cbd
25931e0
b75b43e
0effd2d
b1d6581
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
**/*.rs.bk | ||
**/.DS_Store | ||
cargo-timing* | ||
/examples/wasm-example/dist | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[package] | ||
name = "wasm-example" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
futures = "0.3.28" | ||
subxt = { path = "../../subxt", default-features = false, features = ["jsonrpsee-web"], target_arch = "wasm32" } | ||
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DQ: does |
||
web-sys = "0.3.63" | ||
hex = "0.4.3" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<link data-trunk rel="scss" href="index.scss" /> | ||
<title>Yew App</title> | ||
</head> | ||
</html> |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,57 @@ | ||||||||
$primary: #24cc85; | ||||||||
$secondary: #1f624a; | ||||||||
$dark: #242a35; | ||||||||
|
||||||||
*{ | ||||||||
font-family: monospace; | ||||||||
color: $dark; | ||||||||
|
||||||||
} | ||||||||
|
||||||||
html{ | ||||||||
background-color: $dark; | ||||||||
display: flex; | ||||||||
justify-content: center; | ||||||||
height: 100%; | ||||||||
} | ||||||||
|
||||||||
h1{ | ||||||||
font-weight: bolder; | ||||||||
color: $dark; | ||||||||
} | ||||||||
|
||||||||
body{ | ||||||||
width: 800px; | ||||||||
max-width: 100%; | ||||||||
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; | ||||||||
height: 100%; | ||||||||
margin: 0px; | ||||||||
padding: 16px; | ||||||||
background-color: $primary; | ||||||||
} | ||||||||
|
||||||||
|
||||||||
p{ | ||||||||
white-space: pre-wrap; | ||||||||
border-radius: 8px; | ||||||||
padding: 8px; | ||||||||
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; | ||||||||
background-color: $dark; | ||||||||
color: white; | ||||||||
} | ||||||||
button{ | ||||||||
font-size: large; | ||||||||
padding: 8px 16px; | ||||||||
font-weight: bold; | ||||||||
background-color: $dark; | ||||||||
border: none; | ||||||||
border-radius: 8px; | ||||||||
color: white; | ||||||||
cursor: pointer; | ||||||||
display: block; | ||||||||
margin-top: 8px; | ||||||||
} | ||||||||
|
||||||||
button:hover{ | ||||||||
background-color: $secondary; | ||||||||
} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
//! This is a small WASM app using the Yew UI framework showcasing how to use Subxt's features in a WASM environment. | ||
//! | ||
//! To run the app locally use Trunk, a WASM bundler: | ||
//! ``` | ||
//! cargo install --locked trunk | ||
//! ``` | ||
//! Run the app locally: | ||
//! ``` | ||
//! trunk serve --open | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After running this command on the repo |
||
//! ``` | ||
//! You need to have a local polkadot/substrate node with it's JSON-RPC HTTP server running at 127.0.0.1:9933 in order for the examples to be working. | ||
//! Also make sure your browser supports WASM. | ||
use futures::{self, FutureExt}; | ||
|
||
use yew::prelude::*; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: do we need to import everything here? |
||
mod services; | ||
|
||
fn main() { | ||
yew::Renderer::<SubxtExamplesComponent>::new().render(); | ||
} | ||
|
||
struct SubxtExamplesComponent { | ||
operation_title: Option<AttrValue>, | ||
lines: Vec<AttrValue>, | ||
} | ||
|
||
enum Message { | ||
Error(subxt::Error), | ||
Reload, | ||
Line(AttrValue), | ||
Lines(Vec<AttrValue>), | ||
ButtonClick(Button), | ||
} | ||
|
||
enum Button { | ||
SubscribeFinalized, | ||
FetchConstant, | ||
FetchEvents, | ||
} | ||
|
||
impl Component for SubxtExamplesComponent { | ||
type Message = Message; | ||
type Properties = (); | ||
|
||
fn create(_ctx: &Context<Self>) -> Self { | ||
SubxtExamplesComponent { | ||
lines: Vec::new(), | ||
operation_title: None, | ||
} | ||
} | ||
|
||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { | ||
match msg { | ||
Message::Error(err) => { | ||
self.lines.push(err.to_string().into()); | ||
} | ||
Message::Reload => { | ||
let window = web_sys::window().expect("Failed to access the window object"); | ||
window | ||
.location() | ||
.reload() | ||
.expect("Failed to reload the page"); | ||
} | ||
Message::Line(line) => { | ||
self.lines.push(line); | ||
} | ||
Message::Lines(mut lines) => { | ||
self.lines.append(&mut lines); | ||
} | ||
Message::ButtonClick(button) => match button { | ||
Button::SubscribeFinalized => { | ||
self.operation_title = Some("Subscribe to finalized blocks:".into()); | ||
let cb: Callback<AttrValue> = ctx.link().callback(Message::Line); | ||
ctx.link() | ||
.send_future(services::subscribe_to_finalized_blocks(cb).map(|result| { | ||
let err = result.unwrap_err(); | ||
Message::Error(err) | ||
})); | ||
} | ||
Button::FetchConstant => { | ||
self.operation_title = | ||
Some("Fetch the constant \"block_length\" of \"System\" pallet:".into()); | ||
ctx.link() | ||
.send_future(services::fetch_constant_block_length().map(|result| { | ||
match result { | ||
Ok(value) => Message::Line( | ||
format!( | ||
"constant \"block_length\" of \"System\" pallet:\n {value}" | ||
) | ||
.into(), | ||
), | ||
Err(err) => Message::Error(err), | ||
} | ||
})) | ||
} | ||
Button::FetchEvents => { | ||
self.operation_title = Some("Fetch events:".into()); | ||
ctx.link() | ||
.send_future(services::fetch_events_dynamically().map( | ||
|result| match result { | ||
Ok(value) => { | ||
Message::Lines(value.into_iter().map(AttrValue::from).collect()) | ||
} | ||
Err(err) => Message::Error(err), | ||
}, | ||
)) | ||
} | ||
}, | ||
} | ||
true | ||
} | ||
|
||
fn view(&self, ctx: &Context<Self>) -> Html { | ||
let reload: Callback<MouseEvent> = ctx.link().callback(|_| Message::Reload); | ||
|
||
let subscribe_finalized = ctx | ||
.link() | ||
.callback(|_| Message::ButtonClick(Button::SubscribeFinalized)); | ||
|
||
let fetch_constant = ctx | ||
.link() | ||
.callback(|_| Message::ButtonClick(Button::FetchConstant)); | ||
|
||
let fetch_events = ctx | ||
.link() | ||
.callback(|_| Message::ButtonClick(Button::FetchEvents)); | ||
|
||
html! { | ||
<div> | ||
if let Some(operation_title) = &self.operation_title{ | ||
<button onclick={reload}>{"🡄 Back"}</button> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: the prefixed characters do not render properly on my machine, could we change it to something else? |
||
<h1>{operation_title}</h1> | ||
if self.lines.is_empty(){ | ||
<p>{"Loading..."}</p> | ||
} | ||
else{ | ||
|
||
} | ||
{ for self.lines.iter().map(|line| html! {<p> {line} </p>}) } | ||
} | ||
else{ | ||
<> | ||
<h1>{"Subxt Examples"}</h1> | ||
<button onclick={subscribe_finalized}>{"Example: Subscribe to Finalized blocks"}</button> | ||
<button onclick={fetch_constant}>{"Example: Fetch constant value"}</button> | ||
<button onclick={fetch_events}>{"Example: Fetch events"}</button> | ||
</> | ||
} | ||
</div> | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
use futures::StreamExt; | ||
use std::fmt::Write; | ||
use subxt::{self, OnlineClient, PolkadotConfig}; | ||
use yew::{AttrValue, Callback}; | ||
|
||
#[subxt::subxt(runtime_metadata_path = "../../artifacts/polkadot_metadata_small.scale")] | ||
mod polkadot {} | ||
|
||
pub(crate) async fn fetch_constant_block_length() -> Result<String, subxt::Error> { | ||
let api = OnlineClient::<PolkadotConfig>::new().await?; | ||
let constant_query = polkadot::constants().system().block_length(); | ||
|
||
let value = api.constants().at(&constant_query)?; | ||
Ok(format!("{value:?}")) | ||
} | ||
|
||
pub(crate) async fn fetch_events_dynamically() -> Result<Vec<String>, subxt::Error> { | ||
let api = OnlineClient::<PolkadotConfig>::new().await?; | ||
let events = api.events().at_latest().await?; | ||
let mut event_strings = Vec::<String>::new(); | ||
for event in events.iter() { | ||
let event = event?; | ||
let pallet = event.pallet_name(); | ||
let variant = event.variant_name(); | ||
let field_values = event.field_values()?; | ||
event_strings.push(format!("{pallet}::{variant}: {field_values}")); | ||
} | ||
Ok(event_strings) | ||
} | ||
|
||
/// subscribes to finalized blocks. When a block is received, it is formatted as a string and sent via the callback. | ||
pub(crate) async fn subscribe_to_finalized_blocks( | ||
cb: Callback<AttrValue>, | ||
) -> Result<(), subxt::Error> { | ||
let api = OnlineClient::<PolkadotConfig>::new().await?; | ||
// Subscribe to all finalized blocks: | ||
let mut blocks_sub = api.blocks().subscribe_finalized().await?; | ||
while let Some(block) = blocks_sub.next().await { | ||
let block = block?; | ||
let mut output = String::new(); | ||
writeln!(output, "Block #{}:", block.header().number).ok(); | ||
writeln!(output, " Hash: {}", block.hash()).ok(); | ||
writeln!(output, " Extrinsics:").ok(); | ||
let body = block.body().await?; | ||
for ext in body.extrinsics().iter() { | ||
let ext = ext?; | ||
let idx = ext.index(); | ||
let events = ext.events().await?; | ||
let bytes_hex = format!("0x{}", hex::encode(ext.bytes())); | ||
|
||
// See the API docs for more ways to decode extrinsics: | ||
let decoded_ext = ext.as_root_extrinsic::<polkadot::Call>(); | ||
|
||
writeln!(output, " Extrinsic #{idx}:").ok(); | ||
writeln!(output, " Bytes: {bytes_hex}").ok(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: is there something we could do here to wrap the bytes on the screens width? I'm seeing the bytes exceeding the green and black boxes on my machine There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly a |
||
writeln!(output, " Decoded: {decoded_ext:?}").ok(); | ||
writeln!(output, " Events:").ok(); | ||
|
||
for evt in events.iter() { | ||
let evt = evt?; | ||
|
||
let pallet_name = evt.pallet_name(); | ||
let event_name = evt.variant_name(); | ||
let event_values = evt.field_values()?; | ||
|
||
writeln!(output, " {pallet_name}_{event_name}").ok(); | ||
writeln!(output, " {}", event_values).ok(); | ||
} | ||
} | ||
cb.emit(output.into()) | ||
} | ||
Ok(()) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: