diff --git a/CHANGELOG.md b/CHANGELOG.md index a90130a81..e8bc812d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ ## Unreleased changes + - The sdk now requires a `rustc` version at least 1.67 (Before it required version 1.66). - Add a `contract_update` helper analogous to `contract_init` to extract an execution tree from a smart contract update transaction. - Add a `ccd_cost` helper to `ChainParameters` to convert NRG cost to CCD. +- Add support for `DryRun`. Requires a node version at least 6.2. ## 3.1.0 diff --git a/Cargo.toml b/Cargo.toml index 0e8be0ddf..6d02ce28b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ num-bigint = "0.4" num-traits = "0.2" tokio-postgres = { version = "^0.7.8", features = ["with-serde_json-1"], optional = true } http = "0.2" +tokio-stream = "0.1" concordium_base = { version = "3.1.0", path = "./concordium-base/rust-src/concordium_base/", features = ["encryption"] } concordium-smart-contract-engine = { version = "3.0", path = "./concordium-base/smart-contracts/wasm-chain-integration/", default-features = false, features = ["async"]} diff --git a/examples/v2_dry_run.rs b/examples/v2_dry_run.rs new file mode 100644 index 000000000..fffb67550 --- /dev/null +++ b/examples/v2_dry_run.rs @@ -0,0 +1,120 @@ +//! Example of dry-run functionality of the node. + +use anyhow::Context; +use clap::AppSettings; +use concordium_base::{ + base::Energy, + common::types::Timestamp, + contracts_common::{Address, Amount, ContractAddress, EntrypointName}, + smart_contracts::{OwnedParameter, OwnedReceiveName}, + transactions::Payload, +}; +use concordium_rust_sdk::{ + types::smart_contracts::ContractContext, + v2::{self, dry_run::DryRunTransaction, BlockIdentifier}, +}; +use structopt::StructOpt; + +#[derive(StructOpt)] +struct App { + #[structopt( + long = "node", + help = "GRPC interface of the node.", + default_value = "http://localhost:20000" + )] + endpoint: v2::Endpoint, +} + +/// Test all dry run operations. +async fn test_all(endpoint: v2::Endpoint) -> anyhow::Result<()> { + // Connect to endpoint. + let mut client = v2::Client::new(endpoint).await.context("Cannot connect.")?; + // Start the dry run session. + let mut dry_run = client.begin_dry_run().await?; + println!( + "Timeout: {:?}\nEnergy quota: {:?}", + dry_run.timeout(), + dry_run.energy_quota() + ); + // Load the best block. + let fut1 = dry_run + .begin_load_block_state(BlockIdentifier::Best) + .await?; + // Load the last finalized block. + let fut2 = dry_run + .begin_load_block_state(BlockIdentifier::LastFinal) + .await?; + // Await the results of the loads in the reverse order. + let res2 = fut2.await?; + let res1 = fut1.await?; + println!( + "Best block: {} ({:?})", + res1.inner.block_hash, res1.inner.current_timestamp + ); + println!( + "Last final: {} ({:?})", + res2.inner.block_hash, res2.inner.current_timestamp + ); + // Get account info for account at index 0. + let res3 = dry_run + .get_account_info(&v2::AccountIdentifier::Index(0.into())) + .await?; + println!("Account 0: {}", res3.inner.account_address); + // Get contract info for contract at address <0,0>. + let contract_addr = ContractAddress { + index: 0, + subindex: 0, + }; + let res4 = dry_run.get_instance_info(&contract_addr).await?; + println!( + "Instance {contract_addr}: {} {:?}", + res4.inner.name(), + res4.inner.entrypoints() + ); + // Try to invoke the entrypoint "view" on the <0,0> contract. + let invoke_target = OwnedReceiveName::construct( + res4.inner.name().as_contract_name(), + EntrypointName::new("view")?, + )?; + let parameter = OwnedParameter::empty(); + let context = ContractContext { + invoker: Some(Address::Account(res3.inner.account_address)), + contract: contract_addr, + amount: Amount::zero(), + method: invoke_target.clone(), + parameter: parameter.clone(), + energy: 10000.into(), + }; + let res5 = dry_run.invoke_instance(&context).await; + println!("Invoked view on {contract_addr}: {:?}", res5); + // Mint to account 0. + let _res6 = dry_run + .mint_to_account(&res3.inner.account_address, Amount::from_ccd(21)) + .await?; + // Update the timestamp to now. + let _fut7 = dry_run.begin_set_timestamp(Timestamp::now()).await?; + // Execute a transfer to the encrypted balance on account 0. + let payload = Payload::TransferToEncrypted { + amount: Amount::from_ccd(20), + }; + let transaction = + DryRunTransaction::new(res3.inner.account_address, Energy::from(500), &payload); + let fut8 = dry_run.begin_run_transaction(transaction).await?; + // We are done sending requests, so close the request stream. + dry_run.close(); + let res8 = fut8.await?; + println!("Transferred to encrypted: {:?}", res8); + + Ok(()) +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> anyhow::Result<()> { + let app = { + let app = App::clap().global_setting(AppSettings::ColoredHelp); + let matches = app.get_matches(); + App::from_clap(&matches) + }; + + test_all(app.endpoint.clone()).await +} diff --git a/src/types/smart_contracts.rs b/src/types/smart_contracts.rs index a167cfb44..367ef2e81 100644 --- a/src/types/smart_contracts.rs +++ b/src/types/smart_contracts.rs @@ -233,7 +233,7 @@ impl ContractContext { 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)] +#[derive(SerdeDeserialize, SerdeSerialize, Debug, Clone, Into, From)] #[serde(transparent)] pub struct ReturnValue { #[serde(with = "crate::internal::byte_array_hex")] diff --git a/src/v2/conversions.rs b/src/v2/conversions.rs index 951286b17..0b2c42a4b 100644 --- a/src/v2/conversions.rs +++ b/src/v2/conversions.rs @@ -842,6 +842,14 @@ impl From for concordium_base::common::types::Timestamp { fn from(value: Timestamp) -> Self { value.value.into() } } +impl From for Timestamp { + fn from(value: concordium_base::common::types::Timestamp) -> Self { + Timestamp { + value: value.millis, + } + } +} + impl TryFrom for chrono::DateTime { type Error = tonic::Status; @@ -1193,6 +1201,18 @@ impl TryFrom } } +impl TryFrom for super::types::AccountTransactionDetails { + type Error = tonic::Status; + + fn try_from(v: AccountTransactionDetails) -> Result { + Ok(Self { + cost: v.cost.require()?.into(), + sender: v.sender.require()?.try_into()?, + effects: v.effects.require()?.try_into()?, + }) + } +} + impl TryFrom for super::types::BlockItemSummary { type Error = tonic::Status; @@ -1203,13 +1223,7 @@ impl TryFrom for super::types::BlockItemSummary { hash: value.hash.require()?.try_into()?, details: match value.details.require()? { block_item_summary::Details::AccountTransaction(v) => { - super::types::BlockItemSummaryDetails::AccountTransaction( - super::types::AccountTransactionDetails { - cost: v.cost.require()?.into(), - sender: v.sender.require()?.try_into()?, - effects: v.effects.require()?.try_into()?, - }, - ) + super::types::BlockItemSummaryDetails::AccountTransaction(v.try_into()?) } block_item_summary::Details::AccountCreation(v) => { super::types::BlockItemSummaryDetails::AccountCreation( diff --git a/src/v2/dry_run.rs b/src/v2/dry_run.rs new file mode 100644 index 000000000..c0c5aeefb --- /dev/null +++ b/src/v2/dry_run.rs @@ -0,0 +1,1240 @@ +use super::{ + generated::{ + self, account_transaction_payload, dry_run_error_response, dry_run_request, + DryRunInvokeInstance, DryRunMintToAccount, DryRunSignature, DryRunStateOperation, + }, + AccountIdentifier, IntoBlockIdentifier, +}; +use crate::{ + types::{ + smart_contracts::{ContractContext, InstanceInfo, ReturnValue}, + AccountInfo, AccountTransactionDetails, RejectReason, + }, + v2::{generated::DryRunStateQuery, Require}, +}; +use concordium_base::{ + base::{Energy, ProtocolVersion}, + common::types::{CredentialIndex, KeyIndex, Timestamp}, + contracts_common::{AccountAddress, Amount, ContractAddress}, + hashes::BlockHash, + smart_contracts::ContractTraceElement, + transactions::{EncodedPayload, PayloadLike}, +}; +use futures::*; + +mod shared_receiver { + use futures::{stream::Stream, StreamExt}; + use tokio::{ + sync::{mpsc, oneshot}, + task::JoinHandle, + }; + + /// A `SharedReceiver` wraps an underlying stream so that multiple clients + /// can queue to receive items from the stream. + pub struct SharedReceiver { + senders: mpsc::UnboundedSender>, + task: JoinHandle<()>, + } + + impl Drop for SharedReceiver { + fn drop(&mut self) { self.task.abort(); } + } + + impl SharedReceiver + where + I: Send + 'static, + { + /// Construct a new shared receiver. This spawns a background task that + /// pairs waiters with incoming stream items. + pub fn new(stream: impl Stream + Unpin + Send + 'static) -> Self { + let (senders, rec_senders) = mpsc::unbounded_channel::>(); + let task = tokio::task::spawn(async { + let mut zipped = stream.zip(tokio_stream::wrappers::UnboundedReceiverStream::from( + rec_senders, + )); + while let Some((item, sender)) = zipped.next().await { + let _ = sender.send(item); + } + }); + SharedReceiver { senders, task } + } + + /// Claim the next item from the stream when it becomes available. + /// Returns `None` if the stream is already closed. Otherwise returns a + /// [`oneshot::Receiver`] that can be `await`ed to retrieve the item. + pub fn next(&self) -> Option> { + let (send, recv) = oneshot::channel(); + self.senders.send(send).ok()?; + Some(recv) + } + } +} + +/// An error response to a dry-run request. +#[derive(thiserror::Error, Debug)] +pub enum ErrorResult { + /// The current block state is undefined. It should be initialized with a + /// `load_block_state` request before any other operations. + #[error("block state not loaded")] + NoState, + /// The requested block was not found, so its state could not be loaded. + /// Response to `load_block_state`. + #[error("block not found")] + BlockNotFound, + /// The specified account was not found. + /// Response to `get_account_info`, `mint_to_account` and `run_transaction`. + #[error("account not found")] + AccountNotFound, + /// The specified instance was not found. + /// Response to `get_instance_info`. + #[error("contract instance not found")] + InstanceNotFound, + /// The amount to mint would overflow the total CCD supply. + /// Response to `mint_to_account`. + #[error("mint amount exceeds limit")] + AmountOverLimit { + /// The maximum amount that can be minted. + amount_limit: Amount, + }, + /// The balance of the sender account is not sufficient to pay for the + /// operation. Response to `run_transaction`. + #[error("account balance insufficient")] + BalanceInsufficient { + /// The balance required to pay for the operation. + required_amount: Amount, + /// The actual amount available on the account to pay for the operation. + available_amount: Amount, + }, + /// The energy supplied for the transaction was not sufficient to perform + /// the basic checks. Response to `run_transaction`. + #[error("energy insufficient")] + EnergyInsufficient { + /// The energy required to perform the basic checks on the transaction. + /// Note that this may not be sufficient to also execute the + /// transaction. + energy_required: Energy, + }, + /// The contract invocation failed. + /// Response to `invoke_instance`. + #[error("invoke instance failed")] + InvokeFailure { + /// If invoking a V0 contract this is not provided, otherwise it is + /// the return value produced by the call unless the call failed + /// with out of energy or runtime error. If the V1 contract + /// terminated with a logic error then the return value is + /// present. + return_value: Option, + /// Energy used by the execution. + used_energy: Energy, + /// Contract execution failed for the given reason. + reason: RejectReason, + }, +} + +impl TryFrom for ErrorResult { + type Error = tonic::Status; + + fn try_from(value: dry_run_error_response::Error) -> Result { + use dry_run_error_response::Error; + let res = match value { + Error::NoState(_) => Self::NoState, + Error::BlockNotFound(_) => Self::BlockNotFound, + Error::AccountNotFound(_) => Self::AccountNotFound, + Error::InstanceNotFound(_) => Self::InstanceNotFound, + Error::AmountOverLimit(e) => Self::AmountOverLimit { + amount_limit: e.amount_limit.require()?.into(), + }, + Error::BalanceInsufficient(e) => Self::BalanceInsufficient { + required_amount: e.required_amount.require()?.into(), + available_amount: e.available_amount.require()?.into(), + }, + Error::EnergyInsufficient(e) => Self::EnergyInsufficient { + energy_required: e.energy_required.require()?.into(), + }, + Error::InvokeFailed(e) => Self::InvokeFailure { + return_value: e.return_value.map(ReturnValue::from), + used_energy: e.used_energy.require()?.into(), + reason: e.reason.require()?.try_into()?, + }, + }; + Ok(res) + } +} + +/// An error resulting from a dry-run operation. +#[derive(thiserror::Error, Debug)] +pub enum DryRunError { + /// The server responded with an error code. + /// In this case, no futher requests will be accepted in the dry-run + /// session. + #[error("gRPC error: {0}")] + CallError(#[from] tonic::Status), + /// The dry-run operation failed. + /// In this case, further dry-run requests are possible in the same session. + #[error("dry-run operation failed: {result}")] + OperationFailed { + /// The error result. + #[source] + result: ErrorResult, + /// The energy quota remaining for subsequent dry-run requests in the + /// session. + quota_remaining: Energy, + }, +} + +/// A result value together with the remaining energy quota at the completion of +/// the operation. +#[derive(Debug, Clone)] +pub struct WithRemainingQuota { + /// The result value. + pub inner: T, + /// The remaining energy quota. + pub quota_remaining: Energy, +} + +/// The successful result of [`DryRun::load_block_state`]. +#[derive(Debug, Clone)] +pub struct BlockStateLoaded { + /// The timestamp of the block, taken to be the current timestamp when + /// executing transactions. + pub current_timestamp: Timestamp, + /// The hash of the block that was loaded. + pub block_hash: BlockHash, + /// The protocol version at the specified block. The behavior of operations + /// can vary across protocol version. + pub protocol_version: ProtocolVersion, +} + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!(result, ErrorResult::BlockNotFound) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + match response { + generated::dry_run_success_response::Response::BlockStateLoaded(loaded) => { + let protocol_version = + generated::ProtocolVersion::from_i32(loaded.protocol_version) + .ok_or_else(|| tonic::Status::unknown("Unknown protocol version"))? + .into(); + let loaded = BlockStateLoaded { + current_timestamp: loaded.current_timestamp.require()?.into(), + block_hash: loaded.block_hash.require()?.try_into()?, + protocol_version, + }; + Ok(WithRemainingQuota { + inner: loaded, + quota_remaining, + }) + } + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!(result, ErrorResult::NoState | ErrorResult::AccountNotFound) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + use generated::dry_run_success_response::*; + match response { + Response::AccountInfo(info) => Ok(WithRemainingQuota { + inner: info.try_into()?, + quota_remaining, + }), + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!(result, ErrorResult::NoState | ErrorResult::InstanceNotFound) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + use generated::dry_run_success_response::*; + match response { + Response::InstanceInfo(info) => Ok(WithRemainingQuota { + inner: info.try_into()?, + quota_remaining, + }), + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +impl From<&ContractContext> for DryRunInvokeInstance { + fn from(context: &ContractContext) -> Self { + DryRunInvokeInstance { + invoker: context.invoker.as_ref().map(|a| a.into()), + instance: Some((&context.contract).into()), + 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()), + } + } +} + +/// The successful result of [`DryRun::invoke_instance`]. +#[derive(Debug, Clone)] +pub struct InvokeInstanceSuccess { + /// The return value for a V1 contract call. Absent for a V0 contract call. + pub return_value: Option, + /// The effects produced by contract execution. + pub events: Vec, + /// The energy used by the execution. + pub used_energy: Energy, +} + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!( + result, + ErrorResult::NoState + | ErrorResult::InvokeFailure { + return_value: _, + used_energy: _, + reason: _, + } + ) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + use generated::dry_run_success_response::*; + match response { + Response::InvokeSucceeded(result) => { + let inner = InvokeInstanceSuccess { + return_value: result.return_value.map(|a| ReturnValue { value: a }), + events: result + .effects + .into_iter() + .map(TryFrom::try_from) + .collect::>()?, + used_energy: result.used_energy.require()?.into(), + }; + Ok(WithRemainingQuota { + inner, + quota_remaining, + }) + } + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +/// The successful result of [`DryRun::set_timestamp`]. +#[derive(Clone, Debug, Copy)] +pub struct TimestampSet; + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!(result, ErrorResult::NoState) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + match response { + generated::dry_run_success_response::Response::TimestampSet(_) => { + let inner = TimestampSet {}; + Ok(WithRemainingQuota { + inner, + quota_remaining, + }) + } + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +/// The successful result of [`DryRun::mint_to_account`]. +#[derive(Clone, Debug, Copy)] +pub struct MintedToAccount; + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!( + result, + ErrorResult::NoState | ErrorResult::AmountOverLimit { amount_limit: _ } + ) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + match response { + generated::dry_run_success_response::Response::MintedToAccount(_) => { + let inner = MintedToAccount {}; + Ok(WithRemainingQuota { + inner, + quota_remaining, + }) + } + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +/// Representation of a transaction for the purposes of dry-running it. +/// Compared to a genuine transaction, this does not include an expiry time or +/// signatures. It is possible to specify which credentials and keys are assumed +/// to sign the transaction. This is only useful for transactions from +/// multi-signature accounts. In particular, it can ensure that the calculated +/// cost is correct when multiple signatures are used. For transactions that +/// update the keys on a multi-credential account, the transaction must be +/// signed by the credential whose keys are updated, so specifying which keys +/// sign is required here. +#[derive(Clone, Debug)] +pub struct DryRunTransaction { + /// The account originating the transaction. + pub sender: AccountAddress, + /// The limit on the energy that may be used by the transaction. + pub energy_amount: Energy, + /// The transaction payload to execute. + pub payload: EncodedPayload, + /// The credential-keys that are treated as signing the transaction. + /// If this is the empty vector, it is treated as the single key (0,0) + /// signing. + pub signatures: Vec<(CredentialIndex, KeyIndex)>, +} + +impl DryRunTransaction { + /// Create a [`DryRunTransaction`] given the sender address, energy limit + /// and payload. The empty list is used for the signatures, meaning that + /// it will be treated as though key 0 of credential 0 is the sole + /// signature on the transaction. For most purposes, this is sufficient. + pub fn new(sender: AccountAddress, energy_amount: Energy, payload: &impl PayloadLike) -> Self { + DryRunTransaction { + sender, + energy_amount, + payload: payload.encode(), + signatures: vec![], + } + } +} + +impl From> + for DryRunTransaction +{ + fn from(value: concordium_base::transactions::AccountTransaction

) -> Self { + DryRunTransaction { + sender: value.header.sender, + energy_amount: value.header.energy_amount, + payload: value.payload.encode(), + signatures: value + .signature + .signatures + .into_iter() + .flat_map(|(c, v)| std::iter::repeat(c).zip(v.into_keys())) + .collect(), + } + } +} + +impl From for generated::DryRunTransaction { + fn from(transaction: DryRunTransaction) -> Self { + let payload = account_transaction_payload::Payload::RawPayload(transaction.payload.into()); + generated::DryRunTransaction { + sender: Some(transaction.sender.into()), + energy_amount: Some(transaction.energy_amount.into()), + payload: Some(generated::AccountTransactionPayload { + payload: Some(payload), + }), + signatures: transaction + .signatures + .into_iter() + .map(|(cred, key)| DryRunSignature { + credential: cred.index.into(), + key: key.0.into(), + }) + .collect(), + } + } +} + +/// The successful result of [`DryRun::run_transaction`]. +/// Note that a transaction can still be rejected (i.e. produce no effect beyond +/// charging the sender) even if it is executed. +#[derive(Clone, Debug)] +pub struct TransactionExecuted { + /// The actual energy cost of executing the transaction. + pub energy_cost: Energy, + /// Detailed result of the transaction execution. + pub details: AccountTransactionDetails, + /// For V1 contract update transactions, the return value. + pub return_value: Option>, +} + +impl TryFrom>> + for WithRemainingQuota +{ + type Error = DryRunError; + + fn try_from( + value: Option>, + ) -> Result { + let response = + value.ok_or_else(|| tonic::Status::cancelled("server closed dry run stream"))??; + let quota_remaining = response.quota_remaining.require()?.into(); + use generated::dry_run_response::*; + match response.response.require()? { + Response::Error(e) => { + let result = e.error.require()?.try_into()?; + if !matches!( + result, + ErrorResult::NoState + | ErrorResult::AccountNotFound + | ErrorResult::BalanceInsufficient { .. } + | ErrorResult::EnergyInsufficient { .. } + ) { + Err(tonic::Status::unknown("unexpected error response type"))? + } + Err(DryRunError::OperationFailed { + result, + quota_remaining, + }) + } + Response::Success(s) => { + let response = s.response.require()?; + match response { + generated::dry_run_success_response::Response::TransactionExecuted(res) => { + let inner = TransactionExecuted { + energy_cost: res.energy_cost.require()?.into(), + details: res.details.require()?.try_into()?, + return_value: res.return_value, + }; + Ok(WithRemainingQuota { + inner, + quota_remaining, + }) + } + _ => Err(tonic::Status::unknown("unexpected success response type"))?, + } + } + } + } +} + +pub type DryRunResult = Result, DryRunError>; + +/// A dry-run session. +/// +/// The operations available in two variants, with and without the `begin_` +/// prefix. The variants without a prefix will send the request and wait for the +/// result when `await`ed. This is typically the simplest to use. +/// The variants with the `begin_` prefix send the request when `await`ed, +/// returning a future that can be `await`ed to retrieve the result. +/// (This can be used to front-load operations where queries do not depend on +/// the results of previous queries. This may be more efficient in high-latency +/// situations.) +/// +/// Before any other operations, [`DryRun::load_block_state`] (or +/// [`DryRun::begin_load_block_state`]) should be called to ensure a block state +/// is loaded. If it is not (or if loading the block state fails - for instance +/// if an invalid block hash is supplied), other operations will result in an +/// [`ErrorResult::NoState`] error. +pub struct DryRun { + /// The channel used for sending requests to the server. + /// This is `None` if the session has been closed. + request_send: Option>, + /// The channel used for receiving responses from the server. + response_recv: shared_receiver::SharedReceiver>, + /// The timeout in milliseconds for the dry-run session to complete. + timeout: u64, + /// The energy quota for the dry-run session as a whole. + energy_quota: u64, +} + +impl DryRun { + /// Start a new dry-run session. + /// This may return `UNIMPLEMENTED` if the endpoint is not available on the + /// server. It may return `UNAVAILABLE` if the endpoint is not currently + /// available to due resource limitations. + pub(crate) async fn new( + client: &mut generated::queries_client::QueriesClient, + ) -> tonic::Result { + let (request_send, request_recv) = channel::mpsc::channel(10); + let response = client.dry_run(request_recv).await?; + let parse_meta_u64 = |key| response.metadata().get(key)?.to_str().ok()?.parse().ok(); + let timeout: u64 = parse_meta_u64("timeout").ok_or_else(|| { + tonic::Status::internal("timeout metadata could not be parsed from server response") + })?; + let energy_quota: u64 = parse_meta_u64("quota").ok_or_else(|| { + tonic::Status::internal( + "energy quota metadata could not be parsed from server response", + ) + })?; + let response_stream = response.into_inner(); + let response_recv = + shared_receiver::SharedReceiver::new(futures::stream::StreamExt::fuse(response_stream)); + Ok(DryRun { + request_send: Some(request_send), + response_recv, + timeout, + energy_quota, + }) + } + + /// Get the timeout for the dry-run session set by the server. + /// Returns `None` if the initial metadata did not include the timeout, or + /// it could not be parsed. + 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. + /// This can result in an error if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// If successful, this returns a future that can be used to wait for the + /// result of the operation. The following results are possible: + /// + /// * [`BlockStateLoaded`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::BlockNotFound`] if the block could not be found. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 2000. + pub async fn begin_load_block_state( + &mut self, + bi: impl IntoBlockIdentifier, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::LoadBlockState( + (&bi.into_block_identifier()).into(), + )), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Load the state from a specified block. + /// The following results are possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`BlockStateLoaded`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::BlockNotFound`] if the block could not be found. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 2000. + pub async fn load_block_state( + &mut self, + bi: impl IntoBlockIdentifier, + ) -> DryRunResult { + self.begin_load_block_state(bi).await?.await + } + + /// Get the account information for a specified account in the current + /// state. This can result in an error if the dry-run session has + /// already been closed, either by [`DryRun::close`] or by the server + /// closing the session. In this case, the response code indicates the + /// cause. If successful, this returns a future that can be used to wait + /// for the result of the operation. The following results are possible: + /// + /// * [`AccountInfo`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AccountNotFound`] if the account could not be found. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 200. + pub async fn begin_get_account_info( + &mut self, + acc: &AccountIdentifier, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::StateQuery(DryRunStateQuery { + query: Some(generated::dry_run_state_query::Query::GetAccountInfo( + acc.into(), + )), + })), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Get the account information for a specified account in the current + /// state. The following results are possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`AccountInfo`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AccountNotFound`] if the account could not be found. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 200. + pub async fn get_account_info(&mut self, acc: &AccountIdentifier) -> DryRunResult { + self.begin_get_account_info(acc).await?.await + } + + /// Get the details of a specified smart contract instance in the current + /// state. This operation can result in an error if the dry-run session has + /// already been closed, either by [`DryRun::close`] or by the server + /// closing the session. In this case, the response code indicates the + /// cause. If successful, this returns a future that can be used to wait + /// for the result of the operation. The following results are possible: + /// + /// * [`InstanceInfo`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AccountNotFound`] if the account could not be found. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 200. + pub async fn begin_get_instance_info( + &mut self, + address: &ContractAddress, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::StateQuery(DryRunStateQuery { + query: Some(generated::dry_run_state_query::Query::GetInstanceInfo( + address.into(), + )), + })), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Get the details of a specified smart contract instance in the current + /// state. The following results are possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`InstanceInfo`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AccountNotFound`] if the account could not be found. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 200. + pub async fn get_instance_info( + &mut self, + address: &ContractAddress, + ) -> DryRunResult { + self.begin_get_instance_info(address).await?.await + } + + /// Invoke an entrypoint on a smart contract instance in the current state. + /// Any changes this would make to the state will be rolled back so they are + /// not observable by subsequent operations in the dry-run session. (To make + /// updates that are observable within the dry-run session, use + /// [`DryRun::run_transaction`] instead.) This operation can result in an + /// error if the dry-run session has already been closed, either by + /// [`DryRun::close`] or by the server closing the session. In this case, + /// the response code indicates the cause. If successful, this returns a + /// future that can be used to wait for the result of the operation. The + /// following results are possible: + /// + /// * [`InvokeInstanceSuccess`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::InvokeFailure`] if the invocation failed. (This can + /// be because the contract logic produced a reject, or a number of + /// other reasons, such as the endpoint not existing.) + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 200 plus the energy used by the + /// execution of the contract endpoint. + pub async fn begin_invoke_instance( + &mut self, + context: &ContractContext, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::StateQuery(DryRunStateQuery { + query: Some(generated::dry_run_state_query::Query::InvokeInstance( + context.into(), + )), + })), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Invoke an entrypoint on a smart contract instance in the current state. + /// Any changes this would make to the state will be rolled back so they are + /// not observable by subsequent operations in the dry-run session. (To make + /// updates that are observable within the dry-run session, use + /// [`DryRun::run_transaction`] instead.) The following results are + /// possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`InvokeInstanceSuccess`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::InvokeFailure`] if the invocation failed. (This can + /// be because the contract logic produced a reject, or a number of + /// other reasons, such as the endpoint not existing.) + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 200 plus the energy used by the + /// execution of the contract endpoint. + pub async fn invoke_instance( + &mut self, + context: &ContractContext, + ) -> DryRunResult { + self.begin_invoke_instance(context).await?.await + } + + /// Update the current timestamp for subsequent dry-run operations. The + /// timestamp is automatically set to the timestamp of the block loaded + /// by [`DryRun::load_block_state`]. For smart contracts that are time + /// sensitive, overriding the timestamp can be useful. This operation can + /// result in an error if the dry-run session has already been closed, + /// either by [`DryRun::close`] or by the server closing the session. In + /// this case, the response code indicates the cause. If successful, + /// this returns a future that can be used to wait for the result of the + /// operation. The following results are possible: + /// + /// * [`TimestampSet`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 50. + pub async fn begin_set_timestamp( + &mut self, + timestamp: Timestamp, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::StateOperation( + DryRunStateOperation { + operation: Some(generated::dry_run_state_operation::Operation::SetTimestamp( + timestamp.into(), + )), + }, + )), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Update the current timestamp for subsequent dry-run operations. The + /// timestamp is automatically set to the timestamp of the block loaded + /// by [`DryRun::load_block_state`]. For smart contracts that are time + /// sensitive, overriding the timestamp can be useful. The following results + /// are possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`TimestampSet`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 50. + pub async fn set_timestamp(&mut self, timestamp: Timestamp) -> DryRunResult { + self.begin_set_timestamp(timestamp).await?.await + } + + /// Mint a specified amount and award it to a specified account. This + /// operation can result in an error if the dry-run session has already + /// been closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. If + /// successful, this returns a future that can be used to wait for the + /// result of the operation. The following results are possible: + /// + /// * [`MintedToAccount`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AmountOverLimit`] if the minted amount would + /// overflow the total CCD supply. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 400. + pub async fn begin_mint_to_account( + &mut self, + account_address: &AccountAddress, + mint_amount: Amount, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::StateOperation( + DryRunStateOperation { + operation: Some( + generated::dry_run_state_operation::Operation::MintToAccount( + DryRunMintToAccount { + account: Some(account_address.into()), + amount: Some(mint_amount.into()), + }, + ), + ), + }, + )), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Mint a specified amount and award it to a specified account. The + /// following results are possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`MintedToAccount`] if the operation is successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AmountOverLimit`] if the minted amount would + /// overflow the total CCD supply. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 400. + pub async fn mint_to_account( + &mut self, + account_address: &AccountAddress, + mint_amount: Amount, + ) -> DryRunResult { + self.begin_mint_to_account(account_address, mint_amount) + .await? + .await + } + + /// Dry-run a transaction, updating the state of the dry-run session + /// accordingly. This operation can result in an error if the dry-run + /// session has already been closed, either by [`DryRun::close`] or by the + /// server closing the session. In this case, the response code + /// indicates the cause. If successful, this returns a future that can + /// be used to wait for the result of the operation. The following + /// results are possible: + /// + /// * [`TransactionExecuted`] if the transaction was executed. This case + /// applies both if the transaction is rejected or successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AccountNotFound`] if the sender account does not + /// exist. + /// - [`ErrorResult::BalanceInsufficient`] if the sender account does not + /// have sufficient balance to pay the deposit for the transaction. + /// - [`ErrorResult::EnergyInsufficient`] if the specified energy is not + /// sufficient to cover the cost of the basic checks required for a + /// transaction to be included in the chain. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 400. + pub async fn begin_run_transaction( + &mut self, + transaction: DryRunTransaction, + ) -> tonic::Result>> { + let request = generated::DryRunRequest { + request: Some(dry_run_request::Request::StateOperation( + DryRunStateOperation { + operation: Some( + generated::dry_run_state_operation::Operation::RunTransaction( + transaction.into(), + ), + ), + }, + )), + }; + Ok(self.request(request).await?.map(|z| z.try_into())) + } + + /// Dry-run a transaction, updating the state of the dry-run session + /// accordingly. The following results are possible: + /// + /// * [`DryRunError::CallError`] if the dry-run session has already been + /// closed, either by [`DryRun::close`] or by the server closing the + /// session. In this case, the response code indicates the cause. + /// * [`TransactionExecuted`] if the transaction was executed. This case + /// applies both if the transaction is rejected or successful. + /// * [`DryRunError::OperationFailed`] if the operation failed, with one of + /// the following results: + /// - [`ErrorResult::NoState`] if no block state has been loaded. + /// - [`ErrorResult::AccountNotFound`] if the sender account does not + /// exist. + /// - [`ErrorResult::BalanceInsufficient`] if the sender account does not + /// have sufficient balance to pay the deposit for the transaction. + /// - [`ErrorResult::EnergyInsufficient`] if the specified energy is not + /// sufficient to cover the cost of the basic checks required for a + /// transaction to be included in the chain. + /// * [`DryRunError::CallError`] if the server produced an error code, or + /// if the server's response was unexpected. + /// - If the server's response could not be interpreted, the result code + /// `INVALID_ARGUMENT` or `UNKNOWN` is returned. + /// - If the execution of the query would exceed the energy quota, + /// `RESOURCE_EXHAUSTED` is returned. + /// - If the timeout for the dry-run session has expired, + /// `DEADLINE_EXCEEDED` is returned. + /// + /// The energy cost of this operation is 400. + pub async fn run_transaction( + &mut self, + transaction: DryRunTransaction, + ) -> DryRunResult { + self.begin_run_transaction(transaction).await?.await + } + + /// Close the request stream. Any subsequent dry-run requests will result in + /// a `CANCELLED` status code. Closing the request stream allows the + /// server to free resources associated with the dry-run session. It is + /// recommended to close the request stream if the [`DryRun`] object will + /// be retained for any significant length of time after the last request is + /// made. + /// + /// Note that dropping the [`DryRun`] object will stop the background task + /// that services in-flight requests, so it should not be dropped before + /// `await`ing any such requests. Closing the request stream does not stop + /// the background task. + pub fn close(&mut self) { self.request_send = None; } + + /// Helper function that issues a dry-run request and returns a future for + /// the corresponding response. + async fn request( + &mut self, + request: generated::DryRunRequest, + ) -> tonic::Result>>> { + let lazy_cancelled = || tonic::Status::cancelled("dry run already completed"); + let sender = self.request_send.as_mut().ok_or_else(lazy_cancelled)?; + let send_result = sender.send(request).await; + let receive_result = self.response_recv.next().ok_or_else(lazy_cancelled); + match send_result { + Ok(_) => receive_result.map(|r| r.map(|x| x.ok())), + Err(_) => { + // In this case, the server must have closed the stream. We query the response + // stream to see if there is an error indicating the reason. + if let Ok(Err(e)) = receive_result?.await { + Err(e) + } else { + Err(lazy_cancelled()) + } + } + } + } +} diff --git a/src/v2/generated/concordium.v2.rs b/src/v2/generated/concordium.v2.rs index ec7ea5784..96702bc37 100644 --- a/src/v2/generated/concordium.v2.rs +++ b/src/v2/generated/concordium.v2.rs @@ -2723,7 +2723,7 @@ pub mod block_item_summary { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Details { - /// Detailsa about an account transaction. + /// Details about an account transaction. #[prost(message, tag = "4")] AccountTransaction(super::AccountTransactionDetails), /// Details about an account creation. @@ -4237,7 +4237,7 @@ pub struct AccountTransactionHeader { /// Sequence number of the transaction. #[prost(message, optional, tag = "2")] pub sequence_number: ::core::option::Option, - /// Maximum amount of nergy the transaction can take to execute. + /// Maximum amount of energy the transaction can take to execute. #[prost(message, optional, tag = "3")] pub energy_amount: ::core::option::Option, /// Latest time the transaction can included in a block. @@ -4867,6 +4867,426 @@ pub struct WinningBaker { #[prost(bool, tag = "3")] pub present: bool, } +/// An operation to dry run. The first operation in a dry-run sequence should +/// be `load_block_state`: any other operation will be met with `NoState` until +/// a state is successfully loaded. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunRequest { + #[prost(oneof = "dry_run_request::Request", tags = "1, 2, 3")] + pub request: ::core::option::Option, +} +/// Nested message and enum types in `DryRunRequest`. +pub mod dry_run_request { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Request { + /// Load the state of the specified block to use for subsequent + /// requests. The state is taken at the end of execution of the + /// block, and the block’s timestamp is used as the current + /// timestamp. + /// + /// The energy cost for this operation is 2000. + #[prost(message, tag = "1")] + LoadBlockState(super::BlockHashInput), + /// Run a query on the state. + #[prost(message, tag = "2")] + StateQuery(super::DryRunStateQuery), + /// Run a (non-transaction) operation to modify the state. + #[prost(message, tag = "3")] + StateOperation(super::DryRunStateOperation), + } +} +/// Run a query as part of a dry run. Queries do not update the block state. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunStateQuery { + #[prost(oneof = "dry_run_state_query::Query", tags = "1, 2, 3")] + pub query: ::core::option::Option, +} +/// Nested message and enum types in `DryRunStateQuery`. +pub mod dry_run_state_query { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Query { + /// Look up information on a particular account. + /// + /// The energy cost for this query is 200. + #[prost(message, tag = "1")] + GetAccountInfo(super::AccountIdentifierInput), + /// Look up information about a particular smart contract. + /// + /// The energy cost for this query is 200. + #[prost(message, tag = "2")] + GetInstanceInfo(super::ContractAddress), + /// Invoke an entrypoint on a smart contract instance. + /// No changes made to the state are retained at the completion of the + /// operation. + /// + /// The energy cost for this query is 200 plus the energy used by the + /// smart contract execution. + #[prost(message, tag = "3")] + InvokeInstance(super::DryRunInvokeInstance), + } +} +/// Invoke an entrypoint on a smart contract instance. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunInvokeInstance { + /// Invoker of the contract. If this is not supplied then the contract will + /// be invoked by an account with address 0, no credentials and + /// sufficient amount of CCD to cover the transfer amount. If given, the + /// relevant address (either account or contract) must exist in the + /// blockstate. + #[prost(message, optional, tag = "1")] + pub invoker: ::core::option::Option

