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}