diff --git a/Cargo.toml b/Cargo.toml index f0507fd..96d974b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ High level client library for transacting to the NEAR network. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +base64 = "0.21" serde = "1.0" serde_json = "1.0" thiserror = "1" diff --git a/src/error.rs b/src/error.rs index 7fa4694..1978eb2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,7 @@ use near_jsonrpc_primitives::types::blocks::RpcBlockError; use near_jsonrpc_primitives::types::chunks::RpcChunkError; use near_jsonrpc_primitives::types::query::RpcQueryError; use near_jsonrpc_primitives::types::transactions::RpcTransactionError; +use near_primitives::errors::TxExecutionError; pub type Result = core::result::Result; @@ -25,10 +26,14 @@ pub enum Error { /// Catch all RPC error. This is usually resultant from query calls. #[error("rpc: {0}")] Rpc(Box), + #[error(transparent)] + TxExecution(Box), #[error(transparent)] Serialization(#[from] serde_json::Error), #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] Io(#[from] std::io::Error), #[error("invalid args were passed: {0}")] InvalidArgs(&'static str), diff --git a/src/lib.rs b/src/lib.rs index 9651a83..3f46724 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ pub mod error; pub mod ops; pub mod query; pub mod signer; +pub mod result; use crate::error::Result; use crate::signer::ExposeAccountId; diff --git a/src/ops.rs b/src/ops.rs index 5c0df07..72be29f 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -1,3 +1,5 @@ +//! All operation types that are generated/used when commiting transactions to the network. + use near_account_id::AccountId; use near_crypto::{PublicKey, Signer}; use near_gas::NearGas; @@ -11,11 +13,20 @@ use near_primitives::transaction::{ use near_primitives::views::FinalExecutionOutcomeView; use near_token::NearToken; +use crate::result::ExecutionFinalResult; use crate::signer::ExposeAccountId; use crate::{Client, Error, Result}; +/// Maximum amount of gas that can be used in a single transaction. pub const MAX_GAS: NearGas = NearGas::from_tgas(300); + +/// Default amount of gas to be used when calling into a function on a contract. +/// This is set to 10 TGas as a default for convenience. pub const DEFAULT_CALL_FN_GAS: NearGas = NearGas::from_tgas(10); + +/// Default amount of deposit to be used when calling into a function on a contract. +/// This is set to 0 NEAR as a default for convenience. Note, that some contracts +/// will require 1 yoctoNEAR to be deposited in order to perform a function. pub const DEFAULT_CALL_DEPOSIT: NearToken = NearToken::from_near(0); /// A set of arguments we can provide to a transaction, containing @@ -155,7 +166,7 @@ where S: Signer + ExposeAccountId + 'static, { /// Process the transaction, and return the result of the execution. - pub async fn transact(self) -> Result { + pub async fn transact(self) -> Result { self.client .send_tx( &self.signer, @@ -163,6 +174,7 @@ where vec![self.function.into_action()?.into()], ) .await + .map(ExecutionFinalResult::from_view) } /// Send the transaction to the network to be processed. This will be done asynchronously diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..145fc2c --- /dev/null +++ b/src/result.rs @@ -0,0 +1,348 @@ +//! Result and execution types from results of RPC calls to the network. + +use near_gas::NearGas; +use near_primitives::borsh; +use near_primitives::views::{ + ExecutionOutcomeWithIdView, ExecutionStatusView, FinalExecutionOutcomeView, + FinalExecutionStatus, SignedTransactionView, +}; + +use base64::{engine::general_purpose, Engine as _}; + +use crate::error::Result; +use crate::Error; + +/// Execution related info as a result of performing a successful transaction +/// execution on the network. This value can be converted into the returned +/// value of the transaction via [`ExecutionSuccess::json`] or [`ExecutionSuccess::borsh`] +pub type ExecutionSuccess = ExecutionResult; + +/// The transaction/receipt details of a transaction execution. This object +/// can be used to retrieve data such as logs and gas burnt per transaction +/// or receipt. +#[derive(PartialEq, Eq, Clone, Debug)] +#[non_exhaustive] +pub struct ExecutionDetails { + /// Original signed transaction. + pub transaction: SignedTransactionView, + /// The execution outcome of the signed transaction. + pub transaction_outcome: ExecutionOutcomeWithIdView, + /// The execution outcome of receipts. + pub receipts_outcome: Vec, +} + +impl ExecutionDetails { + /// Returns just the transaction outcome. + pub fn outcome(&self) -> &ExecutionOutcomeWithIdView { + &self.transaction_outcome + } + + /// Grab all outcomes after the execution of the transaction. This includes outcomes + /// from the transaction and all the receipts it generated. + pub fn outcomes(&self) -> Vec<&ExecutionOutcomeWithIdView> { + let mut outcomes = vec![self.outcome()]; + outcomes.extend(self.receipt_outcomes()); + outcomes + } + + /// Grab all outcomes after the execution of the transaction. This includes outcomes + /// only from receipts generated by this transaction. + pub fn receipt_outcomes(&self) -> &[ExecutionOutcomeWithIdView] { + &self.receipts_outcome + } + + /// Grab all outcomes that did not succeed the execution of this transaction. This + /// will also include the failures from receipts as well. + pub fn failures(&self) -> Vec<&ExecutionOutcomeWithIdView> { + let mut failures = Vec::new(); + if matches!( + self.transaction_outcome.outcome.status, + ExecutionStatusView::Failure(_) + ) { + failures.push(&self.transaction_outcome); + } + failures.extend(self.receipt_failures()); + failures + } + + /// Just like `failures`, grab only failed receipt outcomes. + pub fn receipt_failures(&self) -> Vec<&ExecutionOutcomeWithIdView> { + self.receipt_outcomes() + .iter() + .filter(|receipt| matches!(receipt.outcome.status, ExecutionStatusView::Failure(_))) + .collect() + } + + /// Grab all logs from both the transaction and receipt outcomes. + pub fn logs(&self) -> Vec<&str> { + self.outcomes() + .into_iter() + .flat_map(|outcome| &outcome.outcome.logs) + .map(String::as_str) + .collect() + } +} + +/// The result after evaluating the status of an execution. This can be [`ExecutionSuccess`] +/// for successful executions or a [`ExecutionFailure`] for failed ones. +#[derive(PartialEq, Eq, Debug, Clone)] +#[non_exhaustive] +pub struct ExecutionResult { + /// Total gas burnt by the execution + pub total_gas_burnt: NearGas, + + /// Value returned from an execution. This is a base64 encoded str for a successful + /// execution or a `TxExecutionError` if a failed one. + pub value: T, + + /// Additional details related to the execution. + pub details: ExecutionDetails, +} + +/// Execution related info found after performing a transaction. Can be converted +/// into [`ExecutionSuccess`] or [`ExecutionFailure`] through [`into_result`] +/// +/// [`into_result`]: crate::result::ExecutionFinalResult::into_result +#[derive(PartialEq, Eq, Clone, Debug)] +// #[must_use = "use `into_result()` to handle potential execution errors"] +pub struct ExecutionFinalResult { + status: FinalExecutionStatus, + pub details: ExecutionDetails, +} + +impl ExecutionFinalResult { + pub(crate) fn from_view(view: FinalExecutionOutcomeView) -> Self { + Self { + status: view.status, + details: ExecutionDetails { + transaction: view.transaction, + transaction_outcome: view.transaction_outcome, + receipts_outcome: view.receipts_outcome, + }, + } + } + + /// Converts this object into a [`Result`] holding either [`ExecutionSuccess`] or [`ExecutionFailure`]. + pub fn into_result(self) -> Result { + let total_gas_burnt = self.total_gas_burnt(); + match self.status { + FinalExecutionStatus::SuccessValue(value) => Ok(ExecutionResult { + total_gas_burnt, + value: Value::from_string(general_purpose::STANDARD.encode(value)), + details: self.details, + }), + FinalExecutionStatus::Failure(tx_error) => Err(Error::TxExecution(Box::new(tx_error))), + _ => unreachable!(), + } + } + + pub fn total_gas_burnt(&self) -> NearGas { + NearGas::from_gas( + self.details.transaction_outcome.outcome.gas_burnt + + self + .details + .receipts_outcome + .iter() + .map(|t| t.outcome.gas_burnt) + .sum::(), + ) + } + + /// Returns the contained Ok value, consuming the self value. + /// + /// Because this function may panic, its use is generally discouraged. Instead, prefer + /// to call into [`into_result`] then pattern matching and handle the Err case explicitly. + /// + /// [`into_result`]: crate::result::ExecutionFinalResult::into_result + pub fn unwrap(self) -> ExecutionSuccess { + self.into_result().unwrap() + } + + /// Deserialize an instance of type `T` from bytes of JSON text sourced from the + /// execution result of this call. This conversion can fail if the structure of + /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s + /// requirements. + pub fn json(self) -> Result { + self.into_result()?.json() + } + + /// Deserialize an instance of type `T` from bytes sourced from the execution + /// result. This conversion can fail if the structure of the internal state does + /// not meet up with [`borsh::BorshDeserialize`]'s requirements. + pub fn borsh(self) -> Result { + self.into_result()?.borsh() + } + + /// Grab the underlying raw bytes returned from calling into a contract's function. + /// If we want to deserialize these bytes into a rust datatype, use [`ExecutionResult::json`] + /// or [`ExecutionResult::borsh`] instead. + pub fn raw_bytes(self) -> Result> { + self.into_result()?.raw_bytes() + } + + /// Checks whether the transaction was successful. Returns true if + /// the transaction has a status of [`FinalExecutionStatus::SuccessValue`]. + pub fn is_success(&self) -> bool { + matches!(self.status(), FinalExecutionStatus::SuccessValue(_)) + } + + /// Checks whether the transaction has failed. Returns true if + /// the transaction has a status of [`FinalExecutionStatus::Failure`]. + pub fn is_failure(&self) -> bool { + matches!(self.status(), FinalExecutionStatus::Failure(_)) + } + + /// Returns just the transaction outcome. + pub fn outcome(&self) -> &ExecutionOutcomeWithIdView { + self.details.outcome() + } + + /// Grab all outcomes after the execution of the transaction. This includes outcomes + /// from the transaction and all the receipts it generated. + pub fn outcomes(&self) -> Vec<&ExecutionOutcomeWithIdView> { + self.details.outcomes() + } + + /// Grab all outcomes after the execution of the transaction. This includes outcomes + /// only from receipts generated by this transaction. + pub fn receipt_outcomes(&self) -> &[ExecutionOutcomeWithIdView] { + self.details.receipt_outcomes() + } + + /// Grab all outcomes that did not succeed the execution of this transaction. This + /// will also include the failures from receipts as well. + pub fn failures(&self) -> Vec<&ExecutionOutcomeWithIdView> { + self.details.failures() + } + + /// Just like `failures`, grab only failed receipt outcomes. + pub fn receipt_failures(&self) -> Vec<&ExecutionOutcomeWithIdView> { + self.details.receipt_failures() + } + + /// Grab all logs from both the transaction and receipt outcomes. + pub fn logs(&self) -> Vec<&str> { + self.details.logs() + } + + pub fn status(&self) -> &FinalExecutionStatus { + &self.status + } +} + +impl ExecutionSuccess { + /// Deserialize an instance of type `T` from bytes of JSON text sourced from the + /// execution result of this call. This conversion can fail if the structure of + /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s + /// requirements. + pub fn json(&self) -> Result { + self.value.json() + } + + /// Deserialize an instance of type `T` from bytes sourced from the execution + /// result. This conversion can fail if the structure of the internal state does + /// not meet up with [`borsh::BorshDeserialize`]'s requirements. + pub fn borsh(&self) -> Result { + self.value.borsh() + } + + /// Grab the underlying raw bytes returned from calling into a contract's function. + /// If we want to deserialize these bytes into a rust datatype, use [`ExecutionResult::json`] + /// or [`ExecutionResult::borsh`] instead. + pub fn raw_bytes(&self) -> Result> { + self.value.raw_bytes() + } +} + +impl ExecutionResult { + /// Returns just the transaction outcome. + pub fn outcome(&self) -> &ExecutionOutcomeWithIdView { + &self.details.transaction_outcome + } + + /// Grab all outcomes after the execution of the transaction. This includes outcomes + /// from the transaction and all the receipts it generated. + pub fn outcomes(&self) -> Vec<&ExecutionOutcomeWithIdView> { + let mut outcomes = vec![self.outcome()]; + outcomes.extend(self.receipt_outcomes()); + outcomes + } + + /// Grab all outcomes after the execution of the transaction. This includes outcomes + /// only from receipts generated by this transaction. + pub fn receipt_outcomes(&self) -> &[ExecutionOutcomeWithIdView] { + &self.details.receipts_outcome + } + + /// Grab all outcomes that did not succeed the execution of this transaction. This + /// will also include the failures from receipts as well. + pub fn failures(&self) -> Vec<&ExecutionOutcomeWithIdView> { + let mut failures = Vec::new(); + if matches!( + self.details.transaction_outcome.outcome.status, + ExecutionStatusView::Failure(_) + ) { + failures.push(&self.details.transaction_outcome); + } + failures.extend(self.receipt_failures()); + failures + } + + /// Just like `failures`, grab only failed receipt outcomes. + pub fn receipt_failures(&self) -> Vec<&ExecutionOutcomeWithIdView> { + self.receipt_outcomes() + .iter() + .filter(|receipt| matches!(receipt.outcome.status, ExecutionStatusView::Failure(_))) + .collect() + } + + /// Grab all logs from both the transaction and receipt outcomes. + pub fn logs(&self) -> Vec<&str> { + self.outcomes() + .into_iter() + .flat_map(|outcome| &outcome.outcome.logs) + .map(String::as_str) + .collect() + } +} + +/// Value type returned from an [`ExecutionOutcome`] or receipt result. This value +/// can be converted into the underlying Rust datatype, or directly grab the raw +/// bytes associated to the value. +#[derive(Debug)] +pub struct Value { + repr: String, +} + +impl Value { + fn from_string(value: String) -> Self { + Self { repr: value } + } + + /// Deserialize an instance of type `T` from bytes of JSON text sourced from the + /// execution result of this call. This conversion can fail if the structure of + /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s + /// requirements. + pub fn json(&self) -> Result { + let buf = self.raw_bytes()?; + Ok(serde_json::from_slice(&buf)?) + } + + /// Deserialize an instance of type `T` from bytes sourced from the execution + /// result. This conversion can fail if the structure of the internal state does + /// not meet up with [`borsh::BorshDeserialize`]'s requirements. + pub fn borsh(&self) -> Result { + let buf = self.raw_bytes()?; + Ok(borsh::BorshDeserialize::try_from_slice(&buf)?) + } + + /// Grab the underlying raw bytes returned from calling into a contract's function. + /// If we want to deserialize these bytes into a rust datatype, use [`json`] + /// or [`borsh`] instead. + /// + /// [`json`]: Value::json + /// [`borsh`]: Value::borsh + pub fn raw_bytes(&self) -> Result> { + Ok(general_purpose::STANDARD.decode(&self.repr)?) + } +}