fix: Convert vendor/pezkuwi-subxt from submodule to regular directory
This commit is contained in:
+5
@@ -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
|
||||
+3803
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 Subxt’s 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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 C‐ABI function to transfer `amount` to a hex‐encoded `dest`.
|
||||
/// Assumes a running node’s 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 32‐byte 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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")
|
||||
+3809
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(())
|
||||
}
|
||||
+3390
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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`";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user