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
+5
View File
@@ -0,0 +1,5 @@
# Subxt Examples
Each folder here contains a complete example which makes use of subxt in some way.
For smaller single-file examples, see the `./subxt/examples` folder.
@@ -0,0 +1,10 @@
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
db.sqlite
db.sqlite-shm
db.sqlite-wal
.tool-versions
/target
/build
/node_modules
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "subxt-ffi"
version = "0.1.0"
edition = "2024"
[dependencies]
hex = "0.4.3"
subxt = { path = "../../subxt" }
subxt-signer = { path = "../../signer" }
tokio = { version = "1", features = ["full"] }
[lib]
crate-type = ["cdylib"]
name = "subxt_ffi"
+120
View File
@@ -0,0 +1,120 @@
# ffi-example
This example shows how to expose a small piece of Subxt functionality, in our case, a single balance-transfer call, as a native C-ABI library, consumable from Python and Node.js.
## Overview
- We want to let non-Rust clients interact with any Substrate-based node (Polkadot in this example) via a tiny FFI layer.
- Instead of exposing Subxts full, Rust-centric API, we build a thin **facade crate** that:
1. Calls Subxt under the hood
2. Exposes just the functions we need via `pub extern "C" fn …`
- Client languages (Python, JavaScript, Swift, Kotlin, etc.) load the compiled `.so`/`.dylib`/`.dll` and call these C-ABI functions directly.
```mermaid
flowchart LR
subgraph Rust side
subxt[Subxt Public API]
facade[Facade crate]
node[Substrate node]
cabi[C ABI library]
subxt --> facade
facade --> node
facade --> cabi
end
subgraph Client side
swift[Swift client]
python[Python client]
kotlin[Kotlin client]
js[JavaScript client]
swift --> cabi
python --> cabi
kotlin --> cabi
js --> cabi
end
```
Our one example function is:
```rust
pub extern "C" fn do_transfer(dest_hex: *const c_char, amount: u64) -> i32
```
which does a single balance transfer and returns 0 on success, 1 on error.
## Prerequisites
- Rust toolchain (with cargo)
- Python 3
- Node.js (for the JS example. Version 19 worked on my M2 Mac, but version 22 did not, so YMMV).
- A running Substrate node (Polkadot) on ws://127.0.0.1:8000. One can use Chopsticks for a quick local Polkadot node:
```shell
npx @acala-network/chopsticks \
--config=https://raw.githubusercontent.com/AcalaNetwork/chopsticks/master/configs/polkadot.yml
```
Or, if you have a `substrate-node` binary, just run `substrate-node --dev --rpc-port 8000`.
- In our Python and Javascript files, we introduce a **dest** variable that represents the destination account for the transfer, we gave it a hard coded value (Bob's account public key) from the running Chopsticks node. Feel free to change it to any other account, or better yet, make it generic!
If you run into any issues running the Node version, I found that I needed to run `brew install python-setuptools` too.
## Building
### Build the Rust facade library
```shell
cargo build
```
This will produce a dynamic library in target/debug/ (or target/release/ if you pass --release):
- macOS: libsubxt_ffi.dylib
- Linux: libsubxt_ffi.so
- Windows: subxt_ffi.dll
## Running
### Python
#### on macOS / Linux
```shell
python3 src/main.py
```
Expected output:
✓ transfer succeeded
### Node.js
#### Install npm dependencies
In the root of the project run:
```shell
npm install
```
then:
``` shell
node src/main.js
```
Expected output:
✓ transfer succeeded
# Development notes
- Hex handling: We strip an optional 0x prefix and decode into 32 bytes.
- FFI safety: We only pass pointers and primitive types (u64, i32) across the boundary.
- Error codes: We return 0 on success, -1 on any kind of failure (decode error, RPC error, etc.).
- You can extend this facade crate with any additional functions you need—just expose them as pub extern "C" and follow the same pattern.
# Limitations
Translating a complex Rust API like Subxt to a bare bones C ABI ready to be consumed by foreign languages has its limitations. Here's a few of them:
- Complex types (strings, structs) require to design C-safe representations.
- Only C primitive types (integers, pointers) are FFI-safe; anything else must be translated.
- Manual memory management glue code is needed if owned data is returned.
- Needs a manual translation to every foreign language we export to, every time the Rust library changes.
+123
View File
@@ -0,0 +1,123 @@
{
"name": "subxt-ffi",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "subxt-ffi",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"ffi-napi": "^4.0.3"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/ffi-napi": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1",
"ref-napi": "^2.0.1 || ^3.0.2",
"ref-struct-di": "^1.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/get-symbol-from-current-process-h": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==",
"license": "MIT"
},
"node_modules/get-uv-event-loop-napi-h": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"license": "MIT",
"dependencies": {
"get-symbol-from-current-process-h": "^1.0.1"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
"license": "MIT"
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/ref-napi": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/ref-struct-di": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"license": "MIT",
"dependencies": {
"debug": "^3.1.0"
}
},
"node_modules/ref-struct-di/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "subxt-ffi",
"version": "1.0.0",
"description": "Example of exposing some Subxt functionality to other languages through FFI",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wassimans/subxt-ffi.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/wassimans/subxt-ffi/issues"
},
"homepage": "https://github.com/wassimans/subxt-ffi#readme",
"dependencies": {
"ffi-napi": "^4.0.3"
}
}
+74
View File
@@ -0,0 +1,74 @@
use hex::decode;
use std::{ffi::CStr, os::raw::c_char, sync::OnceLock};
use subxt::{OnlineClient, PolkadotConfig, dynamic::Value, ext::scale_value::Composite, tx};
use pezkuwi_subxt_signer::sr25519::dev;
use tokio::runtime::Runtime;
static TOKIO: OnceLock<Runtime> = OnceLock::new();
fn tokio_rt() -> &'static Runtime {
TOKIO.get_or_init(|| Runtime::new().expect("failed to start tokio"))
}
/// A simple CABI function to transfer `amount` to a hexencoded `dest`.
/// Assumes a running nodes WS endpoint is at ws://127.0.0.1:8000
#[unsafe(no_mangle)]
pub extern "C" fn do_transfer(dest_hex: *const c_char, amount: u64) -> i32 {
let amount = amount as u128;
// We need to convert C string to Rust str
let raw_s = unsafe { CStr::from_ptr(dest_hex).to_str().unwrap_or_default() };
// Strip optional 0x prefix
let s = raw_s.strip_prefix("0x").unwrap_or(raw_s);
// Decode hex, force a 32byte AccountId
let raw = decode(s).expect("hex decode");
let arr: [u8; 32] = raw.as_slice().try_into().expect("must be 32 bytes");
// Wrap into a MultiAddress::Id variant for dynamic calls:
let dst = Value::variant(
"Id",
Composite::unnamed(vec![
// scale encode
Value::from_bytes(arr.to_vec()),
]),
);
// Spin up (or reuse) our Tokio runtime and connect:
let client = tokio_rt().block_on(async {
OnlineClient::<PolkadotConfig>::from_url("ws://127.0.0.1:8000")
.await
.unwrap()
});
let signer = dev::alice();
// Build the dynamic metadata extrinsic:
let tx = tx::dynamic(
"Balances",
"transfer_keep_alive",
vec![
dst.clone(),
// primitive numeric value
Value::u128(amount),
],
);
// Submit and wait for finalize
let res: Result<(), subxt::Error> = tokio_rt().block_on(async {
let progress = client
.tx()
.sign_and_submit_then_watch_default(&tx, &signer)
.await?;
progress.wait_for_finalized_success().await?;
Ok(())
});
// Return code
match res {
Ok(_) => 0,
Err(e) => {
// print the Subxt error
eprintln!("do_transfer failed: {:#?}", e);
-1
}
}
}
+28
View File
@@ -0,0 +1,28 @@
const path = require("path");
const ffi = require("ffi-napi");
// Pick the correct library file name depending on the platform
const libPath = path.resolve(__dirname, "../target/debug", {
darwin: "libsubxt_ffi.dylib",
linux: "libsubxt_ffi.so",
win32: "subxt_ffi.dll"
}[process.platform]);
// Declare the FFI interface
const lib = ffi.Library(libPath, {
"do_transfer": ["int", ["string", "uint64"]]
});
function doTransfer(destHex, amount) {
const code = lib.do_transfer(destHex, amount);
if (code === 0) {
console.log("✓ transfer succeeded");
} else {
console.error("✗ transfer failed, code =", code);
}
}
// Example usage:
const dest = "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48";
const amount = 1_000_000_000_000; // fits in u64
doTransfer(dest, amount);
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import os, sys, ctypes, platform
from ctypes import c_char_p, c_uint64, c_int
# The library name depends on the playform type
if platform.system() == "Linux":
libname = "libsubxt_ffi.so"
elif platform.system() == "Darwin":
libname = "libsubxt_ffi.dylib"
elif platform.system() == "Windows":
libname = "subxt_ffi.dll"
else:
raise RuntimeError(f"Unsupported platform: {platform.system()}")
# Load the library
lib_path = os.path.join(os.path.dirname(__file__), "..", "target", "debug", libname)
lib = ctypes.CDLL(lib_path)
# Tell ctypes about our function signature, the one we defined in the Rust library
lib.do_transfer.argtypes = (c_char_p, c_uint64)
lib.do_transfer.restype = c_int
def do_transfer(dest_hex: str, amount: int) -> int:
"""
Perform a transfer.
dest_hex: hex-string of the 32-byte AccountId (e.g. "0x...")
amount: integer amount (fits in u64)
Returns 0 on success, 1 on error.
"""
# ensure we pass a C-string pointer
dest_bytes = dest_hex.encode("utf8")
return lib.do_transfer(dest_bytes, amount)
if __name__ == "__main__":
# example usage
dest = "0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
amt = 1_000_000_000_000
code = do_transfer(dest, amt)
if code == 0:
print("✓ transfer succeeded")
else:
print("✗ transfer failed")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
[package]
name = "parachain-example"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
subxt = { path = "../../subxt" }
subxt-signer = { path = "../../signer" }
tokio = { version = "1.44.2", features = ["macros", "time", "rt-multi-thread"] }
@@ -0,0 +1,78 @@
# parachain-example
This example showcases working with Subxt and Zombienet to try out connecting to a locally deployed parachain, here
["Statemint"](https://parachains.info/details/statemint), also known as "Asset Hub".
## Running the example
### 1. Install `zombienet`
[Zombienet](https://github.com/paritytech/zombienet) is a tool for quickly spinning up a (local) blockchain
network. We will use it to start up a local Asset Hub for us.
Please follow the install guide in the [zombienet github repo](https://github.com/paritytech/zombienet) to
install it.
### 2. `polkadot`
We need a relay chain. Build the polkadot binary from the [polkadot github repo](https://github.com/paritytech/polkadot)
and install it in your path:
```txt
git clone https://github.com/paritytech/polkadot.git
cd polkadot
cargo install --path .
```
### 3. `polkadot-parachain`
The Asset Hub is part of the [cumulus github repo](https://github.com/paritytech/cumulus), an SDK for developing
parachains. Building the cumulus workspace produces a binary called `polkadot-parachain` which can be used to run
Asset Hub nodes.
```txt
git clone https://github.com/paritytech/cumulus.git
cd cumulus
cargo install --path polkadot-parachain
```
### 4. Run the parachain locally
With these binaries installed, Zombienet can now get the parachain running locally from a configuration file, `asset-hub-zombienet.toml`
in this case. We need to have at least 2 validator nodes running via the `polkadot` binary, and an Asset Hub node running via the
`polkadot-parachain` binary. Zombienet starts these up, and gets the parachain registered with the validator nodes for us. To do that,
run:
```txt
zombienet -p native spawn asset-hub-zombienet.toml
```
Zombienet uses Kubernetes by default, but we can use it without Kubernetes by providing the `-p native` flag.
You might have noticed that we use `chain = "rococo-local"` in the `asset-hub-zombienet.toml` file for the relay chain. This is just to
make the epoch time shorter and should have no effect on your interactions with the parachain. Polkadot / Kusama / Rococo have different
epoch times of `24h` / `2h` / `2min` respectively.
### 5. Run the example
The parachain is only registered after the first epoch. So after the previous step, we need to wait 2 minutes until the parachain becomes
interactive and produces blocks. At this point, we can run:
```
cargo run --bin parachain-example
```
To run our example code.
## Dev notes
We can obtain the metadata for Statemint via the [subxt cli](https://crates.io/crates/subxt-cli) tool, like so:
```txt
subxt metadata --url wss://polkadot-asset-hub-rpc.polkadot.io:443 > statemint_metadata.scale
```
It is important to explicitly specify the port as `443`.
One way to find a suitable URL to obtain this from is by looking through the sidebar on [Polkadot.js](https://polkadot.js.org/apps/)
to find the Asset Hub entry, and seeing which RPC node URLs it uses.
@@ -0,0 +1,26 @@
[relaychain]
default_image = "docker.io/parity/polkadot:latest"
default_command = "polkadot"
default_args = ["-lparachain=debug"]
chain = "rococo-local"
[[relaychain.nodes]]
name = "alice"
validator = true
[[relaychain.nodes]]
name = "bob"
validator = true
[[parachains]]
id = 100
chain = "asset-hub-polkadot-local"
[parachains.collator]
name = "collator01"
image = "docker.io/parity/polkadot-parachain:latest"
ws_port = 42069
command = "polkadot-parachain"
args = ["-lparachain=debug"]
@@ -0,0 +1,80 @@
use subxt::{
PolkadotConfig,
utils::{AccountId32, MultiAddress},
OnlineClient,
};
use pezkuwi_subxt_signer::sr25519::dev::{self};
#[subxt::subxt(runtime_metadata_path = "statemint_metadata.scale")]
pub mod statemint {}
// PolkadotConfig or SubstrateConfig will suffice for this example at the moment,
// but PolkadotConfig is a little more correct, having the right `Address` type.
type StatemintConfig = PolkadotConfig;
#[tokio::main]
pub async fn main() {
if let Err(err) = run().await {
eprintln!("{err}");
}
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
// (the port 42069 is specified in the asset-hub-zombienet.toml)
let api = OnlineClient::<StatemintConfig>::from_url("ws://127.0.0.1:42069").await?;
println!("Connection with parachain established.");
let alice: MultiAddress<AccountId32, ()> = dev::alice().public_key().into();
let alice_pair_signer = dev::alice();
const COLLECTION_ID: u32 = 12;
const NTF_ID: u32 = 234;
// create a collection with id `12`
let collection_creation_tx = statemint::tx()
.uniques()
.create(COLLECTION_ID, alice.clone());
let _collection_creation_events = api
.tx()
.sign_and_submit_then_watch_default(&collection_creation_tx, &alice_pair_signer)
.await
.map(|e| {
println!("Collection creation submitted, waiting for transaction to be finalized...");
e
})?
.wait_for_finalized_success()
.await?;
println!("Collection created.");
// create an nft in that collection with id `234`
let nft_creation_tx = statemint::tx()
.uniques()
.mint(COLLECTION_ID, NTF_ID, alice.clone());
let _nft_creation_events = api
.tx()
.sign_and_submit_then_watch_default(&nft_creation_tx, &alice_pair_signer)
.await
.map(|e| {
println!("NFT creation submitted, waiting for transaction to be finalized...");
e
})?
.wait_for_finalized_success()
.await?;
println!("NFT created.");
// check in storage, that alice is the official owner of the NFT:
let nft_owner_storage_query = statemint::storage().uniques().asset();
let nft_storage_details = api
.storage()
.at_latest()
.await?
.fetch(nft_owner_storage_query, (COLLECTION_ID, NTF_ID))
.await?
.decode()?;
// make sure that alice is the owner of the NFT:
assert_eq!(nft_storage_details.owner, dev::alice().public_key().into());
println!("Storage Item Details: {:?}", nft_storage_details);
Ok(())
}
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)
}