use std::fmt;
use base64::{Engine as _, engine::general_purpose};
use borsh;
use near_openapi_types::{
CallResult, ExecutionStatusView, FinalExecutionOutcomeView, FinalExecutionStatus,
TxExecutionError, TxExecutionStatus,
};
use crate::{
AccountId, CryptoHash, NearGas, NearToken, Signature,
errors::{DataConversionError, ExecutionError},
transaction::{SignedTransaction, Transaction},
};
pub type ExecutionSuccess = ExecutionResult<Value>;
pub type ExecutionFailure = ExecutionResult<TxExecutionError>;
#[non_exhaustive]
#[must_use = "use `into_result()` to handle potential execution errors"]
pub struct Execution<T> {
pub result: T,
pub details: ExecutionFinalResult,
}
impl<T> Execution<T> {
#[track_caller]
pub fn assert_success(self) -> T {
#[allow(clippy::unwrap_used)]
self.into_result().unwrap()
}
#[allow(clippy::result_large_err)]
pub fn into_result(self) -> Result<T, ExecutionFailure> {
self.details.into_result()?;
Ok(self.result)
}
pub const fn is_success(&self) -> bool {
self.details.is_success()
}
pub const fn is_failure(&self) -> bool {
self.details.is_failure()
}
}
#[derive(Clone)]
pub(crate) struct ExecutionDetails {
pub(crate) transaction_outcome: ExecutionOutcome,
pub(crate) transaction: SignedTransaction,
pub(crate) receipts: Vec<ExecutionOutcome>,
}
impl ExecutionDetails {
pub const fn outcome(&self) -> &ExecutionOutcome {
&self.transaction_outcome
}
pub const fn transaction(&self) -> &Transaction {
&self.transaction.transaction
}
pub const fn signature(&self) -> &Signature {
&self.transaction.signature
}
pub fn outcomes(&self) -> Vec<&ExecutionOutcome> {
let mut outcomes = vec![&self.transaction_outcome];
outcomes.extend(self.receipt_outcomes());
outcomes
}
pub fn receipt_outcomes(&self) -> &[ExecutionOutcome] {
&self.receipts
}
pub fn failures(&self) -> Vec<&ExecutionOutcome> {
let mut failures = Vec::new();
if matches!(
self.transaction_outcome.status,
ExecutionStatusView::Failure(_)
) {
failures.push(&self.transaction_outcome);
}
failures.extend(self.receipt_failures());
failures
}
pub fn receipt_failures(&self) -> Vec<&ExecutionOutcome> {
self.receipts
.iter()
.filter(|receipt| matches!(receipt.status, ExecutionStatusView::Failure(_)))
.collect()
}
pub fn logs(&self) -> Vec<&str> {
self.outcomes()
.iter()
.flat_map(|outcome| &outcome.logs)
.map(String::as_str)
.collect()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ExecutionResult<T> {
pub total_gas_burnt: NearGas,
pub(crate) value: T,
pub(crate) details: ExecutionDetails,
}
impl<T: fmt::Debug> fmt::Debug for ExecutionResult<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutionResult")
.field("total_gas_burnt", &self.total_gas_burnt)
.field("transaction", &self.details.transaction)
.field("receipts", &self.details.receipts)
.field("value", &self.value)
.finish()
}
}
impl fmt::Display for ExecutionResult<TxExecutionError> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ExecutionFailure: {:?}", self.value)
}
}
impl std::error::Error for ExecutionResult<TxExecutionError> {}
#[derive(Clone)]
#[must_use = "use `into_result()` to handle potential execution errors"]
pub struct ExecutionFinalResult {
pub total_gas_burnt: NearGas,
pub(crate) status: FinalExecutionStatus,
pub(crate) details: ExecutionDetails,
}
impl fmt::Debug for ExecutionFinalResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExecutionFinalResult")
.field("total_gas_burnt", &self.total_gas_burnt)
.field("transaction", &self.details.transaction)
.field("receipts", &self.details.receipts)
.field("status", &self.status)
.finish()
}
}
impl TryFrom<FinalExecutionOutcomeView> for ExecutionFinalResult {
type Error = DataConversionError;
fn try_from(view: FinalExecutionOutcomeView) -> Result<Self, Self::Error> {
let FinalExecutionOutcomeView {
receipts_outcome,
status,
transaction,
transaction_outcome,
} = view;
let total_gas_burnt = transaction_outcome.outcome.gas_burnt.as_gas()
+ receipts_outcome
.iter()
.map(|t| t.outcome.gas_burnt.as_gas())
.sum::<u64>();
let transaction_outcome = transaction_outcome.into();
let receipts = receipts_outcome
.into_iter()
.map(ExecutionOutcome::from)
.collect();
let total_gas_burnt = NearGas::from_gas(total_gas_burnt);
Ok(Self {
total_gas_burnt,
status,
details: ExecutionDetails {
transaction_outcome,
transaction: SignedTransaction::try_from(transaction)?,
receipts,
},
})
}
}
impl ExecutionFinalResult {
#[allow(clippy::result_large_err)]
pub fn into_result(self) -> Result<ExecutionSuccess, ExecutionFailure> {
match self.status {
FinalExecutionStatus::SuccessValue(value) => Ok(ExecutionResult {
total_gas_burnt: self.total_gas_burnt,
value: Value::from_string(value),
details: self.details,
}),
FinalExecutionStatus::Failure(tx_error) => Err(ExecutionResult {
total_gas_burnt: self.total_gas_burnt,
value: tx_error,
details: self.details,
}),
FinalExecutionStatus::NotStarted | FinalExecutionStatus::Started => {
panic!(
"called `into_result()` on a transaction that is still pending \
(status: {:?}). Use `is_pending()` to check before calling this method, \
or use `Transaction::status_with_options()` with a `wait_until` value \
of `ExecutedOptimistic` or higher.",
self.status
)
}
}
}
#[track_caller]
pub fn assert_success(self) -> ExecutionSuccess {
#[allow(clippy::unwrap_used)]
self.into_result().unwrap()
}
#[track_caller]
pub fn assert_failure(self) -> ExecutionResult<TxExecutionError> {
#[allow(clippy::unwrap_used)]
self.into_result().unwrap_err()
}
pub fn json<T: serde::de::DeserializeOwned>(self) -> Result<T, ExecutionError> {
if self.is_pending() {
return Err(ExecutionError::ExecutionPendingOrUnknown);
}
let val = self.into_result()?;
match val.json() {
Err(err) => {
if matches!(
err,
ExecutionError::DataConversionError(
DataConversionError::JsonDeserializationError(_)
)
) && val.value.repr.is_empty()
{
return Err(ExecutionError::EofWhileParsingValue);
}
Err(err)
}
ok => ok,
}
}
pub fn borsh<T: borsh::BorshDeserialize>(self) -> Result<T, ExecutionError> {
if self.is_pending() {
return Err(ExecutionError::ExecutionPendingOrUnknown);
}
self.into_result()?.borsh()
}
pub fn raw_bytes(self) -> Result<Vec<u8>, ExecutionError> {
if self.is_pending() {
return Err(ExecutionError::ExecutionPendingOrUnknown);
}
self.into_result()?.raw_bytes()
}
pub const fn is_success(&self) -> bool {
matches!(self.status, FinalExecutionStatus::SuccessValue(_))
}
pub const fn is_failure(&self) -> bool {
matches!(self.status, FinalExecutionStatus::Failure(_))
}
pub const fn is_pending(&self) -> bool {
matches!(
self.status,
FinalExecutionStatus::NotStarted | FinalExecutionStatus::Started
)
}
pub const fn outcome(&self) -> &ExecutionOutcome {
self.details.outcome()
}
pub const fn transaction(&self) -> &Transaction {
self.details.transaction()
}
pub fn outcomes(&self) -> Vec<&ExecutionOutcome> {
self.details.outcomes()
}
pub fn receipt_outcomes(&self) -> &[ExecutionOutcome] {
self.details.receipt_outcomes()
}
pub fn failures(&self) -> Vec<&ExecutionOutcome> {
self.details.failures()
}
pub fn receipt_failures(&self) -> Vec<&ExecutionOutcome> {
self.details.receipt_failures()
}
pub fn logs(&self) -> Vec<&str> {
self.details.logs()
}
}
#[derive(Clone, Debug)]
#[must_use = "use `into_result()` to handle potential execution errors and cases when transaction is pending"]
pub enum TransactionResult {
Pending { status: TxExecutionStatus },
Full(Box<ExecutionFinalResult>),
}
impl TransactionResult {
#[allow(clippy::result_large_err)]
pub fn into_result(self) -> Result<ExecutionSuccess, TransactionResultError> {
match self {
Self::Full(result) => result
.into_result()
.map_err(|e| TransactionResultError::Failure(Box::new(e))),
Self::Pending { status } => Err(TransactionResultError::Pending(status)),
}
}
#[track_caller]
pub fn assert_success(self) -> ExecutionSuccess {
match self {
Self::Full(result) => result.assert_success(),
Self::Pending { status } => panic!(
"called `assert_success()` on a pending transaction (status: {status:?}). \
Use wait_until(TxExecutionStatus::Final) or handle the pending case."
),
}
}
pub const fn is_full(&self) -> bool {
matches!(self, Self::Full(_))
}
pub const fn is_pending(&self) -> bool {
matches!(self, Self::Pending { .. })
}
pub fn into_full(self) -> Option<ExecutionFinalResult> {
match self {
Self::Full(result) => Some(*result),
Self::Pending { .. } => None,
}
}
pub fn pending_status(self) -> Option<TxExecutionStatus> {
match self {
Self::Pending { status } => Some(status),
Self::Full(_) => None,
}
}
#[track_caller]
pub fn assert_failure(self) -> ExecutionResult<TxExecutionError> {
match self {
Self::Full(result) => result.assert_failure(),
Self::Pending { status } => panic!(
"called `assert_failure()` on a pending transaction (status: {status:?}). \
Use wait_until(TxExecutionStatus::Final) or handle the pending case."
),
}
}
pub const fn is_failure(&self) -> bool {
match self {
Self::Full(result) => result.is_failure(),
Self::Pending { .. } => false,
}
}
pub const fn is_success(&self) -> bool {
match self {
Self::Full(result) => result.is_success(),
Self::Pending { .. } => false,
}
}
#[track_caller]
pub fn transaction(&self) -> &Transaction {
match self {
Self::Full(result) => result.transaction(),
Self::Pending { status } => panic!(
"called `transaction()` on a pending transaction (status: {status:?}). \
Use wait_until(TxExecutionStatus::Final) or handle the pending case."
),
}
}
#[track_caller]
pub fn logs(&self) -> Vec<&str> {
match self {
Self::Full(result) => result.logs(),
Self::Pending { status } => panic!(
"called `logs()` on a pending transaction (status: {status:?}). \
Use wait_until(TxExecutionStatus::Final) or handle the pending case."
),
}
}
}
#[derive(Debug)]
pub enum TransactionResultError {
Failure(Box<ExecutionFailure>),
Pending(TxExecutionStatus),
}
impl fmt::Display for TransactionResultError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Failure(err) => write!(f, "Transaction failed: {err}"),
Self::Pending(status) => write!(
f,
"Transaction is pending (status: {status:?}). \
Execution results are not yet available."
),
}
}
}
impl std::error::Error for TransactionResultError {}
impl ExecutionSuccess {
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, ExecutionError> {
Ok(self.value.json()?)
}
pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T, ExecutionError> {
Ok(self.value.borsh()?)
}
pub fn raw_bytes(&self) -> Result<Vec<u8>, ExecutionError> {
Ok(self.value.raw_bytes()?)
}
}
impl<T> ExecutionResult<T> {
pub const fn outcome(&self) -> &ExecutionOutcome {
self.details.outcome()
}
pub const fn transaction(&self) -> &Transaction {
self.details.transaction()
}
pub const fn signature(&self) -> &Signature {
self.details.signature()
}
pub fn outcomes(&self) -> Vec<&ExecutionOutcome> {
self.details.outcomes()
}
pub fn receipt_outcomes(&self) -> &[ExecutionOutcome] {
self.details.receipt_outcomes()
}
pub fn failures(&self) -> Vec<&ExecutionOutcome> {
self.details.failures()
}
pub fn receipt_failures(&self) -> Vec<&ExecutionOutcome> {
self.details.receipt_failures()
}
pub fn logs(&self) -> Vec<&str> {
self.details.logs()
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[non_exhaustive]
pub struct ViewResultDetails {
pub result: Vec<u8>,
pub logs: Vec<String>,
}
impl ViewResultDetails {
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, DataConversionError> {
Ok(serde_json::from_slice(&self.result)?)
}
pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T, DataConversionError> {
Ok(borsh::BorshDeserialize::try_from_slice(&self.result)?)
}
}
impl From<CallResult> for ViewResultDetails {
fn from(result: CallResult) -> Self {
Self {
result: result.result,
logs: result.logs,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ExecutionOutcome {
pub transaction_hash: CryptoHash,
pub block_hash: CryptoHash,
pub logs: Vec<String>,
pub receipt_ids: Vec<CryptoHash>,
pub gas_burnt: NearGas,
pub tokens_burnt: NearToken,
pub executor_id: AccountId,
pub(crate) status: ExecutionStatusView,
}
impl ExecutionOutcome {
pub const fn is_success(&self) -> bool {
matches!(
self.status,
ExecutionStatusView::SuccessValue(_) | ExecutionStatusView::SuccessReceiptId(_)
)
}
pub const fn is_failure(&self) -> bool {
matches!(
self.status,
ExecutionStatusView::Failure(_) | ExecutionStatusView::Unknown
)
}
pub fn into_result(self) -> Result<ValueOrReceiptId, ExecutionError> {
match self.status {
ExecutionStatusView::SuccessValue(value) => {
Ok(ValueOrReceiptId::Value(Value::from_string(value)))
}
ExecutionStatusView::SuccessReceiptId(hash) => {
Ok(ValueOrReceiptId::ReceiptId(hash.into()))
}
ExecutionStatusView::Failure(err) => {
Err(ExecutionError::TransactionExecutionFailed(Box::new(err)))
}
ExecutionStatusView::Unknown => Err(ExecutionError::ExecutionPendingOrUnknown),
}
}
}
#[derive(Debug)]
pub enum ValueOrReceiptId {
Value(Value),
ReceiptId(CryptoHash),
}
#[derive(Debug, Clone)]
pub struct Value {
repr: String,
}
impl Value {
const fn from_string(value: String) -> Self {
Self { repr: value }
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, DataConversionError> {
let buf = self.raw_bytes()?;
Ok(serde_json::from_slice(&buf)?)
}
pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T, DataConversionError> {
let buf = self.raw_bytes()?;
Ok(borsh::BorshDeserialize::try_from_slice(&buf)?)
}
pub fn raw_bytes(&self) -> Result<Vec<u8>, DataConversionError> {
Ok(general_purpose::STANDARD.decode(&self.repr)?)
}
}
impl From<near_openapi_types::ExecutionOutcomeWithIdView> for ExecutionOutcome {
fn from(view: near_openapi_types::ExecutionOutcomeWithIdView) -> Self {
let near_openapi_types::ExecutionOutcomeWithIdView {
id,
block_hash,
outcome,
proof: _, } = view;
Self {
transaction_hash: id.into(),
block_hash: block_hash.into(),
logs: outcome.logs,
receipt_ids: outcome
.receipt_ids
.into_iter()
.map(CryptoHash::from)
.collect(),
gas_burnt: outcome.gas_burnt,
tokens_burnt: outcome.tokens_burnt,
executor_id: outcome.executor_id,
status: outcome.status,
}
}
}