diff --git a/Cargo.lock b/Cargo.lock index 901e0e10f..9887bd521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -966,7 +966,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.1", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -2004,8 +2004,7 @@ checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" [[package]] name = "contract-build" version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857769855bf40d230e41baf6575cc44bd5e6869f69f88f45f6791da793f49a0c" +source = "git+https://github.com/use-ink/cargo-contract?branch=peter/chore-make-types-pub#7c8fc481912d7a6f416a0f72e37840123064f90d" dependencies = [ "anyhow", "blake2", @@ -2045,8 +2044,7 @@ dependencies = [ [[package]] name = "contract-extrinsics" version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77ad38bef6454f97ca33481e960e9b105cb97f3794656071cbf50229445a89d" +source = "git+https://github.com/use-ink/cargo-contract?branch=peter/chore-make-types-pub#7c8fc481912d7a6f416a0f72e37840123064f90d" dependencies = [ "anyhow", "blake2", @@ -2078,8 +2076,7 @@ dependencies = [ [[package]] name = "contract-metadata" version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733a6624ea05dd71050641c3cd9baff7a1445032a0082f0e55c800c078716424" +source = "git+https://github.com/use-ink/cargo-contract?branch=peter/chore-make-types-pub#7c8fc481912d7a6f416a0f72e37840123064f90d" dependencies = [ "anyhow", "impl-serde 0.5.0", @@ -2092,8 +2089,7 @@ dependencies = [ [[package]] name = "contract-transcode" version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9131028be7b8eefdd9151a0a682ed428c1418d5d1ec142c35d2dbe6b9653c6" +source = "git+https://github.com/use-ink/cargo-contract?branch=peter/chore-make-types-pub#7c8fc481912d7a6f416a0f72e37840123064f90d" dependencies = [ "anyhow", "base58", @@ -9339,6 +9335,8 @@ dependencies = [ "sp-weights", "strum 0.26.3", "strum_macros 0.26.4", + "subxt", + "subxt-signer", "tempfile", "tokio", "tower-http 0.6.2", @@ -9398,6 +9396,8 @@ dependencies = [ "sp-weights", "strum 0.26.3", "strum_macros 0.26.4", + "subxt", + "subxt-signer", "tar", "tempfile", "thiserror 1.0.69", @@ -13815,14 +13815,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", diff --git a/Cargo.toml b/Cargo.toml index c84ce914d..3effb7c2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,11 +49,15 @@ 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"] } +# TODO: git deps +#contract-build = "5.0.0-alpha" +contract-build = { git = "https://github.com/use-ink/cargo-contract", branch = "peter/chore-make-types-pub" } +#contract-extrinsics = "5.0.0-alpha" +contract-extrinsics = { git = "https://github.com/use-ink/cargo-contract", branch = "peter/chore-make-types-pub" } +#contract-transcode = "5.0.0" +contract-transcode = { git = "https://github.com/use-ink/cargo-contract", branch = "peter/chore-make-types-pub" } heck = "0.5.0" hex = { version = "0.4.3", default-features = false } diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index 4f3ab59ab..6d2d6ef42 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -49,11 +49,13 @@ pop-common = { path = "../pop-common", version = "0.5.0" } # wallet-integration axum.workspace = true -tower-http = { workspace = true, features = ["fs"] } +tower-http = { workspace = true, features = ["fs", "cors"] } [dev-dependencies] assert_cmd.workspace = true predicates.workspace = true +subxt.workspace = true +subxt-signer.workspace = true [features] default = ["contract", "parachain", "telemetry"] diff --git a/crates/pop-cli/src/assets/index.html b/crates/pop-cli/src/assets/index.html new file mode 100644 index 000000000..d5659fbd8 --- /dev/null +++ b/crates/pop-cli/src/assets/index.html @@ -0,0 +1,134 @@ + + + + + + + + Pop CLI Signing Portal + + + + + +
+ + + diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index 964237642..dfb95380e 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -4,14 +4,17 @@ use crate::{ cli::{traits::Cli as _, Cli}, common::contracts::{check_contracts_node_and_prompt, has_contract_been_built}, style::style, + wallet_integration::{FrontendFromString, TransactionData, WalletIntegrationManager}, }; use clap::Args; -use cliclack::{confirm, log, log::error, spinner}; +use cliclack::{confirm, log, log::error, spinner, ProgressBar}; use console::{Emoji, Style}; use pop_contracts::{ build_smart_contract, dry_run_gas_estimate_instantiate, dry_run_upload, - instantiate_smart_contract, is_chain_alive, parse_hex_bytes, run_contracts_node, - set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, Verbosity, + get_code_hash_from_event, get_contract_code, get_instantiate_payload, get_upload_payload, + instantiate_contract_signed, instantiate_smart_contract, is_chain_alive, parse_hex_bytes, + run_contracts_node, set_up_deployment, set_up_upload, upload_contract_signed, + upload_smart_contract, UpOpts, Verbosity, }; use sp_core::Bytes; use sp_weights::Weight; @@ -64,6 +67,15 @@ pub struct UpContractCommand { /// - with a password "//Alice///SECRET_PASSWORD" #[clap(name = "suri", long, short, default_value = "//Alice")] suri: String, + /// Use your browser wallet to sign a transaction. + #[clap( + name = "use-wallet", + long, + default_value = "false", + short('w'), + conflicts_with = "suri" + )] + use_wallet: bool, /// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction. #[clap(long)] dry_run: bool, @@ -164,6 +176,76 @@ impl UpContractCommand { None }; + // Run steps for signing with wallet integration. Returns early. + if self.use_wallet { + let (call_data, hash) = match self.get_contract_data().await { + Ok(data) => data, + Err(e) => { + error(format!("An error occurred getting the call data: {e}"))?; + Self::terminate_node(process)?; + Cli.outro_cancel(FAILED)?; + return Ok(()); + }, + }; + + let maybe_payload = self.wait_for_signature(call_data).await?; + if let Some(payload) = maybe_payload { + log::success("Signed payload received.")?; + let spinner = spinner(); + spinner.start("Uploading contract..."); + + if self.upload_only { + let result = upload_contract_signed(self.url.as_str(), payload).await; + // TODO: dry (see else below) + if let Err(e) = result { + spinner.error(format!("An error occurred uploading your contract: {e}")); + Self::terminate_node(process)?; + Cli.outro_cancel(FAILED)?; + return Ok(()); + } + let upload_result = result.expect("Error check above."); + + match get_code_hash_from_event(&upload_result, hash) { + Ok(r) => { + spinner.stop(format!("Contract uploaded: The code hash is {:?}", r)); + }, + Err(e) => { + spinner + .error(format!("An error occurred uploading your contract: {e}")); + }, + }; + } else { + let result = instantiate_contract_signed(self.url.as_str(), payload).await; + if let Err(e) = result { + spinner.error(format!("An error occurred uploading your contract: {e}")); + Self::terminate_node(process)?; + Cli.outro_cancel(FAILED)?; + return Ok(()); + } + + let contract_info = result.unwrap(); + let hash = contract_info.code_hash.map(|code_hash| format!("{:?}", code_hash)); + display_contract_info( + &spinner, + contract_info.contract_address.to_string(), + hash, + ); + }; + + if self.upload_only { + log::warning("NOTE: The contract has not been instantiated.")?; + } + } else { + Cli.outro_cancel("Signed payload doesn't exist.")?; + Self::terminate_node(process)?; + return Ok(()); + } + + Self::terminate_node(process)?; + Cli.outro(COMPLETE)?; + return Ok(()) + } + // Check for upload only. if self.upload_only { let result = self.upload_contract().await; @@ -180,19 +262,7 @@ impl UpContractCommand { } // Otherwise instantiate. - let instantiate_exec = match set_up_deployment(UpOpts { - path: self.path.clone(), - constructor: self.constructor.clone(), - args: self.args.clone(), - value: self.value.clone(), - gas_limit: self.gas_limit, - proof_size: self.proof_size, - salt: self.salt.clone(), - url: self.url.clone(), - suri: self.suri.clone(), - }) - .await - { + let instantiate_exec = match set_up_deployment(self.clone().into()).await { Ok(i) => i, Err(e) => { error(format!("An error occurred instantiating the contract: {e}"))?; @@ -226,29 +296,12 @@ impl UpContractCommand { let spinner = spinner(); spinner.start("Uploading and instantiating the contract..."); let contract_info = instantiate_smart_contract(instantiate_exec, weight_limit).await?; - spinner.stop(format!( - "Contract deployed and instantiated:\n{}", - style(format!( - "{}\n{}", - style(format!( - "{} The contract address is {:?}", - console::Emoji("●", ">"), - contract_info.address - )) - .dim(), - contract_info - .code_hash - .map(|hash| style(format!( - "{} The contract code hash is {:?}", - console::Emoji("●", ">"), - hash - )) - .dim() - .to_string()) - .unwrap_or_default(), - )) - .dim() - )); + display_contract_info( + &spinner, + contract_info.address.to_string(), + contract_info.code_hash, + ); + Self::terminate_node(process)?; Cli.outro(COMPLETE)?; } @@ -313,6 +366,54 @@ impl UpContractCommand { Ok(()) } + + // get the call data and contract code hash + async fn get_contract_data(&self) -> anyhow::Result<(Vec, [u8; 32])> { + let contract_code = get_contract_code(self.path.as_ref()).await?; + let hash = contract_code.code_hash(); + if self.upload_only { + let call_data = get_upload_payload(contract_code, self.url.as_str()).await?; + Ok((call_data, hash)) + } else { + let instantiate_exec = set_up_deployment(self.clone().into()).await?; + + let weight_limit = if self.gas_limit.is_some() && self.proof_size.is_some() { + Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap()) + } else { + // Frontend will do dry run and update call data. + Weight::from_parts(0, 0) + }; + let call_data = get_instantiate_payload(instantiate_exec, weight_limit).await?; + Ok((call_data, hash)) + } + } + + async fn wait_for_signature(&self, call_data: Vec) -> anyhow::Result> { + let ui = FrontendFromString::new(include_str!("../../assets/index.html").to_string()); + + let transaction_data = TransactionData::new(self.url.to_string(), call_data); + // starts server + let mut wallet = WalletIntegrationManager::new(ui, transaction_data); + log::step(format!("Wallet signing portal started at http://{}", wallet.rpc_url))?; + + log::step("Waiting for signature... Press Ctrl+C to terminate early.")?; + loop { + // Display error, if any. + if let Some(error) = wallet.take_error().await { + log::error(format!("Signing portal error: {error}"))?; + } + + let state = wallet.state.lock().await; + // If the payload is submitted we terminate the frontend. + if !wallet.is_running() || state.signed_payload.is_some() { + wallet.task_handle.await??; + break; + } + } + + let signed_payload = wallet.state.lock().await.signed_payload.clone(); + Ok(signed_payload) + } } impl From for UpOpts { @@ -331,14 +432,37 @@ impl From for UpOpts { } } +fn display_contract_info(spinner: &ProgressBar, address: String, code_hash: Option) { + spinner.stop(format!( + "Contract deployed and instantiated:\n{}", + style(format!( + "{}\n{}", + style(format!("{} The contract address is {:?}", console::Emoji("●", ">"), address)) + .dim(), + code_hash + .map(|hash| style(format!( + "{} The contract code hash is {:?}", + console::Emoji("●", ">"), + hash + )) + .dim() + .to_string()) + .unwrap_or_default(), + )) + .dim() + )); +} + #[cfg(test)] mod tests { use super::*; + use duct::cmd; + use std::fs::{self, File}; + use subxt::{client::OfflineClientT, utils::to_hex}; use url::Url; - #[test] - fn conversion_up_contract_command_to_up_opts_works() -> anyhow::Result<()> { - let command = UpContractCommand { + fn default_up_contract_command() -> UpContractCommand { + UpContractCommand { path: None, constructor: "new".to_string(), args: vec![], @@ -346,12 +470,18 @@ mod tests { gas_limit: None, proof_size: None, salt: None, - url: Url::parse("ws://localhost:9944")?, + url: Url::parse("ws://localhost:9944").expect("default url is valid"), suri: "//Alice".to_string(), dry_run: false, upload_only: false, skip_confirm: false, - }; + use_wallet: false, + } + } + + #[test] + fn conversion_up_contract_command_to_up_opts_works() -> anyhow::Result<()> { + let command = default_up_contract_command(); let opts: UpOpts = command.into(); assert_eq!( opts, @@ -369,4 +499,92 @@ mod tests { ); Ok(()) } + + #[test] + fn has_contract_been_built_works() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir()?; + let path = temp_dir.path(); + + // Standard rust project + let name = "hello_world"; + cmd("cargo", ["new", name]).dir(&path).run()?; + let contract_path = path.join(name); + assert!(!has_contract_been_built(Some(&contract_path))); + + cmd("cargo", ["build"]).dir(&contract_path).run()?; + // Mock build directory + fs::create_dir(&contract_path.join("target/ink"))?; + assert!(!has_contract_been_built(Some(&path.join(name)))); + // Create a mocked .contract file inside the target directory + File::create(contract_path.join(format!("target/ink/{}.contract", name)))?; + assert!(has_contract_been_built(Some(&path.join(name)))); + Ok(()) + } + + // TODO: delete this test. + // This is a helper test for an actual running pop CLI. + // It can serve as the "frontend" to query the payload, sign it + // and submit back to the CLI. + #[tokio::test] + async fn sign_call_data() -> anyhow::Result<()> { + use subxt::{config::DefaultExtrinsicParamsBuilder as Params, tx::Payload}; + // This struct implements the [`Payload`] trait and is used to submit + // pre-encoded SCALE call data directly, without the dynamic construction of transactions. + struct CallData(Vec); + + impl Payload for CallData { + fn encode_call_data_to( + &self, + _: &subxt::Metadata, + out: &mut Vec, + ) -> Result<(), subxt::ext::subxt_core::Error> { + out.extend_from_slice(&self.0); + Ok(()) + } + } + + use subxt_signer::sr25519::dev; + let payload = reqwest::get(&format!("{}/payload", "http://127.0.0.1:9090")) + .await + .expect("Failed to get payload") + .json::() + .await + .expect("Failed to parse payload"); + + let url = "ws://localhost:9944"; + let rpc_client = subxt::backend::rpc::RpcClient::from_url(url).await?; + let client = + subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + let signer = dev::alice(); + + let payload = CallData(payload.call_data()); + let ext_params = Params::new().build(); + let signed = client.tx().create_signed(&payload, &signer, ext_params).await?; + + let response = reqwest::Client::new() + .post(&format!("{}/submit", "http://localhost:9090")) + .json(&to_hex(signed.encoded())) + .send() + .await + .expect("Failed to submit payload") + .text() + .await + .expect("Failed to parse JSON response"); + + Ok(()) + } + + #[tokio::test] + async fn get_upload_call_data_works() -> anyhow::Result<()> { + todo!() + } + + async fn get_instantiate_call_data_works() -> anyhow::Result<()> { + todo!() + } + + async fn wait_for_signature_works() -> anyhow::Result<()> { + todo!() + } } diff --git a/crates/pop-cli/src/wallet_integration.rs b/crates/pop-cli/src/wallet_integration.rs index 2d7c4cdf4..57b2924e5 100644 --- a/crates/pop-cli/src/wallet_integration.rs +++ b/crates/pop-cli/src/wallet_integration.rs @@ -1,4 +1,5 @@ use axum::{ + http::HeaderValue, response::Html, routing::{get, post}, Router, @@ -9,7 +10,7 @@ use tokio::{ sync::{oneshot, Mutex}, task::JoinHandle, }; -use tower_http::services::ServeDir; +use tower_http::{cors::Any, services::ServeDir}; /// Make frontend sourcing more flexible by allowing a custom route /// to be defined. @@ -29,6 +30,10 @@ impl TransactionData { pub fn new(chain_rpc: String, call_data: Vec) -> Self { Self { chain_rpc, call_data } } + #[allow(dead_code)] + pub fn call_data(&self) -> Vec { + self.call_data.clone() + } } /// Shared state between routes. Serves two purposes: @@ -82,12 +87,20 @@ impl WalletIntegrationManager { let payload = Arc::new(payload); + // TODO: temporary until we host from here. + let cors = tower_http::cors::CorsLayer::new() + .allow_origin("http://localhost:9090".parse::().unwrap()) + .allow_origin("http://127.0.0.1:9090".parse::().unwrap()) + .allow_methods(Any) // Allow any HTTP method + .allow_headers(Any); // Allow any headers (like 'Content-Type') + let app = Router::new() .route("/payload", get(routes::get_payload_handler).with_state(payload)) .route("/submit", post(routes::submit_handler).with_state(state.clone())) .route("/error", post(routes::error_handler).with_state(state.clone())) .route("/terminate", post(routes::terminate_handler).with_state(state.clone())) - .merge(frontend.serve_content()); // Custom route for serving frontend. + .merge(frontend.serve_content()) // Custom route for serving frontend. + .layer(cors); let rpc_owned = rpc.to_string(); @@ -110,6 +123,7 @@ impl WalletIntegrationManager { } /// Signals the wallet integration server to shut down. + #[allow(dead_code)] pub async fn terminate(&mut self) -> anyhow::Result<()> { terminate_helper(&self.state).await } @@ -212,6 +226,7 @@ async fn terminate_helper(handle: &Arc>) -> anyhow::Result<( pub struct FrontendFromDir { content: PathBuf, } +#[allow(dead_code)] impl FrontendFromDir { pub fn new(content: PathBuf) -> Self { Self { content } @@ -229,6 +244,7 @@ pub struct FrontendFromString { content: String, } +#[allow(dead_code)] impl FrontendFromString { pub fn new(content: String) -> Self { Self { content } @@ -340,9 +356,9 @@ mod tests { .send() .await .expect("Failed to submit payload") - .json::() + .text() .await - .expect("Failed to parse JSON response"); + .expect("Failed to parse response"); assert_eq!(response, json!({"status": "success"})); assert_eq!(wim.state.lock().await.signed_payload, Some("0xDEADBEEF".to_string())); diff --git a/crates/pop-contracts/Cargo.toml b/crates/pop-contracts/Cargo.toml index b3ab82cb8..94a2c6e72 100644 --- a/crates/pop-contracts/Cargo.toml +++ b/crates/pop-contracts/Cargo.toml @@ -27,11 +27,13 @@ sp-core.workspace = true sp-weights.workspace = true strum.workspace = true strum_macros.workspace = true +subxt-signer.workspace = true +subxt.workspace = true # cargo-contracts contract-build.workspace = true contract-extrinsics.workspace = true -contract-transcode.workspace = true +contract-transcode.workspace = true scale-info.workspace = true # pop pop-common = { path = "../pop-common", version = "0.5.0" } diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index c5e76e2f1..69897be33 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -22,8 +22,10 @@ pub use templates::{Contract, ContractType}; pub use test::{test_e2e_smart_contract, test_smart_contract}; pub use testing::{mock_build_process, new_environment}; pub use up::{ - dry_run_gas_estimate_instantiate, dry_run_upload, instantiate_smart_contract, - set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, + dry_run_gas_estimate_instantiate, dry_run_upload, get_code_hash_from_event, get_contract_code, + get_instantiate_payload, get_upload_payload, instantiate_contract_signed, + instantiate_smart_contract, set_up_deployment, set_up_upload, submit_signed_payload, + upload_contract_signed, upload_smart_contract, ContractInfo, UpOpts, }; pub use utils::{ metadata::{get_messages, ContractFunction}, diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index bed7dfa45..8ae38e405 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -8,17 +8,27 @@ use crate::{ }, }; use contract_extrinsics::{ + events::{CodeStored, ContractInstantiated}, + extrinsic_calls::{Instantiate, InstantiateWithCode}, BalanceVariant, ErrorVariant, ExtrinsicOptsBuilder, InstantiateCommandBuilder, InstantiateExec, - TokenMetadata, UploadCommandBuilder, UploadExec, + InstantiateExecResult, TokenMetadata, UploadCommandBuilder, UploadExec, UploadResult, WasmCode, }; use ink_env::{DefaultEnvironment, Environment}; use pop_common::{create_signer, DefaultConfig, Keypair}; -use sp_core::Bytes; +use sp_core::{bytes::from_hex, Bytes}; use sp_weights::Weight; -use std::{fmt::Write, path::PathBuf}; +use std::{ + fmt::Write, + path::{Path, PathBuf}, +}; +use subxt::{ + blocks::ExtrinsicEvents, + tx::{Payload, SubmittableExtrinsic}, + Config, SubstrateConfig, +}; /// Attributes for the `up` command -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct UpOpts { /// Path to the contract build directory. pub path: Option, @@ -103,6 +113,174 @@ pub async fn set_up_upload( Ok(upload_exec) } +/// Gets the encoded payload call data for contract upload (not instantiate). +/// +/// # Arguments +/// * `code` - contract code to upload. +/// * `url` - the rpc of the chain node. +pub async fn get_upload_payload(code: WasmCode, url: &str) -> anyhow::Result> { + let storage_deposit_limit: Option = None; + let upload_code = contract_extrinsics::extrinsic_calls::UploadCode::new( + code, + storage_deposit_limit, + contract_extrinsics::upload::Determinism::Enforced, + ); + + // TODO: review placement + let rpc_client = subxt::backend::rpc::RpcClient::from_url(url).await?; + let client = subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + let call_data = upload_code.build(); + let mut encoded_data = Vec::::new(); + call_data.encode_call_data_to(&client.metadata(), &mut encoded_data)?; + Ok(encoded_data) +} +/// Gets the encoded payload call data for a contract instantiation. +/// +/// # Arguments +/// * `instantiate_exec` - arguments for contract instantiate. +/// * `gas_limit` - max amount of gas to be used for instantiation. +pub async fn get_instantiate_payload( + instantiate_exec: InstantiateExec, + gas_limit: Weight, +) -> anyhow::Result> { + let storage_deposit_limit: Option = None; + let mut encoded_data = Vec::::new(); + match instantiate_exec.args().code() { + contract_extrinsics::Code::Upload(code) => InstantiateWithCode::new( + instantiate_exec.args().value(), + gas_limit, + storage_deposit_limit, + code.clone(), + instantiate_exec.args().data().into(), + instantiate_exec.args().salt().into(), + ) + .build() + .encode_call_data_to(&instantiate_exec.client().metadata(), &mut encoded_data), + contract_extrinsics::Code::Existing(hash) => Instantiate::new( + instantiate_exec.args().value(), + gas_limit, + storage_deposit_limit, + hash, + instantiate_exec.args().data().into(), + instantiate_exec.args().salt().into(), + ) + .build() + .encode_call_data_to(&instantiate_exec.client().metadata(), &mut encoded_data), + }?; + + Ok(encoded_data) +} + +/// Reads the contract code from contract file. +/// +/// # Arguments +/// * `path` - path to the contract file. +pub async fn get_contract_code( + path: Option<&PathBuf>, +) -> anyhow::Result { + let manifest_path = get_manifest_path(path.map(|p| p as &Path))?; + + // signer does not matter for this + let signer = create_signer("//Alice")?; + let extrinsic_opts = + ExtrinsicOptsBuilder::::new(signer) + .manifest_path(Some(manifest_path)) + .done(); + let artifacts = extrinsic_opts.contract_artifacts()?; + + let artifacts_path = artifacts.artifact_path().to_path_buf(); + let code = artifacts.code.ok_or_else(|| { + Error::UploadContractError(format!( + "Contract code not found from artifact file {}", + artifacts_path.display() + )) + })?; + Ok(code) +} + +/// Submit a pre-signed payload for uploading a contract. +/// +/// # Arguments +/// * `url` - rpc for chain. +/// * `payload` - the signed payload to submit (encoded call data). +pub async fn upload_contract_signed( + url: &str, + payload: String, +) -> anyhow::Result> { + let events = submit_signed_payload(url, payload).await?; + + let code_stored = events.find_first::>()?; + + Ok(UploadResult { code_stored, events }) +} + +/// Submit a pre-signed payload for instantiating a contract. +/// +/// # Arguments +/// * `url` - rpc for chain. +/// * `payload` - the signed payload to submit (encoded call data). +pub async fn instantiate_contract_signed( + url: &str, + payload: String, +) -> anyhow::Result> { + let events = submit_signed_payload(url, payload).await?; + + // The CodeStored event is only raised if the contract has not already been + // uploaded. + let code_hash = events + .find_first::>()? + .map(|code_stored| code_stored.code_hash); + + let instantiated = events + .find_first::>()? + .ok_or_else(|| { + Error::InstantiateContractError("Failed to find Instantiated event".to_string()) + })?; + + Ok(InstantiateExecResult { events, code_hash, contract_address: instantiated.contract }) +} + +/// Submit a pre-signed payload. +/// +/// # Arguments +/// * `url` - rpc for chain. +/// * `payload` - the signed payload to submit (encoded call data). +pub async fn submit_signed_payload( + url: &str, + payload: String, +) -> anyhow::Result> { + let rpc_client = subxt::backend::rpc::RpcClient::from_url(url).await?; + let client = subxt::OnlineClient::::from_rpc_client(rpc_client).await?; + + let hex_encoded = from_hex(&payload)?; + + let extrinsic = SubmittableExtrinsic::from_bytes(client, hex_encoded); + + // src: https://github.com/use-ink/cargo-contract/blob/68691b9b6cdb7c6ec52ea441b3dc31fcb1ce08e0/crates/extrinsics/src/lib.rs#L143 + + use subxt::{ + error::{RpcError, TransactionError}, + tx::TxStatus, + }; + + let mut tx = extrinsic.submit_and_watch().await?; + + while let Some(status) = tx.next().await { + match status? { + TxStatus::InFinalizedBlock(tx_in_block) => { + let events = tx_in_block.wait_for_success().await?; + return Ok(events) + }, + TxStatus::Error { message } => return Err(TransactionError::Error(message).into()), + TxStatus::Invalid { message } => return Err(TransactionError::Invalid(message).into()), + TxStatus::Dropped { message } => return Err(TransactionError::Dropped(message).into()), + _ => continue, + } + } + Err(RpcError::SubscriptionDropped.into()) +} + /// Estimate the gas required for instantiating a contract without modifying the state of the /// blockchain. /// @@ -204,14 +382,26 @@ pub async fn upload_smart_contract( .upload_code() .await .map_err(|error_variant| Error::UploadContractError(format!("{:?}", error_variant)))?; - if let Some(code_stored) = upload_result.code_stored { + get_code_hash_from_event(&upload_result, upload_exec.code().code_hash()) +} + +/// Get the code hash of a contract from the upload event. +/// +/// # Arguments +/// * `upload_result` - the result of uploading the contract. +/// * `metadata_code_hash` - the code hash from the metadata Used only for error reporting. +pub fn get_code_hash_from_event( + upload_result: &UploadResult, + // used for error reporting + metadata_code_hash: [u8; 32], +) -> Result { + if let Some(code_stored) = upload_result.code_stored.as_ref() { Ok(format!("{:?}", code_stored.code_hash)) } else { - let code_hash: String = - upload_exec.code().code_hash().iter().fold(String::new(), |mut output, b| { - write!(output, "{:02x}", b).expect("expected to write to string"); - output - }); + let code_hash: String = metadata_code_hash.iter().fold(String::new(), |mut output, b| { + write!(output, "{:02x}", b).expect("expected to write to string"); + output + }); Err(Error::UploadContractError(format!( "This contract has already been uploaded with code hash: 0x{code_hash}" )))