, + /// Address of the contract instance to invoke. + #[prost(message, optional, tag = "2")] + pub instance: ::core::option::Option, + /// Amount to invoke the smart contract instance with. + #[prost(message, optional, tag = "3")] + pub amount: ::core::option::Option, + /// The entrypoint of the smart contract instance to invoke. + #[prost(message, optional, tag = "4")] + pub entrypoint: ::core::option::Option, + /// The parameter bytes to include in the invocation of the entrypoint. + #[prost(message, optional, tag = "5")] + pub parameter: ::core::option::Option, + /// The maximum energy to allow for the invocation. Note that the node + /// imposes an energy quota that is enforced in addition to this limit. + #[prost(message, optional, tag = "6")] + pub energy: ::core::option::Option, +} +/// An operation that can update the state as part of a dry run. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunStateOperation { + #[prost(oneof = "dry_run_state_operation::Operation", tags = "1, 2, 3")] + pub operation: ::core::option::Option, +} +/// Nested message and enum types in `DryRunStateOperation`. +pub mod dry_run_state_operation { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Operation { + /// Sets the current block time to the given timestamp for the purposes + /// of future transactions. + /// + /// The energy cost of this operation is 50. + #[prost(message, tag = "1")] + SetTimestamp(super::Timestamp), + /// Add a specified amount of newly-minted CCDs to a specified account. + /// The amount cannot cause the total circulating supply to overflow. + /// + /// The energy cost of this operation is 400. + #[prost(message, tag = "2")] + MintToAccount(super::DryRunMintToAccount), + /// Dry run a transaction, updating the state if it succeeds. + /// + /// The energy cost of this operation is 400 plus the energy used by + /// executing the transaction. + #[prost(message, tag = "3")] + RunTransaction(super::DryRunTransaction), + } +} +/// Mint a specified amount and credit it to the specified account as part of a +/// dry run. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunMintToAccount { + /// The account to mint to. + #[prost(message, optional, tag = "1")] + pub account: ::core::option::Option, + /// The amount to mint and credit to the account. + #[prost(message, optional, tag = "2")] + pub amount: ::core::option::Option, +} +/// Dry run an account transaction +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunTransaction { + /// The account to use as the sender of the transaction. + #[prost(message, optional, tag = "1")] + pub sender: ::core::option::Option, + /// The energy limit set for executing the transaction. + #[prost(message, optional, tag = "2")] + pub energy_amount: ::core::option::Option, + /// The payload of the transaction. + #[prost(message, optional, tag = "3")] + pub payload: ::core::option::Option, + /// Which credentials and keys should be treated as having signed the + /// transaction. If none is given, then the transaction is treated as + /// having one signature for credential 0, key 0. Therefore, this is + /// only required when the transaction is from a multi-signature + /// account. There are two reasons why you might want to specify signatures: + /// * The cost of the transaction depends on the number of signatures, so + /// if you want to get the correct cost for a multi-signature + /// transaction, then specifying the signatures supports this. + /// * When changing account keys on a multi-credential account, the + /// transaction must be signed by the credential whose keys are being + /// changed. + /// + /// Note that the signature thresholds are not checked as part of the dry + /// run. Duplicated signatures are only counted once. + #[prost(message, repeated, tag = "4")] + pub signatures: ::prost::alloc::vec::Vec, +} +/// A dry run signature is a pair of a credential index and key index, +/// identifying the credential and key that is presumed to have signed the +/// transaction. No actual cryptographic signature is included. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunSignature { + /// Credential index. Must not exceed 255. + #[prost(uint32, tag = "1")] + pub credential: u32, + /// Key index. Must not exceed 255. + #[prost(uint32, tag = "2")] + pub key: u32, +} +/// A response to a `DryRunRequest`. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunResponse { + /// The remaining available energy quota after the dry run operation. + #[prost(message, optional, tag = "3")] + pub quota_remaining: ::core::option::Option, + #[prost(oneof = "dry_run_response::Response", tags = "1, 2")] + pub response: ::core::option::Option, +} +/// Nested message and enum types in `DryRunResponse`. +pub mod dry_run_response { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Response { + /// The request produced an error. The request otherwise has no effect + /// on the state. + #[prost(message, tag = "1")] + Error(super::DryRunErrorResponse), + /// The request was successful. + #[prost(message, tag = "2")] + Success(super::DryRunSuccessResponse), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunErrorResponse { + #[prost( + oneof = "dry_run_error_response::Error", + tags = "1, 2, 3, 4, 5, 6, 8, 9" + )] + pub error: ::core::option::Option, +} +/// Nested message and enum types in `DryRunErrorResponse`. +pub mod dry_run_error_response { + /// The current block state is undefined. It should be initialized with + /// a 'load_block_state' request before any other operations. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct NoState {} + /// The requested block was not found, so its state could not be loaded. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct BlockNotFound {} + /// The specified account was not found. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AccountNotFound {} + /// The specified instance was not found. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct InstanceNotFound {} + /// The amount that was requested to be minted would overflow the total + /// supply. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct AmountOverLimit { + /// The maximum amount that can be minted without overflowing the + /// supply. + #[prost(message, optional, tag = "1")] + pub amount_limit: ::core::option::Option, + } + /// The sender account for the transaction has insufficient balance to pay + /// the preliminary fees for the transaction to be included in a block. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct BalanceInsufficient { + /// The minimum balance required to perform the operation. + #[prost(message, optional, tag = "1")] + pub required_amount: ::core::option::Option, + /// The currently-available balance on the account to pay for the + /// operation. + #[prost(message, optional, tag = "2")] + pub available_amount: ::core::option::Option, + } + /// The energy made available for the transaction is insufficient to cover + /// the basic processing required to include a transaction in a block. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct EnergyInsufficient { + /// The minimum energy required for the transaction to be included in + /// the chain. Note that, even if the energy supplied for the + /// transaction is enough to prevent a `EnergyInsufficient`, the + /// transaction can still be rejected for having insufficient + /// energy. In that case, a `TransactionExecuted` response will be + /// produced, but indicate the transaction was rejected. + #[prost(message, optional, tag = "1")] + pub energy_required: ::core::option::Option, + } + /// Invoking the smart contract instance failed. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct InvokeFailure { + /// If invoking a V0 contract this is not provided, otherwise it is + /// potentially return value produced by the call unless the call failed + /// with out of energy or runtime error. If the V1 contract + /// terminated with a logic error then the return value is + /// present. + #[prost(bytes = "vec", optional, tag = "1")] + pub return_value: ::core::option::Option<::prost::alloc::vec::Vec>, + /// Energy used by the execution. + #[prost(message, optional, tag = "2")] + pub used_energy: ::core::option::Option, + /// Contract execution failed for the given reason. + #[prost(message, optional, tag = "3")] + pub reason: ::core::option::Option, + } + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Error { + /// The current block state is undefined. It should be initialized with + /// a 'load_block_state' request before any other operations. + #[prost(message, tag = "1")] + NoState(NoState), + /// The requested block was not found, so its state could not be loaded. + /// Response to 'load_block_state'. + #[prost(message, tag = "2")] + BlockNotFound(BlockNotFound), + /// The specified account was not found. + /// Response to 'get_account_info', 'mint_to_account' and + /// 'run_transaction'. + #[prost(message, tag = "3")] + AccountNotFound(AccountNotFound), + /// The specified instance was not found. + /// Response to 'get_instance_info'. + #[prost(message, tag = "4")] + InstanceNotFound(InstanceNotFound), + /// The amount to mint would overflow the total CCD supply. + /// Response to 'mint_to_account'. + #[prost(message, tag = "5")] + AmountOverLimit(AmountOverLimit), + /// The balance of the sender account is not sufficient to pay for the + /// operation. Response to 'run_transaction'. + #[prost(message, tag = "6")] + BalanceInsufficient(BalanceInsufficient), + /// The energy supplied for the transaction was not sufficient to + /// perform the basic checks. Response to 'run_transaction'. + #[prost(message, tag = "8")] + EnergyInsufficient(EnergyInsufficient), + /// The contract invocation failed. + /// Response to 'invoke_instance'. + #[prost(message, tag = "9")] + InvokeFailed(InvokeFailure), + } +} +/// The dry run operation completed successfully. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DryRunSuccessResponse { + #[prost( + oneof = "dry_run_success_response::Response", + tags = "1, 2, 3, 4, 5, 6, 7" + )] + pub response: ::core::option::Option, +} +/// Nested message and enum types in `DryRunSuccessResponse`. +pub mod dry_run_success_response { + /// The block state at the specified block was successfully loaded. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct BlockStateLoaded { + /// The timestamp of the block, taken to be the current timestamp. + #[prost(message, optional, tag = "1")] + pub current_timestamp: ::core::option::Option, + /// The hash of the block that was loaded. + #[prost(message, optional, tag = "2")] + pub block_hash: ::core::option::Option, + /// The protocol version at the specified block. The behavior of + /// operations can vary across protocol versions. + #[prost(enumeration = "super::ProtocolVersion", tag = "3")] + pub protocol_version: i32, + } + /// The current apparent timestamp was updated to the specified value. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct TimestampSet {} + /// The specified amount was minted to the specified account. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct MintedToAccount {} + /// The transaction was executed. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct TransactionExecuted { + /// The amount of energy actually expended in executing the transaction. + #[prost(message, optional, tag = "1")] + pub energy_cost: ::core::option::Option, + /// The details of the outcome of the transaction. + #[prost(message, optional, tag = "2")] + pub details: ::core::option::Option, + /// If this is an invocation of a V1 contract that produced a return + /// value, this is that value. Otherwise it is absent. + #[prost(bytes = "vec", optional, tag = "3")] + pub return_value: ::core::option::Option<::prost::alloc::vec::Vec>, + } + /// The smart contract instance was invoked successfully. + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct InvokeSuccess { + /// If invoking a V0 contract this is absent. Otherwise it is the return + /// value produced by the contract. + #[prost(bytes = "vec", optional, tag = "1")] + pub return_value: ::core::option::Option<::prost::alloc::vec::Vec>, + /// Energy used by the execution. + #[prost(message, optional, tag = "2")] + pub used_energy: ::core::option::Option, + /// Effects produced by contract execution. + #[prost(message, repeated, tag = "3")] + pub effects: ::prost::alloc::vec::Vec, + } + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Response { + /// The state from the specified block was successfully loaded. + /// Response to 'load_block_state'. + #[prost(message, tag = "1")] + BlockStateLoaded(BlockStateLoaded), + /// Details of the requested account. + /// Response to 'get_account_info'. + #[prost(message, tag = "2")] + AccountInfo(super::AccountInfo), + /// Details of the requested smart contract instance. + /// Response to 'get_instance_info'. + #[prost(message, tag = "3")] + InstanceInfo(super::InstanceInfo), + /// The smart contract instance was invoked successfully. + #[prost(message, tag = "4")] + InvokeSucceeded(InvokeSuccess), + /// The current timestamp was set successfully. + /// Response to 'set_timestamp'. + #[prost(message, tag = "5")] + TimestampSet(TimestampSet), + /// The specified amount was minted and credited to the account. + /// Response to 'mint_to_account'. + #[prost(message, tag = "6")] + MintedToAccount(MintedToAccount), + /// The specified transaction was executed. Note that the transaction + /// could still have been rejected. + /// Response to 'run_transaction'. + #[prost(message, tag = "7")] + TransactionExecuted(TransactionExecuted), + } +} /// Information about how open the pool is to new delegators. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] @@ -6367,7 +6787,7 @@ pub mod queries_client { /// * `INVALID_ARGUMENT` if the query is for a genesis index at /// consensus version 0. /// * `INVALID_ARGUMENT` if the input `EpochRequest` is malformed. - /// * `UNAVAILABLE` if the endpoint is disabled on the node. + /// * `UNIMPLEMENTED` if the endpoint is disabled on the node. pub async fn get_winning_bakers_epoch( &mut self, request: impl tonic::IntoRequest, @@ -6398,7 +6818,7 @@ pub mod queries_client { /// * `INVALID_ARGUMENT` if the query is for an epoch with no finalized /// blocks for a past genesis index. /// * `INVALID_ARGUMENT` if the input `EpochRequest` is malformed. - /// * `UNAVAILABLE` if the endpoint is disabled on the node. + /// * `UNIMPLEMENTED` if the endpoint is disabled on the node. pub async fn get_first_block_epoch( &mut self, request: impl tonic::IntoRequest, @@ -6414,5 +6834,58 @@ pub mod queries_client { http::uri::PathAndQuery::from_static("/concordium.v2.Queries/GetFirstBlockEpoch"); self.inner.unary(request.into_request(), path, codec).await } + + /// Dry run a series of transactions and operations on a state derived + /// from a specified block. The server should send a single + /// `DryRunResponse` for each `DryRunRequest` received, unless + /// the call fails with an error status code. If a request produces a + /// `DryRunErrorResponse`, then the server will still process + /// subsequent requests, just as if the request causing the error + /// did not happen. + /// + /// The first request should be `load_block_at_state` to determine the + /// block state that will be used for the dry run. + /// + /// The server associates each request with an energy cost, and limits + /// the total energy that may be expended in a single invocation + /// of `DryRun`. This limit is reported as `quota` in the + /// initial metadata returned by the server. If executing an operation + /// exceeds the limit, the server terminates the session with + /// `RESOURCE_EXHAUSTED`. + /// + /// The server also imposes a timeout for a dry-run session to complete. + /// The server reports the timeout duration in milliseconds in + /// the initial metadata field `timeout`. If the session + /// is not completed before the timeout elapses, the server terminates + /// the session with `DEADLINE_EXCEEDED`. + /// + /// The following error cases are possible: + /// * `INVALID_ARGUMENT` if any `DryRunRequest` is malformed. + /// * `RESOURCE_EXHAUSTED` if the energy quota is exceeded. + /// * `DEADLINE_EXCEEDED` if the session does not complete before the + /// server-imposed timeout. + /// * `RESOURCE_EXHAUSTED` if the server is not currently accepting new + /// `DryRun` sessions. (The server may impose a limit on the number + /// of concurrent sessions.) + /// * `INTERNAL` if an interal server error occurs. This should not + /// happen, and likely indicates a bug. + /// * `UNIMPLEMENTED` if the endpoint is disabled on the node. + pub async fn dry_run( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> Result>, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/concordium.v2.Queries/DryRun"); + self.inner + .streaming(request.into_streaming_request(), path, codec) + .await + } } } diff --git a/src/v2/mod.rs b/src/v2/mod.rs index 4b2e5bb26..b130f0b38 100644 --- a/src/v2/mod.rs +++ b/src/v2/mod.rs @@ -49,7 +49,10 @@ pub use tonic::{ Code, Status, }; +use self::dry_run::WithRemainingQuota; + mod conversions; +pub mod dry_run; #[path = "generated/concordium.v2.rs"] #[allow( clippy::large_enum_variant, @@ -1611,6 +1614,31 @@ impl Client { }) } + /// Start a dry-run sequence that can be used to simulate a series of + /// transactions and other operations on the node. + /// + /// Before invoking any other operations on the [`dry_run::DryRun`] object, + /// the state must be loaded by calling + /// [`dry_run::DryRun::load_block_state`]. + pub async fn begin_dry_run(&mut self) -> endpoints::QueryResult { + Ok(dry_run::DryRun::new(&mut self.client).await?) + } + + /// Start a dry-run sequence that can be used to simulate a series of + /// transactions and other operations on the node, starting from the + /// specified block. + pub async fn dry_run( + &mut self, + bi: impl IntoBlockIdentifier, + ) -> dry_run::DryRunResult<(dry_run::DryRun, dry_run::BlockStateLoaded)> { + let mut runner = dry_run::DryRun::new(&mut self.client).await?; + let load_result = runner.load_block_state(bi).await?; + Ok(WithRemainingQuota { + inner: (runner, load_result.inner), + quota_remaining: load_result.quota_remaining, + }) + } + /// Get information, such as height, timings, and transaction counts for the /// given block. If the block does not exist [`QueryError::NotFound`] is /// returned. diff --git a/src/v2/proto_schema_version.rs b/src/v2/proto_schema_version.rs index 50574fbff..e745ac9f1 100644 --- a/src/v2/proto_schema_version.rs +++ b/src/v2/proto_schema_version.rs @@ -1 +1 @@ -pub const PROTO_SCHEMA_VERSION: &str = "c079fde30b8b39d475f6dd8c049a9357bef0ab3c"; +pub const PROTO_SCHEMA_VERSION: &str = "68c46223ccbc05ddc4923836a20d8e10abad02c4";