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:
Tadeo Hepperle
2023-08-02 14:56:41 +02:00
committed by GitHub
parent 2176ec9fa7
commit dc0aeac3d6
14 changed files with 1134 additions and 424 deletions
@@ -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>
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod fetching;
pub mod signing;
+391
View File
@@ -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"))
}