fix: Convert vendor/pezkuwi-subxt from submodule to regular directory

This commit is contained in:
2025-12-19 16:45:24 +03:00
parent 9a52edf0df
commit fdd023c499
393 changed files with 154124 additions and 1 deletions
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[workspace]
[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 = { version = "0.20.0", features = ["csr"] }
web-sys = "0.3.69"
hex = "0.4.3"
yew-router = "0.17.0"
js-sys = "0.3.69"
wasm-bindgen = "0.2.86"
wasm-bindgen-futures = "0.4.36"
anyhow = "1.0.71"
serde = "1.0.163"
serde_json = "1.0.96"
+26
View File
@@ -0,0 +1,26 @@
# wasm-example
This is a small WASM app using the Yew UI framework to showcase how to use Subxt's features in a WASM environment.
To run the app locally we first install Trunk, a WASM bundler:
```
cargo install --locked trunk
```
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.
If you have a `polkadot` binary already, running this should be sufficient:
```
polkadot --dev
```
Then, in another terminal, run the app locally with:
```
trunk serve --open
```
# signing example
For the signing example, we use the `@polkadot/extension-dapp` NPM package to talk to wallets loaded as browser extensions. In order to sign and submit the transaction using the `polkadot --dev` node we spawned above, you'll need to create a dev account in your wallet of choice. Use the recovery phrase `bottom drive obey lake curtain smoke basket hold race lonely fit walk` and the derivation path `//Alice` to create a dev account that can be used.
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link data-trunk rel="scss" href="index.scss" />
<link data-trunk rel="inline" href="index.js" />
<title>Subxt Examples Yew App</title>
</head>
<body></body>
</html>
+89
View File
@@ -0,0 +1,89 @@
/**
* The `@polkadot/extension-dapp` package can be dynamically imported.
* Usually it is wise to use a package manager like npm or yarn to install it as a dependency.
*
* The `getPolkadotJsExtensionMod` closure returns the `@polkadot/extension-dapp` module on demand.
*/
let getPolkadotJsExtensionMod = (() => {
let mod = null;
// initialize `@polkadot/extension-dapp` module on page load
let initPromise = (async () => {
mod = await import(
"https://cdn.jsdelivr.net/npm/@polkadot/extension-dapp@0.46.3/+esm"
);
})();
// return a function that waits for initialization to be finished, in case mod is not initialized yet.
return async () => {
if (mod == null) {
await initPromise;
}
return mod;
};
})();
/**
* Queries wallets from browser extensions like Talisman and the Polkadot.js extension for user accounts.
*
* @returns a json string that contains all the accounts that were found.
*/
async function getAccounts() {
const extensionMod = await getPolkadotJsExtensionMod();
await extensionMod.web3Enable("Subxt Example App");
const allAccounts = await extensionMod.web3Accounts();
const accountObjects = allAccounts.map((account) => ({
name: account.meta.name, // e.g. "Alice"
source: account.meta.source, // e.g. "talisman", "polkadot-js"
ty: account.type, // e.g. "sr25519"
address: account.address // e.g. "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
}));
console.log(accountObjects);
return JSON.stringify(accountObjects);
}
/**
* Signs a payload via browser extension
*
* @param payloadAsStr a string representing a JSON object like this:
* let payload = {
* "specVersion": "0x000024d6",
* "transactionVersion": "0x00000018",
* "address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
* "blockHash": "0xd7aad6185db012b7ffbce710b55234d6c9589170566b925ee50cfa3d7f1e6f8f",
* "blockNumber": "0x00000000",
* "era": "0x0000",
* "genesisHash": "0xd7aad6185db012b7ffbce710b55234d6c9589170566b925ee50cfa3d7f1e6f8f",
* "method": "0x0503001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c0b00c465f14670",
* "nonce": "0x00000000",
* "signedExtensions": [
* "CheckNonZeroSender",
* "CheckSpecVersion",
* "CheckTxVersion",
* "CheckGenesis",
* "CheckMortality",
* "CheckNonce",
* "CheckWeight",
* "ChargeTransactionPayment",
* "PrevalidateAttests"
* ],
* "tip": "0x00000000000000000000000000000000",
* "version": 4
* };
* @param source the extension used for signing as a string
* @param address the ss58 encoded address as a string
* @returns {Promise<*>}
*/
async function signPayload(payloadAsStr, source, address) {
let payload = JSON.parse(payloadAsStr);
const extensionMod = await getPolkadotJsExtensionMod();
const injector = await extensionMod.web3FromSource(source);
const signPayload = injector?.signer?.signPayload;
if (!!signPayload) {
const {signature} = await signPayload(payload);
console.log("signature js:", signature)
return signature;
} else {
throw "The extension's injector does not have a `signPayload` function on its `signer`";
}
}
+102
View File
@@ -0,0 +1,102 @@
$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;
min-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;
}
a {
text-decoration: none;
}
button:hover {
background-color: $secondary;
}
input {
font-size: large;
background-color: white;
color: $dark;
border-radius: 8px;
padding: 8px;
}
.mb {
margin-bottom: 12px;
}
small {
color: #24cc85;
}
.error {
color: red;
background: black;
padding: 8px;
border-radius: 8px;
}
@keyframes loading {
0% {
transform: translateX(0);
opacity: 1;
}
50% {
transform: translateX(20px);
opacity: 0.5;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.loading {
animation: loading 0.7s infinite;
}
+59
View File
@@ -0,0 +1,59 @@
use routes::signing::SigningExamplesComponent;
use yew::prelude::*;
use yew_router::prelude::*;
use crate::routes::fetching::FetchingExamplesComponent;
mod routes;
mod services;
#[derive(Routable, PartialEq, Eq, Clone, Debug)]
pub enum Route {
#[at("/fetching")]
Fetching,
#[at("/signing")]
Signing,
#[not_found]
#[at("/")]
Home,
}
fn main() {
yew::Renderer::<SubxtExamplesApp>::new().render();
}
struct SubxtExamplesApp;
impl Component for SubxtExamplesApp {
type Message = ();
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
SubxtExamplesApp
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<BrowserRouter>
<Switch<Route> render={switch} />
</BrowserRouter>
}
}
}
fn switch(routes: Route) -> Html {
match routes {
Route::Fetching => {
html! { <FetchingExamplesComponent/> }
}
Route::Signing => html! { <SigningExamplesComponent/> },
Route::Home => {
html! {
<div>
<h1>{"Welcome to the Subxt WASM examples!"}</h1>
<a href="/signing"> <button>{"Signing Examples"} </button></a>
<a href="/fetching"> <button>{"Fetching and Subscribing Examples"}</button></a>
</div> }
}
}
}
@@ -0,0 +1,140 @@
use futures::FutureExt;
use yew::prelude::*;
use crate::services;
pub struct FetchingExamplesComponent {
operation_title: Option<AttrValue>,
lines: Vec<AttrValue>,
}
pub enum Message {
Error(subxt::Error),
Reload,
Line(AttrValue),
Lines(Vec<AttrValue>),
ButtonClick(Button),
}
pub enum Button {
SubscribeFinalized,
FetchConstant,
FetchEvents,
}
impl Component for FetchingExamplesComponent {
type Message = Message;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
FetchingExamplesComponent {
lines: Vec::new(),
operation_title: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Message::Error(err) => {
self.lines.insert(0, 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) => {
// newer lines go to the top
self.lines.insert(0, line);
}
Message::Lines(lines) => {
for line in lines {
self.lines.insert(0, line);
}
}
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>
<h1>{operation_title}</h1>
if self.lines.is_empty(){
<p>{"Loading..."}</p>
}
else{
}
{ for self.lines.iter().map(|line| html! {<p> {line} </p>}) }
}
else{
<>
<a href="/"> <button>{"<= Back"}</button></a>
<h1>{"Subxt Fetching and Subscribing 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>
}
}
}
@@ -0,0 +1,2 @@
pub mod fetching;
pub mod signing;
@@ -0,0 +1,413 @@
use anyhow::anyhow;
use futures::FutureExt;
use subxt::{OnlineClient, PolkadotConfig};
use subxt::config::DefaultExtrinsicParamsBuilder;
use subxt::ext::codec::{Decode, Encode};
use subxt::tx::Payload as _;
use subxt::tx::SubmittableTransaction;
use subxt::utils::{AccountId32, MultiSignature};
use crate::services::{extension_signature_for_extrinsic, get_accounts, polkadot, Account};
use web_sys::HtmlInputElement;
use yew::prelude::*;
pub struct SigningExamplesComponent {
message: String,
remark_call_bytes: Vec<u8>,
online_client: Option<OnlineClient<PolkadotConfig>>,
stage: SigningStage,
}
impl SigningExamplesComponent {
/// # Panics
/// panics if self.online_client is None.
fn set_message(&mut self, message: String) {
let remark_call = polkadot::tx().system().remark(message.as_bytes().to_vec());
let online_client = self.online_client.as_ref().unwrap();
let remark_call_bytes = remark_call
.encode_call_data(&online_client.metadata())
.unwrap();
self.remark_call_bytes = remark_call_bytes;
self.message = message;
}
}
pub enum SigningStage {
Error(String),
CreatingOnlineClient,
EnterMessage,
RequestingAccounts,
SelectAccount(Vec<Account>),
Signing(Account),
SigningSuccess {
signer_account: Account,
signature: MultiSignature,
signed_extrinsic_hex: String,
submitting_stage: SubmittingStage,
},
}
pub enum SubmittingStage {
Initial {
signed_extrinsic: SubmittableTransaction<PolkadotConfig, OnlineClient<PolkadotConfig>>,
},
Submitting,
Success {
remark_event: polkadot::system::events::ExtrinsicSuccess,
},
Error(anyhow::Error),
}
pub enum Message {
Error(anyhow::Error),
OnlineClientCreated(OnlineClient<PolkadotConfig>),
ChangeMessage(String),
RequestAccounts,
ReceivedAccounts(Vec<Account>),
/// usize represents account index in Vec<Account>
SignWithAccount(usize),
ReceivedSignature(
MultiSignature,
SubmittableTransaction<PolkadotConfig, OnlineClient<PolkadotConfig>>,
),
SubmitSigned,
ExtrinsicFinalized {
remark_event: polkadot::system::events::ExtrinsicSuccess,
},
ExtrinsicFailed(anyhow::Error),
}
impl Component for SigningExamplesComponent {
type Message = Message;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
ctx.link().send_future(OnlineClient::<PolkadotConfig>::new().map(|res| {
match res {
Ok(online_client) => Message::OnlineClientCreated(online_client),
Err(err) => Message::Error(anyhow!("Online Client could not be created. Make sure you have a local node running:\n{err}")),
}
}));
SigningExamplesComponent {
message: "".to_string(),
stage: SigningStage::CreatingOnlineClient,
online_client: None,
remark_call_bytes: vec![],
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Message::OnlineClientCreated(online_client) => {
self.online_client = Some(online_client);
self.stage = SigningStage::EnterMessage;
self.set_message("Hello".into());
}
Message::ChangeMessage(message) => {
self.set_message(message);
}
Message::RequestAccounts => {
self.stage = SigningStage::RequestingAccounts;
ctx.link().send_future(get_accounts().map(
|accounts_or_err| match accounts_or_err {
Ok(accounts) => Message::ReceivedAccounts(accounts),
Err(err) => Message::Error(err),
},
));
}
Message::ReceivedAccounts(accounts) => {
self.stage = SigningStage::SelectAccount(accounts);
}
Message::Error(err) => self.stage = SigningStage::Error(err.to_string()),
Message::SignWithAccount(i) => {
if let SigningStage::SelectAccount(accounts) = &self.stage {
let account = accounts.get(i).unwrap();
let account_address = account.address.clone();
let account_source = account.source.clone();
let account_id: AccountId32 = account_address.parse().unwrap();
self.stage = SigningStage::Signing(account.clone());
let remark_call = polkadot::tx()
.system()
.remark(self.message.as_bytes().to_vec());
let api = self.online_client.as_ref().unwrap().clone();
ctx.link().send_future(async move {
let Ok(account_nonce) = api.tx().account_nonce(&account_id).await else {
return Message::Error(anyhow!("Fetching account nonce failed"));
};
let Ok(call_data) = api.tx().call_data(&remark_call) else {
return Message::Error(anyhow!("could not encode call data"));
};
let Ok(signature) = extension_signature_for_extrinsic(
&call_data,
&api,
account_nonce,
account_source,
account_address,
)
.await
else {
return Message::Error(anyhow!("Signing via extension failed"));
};
let Ok(multi_signature) = MultiSignature::decode(&mut &signature[..])
else {
return Message::Error(anyhow!("MultiSignature Decoding"));
};
let params = DefaultExtrinsicParamsBuilder::new()
.nonce(account_nonce)
.build();
let Ok(mut partial_signed) =
api.tx().create_partial_offline(&remark_call, params)
else {
return Message::Error(anyhow!("PartialTransaction creation failed"));
};
// Apply the signature
let signed_extrinsic = partial_signed
.sign_with_account_and_signature(&account_id, &multi_signature);
// check the TX validity (to debug in the js console if the extrinsic would work)
let dry_res = signed_extrinsic.validate().await;
web_sys::console::log_1(
&format!("Validation Result: {:?}", dry_res).into(),
);
// return the signature and signed extrinsic
Message::ReceivedSignature(multi_signature, signed_extrinsic)
});
}
}
Message::ReceivedSignature(signature, signed_extrinsic) => {
if let SigningStage::Signing(account) = &self.stage {
let signed_extrinsic_hex =
format!("0x{}", hex::encode(signed_extrinsic.encoded()));
self.stage = SigningStage::SigningSuccess {
signer_account: account.clone(),
signature,
signed_extrinsic_hex,
submitting_stage: SubmittingStage::Initial { signed_extrinsic },
}
}
}
Message::SubmitSigned => {
if let SigningStage::SigningSuccess {
submitting_stage: submitting_stage @ SubmittingStage::Initial { .. },
..
} = &mut self.stage
{
let SubmittingStage::Initial { signed_extrinsic } =
std::mem::replace(submitting_stage, SubmittingStage::Submitting)
else {
panic!("unreachable")
};
ctx.link().send_future(async move {
match submit_wait_finalized_and_get_extrinsic_success_event(
signed_extrinsic,
)
.await
{
Ok(remark_event) => Message::ExtrinsicFinalized { remark_event },
Err(err) => Message::ExtrinsicFailed(err),
}
});
}
}
Message::ExtrinsicFinalized { remark_event } => {
if let SigningStage::SigningSuccess {
submitting_stage, ..
} = &mut self.stage
{
*submitting_stage = SubmittingStage::Success { remark_event }
}
}
Message::ExtrinsicFailed(err) => {
if let SigningStage::SigningSuccess {
submitting_stage, ..
} = &mut self.stage
{
*submitting_stage = SubmittingStage::Error(err)
}
}
};
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let message_as_hex_html = || {
html!(
<div class="mb">
<b>{"Hex representation of \"remark\" call in \"System\" pallet:"}</b> <br/>
{format!("0x{}", hex::encode(&self.remark_call_bytes))}
</div>
)
};
let message_html: Html = match &self.stage {
SigningStage::Error(_)
| SigningStage::EnterMessage
| SigningStage::CreatingOnlineClient => html!(<></>),
_ => {
let _remark_call = polkadot::tx()
.system()
.remark(self.message.as_bytes().to_vec());
html!(
<div>
<div class="mb">
<b>{"Message: "}</b> <br/>
{&self.message}
</div>
{message_as_hex_html()}
</div>
)
}
};
let signer_account_html: Html = match &self.stage {
SigningStage::Signing(signer_account)
| SigningStage::SigningSuccess { signer_account, .. } => {
html!(
<div class="mb">
<b>{"Account used for signing: "}</b> <br/>
{"Extension: "}{&signer_account.source} <br/>
{"Name: "}{&signer_account.name} <br/>
{"Address: "}{&signer_account.address} <br/>
</div>
)
}
_ => html!(<></>),
};
let stage_html: Html = match &self.stage {
SigningStage::Error(error_message) => {
html!(<div class="error"> {"Error: "} {error_message} </div>)
}
SigningStage::CreatingOnlineClient => {
html!(
<div>
<b>{"Creating Online Client..."}</b>
</div>
)
}
SigningStage::EnterMessage => {
let get_accounts_click = ctx.link().callback(|_| Message::RequestAccounts);
let on_input = ctx.link().callback(move |event: InputEvent| {
let input_element = event.target_dyn_into::<HtmlInputElement>().unwrap();
let value = input_element.value();
Message::ChangeMessage(value)
});
html!(
<>
<div class="mb"><b>{"Enter a message for the \"remark\" call in the \"System\" pallet:"}</b></div>
<input oninput={on_input} class="mb" value={AttrValue::from(self.message.clone())}/>
{message_as_hex_html()}
<button onclick={get_accounts_click}> {"=> Select an Account for Signing"} </button>
</>
)
}
SigningStage::RequestingAccounts => {
html!(<div>{"Querying extensions for accounts..."}</div>)
}
SigningStage::SelectAccount(accounts) => {
if accounts.is_empty() {
html!(<div>{"No Web3 extension accounts found. Install Talisman or the Polkadot.js extension and add an account."}</div>)
} else {
html!(
<>
<div class="mb"><b>{"Select an account you want to use for signing:"}</b></div>
{ for accounts.iter().enumerate().map(|(i, account)| {
let sign_with_account = ctx.link().callback(move |_| Message::SignWithAccount(i));
html! {
<button onclick={sign_with_account}>
{&account.source} {" | "} {&account.name}<br/>
<small>{&account.address}</small>
</button>
}
}) }
</>
)
}
}
SigningStage::Signing(_) => {
html!(<div>{"Singing message with browser extension..."}</div>)
}
SigningStage::SigningSuccess {
signature,
signed_extrinsic_hex,
submitting_stage,
..
} => {
let submitting_stage_html = match submitting_stage {
SubmittingStage::Initial { .. } => {
let submit_extrinsic_click =
ctx.link().callback(move |_| Message::SubmitSigned);
html!(<button onclick={submit_extrinsic_click}> {"=> Submit the signed extrinsic"} </button>)
}
SubmittingStage::Submitting => {
html!(<div class="loading"><b>{"Submitting Extrinsic... (please wait a few seconds)"}</b></div>)
}
SubmittingStage::Success { remark_event } => {
html!(<div style="overflow-wrap: break-word;"> <b>{"Successfully submitted Extrinsic. Event:"}</b> <br/> {format!("{:?}", remark_event)} </div>)
}
SubmittingStage::Error(err) => {
html!(<div class="error"> {"Error: "} {err.to_string()} </div>)
}
};
html!(
<>
<div style="overflow-wrap: break-word;" class="mb">
<b>{"Received signature: "}</b><br/>
{hex::encode(signature.encode())}
</div>
<div style="overflow-wrap: break-word;" class="mb">
<b>{"Hex representation of signed extrinsic: "}</b> <br/>
{signed_extrinsic_hex}
</div>
{submitting_stage_html}
</>
)
}
};
html! {
<div>
<a href="/"> <button>{"<= Back"}</button></a>
<h1>{"Subxt Signing Example"}</h1>
{message_html}
{signer_account_html}
{stage_html}
</div>
}
}
}
async fn submit_wait_finalized_and_get_extrinsic_success_event(
extrinsic: SubmittableTransaction<PolkadotConfig, OnlineClient<PolkadotConfig>>,
) -> Result<polkadot::system::events::ExtrinsicSuccess, anyhow::Error> {
let events = extrinsic
.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?;
let events_str = format!("{:?}", &events);
web_sys::console::log_1(&events_str.into());
for event in events.find::<polkadot::system::events::ExtrinsicSuccess>() {
web_sys::console::log_1(&format!("{:?}", event).into());
}
let success = events.find_first::<polkadot::system::events::ExtrinsicSuccess>()?;
success.ok_or(anyhow!("ExtrinsicSuccess not found in events"))
}
@@ -0,0 +1,173 @@
use anyhow::anyhow;
use js_sys::Promise;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fmt::Write;
use subxt::ext::codec::{Compact, Encode};
use subxt::{self, OnlineClient, PolkadotConfig};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use yew::{AttrValue, Callback};
#[subxt::subxt(runtime_metadata_path = "../../artifacts/polkadot_metadata_small.scale")]
pub 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.decode_as_fields::<subxt::dynamic::Value>()?;
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 extrinsics = block.extrinsics().await?;
for ext in extrinsics.iter() {
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();
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.decode_as_fields::<subxt::dynamic::Value>()?;
writeln!(output, " {pallet_name}_{event_name}").ok();
writeln!(output, " {}", event_values).ok();
}
}
cb.emit(output.into())
}
Ok(())
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = getAccounts)]
pub fn js_get_accounts() -> Promise;
#[wasm_bindgen(js_name = signPayload)]
pub fn js_sign_payload(payload: String, source: String, address: String) -> Promise;
}
/// DTO to communicate with JavaScript
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Account {
/// account name
pub name: String,
/// name of the browser extension
pub source: String,
/// the signature type, e.g. "sr25519" or "ed25519"
pub ty: String,
/// ss58 formatted address as string. Can be converted into AccountId32 via it's FromStr implementation.
pub address: String,
}
pub async fn get_accounts() -> Result<Vec<Account>, anyhow::Error> {
let result = JsFuture::from(js_get_accounts())
.await
.map_err(|js_err| anyhow!("{js_err:?}"))?;
let accounts_str = result
.as_string()
.ok_or(anyhow!("Error converting JsValue into String"))?;
let accounts: Vec<Account> = serde_json::from_str(&accounts_str)?;
Ok(accounts)
}
fn to_hex(bytes: impl AsRef<[u8]>) -> String {
format!("0x{}", hex::encode(bytes.as_ref()))
}
fn encode_then_hex<E: Encode>(input: &E) -> String {
format!("0x{}", hex::encode(input.encode()))
}
/// communicates with JavaScript to obtain a signature for the `partial_extrinsic` via a browser extension (e.g. polkadot-js or Talisman)
///
/// Some parameters are hard-coded here and not taken from the partial_extrinsic itself (mortality_checkpoint, era, tip).
pub async fn extension_signature_for_extrinsic(
call_data: &[u8],
api: &OnlineClient<PolkadotConfig>,
account_nonce: u64,
account_source: String,
account_address: String,
) -> Result<Vec<u8>, anyhow::Error> {
let genesis_hash = encode_then_hex(&api.genesis_hash());
// These numbers aren't SCALE encoded; their bytes are just converted to hex:
let spec_version = to_hex(&api.runtime_version().spec_version.to_be_bytes());
let transaction_version = to_hex(&api.runtime_version().transaction_version.to_be_bytes());
let nonce = to_hex(&account_nonce.to_be_bytes());
// If you construct a mortal transaction, then this block hash needs to correspond
// to the block number passed to `Era::mortal()`.
let mortality_checkpoint = encode_then_hex(&api.genesis_hash());
let era = encode_then_hex(&subxt::utils::Era::Immortal);
let method = to_hex(call_data);
let signed_extensions: Vec<String> = api
.metadata()
.extrinsic()
.transaction_extensions_by_version(0)
.unwrap()
.map(|e| e.identifier().to_string())
.collect();
let tip = encode_then_hex(&Compact(0u128));
let payload = json!({
"specVersion": spec_version,
"transactionVersion": transaction_version,
"address": account_address,
"blockHash": mortality_checkpoint,
"blockNumber": "0x00000000",
"era": era,
"genesisHash": genesis_hash,
"method": method,
"nonce": nonce,
"signedExtensions": signed_extensions,
"tip": tip,
"version": 4,
});
let payload = payload.to_string();
let result = JsFuture::from(js_sign_payload(payload, account_source, account_address))
.await
.map_err(|js_err| anyhow!("{js_err:?}"))?;
let signature = result
.as_string()
.ok_or(anyhow!("Error converting JsValue into String"))?;
let signature = hex::decode(&signature[2..])?;
Ok(signature)
}