thruput-sdk 0.58.0

Thruput smart-contract SDK for Rust (rv32 guest runtime and native test harness)
Documentation
use super::fault::*;
use super::{AuthTokenError, CallingError, HandlingError};
use crate::ContractTypeSpec;
use crate::call::CallFault;
use crate::sys::{SysErr, SyscallErrorCode, SyscallNo};
use crate::xdr::*;
use core::error::Error;
use core::fmt::{self, Display};
#[cfg(not(target_arch = "riscv32"))]
use thiserror::Error;

/// An exception generated by SDK internals.
///
/// The wire layout is fixed regardless of crate features -- discriminants
/// and payloads alike. Both halves of that have been broken before:
/// discriminant 8 was once a feature-gated `Storage(StorageError)` nested
/// enum (now the flat `StorageSerialization`, with the other storage
/// conditions at 9 and 10), and the variants carrying a
/// [`FaultString`](crate::exn::FaultString) once encoded its message under
/// `debugging`, so an abort payload differed between a host test and the
/// shipped artifact. `FaultString` now keeps its message host-side and
/// encodes to nothing. Keep new variants payload-stable the same way.
#[derive(Debug, PartialEq, ReadXDR, WriteXDR, ConstXDRSize)]
#[cfg_attr(not(target_arch = "riscv32"), derive(Error))]
#[derive(ContractTypeSpec)]
// `wire_crate` fixes the crate label in the selector preimage at `sdk`:
// every contract throws and matches `LibraryExn`, so its selector is wire
// ABI and must not follow the package name.
#[thruput_macros::exn(version = "1.0.0", wire_crate = "sdk")]
#[repr(i32)]
#[non_exhaustive]
pub enum LibraryExn {
    #[cfg_attr(not(target_arch = "riscv32"), error(transparent))]
    Syscall(SyscallExn) = 0,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("Handling a contract call: {0}")
    )]
    Handling(HandlingError) = 1,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("Making a contract call: {0}")
    )]
    Calling(CallingError) = 2,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("Called contract reverted: {0}")
    )]
    Called(#[cfg_attr(not(target_arch = "riscv32"), source)] FaultString) = 3,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("Encoding auth token: {0}")
    )]
    AuthToken(AuthTokenError) = 4,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("bytestring_get_sized: expected {expected}, found {found}")
    )]
    BytestringWrongSize { expected: u32, found: u32 } = 5,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("non-negative int went negative")
    )]
    NonNegativeIntUnderflow = 6,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("non-negative int overflow")
    )]
    NonNegativeIntOverflow = 7,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("storage value failed to (de)serialize")
    )]
    StorageSerialization = 8,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("storage index out of bounds: len={len}, access={access}")
    )]
    StorageIndexOutOfBounds { len: u64, access: u64 } = 9,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("storage keypath component is too long to encode")
    )]
    StorageKeypathOverflow = 10,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error(
            "retrocausal queue entry: expected {expected} bytes, \
               found {found}"
        )
    )]
    RetrocausalQueueEntryWrongSize { expected: u32, found: u32 } = 11,

    #[cfg_attr(
        not(target_arch = "riscv32"),
        error(
            "capacity constraint: set_capacity conflicts with a prior \
               write this transaction"
        )
    )]
    CapacityConstraintAlreadyWritten = 12,

    /// The invoked method id matched no entrypoint on this contract.
    ///
    /// Thrown by guest dispatch (and by the native mock, for a contract
    /// registered through `register_mock_package`) in place of the bare
    /// trap it used to answer with, which a caller could not tell apart
    /// from an OOB access or a bad instruction. Carries no method id —
    /// the caller picked the id it invoked.
    #[cfg_attr(
        not(target_arch = "riscv32"),
        error("no entrypoint on this contract matches the invoked method")
    )]
    UnknownMethod = 13,
}

impl From<HandlingError> for LibraryExn {
    fn from(e: HandlingError) -> Self {
        Self::Handling(e)
    }
}

impl From<CallingError> for LibraryExn {
    fn from(e: CallingError) -> Self {
        Self::Calling(e)
    }
}

impl From<AuthTokenError> for LibraryExn {
    fn from(e: AuthTokenError) -> Self {
        Self::AuthToken(e)
    }
}

/// `Result<T, LibraryExn>::or_re_throw()` aborts with the contained
/// library exception (typed, decodable by selector) rather than a fault.
impl crate::re_throw::ReThrow for LibraryExn {
    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
    fn re_throw(self) -> ! {
        library_throw(self)
    }
}

impl LibraryExn {
    pub fn as_syscall_exn(&self) -> Option<&SyscallExn> {
        if let LibraryExn::Syscall(se) = self {
            Some(se)
        } else {
            None
        }
    }

