use crate::{
indexer::ContractUpdateInfo,
types::{
smart_contracts::{self, ContractContext, InvokeContractResult, ReturnValue},
transactions, AccountTransactionEffects, ContractInitializedEvent, RejectReason,
},
v2::{
self,
dry_run::{self, DryRunTransaction},
BlockIdentifier, Client, Upward,
},
};
use concordium_base::{
base::{Energy, Nonce},
common::types::{self, TransactionTime},
contracts_common::{
self, schema::VersionedModuleSchema, AccountAddress, Address, Amount, ContractAddress,
Cursor, NewContractNameError, NewReceiveNameError,
},
hashes::TransactionHash,
smart_contracts::{
ContractEvent, ContractTraceElement, ExceedsParameterSize, ModuleReference,
OwnedContractName, OwnedParameter, OwnedReceiveName, WasmModule, WasmVersion,
},
transactions::{
construct::TRANSACTION_HEADER_SIZE, AccountTransaction, EncodedPayload,
InitContractPayload, PayloadLike, UpdateContractPayload,
},
};
pub use concordium_base::{cis2_types::MetadataUrl, cis4_types::*};
use concordium_smart_contract_engine::utils;
use serde_json::Value;
use std::{fmt, marker::PhantomData, sync::Arc};
use v2::{QueryError, RPCError};
#[derive(Debug)]
pub struct ContractClient<Type> {
pub client: Client,
pub address: ContractAddress,
pub contract_name: Arc<contracts_common::OwnedContractName>,
pub schema: Arc<Option<VersionedModuleSchema>>,
phantom: PhantomData<Type>,
}
impl<Type> Clone for ContractClient<Type> {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
address: self.address,
contract_name: self.contract_name.clone(),
phantom: PhantomData,
schema: self.schema.clone(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ContractTransactionMetadata {
pub sender_address: AccountAddress,
pub nonce: Nonce,
pub expiry: types::TransactionTime,
pub energy: transactions::send::GivenEnergy,
pub amount: types::Amount,
}
#[derive(Debug, thiserror::Error)]
pub enum ViewError {
#[error("Invalid receive name: {0}")]
InvalidName(#[from] NewReceiveNameError),
#[error("Node rejected with reason: {0:#?}")]
QueryFailed(v2::Upward<RejectReason>),
#[error("Response was not as expected: {0}")]
InvalidResponse(#[from] contracts_common::ParseError),
#[error("Network error: {0}")]
NetworkError(#[from] v2::QueryError),
#[error("Parameter is too large: {0}")]
ParameterError(#[from] ExceedsParameterSize),
}
impl From<v2::Upward<RejectReason>> for ViewError {
fn from(value: v2::Upward<RejectReason>) -> Self {
Self::QueryFailed(value)
}
}
pub struct TransactionBuilder<const ADD_ENERGY: bool, Inner> {
client: v2::Client,
sender: AccountAddress,
energy: Energy,
add_energy: Option<Energy>,
expiry: Option<TransactionTime>,
nonce: Option<Nonce>,
payload: transactions::Payload,
inner: Inner,
}
impl<const ADD: bool, Inner> TransactionBuilder<ADD, Inner> {
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,
}
}
pub fn expiry(mut self, expiry: TransactionTime) -> Self {
self.expiry = Some(expiry);
self
}
pub fn nonce(mut self, nonce: Nonce) -> Self {
self.nonce = Some(nonce);
self
}
pub fn current_energy(&self) -> Energy {
if ADD {
self.energy
+ self
.add_energy
.unwrap_or_else(|| std::cmp::max(50, self.energy.energy / 10).into())
} else {
self.energy
}
}
pub async fn send_inner<A>(
mut self,
signer: &impl transactions::ExactSizeTransactionSigner,
k: impl FnOnce(TransactionHash, v2::Client) -> A,
) -> v2::QueryResult<A> {
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<Inner> TransactionBuilder<true, Inner> {
pub fn extra_energy(mut self, energy: Energy) -> Self {
self.add_energy = Some(energy);
self
}
}
pub struct ContractInitInner<Type> {
event: ContractInitializedEvent,
phantom: PhantomData<Type>,
}
impl<Type> ContractInitInner<Type> {
fn new(event: ContractInitializedEvent) -> Self {
Self {
event,
phantom: PhantomData,
}
}
}
pub type ContractInitBuilder<Type> = TransactionBuilder<true, ContractInitInner<Type>>;
pub struct ContractInitHandle<Type> {
tx_hash: TransactionHash,
client: v2::Client,
phantom: PhantomData<Type>,
}
impl<Type> std::fmt::Display for ContractInitHandle<Type> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.tx_hash.fmt(f)
}
}
#[derive(Debug, thiserror::Error)]
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(v2::Upward<RejectReason>),
}
impl<Type> ContractInitHandle<Type> {
pub fn hash(&self) -> TransactionHash {
self.tx_hash
}
pub async fn wait_for_finalization(
mut self,
) -> Result<(ContractClient<Type>, Vec<ContractEvent>), 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)),
)))
};
let v2::upward::Upward::Known(details) = result.details else {
return mk_error(
"Expected smart contract initialization status, but received unknown block item \
details.",
);
};
match details {
crate::types::BlockItemSummaryDetails::AccountTransaction(at) => {
let v2::upward::Upward::Known(effects) = at.effects else {
return mk_error(
"Expected smart contract initialization status, but received unknown \
block item details.",
);
};
match effects {
AccountTransactionEffects::ContractInitialized { data } => {
let contract_client =
ContractClient::create(self.client, data.address).await?;
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.",
),
crate::types::BlockItemSummaryDetails::TokenCreationDetails(_) => mk_error(
"Expected smart contract initialization status, but received token creation chain \
update instruction.",
),
}
}
pub async fn wait_for_finalization_timeout(
self,
timeout: std::time::Duration,
) -> Result<(ContractClient<Type>, Vec<ContractEvent>), 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)]
pub enum DryRunNewInstanceError {
#[error("Dry run succeeded, but contract initialization failed due to {0:#?}.")]
Failed(v2::Upward<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 {
min: Energy,
},
}
impl From<v2::Upward<RejectReason>> for DryRunNewInstanceError {
fn from(value: v2::Upward<RejectReason>) -> Self {
Self::Failed(value)
}
}
impl<Type> ContractInitBuilder<Type> {
pub async fn dry_run_new_instance<P: contracts_common::Serial>(
client: Client,
sender: AccountAddress,
mod_ref: ModuleReference,
name: &str,
amount: Amount,
parameter: &P,
) -> Result<Self, DryRunNewInstanceError> {
let parameter = OwnedParameter::from_serial(parameter)?;
Self::dry_run_new_instance_raw(client, sender, mod_ref, name, amount, parameter).await
}
pub async fn dry_run_new_instance_raw(
mut client: Client,
sender: AccountAddress,
mod_ref: ModuleReference,
name: &str,
amount: Amount,
parameter: OwnedParameter,
) -> Result<Self, DryRunNewInstanceError> {
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.known_or_else(|_unknown| {
dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
"Unexpected response from dry-running a contract initialization.",
))
})? {
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),
))
}
pub fn event(&self) -> &ContractInitializedEvent {
&self.inner.event
}
pub async fn send(
self,
signer: &impl transactions::ExactSizeTransactionSigner,
) -> v2::QueryResult<ContractInitHandle<Type>> {
let phantom = self.inner.phantom;
self.send_inner(signer, |tx_hash, client| ContractInitHandle {
tx_hash,
client,
phantom,
})
.await
}
}
pub type ModuleDeployBuilder = TransactionBuilder<false, ModuleReference>;
#[derive(thiserror::Error, Debug)]
pub enum DryRunModuleDeployError {
#[error("Dry run succeeded, but module deployment failed due to {0:#?}.")]
Failed(v2::Upward<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 {
min: Energy,
},
}
impl DryRunModuleDeployError {
pub fn already_exists(&self) -> bool {
let Self::Failed(reason) = self else {
return false;
};
matches!(
reason,
v2::Upward::Known(RejectReason::ModuleHashAlreadyExists { .. })
)
}
}
impl From<v2::Upward<RejectReason>> for DryRunModuleDeployError {
fn from(value: v2::Upward<RejectReason>) -> Self {
Self::Failed(value)
}
}
impl ModuleDeployBuilder {
pub async fn dry_run_module_deploy(
mut client: Client,
sender: AccountAddress,
module: WasmModule,
) -> Result<Self, DryRunModuleDeployError> {
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.known_or_else(|_unknown| {
dry_run::DryRunError::CallError(tonic::Status::invalid_argument(
"Unexpected response from dry-running a module deployment.",
))
})? {
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 module deployment.",
))
.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 {
pub async fn send(
self,
signer: &impl transactions::ExactSizeTransactionSigner,
) -> v2::QueryResult<ModuleDeployHandle> {
self.send_inner(signer, |tx_hash, client| ModuleDeployHandle {
tx_hash,
client,
})
.await
}
}
pub struct ModuleDeployHandle {
tx_hash: TransactionHash,
client: v2::Client,
}
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)]
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(v2::Upward<RejectReason>),
}
#[derive(Debug, Clone, Copy)]
pub struct ModuleDeployData {
pub energy: Energy,
pub cost: Amount,
pub module_reference: ModuleReference,
}
impl ModuleDeployHandle {
pub fn hash(&self) -> TransactionHash {
self.tx_hash
}
pub async fn wait_for_finalization(mut self) -> Result<ModuleDeployData, ModuleDeployError> {
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)),
)))
};
let v2::upward::Upward::Known(details) = result.details else {
return mk_error(
"Expected module deploy status, but received unknown block item details.",
);
};
match details {
crate::types::BlockItemSummaryDetails::AccountTransaction(at) => {
let v2::upward::Upward::Known(effects) = at.effects else {
return mk_error(
"Expected module deploy status, but received unknown block item effect.",
);
};
match 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.")
}
crate::types::BlockItemSummaryDetails::TokenCreationDetails(_) => mk_error(
"Expected module deploy status, but received token creation chain update \
instruction.",
),
}
}
pub async fn wait_for_finalization_timeout(
self,
timeout: std::time::Duration,
) -> Result<ModuleDeployData, ModuleDeployError> {
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.",
)),
))),
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorSchema(pub Value);
impl std::fmt::Display for ErrorSchema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
Value::Object(map) => {
if let Some(key) = map.keys().next() {
write!(f, "{}", key)?;
if let Some(value) = map.values().next() {
if value.is_array() {
write!(f, "{}", ErrorSchema(value.clone()))?;
}
}
}
}
Value::Array(arr) => {
if let Some(value) = arr.iter().next() {
write!(f, "::{}", ErrorSchema(value.clone()))?;
}
}
_ => write!(f, "{}", self.0)?,
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum DecodedReason {
Std {
reject_reason: i32,
parsed: ConcordiumStdRejectReason,
},
Custom {
return_value: ReturnValue,
reject_reason: i32,
parsed: ErrorSchema,
},
}
impl std::fmt::Display for DecodedReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DecodedReason::Std { parsed, .. } => {
write!(f, "{}", parsed)
}
DecodedReason::Custom { parsed, .. } => {
write!(f, "{}", parsed)
}
}
}
}
pub enum InvokeContractOutcome {
Success(SimulatedTransaction),
Failure(RejectedTransaction),
}
pub type SimulatedTransaction = ContractUpdateBuilder;
#[derive(Debug, Clone)]
pub struct RejectedTransaction {
pub return_value: Option<ReturnValue>,
pub reason: Upward<RejectReason>,
pub decoded_reason: Option<DecodedReason>,
pub used_energy: Energy,
pub payload: transactions::Payload,
}
impl InvokeContractOutcome {
pub fn success(self) -> Result<SimulatedTransaction, RejectedTransaction> {
match self {
InvokeContractOutcome::Success(simulated_transaction) => Ok(simulated_transaction),
InvokeContractOutcome::Failure(rejected_transaction) => Err(rejected_transaction),
}
}
}
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
#[repr(i32)]
pub enum ConcordiumStdRejectReason {
#[error("[Unspecified (Default reject)]")]
Unspecified = -2147483648, #[error("[Error ()]")]
Unit = -2147483647, #[error("[ParseError]")]
Parse = -2147483646, #[error("[LogError::Full]")]
LogFull = -2147483645,
#[error("[LogError::Malformed]")]
LogMalformed = -2147483644,
#[error("[NewContractNameError::MissingInitPrefix]")]
NewContractNameMissingInitPrefix = -2147483643,
#[error("[NewContractNameError::TooLong]")]
NewContractNameTooLong = -2147483642,
#[error("[NewReceiveNameError::MissingDotSeparator]")]
NewReceiveNameMissingDotSeparator = -2147483641,
#[error("[NewReceiveNameError::TooLong]")]
NewReceiveNameTooLong = -2147483640,
#[error("[NewContractNameError::ContainsDot]")]
NewContractNameContainsDot = -2147483639,
#[error("[NewContractNameError::InvalidCharacters]")]
NewContractNameInvalidCharacters = -2147483638,
#[error("[NewReceiveNameError::InvalidCharacters]")]
NewReceiveNameInvalidCharacters = -2147483637,
#[error("[NotPayableError]")]
NotPayableError = -2147483636,
#[error("[TransferError::AmountTooLarge]")]
TransferAmountTooLarge = -2147483635,
#[error("[TransferError::MissingAccount]")]
TransferMissingAccount = -2147483634,
#[error("[CallContractError::AmountTooLarge]")]
CallContractAmountTooLarge = -2147483633,
#[error("[CallContractError::MissingAccount]")]
CallContractMissingAccount = -2147483632,
#[error("[CallContractError::MissingContract]")]
CallContractMissingContract = -2147483631,
#[error("[CallContractError::MissingEntrypoint]")]
CallContractMissingEntrypoint = -2147483630,
#[error("[CallContractError::MessageFailed]")]
CallContractMessageFailed = -2147483629,
#[error("[CallContractError::LogicReject]")]
CallContractLogicReject = -2147483628,
#[error("[CallContractError::Trap]")]
CallContractTrap = -2147483627,
#[error("[UpgradeError::MissingModule]")]
UpgradeMissingModule = -2147483626,
#[error("[UpgradeError::MissingContract]")]
UpgradeMissingContract = -2147483625,
#[error("[UpgradeError::UnsupportedModuleVersion]")]
UpgradeUnsupportedModuleVersion = -2147483624,
#[error("[QueryAccountBalanceError]")]
QueryAccountBalanceError = -2147483623,
#[error("[QueryContractBalanceError]")]
QueryContractBalanceError = -2147483622,
}
pub fn decode_concordium_std_error(reject_reason: i32) -> Option<ConcordiumStdRejectReason> {
if (-2147483648..=-2147483622).contains(&reject_reason) {
let reason: ConcordiumStdRejectReason = unsafe { ::std::mem::transmute(reject_reason) };
Some(reason)
} else {
None
}
}
pub fn decode_smart_contract_revert(
return_value: Option<&ReturnValue>,
reject_reason: &RejectReason,
schema: Option<&VersionedModuleSchema>,
) -> Option<DecodedReason> {
match reject_reason {
RejectReason::RejectedReceive {
reject_reason: error_code,
contract_address: _,
receive_name,
parameter: _,
} => {
let receive_name = receive_name.as_receive_name();
if let Some(decoded_error) = decode_concordium_std_error(*error_code) {
return Some(DecodedReason::Std {
reject_reason: *error_code,
parsed: decoded_error,
});
}
let schema = schema?;
let (Some(error_schema), Some(return_value)) = (
schema
.get_receive_error_schema(
receive_name.contract_name(),
receive_name.entrypoint_name().into(),
)
.ok(),
return_value,
) else {
return None;
};
let mut cursor = Cursor::new(&return_value.value);
error_schema
.to_json(&mut cursor)
.ok()
.map(|decoded_reason| DecodedReason::Custom {
return_value: return_value.clone(),
reject_reason: *error_code,
parsed: ErrorSchema(decoded_reason),
})
}
_ => None,
}
}
impl<Type> ContractClient<Type> {
pub async fn create(mut client: Client, address: ContractAddress) -> v2::QueryResult<Self> {
let contract_instance_info = client
.get_instance_info(address, BlockIdentifier::LastFinal)
.await?
.response;
let contract_name = contract_instance_info.name().clone();
let module_reference = contract_instance_info.source_module();
let wasm_module = client
.get_module_source(&module_reference, BlockIdentifier::LastFinal)
.await?
.response;
let schema = match wasm_module.version {
WasmVersion::V0 => utils::get_embedded_schema_v0(wasm_module.source.as_ref()).ok(),
WasmVersion::V1 => utils::get_embedded_schema_v1(wasm_module.source.as_ref()).ok(),
};
Ok(Self {
client,
address,
contract_name: Arc::new(contract_name),
phantom: PhantomData,
schema: Arc::new(schema),
})
}
pub fn new(client: Client, address: ContractAddress, contract_name: OwnedContractName) -> Self {
Self {
client,
address,
contract_name: Arc::new(contract_name),
phantom: PhantomData,
schema: Arc::new(None),
}
}
pub fn new_with_schema(
client: Client,
address: ContractAddress,
contract_name: OwnedContractName,
schema: VersionedModuleSchema,
) -> Self {
Self {
client,
address,
contract_name: Arc::new(contract_name),
phantom: PhantomData,
schema: Arc::new(Some(schema)),
}
}
pub async fn view<P: contracts_common::Serial, A: contracts_common::Deserial, E>(
&mut self,
entrypoint: &str,
parameter: &P,
bi: impl v2::IntoBlockIdentifier,
) -> Result<A, E>
where
E: From<NewReceiveNameError>
+ From<v2::Upward<RejectReason>>
+ From<contracts_common::ParseError>
+ From<v2::QueryError>
+ From<ExceedsParameterSize>,
{
let parameter = OwnedParameter::from_serial(parameter)?;
self.view_raw::<A, E>(entrypoint, parameter, bi).await
}
pub async fn view_raw<A: contracts_common::Deserial, E>(
&mut self,
entrypoint: &str,
parameter: OwnedParameter,
bi: impl v2::IntoBlockIdentifier,
) -> Result<A, E>
where
E: From<NewReceiveNameError>
+ From<v2::Upward<RejectReason>>
+ From<contracts_common::ParseError>
+ From<v2::QueryError>,
{
let ir = self
.invoke_raw::<E>(entrypoint, Amount::zero(), None, parameter, bi)
.await?;
match ir {
smart_contracts::InvokeContractResult::Success { return_value, .. } => {
let Some(bytes) = return_value else {
return Err(contracts_common::ParseError {}.into());
};
let response: A = contracts_common::from_bytes(&bytes.value)?;
Ok(response)
}
smart_contracts::InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
}
}
pub async fn invoke_raw<E>(
&mut self,
entrypoint: &str,
amount: Amount,
invoker: Option<Address>,
parameter: OwnedParameter,
bi: impl v2::IntoBlockIdentifier,
) -> Result<InvokeContractResult, E>
where
E: From<NewReceiveNameError> + From<v2::Upward<RejectReason>> + From<v2::QueryError>,
{
let contract_name = self.contract_name.as_contract_name().contract_name();
let method = OwnedReceiveName::try_from(format!("{contract_name}.{entrypoint}"))?;
let context = ContractContext {
invoker,
contract: self.address,
amount,
method,
parameter,
energy: None,
};
let invoke_result = self.client.invoke_instance(bi, &context).await?.response;
Ok(invoke_result)
}
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<v2::Upward<RejectReason>>
+ From<v2::QueryError>
+ From<ExceedsParameterSize>,
{
let message = OwnedParameter::from_serial(message)?;
self.dry_run_update_raw(entrypoint, amount, sender, message)
.await
}
pub async fn dry_run_update_with_reject_reason_info<P: contracts_common::Serial, E>(
&mut self,
entrypoint: &str,
amount: Amount,
sender: AccountAddress,
message: &P,
) -> Result<InvokeContractOutcome, E>
where
E: From<NewReceiveNameError> + From<v2::QueryError> + From<ExceedsParameterSize>,
{
let message = OwnedParameter::from_serial(message)?;
self.dry_run_update_raw_with_reject_reason_info(entrypoint, amount, sender, message)
.await
}
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<v2::Upward<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,
return_value,
events,
} => Ok(ContractUpdateBuilder::new(
self.client.clone(),
sender,
used_energy,
transactions::Payload::Update { payload },
ContractUpdateInner {
return_value,
events,
},
)),
InvokeContractResult::Failure { reason, .. } => Err(reason.into()),
}
}
pub async fn dry_run_update_raw_with_reject_reason_info<E>(
&mut self,
entrypoint: &str,
amount: Amount,
sender: AccountAddress,
message: OwnedParameter,
) -> Result<InvokeContractOutcome, E>
where
E: From<NewReceiveNameError> + 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: receive_name.clone(),
message,
};
let context = ContractContext::new_from_payload(sender, None, payload.clone());
let invoke_result = self
.client
.invoke_instance(BlockIdentifier::LastFinal, &context)
.await?
.response;
match invoke_result {
InvokeContractResult::Success {
used_energy,
return_value,
events,
} => Ok(InvokeContractOutcome::Success(SimulatedTransaction::new(
self.client.clone(),
sender,
used_energy,
transactions::Payload::Update { payload },
ContractUpdateInner {
return_value,
events,
},
))),
InvokeContractResult::Failure {
reason,
return_value,
used_energy,
} => {
let decoded_reason = reason.as_ref().known().and_then(|reason| {
decode_smart_contract_revert(
return_value.as_ref(),
reason,
(*self.schema).as_ref(),
)
});
Ok(InvokeContractOutcome::Failure(RejectedTransaction {
payload: transactions::Payload::Update { payload },
return_value,
used_energy,
reason,
decoded_reason,
}))
}
}
}
pub fn make_update<P: contracts_common::Serial, E>(
&self,
signer: &impl transactions::ExactSizeTransactionSigner,
metadata: &ContractTransactionMetadata,
entrypoint: &str,
message: &P,
) -> Result<AccountTransaction<EncodedPayload>, E>
where
E: From<NewReceiveNameError> + From<ExceedsParameterSize>,
{
let message = OwnedParameter::from_serial(message)?;
self.make_update_raw::<E>(signer, metadata, entrypoint, message)
}
pub async fn update<P: contracts_common::Serial, E>(
&mut self,
signer: &impl transactions::ExactSizeTransactionSigner,
metadata: &ContractTransactionMetadata,
entrypoint: &str,
message: &P,
) -> Result<TransactionHash, E>
where
E: From<NewReceiveNameError> + From<v2::RPCError> + From<ExceedsParameterSize>,
{
let message = OwnedParameter::from_serial(message)?;
self.update_raw::<E>(signer, metadata, entrypoint, message)
.await
}
pub async fn update_raw<E>(
&mut self,
signer: &impl transactions::ExactSizeTransactionSigner,
metadata: &ContractTransactionMetadata,
entrypoint: &str,
message: OwnedParameter,
) -> Result<TransactionHash, E>
where
E: From<NewReceiveNameError> + From<v2::RPCError>,
{
let tx = self.make_update_raw::<E>(signer, metadata, entrypoint, message)?;
let hash = self.client.send_account_transaction(tx).await?;
Ok(hash)
}
pub fn make_update_raw<E>(
&self,
signer: &impl transactions::ExactSizeTransactionSigner,
metadata: &ContractTransactionMetadata,
entrypoint: &str,
message: OwnedParameter,
) -> Result<AccountTransaction<EncodedPayload>, E>
where
E: From<NewReceiveNameError>,
{
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: metadata.amount,
address: self.address,
receive_name,
message,
};
let tx = transactions::send::make_and_sign_transaction(
signer,
metadata.sender_address,
metadata.nonce,
metadata.expiry,
metadata.energy,
transactions::Payload::Update { payload },
);
Ok(tx)
}
}
pub struct ContractUpdateInner {
return_value: Option<ReturnValue>,
events: Vec<Upward<ContractTraceElement>>,
}
pub type ContractUpdateBuilder = TransactionBuilder<true, ContractUpdateInner>;
impl ContractUpdateBuilder {
pub async fn send(
self,
signer: &impl transactions::ExactSizeTransactionSigner,
) -> v2::QueryResult<ContractUpdateHandle> {
self.send_inner(signer, |tx_hash, client| ContractUpdateHandle {
tx_hash,
client,
})
.await
}
pub fn return_value(&self) -> Option<&ReturnValue> {
self.inner.return_value.as_ref()
}
pub fn events(&self) -> &[Upward<ContractTraceElement>] {
&self.inner.events
}
}
pub struct ContractUpdateHandle {
tx_hash: TransactionHash,
client: v2::Client,
}
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)]
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(v2::Upward<RejectReason>),
}
impl ContractUpdateHandle {
pub fn hash(&self) -> TransactionHash {
self.tx_hash
}
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)),
)))
};
let v2::upward::Upward::Known(details) = result.details else {
return mk_error(
"Expected smart contract update status, but received unknown block item details.",
);
};
match details {
crate::types::BlockItemSummaryDetails::AccountTransaction(at) => {
let v2::upward::Upward::Known(effects) = at.effects else {
return mk_error(
"Expected smart contract update status, but received unknown block item \
effects.",
);
};
match 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 did not receive it."),
}
}
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.",
),
crate::types::BlockItemSummaryDetails::TokenCreationDetails(_) => mk_error(
"Expected smart contract update status, but received token creation chain update \
instruction.",
),
}
}
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.",
)),
))),
}
}
}