unc_workspaces/result.rs
1//! Result and execution types from results of RPC calls to the network.
2
3use std::fmt;
4
5use base64::{engine::general_purpose, Engine as _};
6
7use unc_account_id::AccountId;
8use unc_gas::UncGas;
9use unc_primitives::borsh;
10use unc_primitives::errors::TxExecutionError;
11use unc_primitives::views::{
12 CallResult, ExecutionOutcomeWithIdView, ExecutionStatusView, FinalExecutionOutcomeView,
13 FinalExecutionStatus,
14};
15
16use crate::error::ErrorKind;
17use crate::types::{CryptoHash, Gas, UncToken};
18
19pub type Result<T, E = crate::error::Error> = core::result::Result<T, E>;
20
21/// Execution related info as a result of performing a successful transaction
22/// execution on the network. This value can be converted into the returned
23/// value of the transaction via [`ExecutionSuccess::json`] or [`ExecutionSuccess::borsh`]
24pub type ExecutionSuccess = ExecutionResult<Value>;
25
26/// Execution related info as a result of performing a failed transaction
27/// execution on the network. The related error message can be retrieved
28/// from this object or can be forwarded.
29pub type ExecutionFailure = ExecutionResult<TxExecutionError>;
30
31/// Struct to hold a type we want to return along w/ the execution result view.
32/// This view has extra info about the execution, such as gas usage and whether
33/// the transaction failed to be processed on the chain.
34#[non_exhaustive]
35#[must_use = "use `into_result()` to handle potential execution errors"]
36pub struct Execution<T> {
37 pub result: T,
38 pub details: ExecutionFinalResult,
39}
40
41impl<T> Execution<T> {
42 pub fn unwrap(self) -> T {
43 self.into_result().unwrap()
44 }
45
46 #[allow(clippy::result_large_err)]
47 pub fn into_result(self) -> Result<T, ExecutionFailure> {
48 self.details.into_result()?;
49 Ok(self.result)
50 }
51
52 /// Checks whether the transaction was successful. Returns true if
53 /// the transaction has a status of FinalExecutionStatus::Success.
54 pub fn is_success(&self) -> bool {
55 self.details.is_success()
56 }
57
58 /// Checks whether the transaction has failed. Returns true if
59 /// the transaction has a status of FinalExecutionStatus::Failure.
60 pub fn is_failure(&self) -> bool {
61 self.details.is_failure()
62 }
63}
64
65/// The transaction/receipt details of a transaction execution. This object
66/// can be used to retrieve data such as logs and gas burnt per transaction
67/// or receipt.
68#[derive(PartialEq, Eq, Clone)]
69pub(crate) struct ExecutionDetails {
70 pub(crate) transaction: ExecutionOutcome,
71 pub(crate) receipts: Vec<ExecutionOutcome>,
72}
73
74impl ExecutionDetails {
75 /// Returns just the transaction outcome.
76 pub fn outcome(&self) -> &ExecutionOutcome {
77 &self.transaction
78 }
79
80 /// Grab all outcomes after the execution of the transaction. This includes outcomes
81 /// from the transaction and all the receipts it generated.
82 pub fn outcomes(&self) -> Vec<&ExecutionOutcome> {
83 let mut outcomes = vec![&self.transaction];
84 outcomes.extend(self.receipt_outcomes());
85 outcomes
86 }
87
88 /// Grab all outcomes after the execution of the transaction. This includes outcomes
89 /// only from receipts generated by this transaction.
90 pub fn receipt_outcomes(&self) -> &[ExecutionOutcome] {
91 &self.receipts
92 }
93
94 /// Grab all outcomes that did not succeed the execution of this transaction. This
95 /// will also include the failures from receipts as well.
96 pub fn failures(&self) -> Vec<&ExecutionOutcome> {
97 let mut failures = Vec::new();
98 if matches!(self.transaction.status, ExecutionStatusView::Failure(_)) {
99 failures.push(&self.transaction);
100 }
101 failures.extend(self.receipt_failures());
102 failures
103 }
104
105 /// Just like `failures`, grab only failed receipt outcomes.
106 pub fn receipt_failures(&self) -> Vec<&ExecutionOutcome> {
107 self.receipts
108 .iter()
109 .filter(|receipt| matches!(receipt.status, ExecutionStatusView::Failure(_)))
110 .collect()
111 }
112
113 /// Grab all logs from both the transaction and receipt outcomes.
114 pub fn logs(&self) -> Vec<&str> {
115 self.outcomes()
116 .iter()
117 .flat_map(|outcome| &outcome.logs)
118 .map(String::as_str)
119 .collect()
120 }
121}
122
123/// The result after evaluating the status of an execution. This can be [`ExecutionSuccess`]
124/// for successful executions or a [`ExecutionFailure`] for failed ones.
125#[derive(PartialEq, Eq, Clone)]
126#[non_exhaustive]
127pub struct ExecutionResult<T> {
128 /// Total gas burnt by the execution
129 pub total_gas_burnt: Gas,
130
131 /// Value returned from an execution. This is a base64 encoded str for a successful
132 /// execution or a `TxExecutionError` if a failed one.
133 pub(crate) value: T,
134 // pub(crate) transaction: ExecutionOutcome,
135 // pub(crate) receipts: Vec<ExecutionOutcome>,
136 pub(crate) details: ExecutionDetails,
137}
138
139impl<T: fmt::Debug> fmt::Debug for ExecutionResult<T> {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 f.debug_struct("ExecutionResult")
142 .field("total_gas_burnt", &self.total_gas_burnt)
143 .field("transaction", &self.details.transaction)
144 .field("receipts", &self.details.receipts)
145 .field("value", &self.value)
146 .finish()
147 }
148}
149
150/// Execution related info found after performing a transaction. Can be converted
151/// into [`ExecutionSuccess`] or [`ExecutionFailure`] through [`into_result`]
152///
153/// [`into_result`]: crate::result::ExecutionFinalResult::into_result
154#[derive(PartialEq, Eq, Clone)]
155#[must_use = "use `into_result()` to handle potential execution errors"]
156pub struct ExecutionFinalResult {
157 /// Total gas burnt by the execution
158 pub total_gas_burnt: Gas,
159
160 pub(crate) status: FinalExecutionStatus,
161 pub(crate) details: ExecutionDetails,
162}
163
164impl fmt::Debug for ExecutionFinalResult {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.debug_struct("ExecutionFinalResult")
167 .field("total_gas_burnt", &self.total_gas_burnt)
168 .field("transaction", &self.details.transaction)
169 .field("receipts", &self.details.receipts)
170 .field("status", &self.status)
171 .finish()
172 }
173}
174
175impl ExecutionFinalResult {
176 pub(crate) fn from_view(view: FinalExecutionOutcomeView) -> Self {
177 let total_gas_burnt = view.transaction_outcome.outcome.gas_burnt
178 + view
179 .receipts_outcome
180 .iter()
181 .map(|t| t.outcome.gas_burnt)
182 .sum::<u64>();
183
184 let transaction = view.transaction_outcome.into();
185 let receipts = view
186 .receipts_outcome
187 .into_iter()
188 .map(ExecutionOutcome::from)
189 .collect();
190
191 let total_gas_burnt = UncGas::from_gas(total_gas_burnt);
192 Self {
193 total_gas_burnt,
194 status: view.status,
195 details: ExecutionDetails {
196 transaction,
197 receipts,
198 },
199 }
200 }
201
202 /// Converts this object into a [`Result`] holding either [`ExecutionSuccess`] or [`ExecutionFailure`].
203 #[allow(clippy::result_large_err)]
204 pub fn into_result(self) -> Result<ExecutionSuccess, ExecutionFailure> {
205 match self.status {
206 FinalExecutionStatus::SuccessValue(value) => Ok(ExecutionResult {
207 total_gas_burnt: self.total_gas_burnt,
208 value: Value::from_string(general_purpose::STANDARD.encode(value)),
209 details: self.details,
210 }),
211 FinalExecutionStatus::Failure(tx_error) => Err(ExecutionResult {
212 total_gas_burnt: self.total_gas_burnt,
213 value: tx_error,
214 details: self.details,
215 }),
216 _ => unreachable!(),
217 }
218 }
219
220 /// Returns the contained Ok value, consuming the self value.
221 ///
222 /// Because this function may panic, its use is generally discouraged. Instead, prefer
223 /// to call into [`into_result`] then pattern matching and handle the Err case explicitly.
224 ///
225 /// [`into_result`]: crate::result::ExecutionFinalResult::into_result
226 pub fn unwrap(self) -> ExecutionSuccess {
227 self.into_result().unwrap()
228 }
229
230 /// Deserialize an instance of type `T` from bytes of JSON text sourced from the
231 /// execution result of this call. This conversion can fail if the structure of
232 /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s
233 /// requirements.
234 pub fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
235 let val = self.into_result()?;
236 match val.json() {
237 Err(err) => {
238 // This catches the case: `EOF while parsing a value at line 1 column 0`
239 // for a function that doesn't return anything; this is a more descriptive error.
240 if *err.kind() == ErrorKind::DataConversion && val.value.repr.is_empty() {
241 return Err(ErrorKind::DataConversion.custom(
242 "the function call returned an empty value, which cannot be parsed as JSON",
243 ));
244 }
245
246 Err(err)
247 }
248 ok => ok,
249 }
250 }
251
252 /// Deserialize an instance of type `T` from bytes sourced from the execution
253 /// result. This conversion can fail if the structure of the internal state does
254 /// not meet up with [`borsh::BorshDeserialize`]'s requirements.
255 pub fn borsh<T: borsh::BorshDeserialize>(self) -> Result<T> {
256 self.into_result()?.borsh()
257 }
258
259 /// Grab the underlying raw bytes returned from calling into a contract's function.
260 /// If we want to deserialize these bytes into a rust datatype, use [`ExecutionResult::json`]
261 /// or [`ExecutionResult::borsh`] instead.
262 pub fn raw_bytes(self) -> Result<Vec<u8>> {
263 self.into_result()?.raw_bytes()
264 }
265
266 /// Checks whether the transaction was successful. Returns true if
267 /// the transaction has a status of [`FinalExecutionStatus::SuccessValue`].
268 pub fn is_success(&self) -> bool {
269 matches!(self.status, FinalExecutionStatus::SuccessValue(_))
270 }
271
272 /// Checks whether the transaction has failed. Returns true if
273 /// the transaction has a status of [`FinalExecutionStatus::Failure`].
274 pub fn is_failure(&self) -> bool {
275 matches!(self.status, FinalExecutionStatus::Failure(_))
276 }
277
278 /// Returns just the transaction outcome.
279 pub fn outcome(&self) -> &ExecutionOutcome {
280 self.details.outcome()
281 }
282
283 /// Grab all outcomes after the execution of the transaction. This includes outcomes
284 /// from the transaction and all the receipts it generated.
285 pub fn outcomes(&self) -> Vec<&ExecutionOutcome> {
286 self.details.outcomes()
287 }
288
289 /// Grab all outcomes after the execution of the transaction. This includes outcomes
290 /// only from receipts generated by this transaction.
291 pub fn receipt_outcomes(&self) -> &[ExecutionOutcome] {
292 self.details.receipt_outcomes()
293 }
294
295 /// Grab all outcomes that did not succeed the execution of this transaction. This
296 /// will also include the failures from receipts as well.
297 pub fn failures(&self) -> Vec<&ExecutionOutcome> {
298 self.details.failures()
299 }
300
301 /// Just like `failures`, grab only failed receipt outcomes.
302 pub fn receipt_failures(&self) -> Vec<&ExecutionOutcome> {
303 self.details.receipt_failures()
304 }
305
306 /// Grab all logs from both the transaction and receipt outcomes.
307 pub fn logs(&self) -> Vec<&str> {
308 self.details.logs()
309 }
310}
311
312impl ExecutionSuccess {
313 /// Deserialize an instance of type `T` from bytes of JSON text sourced from the
314 /// execution result of this call. This conversion can fail if the structure of
315 /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s
316 /// requirements.
317 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
318 self.value.json()
319 }
320
321 /// Deserialize an instance of type `T` from bytes sourced from the execution
322 /// result. This conversion can fail if the structure of the internal state does
323 /// not meet up with [`borsh::BorshDeserialize`]'s requirements.
324 pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T> {
325 self.value.borsh()
326 }
327
328 /// Grab the underlying raw bytes returned from calling into a contract's function.
329 /// If we want to deserialize these bytes into a rust datatype, use [`ExecutionResult::json`]
330 /// or [`ExecutionResult::borsh`] instead.
331 pub fn raw_bytes(&self) -> Result<Vec<u8>> {
332 self.value.raw_bytes()
333 }
334}
335
336impl<T> ExecutionResult<T> {
337 /// Returns just the transaction outcome.
338 pub fn outcome(&self) -> &ExecutionOutcome {
339 self.details.outcome()
340 }
341
342 /// Grab all outcomes after the execution of the transaction. This includes outcomes
343 /// from the transaction and all the receipts it generated.
344 pub fn outcomes(&self) -> Vec<&ExecutionOutcome> {
345 self.details.outcomes()
346 }
347
348 /// Grab all outcomes after the execution of the transaction. This includes outcomes
349 /// only from receipts generated by this transaction.
350 pub fn receipt_outcomes(&self) -> &[ExecutionOutcome] {
351 self.details.receipt_outcomes()
352 }
353
354 /// Grab all outcomes that did not succeed the execution of this transaction. This
355 /// will also include the failures from receipts as well.
356 pub fn failures(&self) -> Vec<&ExecutionOutcome> {
357 self.details.failures()
358 }
359
360 /// Just like `failures`, grab only failed receipt outcomes.
361 pub fn receipt_failures(&self) -> Vec<&ExecutionOutcome> {
362 self.details.receipt_failures()
363 }
364
365 /// Grab all logs from both the transaction and receipt outcomes.
366 pub fn logs(&self) -> Vec<&str> {
367 self.details.logs()
368 }
369}
370
371/// The result from a call into a View function. This contains the contents or
372/// the results from the view function call itself. The consumer of this object
373/// can choose how to deserialize its contents.
374#[derive(PartialEq, Eq, Clone, Debug)]
375#[non_exhaustive]
376pub struct ViewResultDetails {
377 /// Our result from our call into a view function.
378 pub result: Vec<u8>,
379 /// Logs generated from the view function.
380 pub logs: Vec<String>,
381}
382
383impl ViewResultDetails {
384 /// Deserialize an instance of type `T` from bytes of JSON text sourced from the
385 /// execution result of this call. This conversion can fail if the structure of
386 /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s
387 /// requirements.
388 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
389 serde_json::from_slice(&self.result).map_err(|e| ErrorKind::DataConversion.custom(e))
390 }
391
392 /// Deserialize an instance of type `T` from bytes sourced from this view call's
393 /// result. This conversion can fail if the structure of the internal state does
394 /// not meet up with [`borsh::BorshDeserialize`]'s requirements.
395 pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T> {
396 borsh::BorshDeserialize::try_from_slice(&self.result)
397 .map_err(|e| ErrorKind::DataConversion.custom(e))
398 }
399}
400
401impl From<CallResult> for ViewResultDetails {
402 fn from(result: CallResult) -> Self {
403 Self {
404 result: result.result,
405 logs: result.logs,
406 }
407 }
408}
409
410/// The execution outcome of a transaction. This type contains all data relevant to
411/// calling into a function, and getting the results back.
412#[derive(Clone, Debug, PartialEq, Eq)]
413#[non_exhaustive]
414pub struct ExecutionOutcome {
415 /// The hash of the transaction that generated this outcome.
416 pub transaction_hash: CryptoHash,
417 /// The hash of the block that generated this outcome.
418 pub block_hash: CryptoHash,
419 /// Logs from this transaction or receipt.
420 pub logs: Vec<String>,
421 /// Receipt IDs generated by this transaction or receipt.
422 pub receipt_ids: Vec<CryptoHash>,
423 /// The amount of the gas burnt by the given transaction or receipt.
424 pub gas_burnt: Gas,
425 /// The amount of tokens burnt corresponding to the burnt gas amount.
426 /// This value doesn't always equal to the `gas_burnt` multiplied by the gas price, because
427 /// the prepaid gas price might be lower than the actual gas price and it creates a deficit.
428 pub tokens_burnt: UncToken,
429 /// The id of the account on which the execution happens. For transaction this is signer_id,
430 /// for receipt this is receiver_id.
431 pub executor_id: AccountId,
432 /// Execution status. Contains the result in case of successful execution.
433 pub(crate) status: ExecutionStatusView,
434}
435
436impl ExecutionOutcome {
437 /// Checks whether this execution outcome was a success. Returns true if a success value or
438 /// receipt id is present.
439 pub fn is_success(&self) -> bool {
440 matches!(
441 self.status,
442 ExecutionStatusView::SuccessValue(_) | ExecutionStatusView::SuccessReceiptId(_)
443 )
444 }
445
446 /// Checks whether this execution outcome was a failure. Returns true if it failed with
447 /// an error or the execution state was unknown or pending.
448 pub fn is_failure(&self) -> bool {
449 matches!(
450 self.status,
451 ExecutionStatusView::Failure(_) | ExecutionStatusView::Unknown
452 )
453 }
454
455 /// Converts this [`ExecutionOutcome`] into a Result type to match against whether the
456 /// particular outcome has failed or not.
457 pub fn into_result(self) -> Result<ValueOrReceiptId> {
458 match self.status {
459 ExecutionStatusView::SuccessValue(value) => Ok(ValueOrReceiptId::Value(
460 Value::from_string(general_purpose::STANDARD.encode(value)),
461 )),
462 ExecutionStatusView::SuccessReceiptId(hash) => {
463 Ok(ValueOrReceiptId::ReceiptId(CryptoHash(hash.0)))
464 }
465 ExecutionStatusView::Failure(err) => Err(ErrorKind::Execution.custom(err)),
466 ExecutionStatusView::Unknown => {
467 Err(ErrorKind::Execution.message("Execution pending or unknown"))
468 }
469 }
470 }
471}
472
473/// Value or ReceiptId from a successful execution.
474#[derive(Debug)]
475pub enum ValueOrReceiptId {
476 /// The final action succeeded and returned some value or an empty vec encoded in base64.
477 Value(Value),
478 /// The final action of the receipt returned a promise or the signed transaction was converted
479 /// to a receipt. Contains the receipt_id of the generated receipt.
480 ReceiptId(CryptoHash),
481}
482
483/// Value type returned from an [`ExecutionOutcome`] or receipt result. This value
484/// can be converted into the underlying Rust datatype, or directly grab the raw
485/// bytes associated to the value.
486#[derive(Debug, Clone)]
487pub struct Value {
488 repr: String,
489}
490
491impl Value {
492 fn from_string(value: String) -> Self {
493 Self { repr: value }
494 }
495
496 /// Deserialize an instance of type `T` from bytes of JSON text sourced from the
497 /// execution result of this call. This conversion can fail if the structure of
498 /// the internal state does not meet up with [`serde::de::DeserializeOwned`]'s
499 /// requirements.
500 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
501 let buf = self.raw_bytes()?;
502 serde_json::from_slice(&buf).map_err(|e| ErrorKind::DataConversion.custom(e))
503 }
504
505 /// Deserialize an instance of type `T` from bytes sourced from the execution
506 /// result. This conversion can fail if the structure of the internal state does
507 /// not meet up with [`borsh::BorshDeserialize`]'s requirements.
508 pub fn borsh<T: borsh::BorshDeserialize>(&self) -> Result<T> {
509 let buf = self.raw_bytes()?;
510 borsh::BorshDeserialize::try_from_slice(&buf)
511 .map_err(|e| ErrorKind::DataConversion.custom(e))
512 }
513
514 /// Grab the underlying raw bytes returned from calling into a contract's function.
515 /// If we want to deserialize these bytes into a rust datatype, use [`json`]
516 /// or [`borsh`] instead.
517 ///
518 /// [`json`]: Value::json
519 /// [`borsh`]: Value::borsh
520 pub fn raw_bytes(&self) -> Result<Vec<u8>> {
521 general_purpose::STANDARD
522 .decode(&self.repr)
523 .map_err(|e| ErrorKind::DataConversion.custom(e))
524 }
525}
526
527impl From<ExecutionOutcomeWithIdView> for ExecutionOutcome {
528 fn from(view: ExecutionOutcomeWithIdView) -> Self {
529 Self {
530 transaction_hash: CryptoHash(view.id.0),
531 block_hash: CryptoHash(view.block_hash.0),
532 logs: view.outcome.logs,
533 receipt_ids: view
534 .outcome
535 .receipt_ids
536 .into_iter()
537 .map(|c| CryptoHash(c.0))
538 .collect(),
539 gas_burnt: UncGas::from_gas(view.outcome.gas_burnt),
540 tokens_burnt: UncToken::from_attounc(view.outcome.tokens_burnt),
541 executor_id: view.outcome.executor_id,
542 status: view.outcome.status,
543 }
544 }
545}