mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-29 11:27:58 +00:00
Browser extension signing example (#1067)
* routing and signing example * cliipy fix * submitting extrinsics * change order of lines * Skip call variants if there aren't any (#980) Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com> * Tidy up some metadata accessing (#978) * Reduce some repetition when obtaining metadata pallets/runtime_traits * make them pub * fix docs and clippy * Bump tokio from 1.28.1 to 1.28.2 (#984) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.28.1 to 1.28.2. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.28.1...tokio-1.28.2) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump regex from 1.8.2 to 1.8.3 (#986) Bumps [regex](https://github.com/rust-lang/regex) from 1.8.2 to 1.8.3. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.8.2...1.8.3) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump quote from 1.0.27 to 1.0.28 (#983) Bumps [quote](https://github.com/dtolnay/quote) from 1.0.27 to 1.0.28. - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.27...1.0.28) --- updated-dependencies: - dependency-name: quote dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump proc-macro2 from 1.0.58 to 1.0.59 (#985) Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.58 to 1.0.59. - [Release notes](https://github.com/dtolnay/proc-macro2/releases) - [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.58...1.0.59) --- updated-dependencies: - dependency-name: proc-macro2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * restrict sign_with_address_and_signature interface (#988) * changing js bridge * dryrunresult ok * submitting extrinsic working * tiny up code and ui * formatting * remove todos * support tip and mortality * Prevent bug when reusing type ids in hashing (#1075) * practice TDD * implement a hashmap 2-phases approach * use nicer types * add test for cache filling * adjust test --------- Co-authored-by: James Wilson <james@jsdw.me> * small adjustment * Merge branch 'master' into tadeo-hepperle-browser-extension-signing-example * fix lock file * tell users how to add Alice account to run signing example * adjust to PR comments * fmt --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: James Wilson <james@jsdw.me> Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,140 +1,59 @@
|
||||
use futures::{self, FutureExt};
|
||||
|
||||
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::<SubxtExamplesComponent>::new().render();
|
||||
yew::Renderer::<SubxtExamplesApp>::new().render();
|
||||
}
|
||||
|
||||
struct SubxtExamplesComponent {
|
||||
operation_title: Option<AttrValue>,
|
||||
lines: Vec<AttrValue>,
|
||||
}
|
||||
struct SubxtExamplesApp;
|
||||
|
||||
enum Message {
|
||||
Error(subxt::Error),
|
||||
Reload,
|
||||
Line(AttrValue),
|
||||
Lines(Vec<AttrValue>),
|
||||
ButtonClick(Button),
|
||||
}
|
||||
impl Component for SubxtExamplesApp {
|
||||
type Message = ();
|
||||
|
||||
enum Button {
|
||||
SubscribeFinalized,
|
||||
FetchConstant,
|
||||
FetchEvents,
|
||||
}
|
||||
|
||||
impl Component for SubxtExamplesComponent {
|
||||
type Message = Message;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
SubxtExamplesComponent {
|
||||
lines: Vec::new(),
|
||||
operation_title: None,
|
||||
}
|
||||
SubxtExamplesApp
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Message::Error(err) => {
|
||||
self.lines.push(err.to_string().into());
|
||||
}
|
||||
Message::Reload => {
|
||||
let window = web_sys::window().expect("Failed to access the window object");
|
||||
window
|
||||
.location()
|
||||
.reload()
|
||||
.expect("Failed to reload the page");
|
||||
}
|
||||
Message::Line(line) => {
|
||||
self.lines.push(line);
|
||||
}
|
||||
Message::Lines(mut lines) => {
|
||||
self.lines.append(&mut lines);
|
||||
}
|
||||
Message::ButtonClick(button) => match button {
|
||||
Button::SubscribeFinalized => {
|
||||
self.operation_title = Some("Subscribe to finalized blocks:".into());
|
||||
let cb: Callback<AttrValue> = ctx.link().callback(Message::Line);
|
||||
ctx.link()
|
||||
.send_future(services::subscribe_to_finalized_blocks(cb).map(|result| {
|
||||
let err = result.unwrap_err();
|
||||
Message::Error(err)
|
||||
}));
|
||||
}
|
||||
Button::FetchConstant => {
|
||||
self.operation_title =
|
||||
Some("Fetch the constant \"block_length\" of \"System\" pallet:".into());
|
||||
ctx.link()
|
||||
.send_future(services::fetch_constant_block_length().map(|result| {
|
||||
match result {
|
||||
Ok(value) => Message::Line(
|
||||
format!(
|
||||
"constant \"block_length\" of \"System\" pallet:\n {value}"
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
Err(err) => Message::Error(err),
|
||||
}
|
||||
}))
|
||||
}
|
||||
Button::FetchEvents => {
|
||||
self.operation_title = Some("Fetch events:".into());
|
||||
ctx.link()
|
||||
.send_future(services::fetch_events_dynamically().map(
|
||||
|result| match result {
|
||||
Ok(value) => {
|
||||
Message::Lines(value.into_iter().map(AttrValue::from).collect())
|
||||
}
|
||||
Err(err) => Message::Error(err),
|
||||
},
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let reload: Callback<MouseEvent> = ctx.link().callback(|_| Message::Reload);
|
||||
|
||||
let subscribe_finalized = ctx
|
||||
.link()
|
||||
.callback(|_| Message::ButtonClick(Button::SubscribeFinalized));
|
||||
|
||||
let fetch_constant = ctx
|
||||
.link()
|
||||
.callback(|_| Message::ButtonClick(Button::FetchConstant));
|
||||
|
||||
let fetch_events = ctx
|
||||
.link()
|
||||
.callback(|_| Message::ButtonClick(Button::FetchEvents));
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
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{
|
||||
<>
|
||||
<h1>{"Subxt Examples"}</h1>
|
||||
<button onclick={subscribe_finalized}>{"Example: Subscribe to Finalized blocks"}</button>
|
||||
<button onclick={fetch_constant}>{"Example: Fetch constant value"}</button>
|
||||
<button onclick={fetch_events}>{"Example: Fetch events"}</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<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,391 @@
|
||||
use anyhow::anyhow;
|
||||
use futures::FutureExt;
|
||||
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
use subxt::ext::codec::{Decode, Encode};
|
||||
use subxt::tx::SubmittableExtrinsic;
|
||||
use subxt::tx::TxPayload;
|
||||
use subxt::utils::{AccountId32, MultiSignature};
|
||||
|
||||
use crate::services::{extension_signature_for_partial_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: SubmittableExtrinsic<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,
|
||||
SubmittableExtrinsic<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 partial_extrinsic =
|
||||
match api.tx().create_partial_signed(&remark_call, &account_id, Default::default()).await {
|
||||
Ok(partial_extrinsic) => partial_extrinsic,
|
||||
Err(err) => {
|
||||
return Message::Error(anyhow!("could not create partial extrinsic:\n{:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(signature) = extension_signature_for_partial_extrinsic(&partial_extrinsic, &api, &account_id, 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 signed_extrinsic = partial_extrinsic.sign_with_address_and_signature(&account_id.into(), &multi_signature);
|
||||
|
||||
// do a dry run (to debug in the js console if the extrinsic would work)
|
||||
let dry_res = signed_extrinsic.dry_run(None).await;
|
||||
web_sys::console::log_1(&format!("Dry Run 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: SubmittableExtrinsic<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"))
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
use anyhow::anyhow;
|
||||
use futures::StreamExt;
|
||||
use js_sys::Promise;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::fmt::Write;
|
||||
use subxt::ext::codec::Encode;
|
||||
use subxt::tx::PartialExtrinsic;
|
||||
use subxt::{self, OnlineClient, PolkadotConfig};
|
||||
use subxt::utils::AccountId32;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use yew::{AttrValue, Callback};
|
||||
|
||||
#[subxt::subxt(runtime_metadata_path = "../../artifacts/polkadot_metadata_small.scale")]
|
||||
mod polkadot {}
|
||||
pub mod polkadot {}
|
||||
|
||||
pub(crate) async fn fetch_constant_block_length() -> Result<String, subxt::Error> {
|
||||
let api = OnlineClient::<PolkadotConfig>::new().await?;
|
||||
@@ -71,3 +80,105 @@ pub(crate) async fn subscribe_to_finalized_blocks(
|
||||
}
|
||||
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_to_hex<E: Encode>(input: &E) -> String {
|
||||
format!("0x{}", hex::encode(input.encode()))
|
||||
}
|
||||
|
||||
/// this is used because numeric types (e.g. u32) are encoded as little-endian via scale (e.g. 9430 -> d6240000)
|
||||
/// while we need a big-endian representation for the json (e.g. 9430 -> 000024d6).
|
||||
fn encode_to_hex_reverse<E: Encode>(input: &E) -> String {
|
||||
let mut bytes = input.encode();
|
||||
bytes.reverse();
|
||||
format!("0x{}", hex::encode(bytes))
|
||||
}
|
||||
|
||||
|
||||
/// 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_partial_extrinsic(
|
||||
partial_extrinsic: &PartialExtrinsic<PolkadotConfig, OnlineClient<PolkadotConfig>>,
|
||||
api: &OnlineClient<PolkadotConfig>,
|
||||
account_id: &AccountId32,
|
||||
account_source: String,
|
||||
account_address: String,
|
||||
) -> Result<Vec<u8>, anyhow::Error> {
|
||||
let spec_version = encode_to_hex_reverse(&api.runtime_version().spec_version);
|
||||
let transaction_version = encode_to_hex_reverse(&api.runtime_version().transaction_version);
|
||||
let mortality_checkpoint = encode_to_hex(&api.genesis_hash());
|
||||
let era = encode_to_hex(&subxt::config::extrinsic_params::Era::Immortal);
|
||||
let genesis_hash = encode_to_hex(&api.genesis_hash());
|
||||
let method = to_hex(partial_extrinsic.call_data());
|
||||
let nonce = api.tx().account_nonce(account_id).await?;
|
||||
let nonce = encode_to_hex_reverse(&nonce);
|
||||
let signed_extensions: Vec<String> = api
|
||||
.metadata()
|
||||
.extrinsic()
|
||||
.signed_extensions()
|
||||
.iter()
|
||||
.map(|e| e.identifier().to_string())
|
||||
.collect();
|
||||
let tip = encode_to_hex(&subxt::config::polkadot::PlainTip::new(0));
|
||||
|
||||
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