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 { let api = OnlineClient::::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, subxt::Error> { let api = OnlineClient::::new().await?; let events = api.events().at_latest().await?; let mut event_strings = Vec::::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, ) -> Result<(), subxt::Error> { let api = OnlineClient::::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::(); 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.field_values()?; 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, 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 = 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(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, account_nonce: u64, account_source: String, account_address: String, ) -> Result, 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 = 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) }