Skip to main content

soroban_sdk_tools/
error.rs

1//! Error handling utilities for Soroban contracts.
2//!
3//! Provides traits and helper types for composable error handling
4//! with the `#[scerr]` macro. Error codes are assigned sequentially
5//! starting at 1, with wrapped inner types flattened at their position
6//! via const-chaining. The `Aborted` variant always uses code 0, and
7//! the `UnknownError` sentinel always uses [`UNKNOWN_ERROR_CODE`].
8
9// Re-export contracterror for users
10pub use soroban_sdk::contracterror;
11
12/// The error code used for the unknown/sentinel error variant.
13/// Uses `i32::MAX as u32` for platform-independent consistency (matches
14/// `isize::MAX as u32` on wasm32 which is the Soroban target).
15pub const UNKNOWN_ERROR_CODE: u32 = i32::MAX as u32;
16
17/// Base trait for contract errors
18pub trait ContractError: Sized {
19    /// Convert this error into a u32 code
20    fn into_code(self) -> u32;
21
22    /// Try to construct this error from a u32 code
23    fn from_code(code: u32) -> Option<Self>;
24}
25
26/// Trait for mapping error variants to/from a 0-based sequential index.
27///
28/// This enables composable error flattening: wrapped inner types are mapped
29/// into the outer enum's code space using `offset + inner.to_seq()` without
30/// any off-by-one arithmetic. Types implementing this trait can be used as
31/// inner types in `#[transparent]` and `#[from_contract_client]` variants.
32///
33/// For `#[scerr]` types, `to_seq()` returns `into_code() - 1` (since codes
34/// start at 1). For `contractimport!` types, the mapping is generated from
35/// the variant order regardless of native error codes.
36pub trait SequentialError: Sized {
37    /// Convert this error to a 0-based sequential index in `[0, TOTAL_CODES)`.
38    fn to_seq(&self) -> u32;
39
40    /// Construct this error from a 0-based sequential index.
41    fn from_seq(seq: u32) -> Option<Self>;
42}
43
44/// Spec entry for a single error variant.
45/// Used for flattening inner error types into outer contract specs.
46#[derive(Debug, Clone, Copy)]
47pub struct ErrorSpecEntry {
48    /// The error code (u32)
49    pub code: u32,
50    /// The variant name (e.g., "DivisionByZero")
51    pub name: &'static str,
52    /// Human-readable description
53    pub description: &'static str,
54}
55
56/// Node in the error spec tree for recursive flattening.
57///
58/// - **Leaf** (`children` is empty): a single error variant with a code,
59///   name, and description.
60/// - **Group** (`children` is non-empty): a wrapped inner error type whose
61///   children should be flattened with a name prefix.
62#[derive(Clone, Copy)]
63pub struct SpecNode {
64    /// Leaf: the error code.  Group: the offset in the parent's code space.
65    pub code: u32,
66    /// Leaf: the variant name.  Group: the prefix for flattened names.
67    pub name: &'static str,
68    /// Leaf: doc string.  Group: unused (empty).
69    pub description: &'static str,
70    /// Leaf: empty.  Group: inner type's `SPEC_TREE`.
71    pub children: &'static [SpecNode],
72}
73
74/// Trait providing spec metadata for error types.
75///
76/// Automatically implemented by `#[scerr]` and `contractimport!`.
77/// Used by root error enums for const-chaining: the length of `SPEC_ENTRIES`
78/// determines how many sequential codes an inner type occupies in the outer
79/// enum's code space.
80pub trait ContractErrorSpec {
81    /// Array of spec entries for all variants in this error type.
82    const SPEC_ENTRIES: &'static [ErrorSpecEntry];
83
84    /// Total number of sequential codes this type occupies.
85    ///
86    /// For basic-mode enums this equals `SPEC_ENTRIES.len()`.
87    /// For root-mode enums this may be larger because wrapped inner types
88    /// occupy multiple sequential codes that are not individually listed
89    /// in `SPEC_ENTRIES`.
90    const TOTAL_CODES: u32 = Self::SPEC_ENTRIES.len() as u32;
91
92    /// Tree of all variants (leaves and groups) for recursive XDR
93    /// flattening.  Empty by default for backward compatibility with
94    /// types that haven't been recompiled yet.
95    const SPEC_TREE: &'static [SpecNode] = &[];
96}
97
98// -----------------------------------------------------------------------------
99// Const-fn XDR builder – runs entirely at compile time
100// -----------------------------------------------------------------------------
101
102/// Compute the total byte size of a `ScSpecUdtErrorEnumV0` entry encoded as
103/// XDR, including the 4-byte union discriminant.
104///
105/// The XDR layout is:
106/// ```text
107///   4  bytes   union discriminant (4 = UdtErrorEnumV0)
108///   string     doc
109///   string     lib (always empty → 4 bytes)
110///   string     name
111///   4  bytes   cases count
112///   per case:
113///     string   doc
114///     string   name (with accumulated prefix)
115///     4 bytes  value (u32)
116/// ```
117pub const fn xdr_error_enum_size(name: &str, doc: &str, tree: &[SpecNode]) -> usize {
118    4                                // union discriminant
119    + xdr_string_size(doc.len())     // doc
120    + xdr_string_size(0)             // lib (empty)
121    + xdr_string_size(name.len())    // name
122    + 4                              // cases count
123    + tree_cases_size(tree, 0) // cases
124}
125
126/// Build the complete XDR bytes for a `ScSpecEntry::UdtErrorEnumV0`.
127///
128/// `N` must equal `xdr_error_enum_size(name, doc, tree)`.
129pub const fn build_error_enum_xdr<const N: usize>(
130    name: &str,
131    doc: &str,
132    tree: &[SpecNode],
133) -> [u8; N] {
134    let mut buf = [0u8; N];
135    let n_cases = count_tree_leaves(tree) as u32;
136    let prefix_buf = [0u8; 256];
137
138    let mut pos = write_u32_be(&mut buf, 0, 4); // union discriminant
139    pos = write_xdr_string(&mut buf, pos, doc.as_bytes());
140    pos = write_xdr_string(&mut buf, pos, &[]); // lib (empty)
141    pos = write_xdr_string(&mut buf, pos, name.as_bytes());
142    pos = write_u32_be(&mut buf, pos, n_cases);
143    pos = write_tree_cases(&mut buf, pos, tree, &prefix_buf, 0, 0);
144
145    // The array size constraint `N` enforces that we wrote exactly the
146    // right number of bytes; verify at compile time on supported toolchains.
147    assert!(pos == N, "XDR size mismatch");
148
149    buf
150}
151
152// --- Internal helpers --------------------------------------------------------
153
154/// Count the total number of leaf nodes in a tree (recursively).
155const fn count_tree_leaves(nodes: &[SpecNode]) -> usize {
156    let mut total = 0usize;
157    let mut idx = 0usize;
158    while idx < nodes.len() {
159        if nodes[idx].children.is_empty() {
160            total += 1;
161        } else {
162            total += count_tree_leaves(nodes[idx].children);
163        }
164        idx += 1;
165    }
166    total
167}
168
169/// Compute the XDR-padded size of a string (4-byte length prefix + content
170/// padded to 4-byte boundary).
171const fn xdr_string_size(len: usize) -> usize {
172    4 + ((len + 3) & !3)
173}
174
175/// Recursively compute byte sizes of all leaf cases in the tree, accounting
176/// for name-prefix accumulation.
177const fn tree_cases_size(nodes: &[SpecNode], prefix_len: usize) -> usize {
178    let mut size = 0usize;
179    let mut idx = 0usize;
180    while idx < nodes.len() {
181        if nodes[idx].children.is_empty() {
182            // Leaf: doc + name (with prefix) + value
183            size += xdr_string_size(nodes[idx].description.len());
184            size += xdr_string_size(prefix_len + nodes[idx].name.len());
185            size += 4;
186        } else {
187            // Group: recurse with extended prefix (name + '_')
188            size += tree_cases_size(nodes[idx].children, prefix_len + nodes[idx].name.len() + 1);
189        }
190        idx += 1;
191    }
192    size
193}
194
195/// Write a big-endian u32, returning the new write position.
196const fn write_u32_be(buf: &mut [u8], pos: usize, val: u32) -> usize {
197    buf[pos] = (val >> 24) as u8;
198    buf[pos + 1] = (val >> 16) as u8;
199    buf[pos + 2] = (val >> 8) as u8;
200    buf[pos + 3] = val as u8;
201    pos + 4
202}
203
204/// Copy `src` bytes into `buf` at `pos`, returning the new write position.
205const fn write_bytes(buf: &mut [u8], pos: usize, src: &[u8]) -> usize {
206    let mut idx = 0usize;
207    while idx < src.len() {
208        buf[pos + idx] = src[idx];
209        idx += 1;
210    }
211    pos + src.len()
212}
213
214/// Write zero-padding bytes to reach 4-byte alignment after `content_len`
215/// bytes of content, returning the new write position.
216const fn write_xdr_padding(buf: &mut [u8], pos: usize, content_len: usize) -> usize {
217    let rem = content_len % 4;
218    if rem == 0 {
219        return pos;
220    }
221    let pad = 4 - rem;
222    let mut idx = 0usize;
223    while idx < pad {
224        buf[pos + idx] = 0;
225        idx += 1;
226    }
227    pos + pad
228}
229
230/// Write an XDR string: 4-byte BE length + content + zero-padding to 4-byte
231/// alignment.
232const fn write_xdr_string(buf: &mut [u8], pos: usize, s: &[u8]) -> usize {
233    let pos = write_u32_be(buf, pos, s.len() as u32);
234    let pos = write_bytes(buf, pos, s);
235    write_xdr_padding(buf, pos, s.len())
236}
237
238/// Write an XDR string that is the concatenation of `prefix[0..prefix_len]`
239/// and `name`, without allocating.
240///
241/// Const fn cannot take `&[u8]` sub-slices, so we accept the fixed-size
242/// prefix buffer and a length instead.
243const fn write_xdr_prefixed_string(
244    buf: &mut [u8],
245    pos: usize,
246    prefix_buf: &[u8; 256],
247    prefix_len: usize,
248    name: &[u8],
249) -> usize {
250    let total_len = prefix_len + name.len();
251    let mut pos = write_u32_be(buf, pos, total_len as u32);
252    // Copy prefix bytes
253    let mut idx = 0usize;
254    while idx < prefix_len {
255        buf[pos + idx] = prefix_buf[idx];
256        idx += 1;
257    }
258    pos += prefix_len;
259    // Copy name bytes
260    pos = write_bytes(buf, pos, name);
261    write_xdr_padding(buf, pos, total_len)
262}
263
264/// Recursively write tree cases into the XDR buffer.
265///
266/// For leaves: `code = base_offset + leaf.code`.
267/// For groups: children get `new_base = base_offset + group.code - 1`
268/// (because leaf codes start at 1 within their inner type).
269const fn write_tree_cases(
270    buf: &mut [u8],
271    pos: usize,
272    nodes: &[SpecNode],
273    prefix_buf: &[u8; 256],
274    prefix_len: usize,
275    base_offset: u32,
276) -> usize {
277    let mut pos = pos;
278    let mut idx = 0usize;
279    while idx < nodes.len() {
280        if nodes[idx].children.is_empty() {
281            // Leaf: doc, name (with prefix), value
282            pos = write_xdr_string(buf, pos, nodes[idx].description.as_bytes());
283            pos = write_xdr_prefixed_string(
284                buf,
285                pos,
286                prefix_buf,
287                prefix_len,
288                nodes[idx].name.as_bytes(),
289            );
290            pos = write_u32_be(buf, pos, base_offset + nodes[idx].code);
291        } else {
292            // Group: extend prefix with "Name_" and recurse
293            let name_bytes = nodes[idx].name.as_bytes();
294            let mut new_prefix = *prefix_buf;
295            new_prefix = copy_into(new_prefix, prefix_len, name_bytes);
296            new_prefix[prefix_len + name_bytes.len()] = b'_';
297
298            pos = write_tree_cases(
299                buf,
300                pos,
301                nodes[idx].children,
302                &new_prefix,
303                prefix_len + name_bytes.len() + 1,
304                base_offset + nodes[idx].code - 1,
305            );
306        }
307        idx += 1;
308    }
309    pos
310}
311
312/// Copy `src` into `dst` starting at `offset`, returning the modified array.
313/// Used instead of `write_bytes` when we need to build a new prefix buffer
314/// without mutating in place.
315const fn copy_into(mut dst: [u8; 256], offset: usize, src: &[u8]) -> [u8; 256] {
316    let mut idx = 0usize;
317    while idx < src.len() {
318        dst[offset + idx] = src[idx];
319        idx += 1;
320    }
321    dst
322}