Skip to main content

xrpl_wasm_stdlib/host/
mod.rs

1//! Host bindings and utilities exposed to WASM smart contracts.
2//!
3//! This module exposes the low-level host ABI plus typed primitives (Result, Error, helpers).
4//! Most users should prefer the safe, high-level APIs in [`crate::core`], which wrap these bindings.
5//!
6//! ## Float Operations for Fungible Tokens (IOUs)
7//!
8//! The host provides float arithmetic functions for XRPL's fungible token amounts.
9//! These operations use rippled's Number class via FFI to ensure exact consensus compatibility:
10//!
11//! - `float_from_int` / `float_from_uint` / `float_from_mant_exp` - Convert values to float format
12//! - `float_from_stamount` / `float_from_stnumber` - Convert XRP ledger types to float format
13//! - `float_to_int` / `float_to_mant_exp` - Convert float to integer or decomposed form
14//! - `float_add` / `float_subtract` / `float_multiply` / `float_divide` - Arithmetic
15//! - `float_pow` / `float_root` - Mathematical functions
16//! - `float_compare` - Comparison operations
17//!
18//! All operations support explicit rounding modes (0=ToNearest, 1=TowardsZero, 2=Downward, 3=Upward).
19//!
20//! See the host_bindings documentation for detailed function signatures.
21
22pub mod error_codes;
23pub mod field_helpers;
24pub mod trace;
25
26// Float rounding mode constants (same as in host_bindings.rs)
27#[allow(unused)]
28pub const FLOAT_ROUNDING_MODES_TO_NEAREST: i32 = 0;
29#[allow(unused)]
30pub const FLOAT_ROUNDING_MODES_TOWARDS_ZERO: i32 = 1;
31#[allow(unused)]
32pub const FLOAT_ROUNDING_MODES_DOWNWARD: i32 = 2;
33#[allow(unused)]
34pub const FLOAT_ROUNDING_MODES_UPWARD: i32 = 3;
35
36// This setup allows us to keep all host functions in the `host::` namespace, but vary the implementation based on
37// target and build profiles.
38// 1) `host_bindings_trait.rs` defines the trait that specifies the host functions available to WASM smart contracts.
39// 2a) When cargo is executed with `test` or with the `test-host-bindings` feature, `host_bindings_test.rs` is included,
40//     which provides stub implementations for coverage testing.
41// 2b) When `cargo build` is executed, then `host_bindings_empty.rs` is included, which provides a no-op implementation
42//     that simply allows the build to pass when the target is not Wasm32.
43// 2c) When `cargo build --target wasm32v1-none` (or any Wasm target) is executed, then `host_bindings_wasm.rs` is
44//     included, which provides the actual host function implementations.
45pub mod host_bindings_trait;
46
47#[cfg(all(
48    not(any(test, feature = "test-host-bindings")),
49    not(target_arch = "wasm32")
50))] // <-- e.g., `cargo build` or `... --features xrpl-wasm-stdlib/test-host-bindings`
51include!("host_bindings_empty.rs");
52
53#[cfg(all(any(test, feature = "test-host-bindings"), not(target_arch = "wasm32")))] // <-- e.g., `cargo test` or cov
54include!("host_bindings_test.rs");
55
56// host functions defined by the host.
57#[cfg(target_arch = "wasm32")] // <-- e.g., `cargo build --target wasm32v1-none`
58include!("host_bindings_wasm.rs");
59
60/// `Result` is a type that represents either a success ([`Ok`]) or failure ([`Err`]) result from the host.
61#[must_use]
62pub enum Result<T> {
63    /// Contains the success value
64    Ok(T),
65    /// Contains the error value
66    Err(Error), // TODO: Test if the WASM size is expanded if we use an enum here instead of i32
67}
68
69impl<T> Result<T> {
70    /// Returns `true` if the result is [`Ok`].
71    #[inline]
72    pub fn is_ok(&self) -> bool {
73        matches!(*self, Result::Ok(_))
74    }
75
76    /// Returns `true` if the result is [`Err`].
77    #[inline]
78    pub fn is_err(&self) -> bool {
79        !self.is_ok()
80    }
81
82    /// Converts from `Result<T>` to `Option<T>`.
83    ///
84    /// Converts `self` into an `Option<T>`, consuming `self`,
85    /// and discarding the error, if any.
86    #[inline]
87    pub fn ok(self) -> Option<T> {
88        match self {
89            Result::Ok(x) => Some(x),
90            Result::Err(_) => None,
91        }
92    }
93
94    /// Converts from `Result<T>` to `Option<Error>`.
95    ///
96    /// Converts `self` into an `Option<Error>`, consuming `self`,
97    /// and discarding the success value, if any.
98    #[inline]
99    pub fn err(self) -> Option<Error> {
100        match self {
101            Result::Ok(_) => None,
102            Result::Err(x) => Some(x),
103        }
104    }
105
106    /// Returns the contained [`Ok`] value, consuming the `self` value.
107    ///
108    /// # Panics
109    ///
110    /// Panics if the value is an [`Err`], with a panic message provided by the
111    /// [`Err`]'s value.
112    #[inline]
113    #[track_caller]
114    pub fn unwrap(self) -> T {
115        match self {
116            Result::Ok(t) => t,
117            Result::Err(error) => {
118                #[cfg(target_arch = "wasm32")]
119                {
120                    let _ = trace::trace_num("error_code=", error.code() as i64);
121                }
122                #[cfg(not(target_arch = "wasm32"))]
123                {
124                    let location = core::panic::Location::caller();
125                    eprintln!(
126                        "Result::unwrap() failed at {}:{}:{} with error_code={}",
127                        location.file(),
128                        location.line(),
129                        location.column(),
130                        error.code()
131                    );
132                }
133                panic!(
134                    "called `Result::unwrap()` on an `Err` with code: {}",
135                    error.code()
136                )
137            }
138        }
139    }
140
141    /// Returns the contained [`Ok`] value or a provided default.
142    #[inline]
143    pub fn unwrap_or(self, default: T) -> T {
144        match self {
145            Result::Ok(t) => t,
146            Result::Err(_) => default,
147        }
148    }
149
150    /// Returns the contained [`Ok`] value or computes it from a closure.
151    #[inline]
152    pub fn unwrap_or_else<F: FnOnce(Error) -> T>(self, op: F) -> T {
153        match self {
154            Result::Ok(t) => t,
155            Result::Err(e) => op(e),
156        }
157    }
158
159    #[inline]
160    #[track_caller]
161    pub fn unwrap_or_panic(self) -> T {
162        self.unwrap_or_else(|error| {
163            let location = core::panic::Location::caller();
164            #[cfg(target_arch = "wasm32")]
165            {
166                let _ = trace::trace_num("error_code=", error.code() as i64);
167            }
168            #[cfg(not(target_arch = "wasm32"))]
169            {
170                eprintln!(
171                    "unwrap_or_panic() failed at {}:{}:{} with error_code={}",
172                    location.file(),
173                    location.line(),
174                    location.column(),
175                    error.code()
176                );
177            }
178            core::panic!("Failed in {}: error_code={}", location, error.code());
179        })
180    }
181
182    #[inline]
183    pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Result<U> {
184        match self {
185            Result::Ok(t) => Result::Ok(op(t)),
186            Result::Err(e) => Result::Err(e),
187        }
188    }
189
190    /// Calls `op` if the result is [`Ok`], otherwise returns the [`Err`] value of `self`.
191    #[inline]
192    pub fn and_then<U, F: FnOnce(T) -> Result<U>>(self, op: F) -> Result<U> {
193        match self {
194            Result::Ok(t) => op(t),
195            Result::Err(e) => Result::Err(e),
196        }
197    }
198}
199
200impl From<i64> for Result<u64> {
201    #[inline(always)] // <-- Inline because this function is very small
202    fn from(value: i64) -> Self {
203        match value {
204            res if res >= 0 => Result::Ok(value as _),
205            _ => Result::Err(Error::from_code(value as _)),
206        }
207    }
208}
209
210/// Transposes an `Option` of a `Result` into a `Result` of an `Option`.
211///
212/// `None` will be mapped to `Ok(None)`.
213/// `Some(Ok(_))` and `Some(Err(_))` will be mapped to `Ok(Some(_))` and `Err(_)`.
214///
215/// # Examples
216///
217/// ```ignore
218/// let x: Option<Result<i32>> = Some(Ok(5));
219/// let y: Result<Option<i32>> = transpose_option(x);
220/// assert_eq!(y, Ok(Some(5)));
221/// ```
222#[inline]
223pub fn transpose_option<T>(opt: Option<Result<T>>) -> Result<Option<T>> {
224    match opt {
225        Some(Result::Ok(x)) => Result::Ok(Some(x)),
226        Some(Result::Err(e)) => Result::Err(e),
227        None => Result::Ok(None),
228    }
229}
230
231/// Possible errors returned by XRPL Programmability APIs.
232///
233/// Errors are global across all Programmability APIs.
234#[derive(Clone, Copy, Debug)]
235#[repr(i32)]
236pub enum Error {
237    /// Reserved for internal invariant trips, generally unrelated to inputs.
238    /// These should be reported with an issue.
239    InternalError = error_codes::INTERNAL_ERROR,
240
241    /// The requested serialized field could not be found in the specified object.
242    /// This error is returned when attempting to access a field that doesn't exist
243    /// in the current transaction or ledger object.
244    FieldNotFound = error_codes::FIELD_NOT_FOUND,
245
246    /// The provided buffer is too small to hold the requested data.
247    /// Increase the buffer size and retry the operation.
248    BufferTooSmall = error_codes::BUFFER_TOO_SMALL,
249
250    /// The API was asked to assume the object under analysis is an STArray but it was not.
251    /// This error occurs when trying to perform array operations on non-array objects.
252    NoArray = error_codes::NO_ARRAY,
253
254    /// The specified field is not a leaf field and cannot be accessed directly.
255    /// Leaf fields are primitive types that contain actual data values.
256    NotLeafField = error_codes::NOT_LEAF_FIELD,
257
258    /// The provided locator string is malformed or invalid.
259    /// Locators must follow the proper format for field identification.
260    LocatorMalformed = error_codes::LOCATOR_MALFORMED,
261
262    /// The specified slot number is outside the valid range.
263    /// Slot numbers must be within the allowed bounds for the current context.
264    SlotOutRange = error_codes::SLOT_OUT_RANGE,
265
266    /// No free slots are available for allocation.
267    /// All available slots are currently in use. Consider reusing existing slots.
268    SlotsFull = error_codes::SLOTS_FULL,
269
270    /// The specified slot did not contain any slotted data (i.e., is empty).
271    /// This error occurs when trying to access a slot that hasn't been allocated
272    /// or has been freed.
273    EmptySlot = error_codes::EMPTY_SLOT,
274
275    /// The requested ledger object could not be found.
276    /// This may occur if the object doesn't exist or the keylet is invalid.
277    LedgerObjNotFound = error_codes::LEDGER_OBJ_NOT_FOUND,
278
279    /// An error occurred while decoding serialized data.
280    /// This typically indicates corrupted or invalidly formatted data.
281    InvalidDecoding = error_codes::INVALID_DECODING,
282
283    /// The data field is too large to be processed.
284    /// Consider reducing the size of the data or splitting it into smaller chunks.
285    DataFieldTooLarge = error_codes::DATA_FIELD_TOO_LARGE,
286
287    /// A pointer or buffer length provided as a parameter described memory outside the allowed memory region.
288    /// This error indicates a memory access violation.
289    PointerOutOfBounds = error_codes::POINTER_OUT_OF_BOUNDS,
290
291    /// No memory has been exported by the WebAssembly module.
292    /// The module must export its memory for host functions to access it.
293    NoMemoryExported = error_codes::NO_MEM_EXPORTED,
294
295    /// One or more of the parameters provided to the API are invalid.
296    /// Check the API documentation for valid parameter ranges and formats.
297    InvalidParams = error_codes::INVALID_PARAMS,
298
299    /// The provided account identifier is invalid.
300    /// Account IDs must be valid 20-byte addresses in the proper format.
301    InvalidAccount = error_codes::INVALID_ACCOUNT,
302
303    /// The specified field identifier is invalid or not recognized.
304    /// Field IDs must correspond to valid XRPL serialization fields.
305    InvalidField = error_codes::INVALID_FIELD,
306
307    /// The specified index is outside the valid bounds of the array or collection.
308    /// Ensure the index is within the valid range for the target object.
309    IndexOutOfBounds = error_codes::INDEX_OUT_OF_BOUNDS,
310
311    /// The input provided for floating-point parsing is malformed.
312    /// Floating-point values must be in the correct format for XFL operations.
313    InvalidFloatInput = error_codes::INVALID_FLOAT_INPUT,
314
315    /// An error occurred during floating-point computation.
316    /// This may indicate overflow, underflow, or other arithmetic errors.
317    InvalidFloatComputation = error_codes::INVALID_FLOAT_COMPUTATION,
318}
319
320impl Error {
321    // TODO: Use Trait instead?
322    #[inline(always)] // <-- Inline because this function is very small
323    pub fn from_code(code: i32) -> Self {
324        unsafe { core::mem::transmute(code) }
325    }
326
327    /// Error code
328    #[inline(always)] // <-- Inline because this function is very small
329    pub fn code(self) -> i32 {
330        self as _
331    }
332}
333
334impl From<Error> for i64 {
335    fn from(val: Error) -> Self {
336        val as i64
337    }
338}