Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wallet integration #371

Merged
merged 26 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fd962e2
feat(wallet-integration): server and API (#362)
peterwht Dec 9, 2024
a0a8397
feat(wallet integration): pop up contract (#365)
peterwht Dec 16, 2024
074e2f9
feat: wallet integration pop call chain (#379)
AlexD10S Dec 16, 2024
6b13134
Test wallet integration (#383)
peterwht Dec 16, 2024
7227abc
test(wallet-integration): remove sign_call_data test (#384)
al3mart Dec 17, 2024
89df5f6
chore: remove hex crate and use alternate means with test
peterwht Dec 17, 2024
ddb8ba6
chore: minor changes to comments, spinners, and deps
peterwht Dec 17, 2024
6035fd8
refactor: shared prompt, determine_signing_method
peterwht Dec 17, 2024
58c7ca6
refactor: use extrinsic instead of transaction
peterwht Dec 17, 2024
30e5c82
chore: latest frontend
peterwht Dec 17, 2024
b884f9d
feat: send remark on contracts-node launch
peterwht Dec 17, 2024
a4e9da2
chore: use cargo-contract crate release
peterwht Dec 17, 2024
0e19b8a
feat(wallet): choose random port for server
peterwht Dec 17, 2024
b2a6dcb
feat(wallet): auto-open browser
peterwht Dec 17, 2024
8d14f8c
chore: merge main
peterwht Dec 17, 2024
72b404c
refactor: find_free_port has preferred port
peterwht Dec 17, 2024
ca1e622
fix: conflicting tests merged to one
peterwht Dec 18, 2024
ecb8eaa
fix(wallet): port selection, update frontend
peterwht Dec 18, 2024
5e12dea
feat: update frontend with warning
peterwht Dec 18, 2024
4663b5e
[CI test] test: include instantiate_call_data_works (#389)
al3mart Dec 18, 2024
b757ac1
refactor: review fixes and improvements (#390)
evilrobot-01 Dec 18, 2024
c479866
chore: use latest frontend
peterwht Dec 18, 2024
b368da9
chore: latest frontend
peterwht Dec 18, 2024
f5c963e
chore: update frontend to remove trailing slash
peterwht Dec 18, 2024
52fdf6b
chore: improve uploading / calling contract message to wait for final…
peterwht Dec 18, 2024
ae7b3a2
test: improve `crates/pop-cli/src/common/contracts.rs` coverage (#391)
al3mart Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
649 changes: 406 additions & 243 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ subxt = "0.38.0"
ink_env = "5.0.0"
sp-core = "32.0.0"
sp-weights = "31.0.0"
contract-build = "5.0.0"
contract-extrinsics = "5.0.0"
contract-transcode = "5.0.0"
scale-info = { version = "2.11.4", default-features = false, features = ["derive"] }
scale-value = { version = "0.17.0", default-features = false, features = ["from-string", "parser-ss58"] }
contract-build = "5.0.2"
contract-extrinsics = "5.0.2"
contract-transcode = "5.0.2"
heck = "0.5.0"
hex = { version = "0.4.3", default-features = false }

# parachains
askama = "0.12"
Expand All @@ -77,3 +76,8 @@ console = "0.15"
os_info = { version = "3", default-features = false }
strum = "0.26"
strum_macros = "0.26"

# wallet-integration
axum = "0.7.9"
open = "5.3.1"
tower-http = "0.6.2"
14 changes: 12 additions & 2 deletions crates/pop-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ duct.workspace = true
env_logger.workspace = true
os_info.workspace = true
reqwest.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tempfile.workspace = true
tokio.workspace = true
Expand All @@ -27,12 +28,12 @@ url.workspace = true
clap.workspace = true
cliclack.workspace = true
console.workspace = true
sp-core.workspace = true
strum.workspace = true
strum_macros.workspace = true

# contracts
pop-contracts = { path = "../pop-contracts", version = "0.5.0", optional = true }
sp-core = { workspace = true, optional = true }
sp-weights = { workspace = true, optional = true }

# parachains
Expand All @@ -46,12 +47,21 @@ pop-telemetry = { path = "../pop-telemetry", version = "0.5.0", optional = true
# common
pop-common = { path = "../pop-common", version = "0.5.0" }

# wallet-integration
axum.workspace = true
open.workspace = true
tower-http = { workspace = true, features = ["fs", "cors"] }

[dev-dependencies]
assert_cmd.workspace = true
contract-extrinsics.workspace = true
predicates.workspace = true
subxt.workspace = true
subxt-signer.workspace = true
sp-weights.workspace = true

[features]
default = ["contract", "parachain", "telemetry"]
contract = ["dep:pop-contracts", "dep:sp-core", "dep:sp-weights", "dep:dirs"]
contract = ["dep:pop-contracts", "dep:sp-weights", "dep:dirs"]
parachain = ["dep:pop-parachains", "dep:dirs"]
telemetry = ["dep:pop-telemetry"]
1,024 changes: 1,024 additions & 0 deletions crates/pop-cli/src/assets/index.html

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion crates/pop-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ impl traits::Confirm for Confirm {

/// A input prompt using cliclack.
struct Input(cliclack::Input);

impl traits::Input for Input {
/// Sets the default value for the input.
fn default_input(mut self, value: &str) -> Self {
Expand Down
128 changes: 106 additions & 22 deletions crates/pop-cli/src/commands/call/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

use std::path::Path;

use crate::cli::{self, traits::*};
use crate::{
cli::{self, traits::*},
common::wallet::{prompt_to_use_wallet, request_signature},
};
use anyhow::{anyhow, Result};
use clap::Args;
use pop_parachains::{
construct_extrinsic, construct_sudo_extrinsic, decode_call_data, encode_call_data,
find_dispatchable_by_name, find_pallet_by_name, parse_chain_metadata, set_up_client,
sign_and_submit_extrinsic, supported_actions, Action, CallData, DynamicPayload, Function,
OnlineClient, Pallet, Param, SubstrateConfig,
sign_and_submit_extrinsic, submit_signed_extrinsic, supported_actions, Action, CallData,
DynamicPayload, Function, OnlineClient, Pallet, Param, Payload, SubstrateConfig,
};
use url::Url;

Expand Down Expand Up @@ -40,6 +43,9 @@ pub struct CallChainCommand {
/// - with a password "//Alice///SECRET_PASSWORD"
#[arg(short, long)]
suri: Option<String>,
/// Use a browser extension wallet to sign the extrinsic.
#[arg(name = "use-wallet", short('w'), long, default_value = "false", conflicts_with = "suri")]
use_wallet: bool,
/// SCALE encoded bytes representing the call data of the extrinsic.
#[arg(name = "call", short, long, conflicts_with_all = ["pallet", "function", "args"])]
call_data: Option<String>,
Expand Down Expand Up @@ -95,7 +101,14 @@ impl CallChainCommand {
};

// Sign and submit the extrinsic.
if let Err(e) = call.submit_extrinsic(&chain.client, &chain.url, xt, &mut cli).await {
let result = if self.use_wallet {
let call_data = xt.encode_call_data(&chain.client.metadata())?;
submit_extrinsic_with_wallet(&chain.client, &chain.url, call_data, &mut cli).await
} else {
call.submit_extrinsic(&chain.client, &chain.url, xt, &mut cli).await
};

if let Err(e) = result {
display_message(&e.to_string(), false, &mut cli)?;
break;
}
Expand Down Expand Up @@ -197,19 +210,16 @@ impl CallChainCommand {
// sudo.
self.configure_sudo(chain, cli)?;

// Resolve who is signing the extrinsic.
let suri = match self.suri.as_ref() {
Some(suri) => suri.clone(),
None =>
cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()?,
};
let (use_wallet, suri) = self.determine_signing_method(cli)?;
self.use_wallet = use_wallet;

return Ok(Call {
function: function.clone(),
args,
suri,
skip_confirm: self.skip_confirm,
sudo: self.sudo,
use_wallet: self.use_wallet,
});
}
}
Expand All @@ -222,11 +232,18 @@ impl CallChainCommand {
call_data: &str,
cli: &mut impl Cli,
) -> Result<()> {
// Resolve who is signing the extrinsic.
let suri = match self.suri.as_ref() {
Some(suri) => suri,
None => &cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()?,
};
let (use_wallet, suri) = self.determine_signing_method(cli)?;

// Perform signing steps with wallet integration and return early.
if use_wallet {
let call_data_bytes =
decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?;
submit_extrinsic_with_wallet(client, url, call_data_bytes, cli)
.await
.map_err(|err| anyhow!("{}", format!("{err:?}")))?;
display_message("Call complete.", true, cli)?;
return Ok(());
}
cli.info(format!("Encoded call data: {}", call_data))?;
if !self.skip_confirm &&
!cli.confirm("Do you want to submit the extrinsic?")
Expand All @@ -244,7 +261,7 @@ impl CallChainCommand {
spinner.start("Signing and submitting the extrinsic and then waiting for finalization, please be patient...");
let call_data_bytes =
decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?;
let result = sign_and_submit_extrinsic(client, url, CallData::new(call_data_bytes), suri)
let result = sign_and_submit_extrinsic(client, url, CallData::new(call_data_bytes), &suri)
.await
.map_err(|err| anyhow!("{}", format!("{err:?}")))?;

Expand All @@ -253,6 +270,29 @@ impl CallChainCommand {
Ok(())
}

// Resolve who is signing the extrinsic. If a `suri` was provided via the command line,
// skip the prompt.
fn determine_signing_method(&self, cli: &mut impl Cli) -> Result<(bool, String)> {
let mut use_wallet = self.use_wallet;
let suri = match self.suri.as_ref() {
Some(suri) => suri.clone(),
None =>
if !self.use_wallet {
if prompt_to_use_wallet(cli)? {
use_wallet = true;
DEFAULT_URI.to_string()
} else {
cli.input("Signer of the extrinsic:")
.default_input(DEFAULT_URI)
.interact()?
}
} else {
DEFAULT_URI.to_string()
},
};
Ok((use_wallet, suri))
}

// Checks if the chain has the Sudo pallet and prompts the user to confirm if they want to
// execute the call via `sudo`.
fn configure_sudo(&mut self, chain: &Chain, cli: &mut impl Cli) -> Result<()> {
Expand Down Expand Up @@ -283,6 +323,7 @@ impl CallChainCommand {
self.function = None;
self.args.clear();
self.sudo = false;
self.use_wallet = false;
}

// Function to check if all required fields are specified.
Expand Down Expand Up @@ -334,6 +375,8 @@ struct Call {
/// - for a dev account "//Alice"
/// - with a password "//Alice///SECRET_PASSWORD"
suri: String,
/// Whether to use your browser wallet to sign the extrinsic.
use_wallet: bool,
/// Whether to automatically sign and submit the extrinsic without prompting for confirmation.
skip_confirm: bool,
/// Whether to dispatch the function call with `Root` origin.
Expand Down Expand Up @@ -411,14 +454,45 @@ impl Call {
.collect();
full_message.push_str(&format!(" --args {}", args.join(" ")));
}
full_message.push_str(&format!(" --url {} --suri {}", chain.url, self.suri));
full_message.push_str(&format!(" --url {}", chain.url));
if self.use_wallet {
full_message.push_str(" --use-wallet");
} else {
full_message.push_str(&format!(" --suri {}", self.suri));
}
if self.sudo {
full_message.push_str(" --sudo");
}
full_message
}
}

// Sign and submit an extrinsic using wallet integration.
async fn submit_extrinsic_with_wallet(
client: &OnlineClient<SubstrateConfig>,
url: &Url,
call_data: Vec<u8>,
cli: &mut impl Cli,
) -> Result<()> {
let maybe_payload = request_signature(call_data, url.to_string()).await?;
if let Some(payload) = maybe_payload {
cli.success("Signed payload received.")?;
let spinner = cliclack::spinner();
spinner.start(
"Submitting the extrinsic and then waiting for finalization, please be patient...",
);

let result = submit_signed_extrinsic(client.clone(), payload)
.await
.map_err(|err| anyhow!("{}", format!("{err:?}")))?;

spinner.stop(format!("Extrinsic submitted with hash: {:?}", result));
} else {
display_message("No signed payload received.", false, cli)?;
}
Ok(())
}

// Displays a message to the user, with formatting based on the success status.
fn display_message(message: &str, success: bool, cli: &mut impl Cli) -> Result<()> {
if success {
Expand Down Expand Up @@ -589,7 +663,7 @@ fn parse_function_name(name: &str) -> Result<String, String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::MockCli;
use crate::{cli::MockCli, common::wallet::USE_WALLET_PROMPT};
use tempfile::tempdir;
use url::Url;

Expand Down Expand Up @@ -642,7 +716,7 @@ mod tests {
)
.expect_input("The value for `remark` might be too large to enter. You may enter the path to a file instead.", "0x11".into())
.expect_confirm("Would you like to dispatch this function call with `Root` origin?", true)
.expect_input("Signer of the extrinsic:", "//Bob".into());
.expect_confirm(USE_WALLET_PROMPT, true);

let chain = call_config.configure_chain(&mut cli).await?;
assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?);
Expand All @@ -651,9 +725,10 @@ mod tests {
assert_eq!(call_chain.function.pallet, "System");
assert_eq!(call_chain.function.name, "remark");
assert_eq!(call_chain.args, ["0x11".to_string()].to_vec());
assert_eq!(call_chain.suri, "//Bob");
assert_eq!(call_chain.suri, "//Alice"); // Default value
assert!(call_chain.use_wallet);
assert!(call_chain.sudo);
assert_eq!(call_chain.display(&chain), "pop call chain --pallet System --function remark --args \"0x11\" --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Bob --sudo");
assert_eq!(call_chain.display(&chain), "pop call chain --pallet System --function remark --args \"0x11\" --url wss://rpc1.paseo.popnetwork.xyz/ --use-wallet --sudo");
cli.verify()
}

Expand Down Expand Up @@ -714,6 +789,7 @@ mod tests {
},
args: vec!["0x11".to_string()].to_vec(),
suri: DEFAULT_URI.to_string(),
use_wallet: false,
skip_confirm: false,
sudo: false,
};
Expand Down Expand Up @@ -753,6 +829,7 @@ mod tests {
function: find_dispatchable_by_name(&pallets, "System", "remark")?.clone(),
args: vec!["0x11".to_string()].to_vec(),
suri: DEFAULT_URI.to_string(),
use_wallet: false,
skip_confirm: false,
sudo: false,
};
Expand All @@ -776,11 +853,13 @@ mod tests {
args: vec![].to_vec(),
url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?),
suri: None,
use_wallet: false,
skip_confirm: false,
call_data: Some("0x00000411".to_string()),
sudo: false,
};
let mut cli = MockCli::new()
.expect_confirm(USE_WALLET_PROMPT, false)
.expect_input("Signer of the extrinsic:", "//Bob".into())
.expect_confirm("Do you want to submit the extrinsic?", false)
.expect_outro_cancel("Extrinsic with call data 0x00000411 was not submitted.");
Expand All @@ -803,8 +882,9 @@ mod tests {
pallet: None,
function: None,
args: vec![].to_vec(),
url: Some(Url::parse("wss://polkadot-rpc.publicnode.com")?),
url: Some(Url::parse(POLKADOT_NETWORK_URL)?),
suri: Some("//Alice".to_string()),
use_wallet: false,
skip_confirm: false,
call_data: Some("0x00000411".to_string()),
sudo: true,
Expand Down Expand Up @@ -836,6 +916,7 @@ mod tests {
function: Some("remark".to_string()),
args: vec!["0x11".to_string()].to_vec(),
url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?),
use_wallet: true,
suri: Some(DEFAULT_URI.to_string()),
skip_confirm: false,
call_data: None,
Expand All @@ -846,6 +927,7 @@ mod tests {
assert_eq!(call_config.function, None);
assert_eq!(call_config.args.len(), 0);
assert!(!call_config.sudo);
assert!(!call_config.use_wallet);
Ok(())
}

Expand All @@ -857,6 +939,7 @@ mod tests {
args: vec!["0x11".to_string()].to_vec(),
url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?),
suri: Some(DEFAULT_URI.to_string()),
use_wallet: false,
skip_confirm: false,
call_data: None,
sudo: false,
Expand All @@ -875,6 +958,7 @@ mod tests {
args: vec!["2000".to_string(), "0x1".to_string(), "0x12".to_string()].to_vec(),
url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?),
suri: Some(DEFAULT_URI.to_string()),
use_wallet: false,
call_data: None,
skip_confirm: false,
sudo: false,
Expand Down
Loading
Loading