diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff2d9684..f2100970f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ - Support for smart contract debugging when running locally. - Remove JSON serialization support of BlockSummary. - Add an additional `indexer` to index all transaction outcomes and special events. +- Make the `energy` field of `ContractContext` optional since it is no longer + required by the node. +- Add `dry_run_update` and `dry_run_update_raw` methods to the `ContractClient` + to simulate smart contract updates. The return values of these can be used to + immediately sign and send a transaction. +- Update `rand` dependency to `0.8`. ## 3.2.0 diff --git a/Cargo.toml b/Cargo.toml index 9443493a1..cb637657f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,9 +28,9 @@ semver = "1" anyhow = "1.0" # See https://github.com/serde-rs/json/issues/505 for how to be careful. rust_decimal = { version = "1.26", features = ["serde-float", "serde-arbitrary-precision"]} -ed25519-dalek = "1" +ed25519-dalek = "2" sha2 = "0.10" -rand = {version = "0.7", features = ["small_rng"]} +rand = {version = "0.8", features = ["small_rng"]} num = "0.4" num-bigint = "0.4" num-traits = "0.2" diff --git a/concordium-base b/concordium-base index de1bacd78..8024ceed0 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit de1bacd78620edb5e5b376932c9a68cbdd9b9364 +Subproject commit 8024ceed057aeccee59f158a64cd108e35f2a5fc diff --git a/examples/v2_dry_run.rs b/examples/v2_dry_run.rs index fffb67550..21aed8954 100644 --- a/examples/v2_dry_run.rs +++ b/examples/v2_dry_run.rs @@ -83,7 +83,7 @@ async fn test_all(endpoint: v2::Endpoint) -> anyhow::Result<()> { amount: Amount::zero(), method: invoke_target.clone(), parameter: parameter.clone(), - energy: 10000.into(), + energy: None, }; let res5 = dry_run.invoke_instance(&context).await; println!("Invoked view on {contract_addr}: {:?}", res5); diff --git a/examples/v2_invoke_instance.rs b/examples/v2_invoke_instance.rs index 90a94d8de..da04fd832 100644 --- a/examples/v2_invoke_instance.rs +++ b/examples/v2_invoke_instance.rs @@ -40,7 +40,7 @@ async fn main() -> anyhow::Result<()> { amount: Amount::zero(), method: app.receive_name, parameter: Default::default(), - energy: 1000000.into(), + energy: None, }; let info = client diff --git a/src/cis0.rs b/src/cis0.rs index 9217e9084..d487945ed 100644 --- a/src/cis0.rs +++ b/src/cis0.rs @@ -5,13 +5,11 @@ use crate::{ types::{self as sdk_types, smart_contracts::ContractContext}, v2::{BlockIdentifier, QueryResponse}, }; -use concordium_base::{ - base::Energy, - contracts_common::{Amount, ContractName, EntrypointName, OwnedReceiveName, ParseError}, +use concordium_base::contracts_common::{ + Amount, ContractName, EntrypointName, OwnedReceiveName, ParseError, }; use sdk_types::{smart_contracts, ContractAddress}; use smart_contracts::concordium_contracts_common as contracts_common; -use std::convert::From; use thiserror::*; /// The query result type for whether a smart contract supports a standard. @@ -222,7 +220,7 @@ pub async fn supports_multi( amount: Amount::from_micro_ccd(0), method, parameter, - energy: Energy::from(500_000u64), + energy: None, }; let res = client.invoke_instance(bi, &ctx).await?; match res.response { diff --git a/src/contract_client.rs b/src/contract_client.rs index 7c1555d20..a1f937a36 100644 --- a/src/contract_client.rs +++ b/src/contract_client.rs @@ -1,15 +1,16 @@ //! This module contains a generic client that provides conveniences for //! interacting with any smart contract instance. use crate::{ + indexer::ContractUpdateInfo, types::{ smart_contracts::{self, ContractContext, InvokeContractResult}, - transactions, RejectReason, + transactions, AccountTransactionEffects, RejectReason, }, v2::{self, BlockIdentifier, Client}, }; use concordium_base::{ - base::Nonce, - common::types, + base::{Energy, Nonce}, + common::types::{self, TransactionTime}, contracts_common::{ self, AccountAddress, Address, Amount, ContractAddress, NewReceiveNameError, }, @@ -19,6 +20,7 @@ use concordium_base::{ }; pub use concordium_base::{cis2_types::MetadataUrl, cis4_types::*}; use std::{marker::PhantomData, sync::Arc}; +use v2::{QueryError, RPCError}; /// A contract client that handles some of the boilerplate such as serialization /// and parsing of responses when sending transactions, or invoking smart @@ -192,13 +194,85 @@ impl ContractClient { amount, method, parameter, - energy: 1_000_000.into(), + energy: None, }; let invoke_result = self.client.invoke_instance(bi, &context).await?.response; Ok(invoke_result) } + /// Dry run an update. If the dry run succeeds the return value is an object + /// that has a send method to send the transaction that was simulated during + /// the dry run. + /// + /// The arguments are + /// - `entrypoint` the name of the entrypoint to be invoked. Note that this + /// is just the entrypoint name without the contract name. + /// - `amount` the amount of CCD to send to the contract instance + /// - `sender` the account that will be sending the transaction + /// - `message` the parameter to the smart contract entrypoint. + pub async fn dry_run_update( + &mut self, + entrypoint: &str, + amount: Amount, + sender: AccountAddress, + message: &P, + ) -> Result + where + E: From + + From + + From + + From, { + let message = OwnedParameter::from_serial(message)?; + self.dry_run_update_raw(entrypoint, amount, sender, message) + .await + } + + /// Like [`dry_run_update`](Self::dry_run_update) but expects an already + /// formed parameter. + pub async fn dry_run_update_raw( + &mut self, + entrypoint: &str, + amount: Amount, + sender: AccountAddress, + message: OwnedParameter, + ) -> Result + where + E: From + From + From, { + let contract_name = self.contract_name.as_contract_name().contract_name(); + let receive_name = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?; + + let payload = UpdateContractPayload { + amount, + address: self.address, + receive_name, + message, + }; + + let context = ContractContext::new_from_payload(sender, None, payload); + + let invoke_result = self + .client + .invoke_instance(BlockIdentifier::LastFinal, &context) + .await? + .response; + let payload = UpdateContractPayload { + amount, + address: context.contract, + receive_name: context.method, + message: context.parameter, + }; + match invoke_result { + InvokeContractResult::Success { used_energy, .. } => Ok(ContractUpdateBuilder::new( + self.client.clone(), + sender, + payload, + used_energy, + )), + InvokeContractResult::Failure { reason, .. } => Err(reason.into()), + } + } + /// Make the payload of a contract update with the specified parameter. pub fn make_update( &self, @@ -275,3 +349,192 @@ impl ContractClient { Ok(tx) } } + +/// 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, +} + +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()) + } + + /// Send the transaction and return a handle that can be queried + /// for the status. + pub async fn send( + mut 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 { + tx_hash, + client: self.client, + }) + } +} + +/// A handle returned when sending a smart contract update transaction. +/// This can be used to get the response of the update. +/// +/// 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 ContractUpdateHandle { + tx_hash: TransactionHash, + client: v2::Client, +} + +/// The [`Display`](std::fmt::Display) implementation displays the hash of the +/// transaction. +impl std::fmt::Display for ContractUpdateHandle { + 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 ContractUpdateError { + #[error("The status of the transaction could not be ascertained: {0}")] + Query(#[from] QueryError), + #[error("Contract update failed with reason: {0:?}")] + Failed(RejectReason), +} + +impl ContractUpdateHandle { + /// 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(ContractUpdateError::from(QueryError::RPCError( + RPCError::CallError(tonic::Status::invalid_argument(msg)), + ))) + }; + + match result.details { + crate::types::BlockItemSummaryDetails::AccountTransaction(at) => match at.effects { + AccountTransactionEffects::ContractUpdateIssued { effects } => { + let Some(execution_tree) = crate::types::execution_tree(effects) else { + return mk_error("Expected smart contract update, but received invalid execution tree."); + }; + Ok(ContractUpdateInfo { + execution_tree, + energy_cost: result.energy_cost, + cost: at.cost, + transaction_hash: self.tx_hash, + sender: at.sender, + }) + } + AccountTransactionEffects::None { + transaction_type: _, + reject_reason, + } => Err(ContractUpdateError::Failed(reject_reason)), + _ => mk_error("Expected smart contract update status, but received ."), + }, + crate::types::BlockItemSummaryDetails::AccountCreation(_) => { + mk_error("Expected smart contract update status, but received account creation.") + } + crate::types::BlockItemSummaryDetails::Update(_) => mk_error( + "Expected smart contract update 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(ContractUpdateError::Query(QueryError::RPCError( + RPCError::CallError(tonic::Status::deadline_exceeded( + "Deadline waiting for result of transaction is exceeded.", + )), + ))), + } + } +} diff --git a/src/types/smart_contracts.rs b/src/types/smart_contracts.rs index 367ef2e81..2b900770d 100644 --- a/src/types/smart_contracts.rs +++ b/src/types/smart_contracts.rs @@ -180,10 +180,9 @@ pub struct ContractContext { /// And with what parameter. #[serde(default)] pub parameter: OwnedParameter, - /// And what amount of energy to allow for execution. This should be small - /// enough so that it can be converted to interpreter energy. - #[serde(default = "return_default_invoke_energy")] - pub energy: Energy, + /// The energy to allow for execution. If not set the node decides on the + /// maximum amount. + pub energy: Option, } pub const DEFAULT_INVOKE_ENERGY: Energy = Energy { energy: 10_000_000 }; @@ -195,8 +194,6 @@ impl ContractContext { /// - the [`amount`](ContractContext::amount) is set to `0CCD` /// - the [`parameter`](ContractContext::parameter) is set to the empty /// parameter - /// - the [`energy`](ContractContext::energy) is set to - /// [`DEFAULT_INVOKE_ENERGY`] pub fn new(contract: ContractAddress, method: OwnedReceiveName) -> Self { Self { invoker: None, @@ -204,7 +201,7 @@ impl ContractContext { amount: Amount::zero(), method, parameter: OwnedParameter::default(), - energy: DEFAULT_INVOKE_ENERGY, + energy: None, } } @@ -216,22 +213,21 @@ impl ContractContext { /// - `payload` - the update contract payload to derive arguments from. pub fn new_from_payload( sender: AccountAddress, - energy: Energy, + energy: impl Into>, payload: UpdateContractPayload, ) -> Self { Self { - invoker: Some(sender.into()), - contract: payload.address, - amount: payload.amount, - method: payload.receive_name, + invoker: Some(sender.into()), + contract: payload.address, + amount: payload.amount, + method: payload.receive_name, parameter: payload.message, - energy, + energy: energy.into(), } } } fn return_zero_amount() -> Amount { Amount::from_micro_ccd(0) } -fn return_default_invoke_energy() -> Energy { DEFAULT_INVOKE_ENERGY } #[derive(SerdeDeserialize, SerdeSerialize, Debug, Clone, Into, From)] #[serde(transparent)] diff --git a/src/v2/conversions.rs b/src/v2/conversions.rs index 663adbea2..41584e642 100644 --- a/src/v2/conversions.rs +++ b/src/v2/conversions.rs @@ -618,7 +618,7 @@ impl TryFrom for crate::id::types::VerifyKey { } } -impl TryFrom for ed25519_dalek::PublicKey { +impl TryFrom for ed25519_dalek::VerifyingKey { type Error = tonic::Status; fn try_from(value: ip_info::IpCdiVerifyKey) -> Result { diff --git a/src/v2/dry_run.rs b/src/v2/dry_run.rs index c0c5aeefb..1aa8eabe5 100644 --- a/src/v2/dry_run.rs +++ b/src/v2/dry_run.rs @@ -337,7 +337,7 @@ impl From<&ContractContext> for DryRunInvokeInstance { amount: Some(context.amount.into()), entrypoint: Some(context.method.as_receive_name().into()), parameter: Some(context.parameter.as_ref().into()), - energy: Some(context.energy.into()), + energy: context.energy.map(From::from), } } } diff --git a/src/v2/mod.rs b/src/v2/mod.rs index aa98c1975..df1d2ac57 100644 --- a/src/v2/mod.rs +++ b/src/v2/mod.rs @@ -994,7 +994,7 @@ impl IntoRequest for (&BlockIdentifier, &Contr amount: Some(context.amount.into()), entrypoint: Some(context.method.as_receive_name().into()), parameter: Some(context.parameter.as_ref().into()), - energy: Some(context.energy.into()), + energy: context.energy.map(From::from), }) } }