diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dfb7b538..a0972f504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased changes +## 4.1.0 + +- Add `ContractInitBuilder` for more ergonomic initialization of new smart + contract instances. + ## 4.0.0 - Add a `From<&AccountInfo>` instance for `AccountAccessStructure` to ease verification of signatures using `GetAccountInfo` response. diff --git a/examples/init-update-contract.rs b/examples/init-update-contract.rs index 67cb22c3c..869e854b8 100644 --- a/examples/init-update-contract.rs +++ b/examples/init-update-contract.rs @@ -5,18 +5,15 @@ use anyhow::Context; use clap::AppSettings; use concordium_rust_sdk::{ - common::{types::TransactionTime, SerdeDeserialize, SerdeSerialize}, + common::{SerdeDeserialize, SerdeSerialize}, + contract_client::{ContractClient, ContractInitBuilder, ViewError}, endpoints, smart_contracts::{ common as concordium_std, - common::{Amount, ContractAddress, OwnedContractName, OwnedReceiveName, Serial}, + common::{Amount, ContractAddress, Serial}, }, - types::{ - smart_contracts::{ModuleReference, OwnedParameter}, - transactions::{send, InitContractPayload, UpdateContractPayload}, - AccountInfo, WalletAccount, - }, - v2::{self, BlockIdentifier}, + types::{smart_contracts::ModuleReference, WalletAccount}, + v2, }; use std::path::PathBuf; use structopt::*; @@ -81,6 +78,8 @@ impl std::str::FromStr for Weather { } } +enum WeatherContractMarker {} + #[tokio::main(flavor = "multi_thread")] async fn main() -> anyhow::Result<()> { let app = { @@ -89,62 +88,60 @@ async fn main() -> anyhow::Result<()> { App::from_clap(&matches) }; - let mut client = v2::Client::new(app.endpoint).await?; + let client = v2::Client::new(app.endpoint).await?; // load account keys and sender address from a file - let keys: WalletAccount = + let account: WalletAccount = WalletAccount::from_json_file(app.keys_path).context("Could not parse the keys file.")?; - // Get the initial nonce at the last finalized block. - let acc_info: AccountInfo = client - .get_account_info(&keys.address.into(), BlockIdentifier::LastFinal) - .await? - .response; - - let nonce = acc_info.account_nonce; - // set expiry to now + 5min - let expiry: TransactionTime = - TransactionTime::from_seconds((chrono::Utc::now().timestamp() + 300) as u64); - - let tx = match app.action { + match app.action { Action::Init { weather, module_ref: mod_ref, } => { - let param = OwnedParameter::from_serial(&weather) - .expect("Known to not exceed parameter size limit."); - let payload = InitContractPayload { - amount: Amount::zero(), + let builder = ContractInitBuilder::::dry_run_new_instance( + client, + account.address, mod_ref, - init_name: OwnedContractName::new_unchecked("init_weather".to_string()), - param, - }; - - send::init_contract(&keys, keys.address, nonce, expiry, payload, 10000u64.into()) + "weather", + Amount::zero(), + &weather, + ) + .await?; + println!( + "The maximum amount of NRG allowed for the transaction is {}.", + builder.current_energy() + ); + let handle = builder.send(&account).await?; + println!("Transaction {handle} submitted. Waiting for finalization."); + let (contract_client, events) = handle.wait_for_finalization().await?; + println!( + "Initialized a new smart contract instance at address {}.", + contract_client.address + ); + println!("The following events were generated."); + for event in events { + println!("{event}"); + } } Action::Update { weather, address } => { - let message = OwnedParameter::from_serial(&weather) - .expect("Known to not exceed parameter size limit."); - let payload = UpdateContractPayload { - amount: Amount::zero(), - address, - receive_name: OwnedReceiveName::new_unchecked("weather.set".to_string()), - message, - }; - - send::update_contract(&keys, keys.address, nonce, expiry, payload, 10000u64.into()) + let mut contract_client = + ContractClient::::create(client, address).await?; + let builder = contract_client + .dry_run_update::<_, ViewError>("set", Amount::zero(), account.address, &weather) + .await?; + println!( + "The maximum amount of execution NRG allowed for the transaction is {}.", + builder.current_energy() + ); + let handle = builder.send(&account).await?; + println!("Transaction {handle} submitted. Waiting for finalization."); + let result = handle.wait_for_finalization().await?; + println!( + "Update smart contract instance. It cost {}CCD.", + result.cost + ); } }; - - // submit the transaction to the chain - let transaction_hash = client.send_account_transaction(tx).await?; - println!( - "Transaction {} submitted (nonce = {}).", - transaction_hash, nonce, - ); - let (bh, bs) = client.wait_until_finalized(&transaction_hash).await?; - println!("Transaction finalized in block {}.", bh); - println!("The outcome is {:#?}", bs); - Ok(()) } diff --git a/examples/v2_contract_deploy_init_update.rs b/examples/v2_contract_deploy_init_update.rs index 0f67f51f7..3b406ebe1 100644 --- a/examples/v2_contract_deploy_init_update.rs +++ b/examples/v2_contract_deploy_init_update.rs @@ -6,7 +6,8 @@ use anyhow::Context; use clap::AppSettings; use concordium_rust_sdk::{ - common::{self, types::TransactionTime, SerdeDeserialize, SerdeSerialize}, + common::{types::TransactionTime, SerdeDeserialize, SerdeSerialize}, + contract_client::ModuleDeployBuilder, smart_contracts::{ common as concordium_std, common::Amount, @@ -180,10 +181,15 @@ async fn main() -> anyhow::Result<()> { send::update_contract(&keys, keys.address, nonce, expiry, payload, 10000u64.into()) } Action::Deploy { module_path } => { - let contents = std::fs::read(module_path).context("Could not read contract module.")?; - let payload: WasmModule = - common::Deserial::deserial(&mut std::io::Cursor::new(contents))?; - send::deploy_module(&keys, keys.address, nonce, expiry, payload) + let module = + WasmModule::from_file(&module_path).context("Could not read contract module.")?; + let builder = + ModuleDeployBuilder::dry_run_module_deploy(client, keys.address, module).await?; + let handle = builder.send(&keys).await?; + println!("Module deployment transaction {handle} submitted."); + let result = handle.wait_for_finalization().await?; + println!("Module {} deployed.", result.module_reference); + return Ok(()); } }; diff --git a/src/contract_client.rs b/src/contract_client.rs index a1f937a36..7bb66c4d2 100644 --- a/src/contract_client.rs +++ b/src/contract_client.rs @@ -1,22 +1,39 @@ //! This module contains a generic client that provides conveniences for -//! interacting with any smart contract instance. +//! interacting with any smart contract instance, as well as for creating new +//! ones. +//! +//! The key types in this module are +//! [`ContractClient`](contract_client::ContractClient), +//! [`ContractInitBuilder`](contract_client::ContractInitBuilder) +//! and [`ModuleDeployBuilder`](contract_client::ModuleDeployBuilder). use crate::{ indexer::ContractUpdateInfo, types::{ - smart_contracts::{self, ContractContext, InvokeContractResult}, - transactions, AccountTransactionEffects, RejectReason, + smart_contracts::{self, ContractContext, InvokeContractResult, ReturnValue}, + transactions, AccountTransactionEffects, ContractInitializedEvent, RejectReason, + }, + v2::{ + self, + dry_run::{self, DryRunTransaction}, + BlockIdentifier, Client, }, - v2::{self, BlockIdentifier, Client}, }; use concordium_base::{ base::{Energy, Nonce}, common::types::{self, TransactionTime}, contracts_common::{ - self, AccountAddress, Address, Amount, ContractAddress, NewReceiveNameError, + self, AccountAddress, Address, Amount, ContractAddress, NewContractNameError, + NewReceiveNameError, }, hashes::TransactionHash, - smart_contracts::{ExceedsParameterSize, OwnedContractName, OwnedParameter, OwnedReceiveName}, - transactions::{AccountTransaction, EncodedPayload, UpdateContractPayload}, + smart_contracts::{ + ContractEvent, ContractTraceElement, ExceedsParameterSize, ModuleReference, + OwnedContractName, OwnedParameter, OwnedReceiveName, WasmModule, + }, + transactions::{ + construct::TRANSACTION_HEADER_SIZE, AccountTransaction, EncodedPayload, + InitContractPayload, PayloadLike, UpdateContractPayload, + }, }; pub use concordium_base::{cis2_types::MetadataUrl, cis4_types::*}; use std::{marker::PhantomData, sync::Arc}; @@ -85,6 +102,572 @@ impl From for ViewError { fn from(value: RejectReason) -> Self { Self::QueryFailed(value) } } +/// A builder of transactions out of minimal data typically obtained by +/// dry-running. +/// +/// The concrete instances of this type, [`ContractInitBuilder`] and +/// [`ContractUpdateBuilder`] have more detailed information on usage. +/// +/// The `ADD_ENERGY` constant is used to indicate whether the builder should +/// allow adding extra energy. This is only useful for transactions that have +/// dynamic cost, namely contract initializations and updates. +pub struct TransactionBuilder { + client: v2::Client, + sender: AccountAddress, + energy: Energy, + add_energy: Option, + expiry: Option, + nonce: Option, + payload: transactions::Payload, + inner: Inner, +} + +impl TransactionBuilder { + fn new( + client: v2::Client, + sender: AccountAddress, + energy: Energy, + payload: transactions::Payload, + inner: Inner, + ) -> Self { + Self { + client, + sender, + energy, + add_energy: None, + expiry: None, + nonce: None, + payload, + inner, + } + } + + /// Set the expiry time for the transaction. If not set the default is one + /// hour from the time the transaction is signed. + pub fn expiry(mut self, expiry: TransactionTime) -> Self { + self.expiry = Some(expiry); + self + } + + /// Set the nonce for the transaction. If not set the default behaviour is + /// to get the nonce from the connected [`Client`](v2::Client) at the + /// time the transaction is sent. + pub fn nonce(mut self, nonce: Nonce) -> Self { + self.nonce = Some(nonce); + self + } + + /// Return the amount of [`Energy`] that will be allowed for the transaction + /// if the transaction was sent with the current parameters. + pub fn current_energy(&self) -> Energy { + if ADD { + // Add 10% to the call, or at least 50. + self.energy + + self + .add_energy + .unwrap_or_else(|| std::cmp::max(50, self.energy.energy / 10).into()) + } else { + self.energy + } + } + + /// Send the transaction and return a handle that can be queried + /// for the status. + pub async fn send_inner( + mut self, + signer: &impl transactions::ExactSizeTransactionSigner, + k: impl FnOnce(TransactionHash, v2::Client) -> A, + ) -> v2::QueryResult { + let nonce = if let Some(nonce) = self.nonce { + nonce + } else { + self.client + .get_next_account_sequence_number(&self.sender) + .await? + .nonce + }; + let expiry = self + .expiry + .unwrap_or_else(|| TransactionTime::hours_after(1)); + let energy = self.current_energy(); + let tx = transactions::send::make_and_sign_transaction( + signer, + self.sender, + nonce, + expiry, + transactions::send::GivenEnergy::Add(energy), + self.payload, + ); + let tx_hash = self.client.send_account_transaction(tx).await?; + Ok(k(tx_hash, self.client)) + } +} + +impl TransactionBuilder { + /// Add extra energy to the call. + /// The default amount is 10%, or at least 50. + /// This should be sufficient in most cases, but for specific + /// contracts no extra energy might be needed, or a greater safety margin + /// could be desired. + pub fn extra_energy(mut self, energy: Energy) -> Self { + self.add_energy = Some(energy); + self + } +} + +/// A helper type to construct [`ContractInitBuilder`]. +/// Users do not directly interact with values of this type. +pub struct ContractInitInner { + /// The event generated from dry running. + event: ContractInitializedEvent, + phantom: PhantomData, +} + +impl ContractInitInner { + fn new(event: ContractInitializedEvent) -> Self { + Self { + event, + phantom: PhantomData, + } + } +} + +/// Builder for initializing a new smart contract instance. +/// +/// The builder is intended to be constructed using +/// [`dry_run_new_instance`](ContractInitBuilder::dry_run_new_instance) +/// or [`dry_run_new_instance_raw`](ContractInitBuilder::dry_run_new_instance_raw) methods. +/// and the transaction is intended to be sent using the +/// [`send`](ContractInitBuilder::send) method. +pub type ContractInitBuilder = TransactionBuilder>; + +/// A handle returned when sending a smart contract init transaction. +/// This can be used to get the response of the initialization. +/// +/// Note that this handle retains a connection to the node. So if it is not +/// going to be used it should be dropped. +pub struct ContractInitHandle { + tx_hash: TransactionHash, + client: v2::Client, + phantom: PhantomData, +} + +/// The [`Display`](std::fmt::Display) implementation displays the hash of the +/// transaction. +impl std::fmt::Display for ContractInitHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.tx_hash.fmt(f) } +} + +#[derive(Debug, thiserror::Error)] +/// An error that may occur when querying the result of a smart contract update +/// transaction. +pub enum ContractInitError { + #[error("The status of the transaction could not be ascertained: {0}")] + Query(#[from] QueryError), + #[error("Contract update failed with reason: {0:?}")] + Failed(RejectReason), +} + +impl ContractInitHandle { + /// Extract the hash of the transaction underlying this handle. + pub fn hash(&self) -> TransactionHash { self.tx_hash } + + /// Wait until the transaction is finalized and return the client for the + /// contract together with a list of events generated by the contract + /// during initialization. + /// + /// Note that this can potentially wait indefinitely. + pub async fn wait_for_finalization( + mut self, + ) -> Result<(ContractClient, Vec), ContractInitError> { + let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?; + + let mk_error = |msg| { + Err(ContractInitError::from(QueryError::RPCError( + RPCError::CallError(tonic::Status::invalid_argument(msg)), + ))) + }; + + match result.details { + crate::types::BlockItemSummaryDetails::AccountTransaction(at) => match at.effects { + AccountTransactionEffects::ContractInitialized { data } => { + let contract_client = + ContractClient::new(self.client, data.address, data.init_name); + Ok((contract_client, data.events)) + } + AccountTransactionEffects::None { + transaction_type: _, + reject_reason, + } => Err(ContractInitError::Failed(reject_reason)), + _ => mk_error( + "Expected smart contract initialization status, but did not receive it.", + ), + }, + crate::types::BlockItemSummaryDetails::AccountCreation(_) => mk_error( + "Expected smart contract initialization status, but received account creation.", + ), + crate::types::BlockItemSummaryDetails::Update(_) => mk_error( + "Expected smart contract initialization status, but received chain update \ + instruction.", + ), + } + } + + /// Wait until the transaction is finalized or until the timeout has elapsed + /// and return the result. + pub async fn wait_for_finalization_timeout( + self, + timeout: std::time::Duration, + ) -> Result<(ContractClient, Vec), ContractInitError> { + let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await; + match result { + Ok(r) => r, + Err(_elapsed) => Err(ContractInitError::Query(QueryError::RPCError( + RPCError::CallError(tonic::Status::deadline_exceeded( + "Deadline waiting for result of transaction is exceeded.", + )), + ))), + } + } +} + +#[derive(thiserror::Error, Debug)] +/// An error that may occur when attempting to dry run a new instance creation. +pub enum DryRunNewInstanceError { + #[error("Dry run succeeded, but contract initialization failed due to {0:#?}.")] + Failed(RejectReason), + #[error("Dry run failed: {0}")] + DryRun(#[from] dry_run::DryRunError), + #[error("Parameter too large: {0}")] + ExceedsParameterSize(#[from] ExceedsParameterSize), + #[error("Node query error: {0}")] + Query(#[from] v2::QueryError), + #[error("Contract name not valid: {0}")] + InvalidContractName(#[from] NewContractNameError), + #[error("The reported energy consumed for the dry run is less than expected ({min}).")] + InvalidEnergy { + /// Minimum amount of energy expected + min: Energy, + }, +} + +impl From for DryRunNewInstanceError { + fn from(value: RejectReason) -> Self { Self::Failed(value) } +} + +impl ContractInitBuilder { + /// Attempt to dry run a smart contract initialization transaction. + /// + /// In contrast to + /// [`dry_run_new_instance_raw`](Self::dry_run_new_instance_raw) this + /// automatically serializes the provided parameter. + pub async fn dry_run_new_instance( + client: Client, + sender: AccountAddress, + mod_ref: ModuleReference, + name: &str, + amount: Amount, + parameter: &P, + ) -> Result { + let parameter = OwnedParameter::from_serial(parameter)?; + Self::dry_run_new_instance_raw(client, sender, mod_ref, name, amount, parameter).await + } + + /// Attempt to dry run a smart contract initialization transaction. + /// In case of success the resulting value can be used to extract + /// the generated events from the dry-run, and sign and send the + /// transaction. + /// + /// The arguments are + /// - `client` - the client to connect to the node + /// - `sender` - the account that will be sending the transaction + /// - `mod_ref` - the reference to the module on chain from which the + /// instance is to be created + /// - `name` - the name of the contract (NB: without the `init_` prefix) + /// - `amount` - the amount of CCD to initialize the instance with + /// - `parameter` - the parameter to send to the initialization method of + /// the contract. + pub async fn dry_run_new_instance_raw( + mut client: Client, + sender: AccountAddress, + mod_ref: ModuleReference, + name: &str, + amount: Amount, + parameter: OwnedParameter, + ) -> Result { + let name = OwnedContractName::new(format!("init_{name}"))?; + let mut dr = client.dry_run(BlockIdentifier::LastFinal).await?; + let payload = InitContractPayload { + amount, + mod_ref, + init_name: name, + param: parameter, + }; + let payload = transactions::Payload::InitContract { payload }; + let encoded_payload = payload.encode(); + let payload_size = encoded_payload.size(); + let tx = DryRunTransaction { + sender, + energy_amount: dr.inner.0.energy_quota(), + payload: encoded_payload, + signatures: Vec::new(), + }; + let result = dr + .inner + .0 + .begin_run_transaction(tx) + .await + .map_err(dry_run::DryRunError::from)? + .await? + .inner; + + let data = match result.details.effects { + AccountTransactionEffects::None { + transaction_type: _, + reject_reason, + } => return Err(reject_reason.into()), + AccountTransactionEffects::ContractInitialized { data } => data, + _ => { + return Err( + dry_run::DryRunError::CallError(tonic::Status::invalid_argument( + "Unexpected response from dry-running a contract initialization.", + )) + .into(), + ) + } + }; + let base_cost = transactions::cost::base_cost( + TRANSACTION_HEADER_SIZE + u64::from(u32::from(payload_size)), + 1, + ); + let energy = result + .energy_cost + .checked_sub(base_cost) + .ok_or(DryRunNewInstanceError::InvalidEnergy { min: base_cost })?; + + Ok(ContractInitBuilder::new( + client, + sender, + energy, + payload, + ContractInitInner::new(data), + )) + } + + /// Access to the generated events. + /// + /// Note that these are events generated as part of a dry run. + /// Since time passes between the dry run and the actual transaction + /// the transaction might behave differently. + pub fn event(&self) -> &ContractInitializedEvent { &self.inner.event } + + /// Send the transaction and return a handle that can be queried + /// for the status. + pub async fn send( + self, + signer: &impl transactions::ExactSizeTransactionSigner, + ) -> v2::QueryResult> { + let phantom = self.inner.phantom; + self.send_inner(signer, |tx_hash, client| ContractInitHandle { + tx_hash, + client, + phantom, + }) + .await + } +} + +pub type ModuleDeployBuilder = TransactionBuilder; + +#[derive(thiserror::Error, Debug)] +/// An error that may occur when attempting to dry run a smart contract module +/// deployment. +pub enum DryRunModuleDeployError { + #[error("Dry run succeeded, but module deployment failed due to {0:#?}.")] + Failed(RejectReason), + #[error("Dry run failed: {0}")] + DryRun(#[from] dry_run::DryRunError), + #[error("Node query error: {0}")] + Query(#[from] v2::QueryError), + #[error("The reported energy consumed for the dry run is less than expected ({min}).")] + InvalidEnergy { + /// Minimum amount of energy expected + min: Energy, + }, +} + +impl DryRunModuleDeployError { + /// Check whether dry-run failed because the module already exists. + pub fn already_exists(&self) -> bool { + let Self::Failed(reason) = self else { + return false; + }; + matches!(reason, RejectReason::ModuleHashAlreadyExists { .. }) + } +} + +impl From for DryRunModuleDeployError { + fn from(value: RejectReason) -> Self { Self::Failed(value) } +} + +impl ModuleDeployBuilder { + /// Attempt to dry run a module deployment transaction. + /// + /// In case of success the return value can be used to send the transaction + /// to affect the module deployment. + pub async fn dry_run_module_deploy( + mut client: Client, + sender: AccountAddress, + module: WasmModule, + ) -> Result { + let mut dr = client.dry_run(BlockIdentifier::LastFinal).await?; + let payload = transactions::Payload::DeployModule { module }; + let encoded_payload = payload.encode(); + let payload_size = encoded_payload.size(); + let tx = DryRunTransaction { + sender, + energy_amount: dr.inner.0.energy_quota(), + payload: encoded_payload, + signatures: Vec::new(), + }; + let result = dr + .inner + .0 + .begin_run_transaction(tx) + .await + .map_err(dry_run::DryRunError::from)? + .await? + .inner; + + let module_ref = match result.details.effects { + AccountTransactionEffects::None { + transaction_type: _, + reject_reason, + } => return Err(reject_reason.into()), + AccountTransactionEffects::ModuleDeployed { module_ref } => module_ref, + _ => { + return Err( + dry_run::DryRunError::CallError(tonic::Status::invalid_argument( + "Unexpected response from dry-running a contract initialization.", + )) + .into(), + ) + } + }; + let base_cost = transactions::cost::base_cost( + TRANSACTION_HEADER_SIZE + u64::from(u32::from(payload_size)), + 1, + ); + let energy = result + .energy_cost + .checked_sub(base_cost) + .ok_or(DryRunModuleDeployError::InvalidEnergy { min: base_cost })?; + Ok(Self::new(client, sender, energy, payload, module_ref)) + } +} + +impl ModuleDeployBuilder { + /// Send the transaction and return a handle that can be queried + /// for the status. + pub async fn send( + self, + signer: &impl transactions::ExactSizeTransactionSigner, + ) -> v2::QueryResult { + self.send_inner(signer, |tx_hash, client| ModuleDeployHandle { + tx_hash, + client, + }) + .await + } +} + +pub struct ModuleDeployHandle { + tx_hash: TransactionHash, + client: v2::Client, +} + +/// The [`Display`](std::fmt::Display) implementation displays the hash of the +/// transaction. +impl std::fmt::Display for ModuleDeployHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.tx_hash.fmt(f) } +} + +#[derive(Debug, thiserror::Error)] +/// An error that may occur when querying the result of a module deploy +/// transaction. +pub enum ModuleDeployError { + #[error("The status of the transaction could not be ascertained: {0}")] + Query(#[from] QueryError), + #[error("Module deployment failed with reason: {0:?}")] + Failed(RejectReason), +} + +#[derive(Debug, Clone, Copy)] +/// Result of successful module deployment. +pub struct ModuleDeployData { + /// Energy used for the trasaction. + pub energy: Energy, + /// The CCD cost of the transaction. + pub cost: Amount, + /// The module reference. + pub module_reference: ModuleReference, +} + +impl ModuleDeployHandle { + /// Extract the hash of the transaction underlying this handle. + pub fn hash(&self) -> TransactionHash { self.tx_hash } + + /// Wait until the transaction is finalized and return the result. + /// Note that this can potentially wait indefinitely. + pub async fn wait_for_finalization(mut self) -> Result { + let (_, result) = self.client.wait_until_finalized(&self.tx_hash).await?; + + let mk_error = |msg| { + Err(ModuleDeployError::from(QueryError::RPCError( + RPCError::CallError(tonic::Status::invalid_argument(msg)), + ))) + }; + + match result.details { + crate::types::BlockItemSummaryDetails::AccountTransaction(at) => match at.effects { + AccountTransactionEffects::ModuleDeployed { module_ref } => Ok(ModuleDeployData { + energy: result.energy_cost, + cost: at.cost, + module_reference: module_ref, + }), + AccountTransactionEffects::None { + transaction_type: _, + reject_reason, + } => Err(ModuleDeployError::Failed(reject_reason)), + _ => mk_error("Expected module deploy status, but did not receive it."), + }, + crate::types::BlockItemSummaryDetails::AccountCreation(_) => { + mk_error("Expected module deploy status, but received account creation.") + } + crate::types::BlockItemSummaryDetails::Update(_) => { + mk_error("Expected module deploy status, but received chain update instruction.") + } + } + } + + /// Wait until the transaction is finalized or until the timeout has elapsed + /// and return the result. + pub async fn wait_for_finalization_timeout( + self, + timeout: std::time::Duration, + ) -> Result { + let result = tokio::time::timeout(timeout, self.wait_for_finalization()).await; + match result { + Ok(r) => r, + Err(_elapsed) => Err(ModuleDeployError::Query(QueryError::RPCError( + RPCError::CallError(tonic::Status::deadline_exceeded( + "Deadline waiting for result of transaction is exceeded.", + )), + ))), + } + } +} + impl ContractClient { /// Construct a [`ContractClient`] by looking up metadata from the chain. /// @@ -263,11 +846,19 @@ impl ContractClient { message: context.parameter, }; match invoke_result { - InvokeContractResult::Success { used_energy, .. } => Ok(ContractUpdateBuilder::new( + InvokeContractResult::Success { + used_energy, + return_value, + events, + } => Ok(ContractUpdateBuilder::new( self.client.clone(), sender, - payload, used_energy, + transactions::Payload::Update { payload }, + ContractUpdateInner { + return_value, + events, + }, )), InvokeContractResult::Failure { reason, .. } => Err(reason.into()), } @@ -350,103 +941,35 @@ impl ContractClient { } } -/// A builder to simplify sending smart contract updates. -pub struct ContractUpdateBuilder { - payload: UpdateContractPayload, - sender: AccountAddress, - energy: Energy, - expiry: Option, - add_energy: Option, - nonce: Option, - client: v2::Client, +/// A helper type to construct [`ContractUpdateBuilder`]. +/// Users do not directly interact with values of this type. +pub struct ContractUpdateInner { + return_value: Option, + events: Vec, } -impl ContractUpdateBuilder { - /// Construct a new builder. - pub fn new( - client: v2::Client, - sender: AccountAddress, - payload: UpdateContractPayload, - energy: Energy, - ) -> Self { - Self { - payload, - sender, - energy, - expiry: None, - add_energy: None, - nonce: None, - client, - } - } - - /// Add extra energy to the call. - /// The default amount is 10%, or at least 50. - /// This should be sufficient in most cases, but for specific - /// contracts no extra energy might be needed, or a greater safety margin - /// could be desired. - pub fn extra_energy(mut self, energy: Energy) -> Self { - self.add_energy = Some(energy); - self - } - - /// Set the expiry time for the transaction. If not set the default is one - /// hour from the time the transaction is signed. - pub fn expiry(mut self, expiry: TransactionTime) -> Self { - self.expiry = Some(expiry); - self - } - - /// Set the nonce for the transaction. If not set the default behaviour is - /// to get the nonce from the connected [`Client`](v2::Client) at the - /// time the transaction is sent. - pub fn nonce(mut self, nonce: Nonce) -> Self { - self.nonce = Some(nonce); - self - } - - /// Return the amount of [`Energy`] allowed for execution if - /// the transaction was sent with the current parameters. - pub fn current_energy(&self) -> Energy { - // Add 10% to the call, or at least 50. - self.energy - + self - .add_energy - .unwrap_or_else(|| std::cmp::max(50, self.energy.energy / 10).into()) - } +/// A builder to simplify sending smart contract updates. +pub type ContractUpdateBuilder = TransactionBuilder; +impl ContractUpdateBuilder { /// Send the transaction and return a handle that can be queried /// for the status. pub async fn send( - mut self, + self, signer: &impl transactions::ExactSizeTransactionSigner, ) -> v2::QueryResult { - let nonce = if let Some(nonce) = self.nonce { - nonce - } else { - self.client - .get_next_account_sequence_number(&self.sender) - .await? - .nonce - }; - let expiry = self - .expiry - .unwrap_or_else(|| TransactionTime::hours_after(1)); - let energy = self.current_energy(); - let tx = transactions::send::update_contract( - signer, - self.sender, - nonce, - expiry, - self.payload, - energy, - ); - let tx_hash = self.client.send_account_transaction(tx).await?; - Ok(ContractUpdateHandle { + self.send_inner(signer, |tx_hash, client| ContractUpdateHandle { tx_hash, - client: self.client, + client, }) + .await } + + /// Get the return value from dry-running. + pub fn return_value(&self) -> Option<&ReturnValue> { self.inner.return_value.as_ref() } + + /// Get the events generated from the dry-run. + pub fn events(&self) -> &[ContractTraceElement] { &self.inner.events } } /// A handle returned when sending a smart contract update transaction. @@ -510,7 +1033,7 @@ impl ContractUpdateHandle { transaction_type: _, reject_reason, } => Err(ContractUpdateError::Failed(reject_reason)), - _ => mk_error("Expected smart contract update status, but received ."), + _ => mk_error("Expected smart contract update status, but did not receive it."), }, crate::types::BlockItemSummaryDetails::AccountCreation(_) => { mk_error("Expected smart contract update status, but received account creation.") diff --git a/src/v2/dry_run.rs b/src/v2/dry_run.rs index 7bb77cc45..6acac1728 100644 --- a/src/v2/dry_run.rs +++ b/src/v2/dry_run.rs @@ -587,7 +587,7 @@ pub struct TransactionExecuted { pub energy_cost: Energy, /// Detailed result of the transaction execution. pub details: AccountTransactionDetails, - /// For V1 contract update transactions, the return value. + /// For V1 contract update and init transactions, the return value. pub return_value: Option>, } @@ -707,8 +707,6 @@ impl DryRun { pub fn timeout(&self) -> std::time::Duration { std::time::Duration::from_millis(self.timeout) } /// Get the total energy quota set for the dry-run session. - /// Returns `None` if the initial metadata did not include the quota, or it - /// could not be parsed. pub fn energy_quota(&self) -> Energy { self.energy_quota.into() } /// Load the state from a specified block.