    /// True if this is a syscall error saying nothing is stored where a
    /// storage read looked: a missing key
    /// ([`kSyscallErrorCodeStorageKeyNotExist`](SyscallErrorCode::kSyscallErrorCodeStorageKeyNotExist))
    /// or an empty retro-causal queue
    /// ([`kSyscallErrorCodeNoRetrocausalQueueEntry`](SyscallErrorCode::kSyscallErrorCodeNoRetrocausalQueueEntry)).
    ///
    /// This is the "graceful" arm of the storage `try_` getters: a `try_get`
    /// / `try_consume` `Err` for which this returns `false` means the read
    /// failed for a reason other than absence (wrong storage type at the
    /// key, a corrupt encoding, a host fault).
    pub fn is_storage_missing(&self) -> bool {
        match self.as_syscall_exn() {
            Some(se) => {
                se.has_code(
                    SyscallErrorCode::kSyscallErrorCodeStorageKeyNotExist,
                ) || se.has_code(
                    SyscallErrorCode::kSyscallErrorCodeNoRetrocausalQueueEntry,
                )
            }
            None => false,
        }
    }
}

/// Recover the pre-`Result` storage-getter semantics from a `try_` read:
/// "absent is an ordinary outcome, anything else is not my problem".
pub trait NoneIfMissing<T> {
    /// `Ok(v)` → `Some(v)`; an [`is_storage_missing`](LibraryExn::is_storage_missing)
    /// error → `None`; any other error re-throws (aborts, typed).
    fn none_if_missing(self) -> Option<T>;
}

impl<T> NoneIfMissing<T> for Result<T, LibraryExn> {
    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
    fn none_if_missing(self) -> Option<T> {
        use crate::re_throw::ReThrow;
        match self {
            Ok(v) => Some(v),
            Err(e) if e.is_storage_missing() => None,
            Err(e) => e.re_throw(),
        }
    }
}

impl crate::__contract_spec::ContractTypeName for LibraryExn {
    const NAME: crate::__contract_spec::ContractSpecIdView<'static> =
        crate::__contract_spec::ContractSpecIdView {
            crate_name: ::core::env!("CARGO_PKG_NAME"),
            major: crate::exn::EXN_ABI_MAJOR,
            module_path: ::core::module_path!(),
            declared: "LibraryExn",
        };
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn library_throw<E: Into<LibraryExn> + core::fmt::Debug>(e: E) -> ! {
    super::abort_encode::library_abort(&e.into())
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn client_library_throw<E: Into<LibraryExn> + core::fmt::Debug>(
    e: E,
) -> ! {
    super::abort_encode::client_library_abort(&e.into())
}

/// Throw an exception because a syscall produced an error.
///
#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn syscall_throw(callno: SyscallNo, errno: SysErr) -> ! {
    library_throw(syscall_exn(callno, errno))
}

pub fn syscall_exn(callno: SyscallNo, errno: SysErr) -> LibraryExn {
    LibraryExn::Syscall(SyscallExn::new(callno, errno))
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn nnint_overflow() -> ! {
    library_throw(LibraryExn::NonNegativeIntOverflow)
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn nnint_underflow() -> ! {
    library_throw(LibraryExn::NonNegativeIntUnderflow)
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn decoding_args_throw(e: &CallFault) -> ! {
    library_throw(HandlingError::DecodingArgs(FaultString::from_display(e)))
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn encoding_success_throw<E: Error>(e: E) -> ! {
    library_throw(HandlingError::EncodingSuccess(FaultString::from_display(e)))
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn encoding_exception_throw<E: Error>(e: E) -> ! {
    library_throw(HandlingError::EncodingException(FaultString::from_display(
        e,
    )))
}

#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn encoding_event_throw<E: Display>(e: E) -> ! {
    library_throw(HandlingError::EncodingEvent(FaultString::from_display(e)))
}

#[derive(Debug, Clone, Copy, ReadXDR, WriteXDR, PartialEq, ConstXDRSize)]
#[cfg_attr(not(target_arch = "riscv32"), derive(Error))]
#[derive(ContractTypeSpec)]
pub struct SyscallExn {
    callno: SyscallNo,
    errno: SysErr,
}

impl SyscallExn {
    pub fn new(callno: SyscallNo, errno: SysErr) -> Self {
        Self { callno, errno }
    }

    pub fn callno(&self) -> SyscallNo {
        self.callno
    }

    pub fn has_code(&self, err_code: SyscallErrorCode) -> bool {
        self.errno.is_code(err_code)
    }

    pub fn is(&self, call_no: SyscallNo, err_code: SyscallErrorCode) -> bool {
        self.callno == call_no && self.errno == err_code.into()
    }
}

impl Display for SyscallExn {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Syscall {:?} failed with {:?}", self.callno, self.errno)
    }
}

impl crate::__contract_spec::ContractTypeName for SyscallExn {
    const NAME: crate::__contract_spec::ContractSpecIdView<'static> =
        crate::__contract_spec::ContractSpecIdView {
            crate_name: ::core::env!("CARGO_PKG_NAME"),
            major: crate::exn::EXN_ABI_MAJOR,
            module_path: ::core::module_path!(),
            declared: "SyscallExn",
        };
}

#[cfg(test)]
mod selector_tests {
    use super::*;
    use crate::exn::Exn;

    #[test]
    fn library_exn_selector_matches_sha256() {
        // Spelled out, not `env!("CARGO_PKG_NAME")`: the point is that the
        // selector stays put when the package is renamed.
        let expected = thruput_contract_util::exception_selector(
            "1.0.0",
            "sdk",
            "LibraryExn",
        );
        assert_eq!(LibraryExn::SELECTOR, expected);
    }

    #[test]
    fn library_exn_selector_nonzero() {
        assert_ne!(LibraryExn::SELECTOR, 0);
    }
}