Skip to content

Commit

Permalink
Merge pull request #150 from Concordium/contract-client-improvements
Browse files Browse the repository at this point in the history
Add new convenience methods to the contract client for sending contract updates
  • Loading branch information
abizjak authored Jan 22, 2024
2 parents 6cd3fec + 1512c0e commit 4e47edc
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 31 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion concordium-base
Submodule concordium-base updated 135 files
2 changes: 1 addition & 1 deletion examples/v2_dry_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/v2_invoke_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions src/cis0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
271 changes: 267 additions & 4 deletions src/contract_client.rs
Original file line number Diff line number Diff line change
@@ -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,
},
Expand All @@ -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
Expand Down Expand Up @@ -192,13 +194,85 @@ impl<Type> ContractClient<Type> {
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<P: contracts_common::Serial, E>(
&mut self,
entrypoint: &str,
amount: Amount,
sender: AccountAddress,
message: &P,
) -> Result<ContractUpdateBuilder, E>
where
E: From<NewReceiveNameError>
+ From<RejectReason>
+ From<v2::QueryError>
+ From<ExceedsParameterSize>, {
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<E>(
&mut self,
entrypoint: &str,
amount: Amount,
sender: AccountAddress,
message: OwnedParameter,
) -> Result<ContractUpdateBuilder, E>
where
E: From<NewReceiveNameError> + From<RejectReason> + From<v2::QueryError>, {
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<P: contracts_common::Serial, E>(
&self,
Expand Down Expand Up @@ -275,3 +349,192 @@ impl<Type> ContractClient<Type> {
Ok(tx)
}
}

/// A builder to simplify sending smart contract updates.
pub struct ContractUpdateBuilder {
payload: UpdateContractPayload,
sender: AccountAddress,
energy: Energy,
expiry: Option<TransactionTime>,
add_energy: Option<Energy>,
nonce: Option<Nonce>,
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<ContractUpdateHandle> {
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<ContractUpdateInfo, ContractUpdateError> {
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<ContractUpdateInfo, ContractUpdateError> {
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.",
)),
))),
}
}
}
Loading

0 comments on commit 4e47edc

Please sign in to comment.