truthlinked_sdk/abi.rs
1//! ABI (Application Binary Interface) helpers for function dispatch and calldata parsing.
2//!
3//! This module provides utilities for:
4//! - Computing function selectors from names
5//! - Extracting selectors from calldata
6//! - Reading typed values from calldata at specific offsets
7//!
8//! # Function Selectors
9//!
10//! TruthLinked uses FNV-1a 32-bit hashing for function selectors (4 bytes).
11//! This is deterministic, no-std compatible, and provides good distribution.
12//!
13//! # Example
14//!
15//! ```ignore
16//! use truthlinked_sdk::abi;
17//!
18//! // Compute selector for "transfer" function
19//! let sel = abi::selector_of("transfer");
20//!
21//! // Parse selector from calldata
22//! let calldata = context::calldata()?;
23//! let selector = abi::selector(&calldata)?;
24//!
25//! // Read arguments
26//! let amount = abi::read_u64(&calldata, 4)?;
27//! let recipient = abi::read_account(&calldata, 12)?;
28//! ```
29
30use crate::error::{Error, Result};
31
32/// Error code for malformed or insufficient calldata.
33pub const ERR_BAD_CALLDATA: i32 = 10;
34
35/// Computes a 4-byte function selector from a function name.
36///
37/// Uses FNV-1a 32-bit hash algorithm for deterministic, no-std-friendly hashing.
38/// The selector is returned in little-endian byte order.
39///
40/// # Arguments
41///
42/// * `name` - Function name (e.g., "transfer", "increment")
43///
44/// # Returns
45///
46/// A 4-byte selector that uniquely identifies the function.
47///
48/// # Example
49///
50/// ```ignore
51/// let sel = abi::selector_of("transfer");
52/// // sel is a deterministic 4-byte array
53/// ```
54pub fn selector_of(name: &str) -> [u8; 4] {
55 // FNV-1a 32-bit hash: deterministic, no-std-friendly selector hash
56 let mut hash: u32 = 0x811c9dc5;
57 for b in name.as_bytes() {
58 hash ^= *b as u32;
59 hash = hash.wrapping_mul(0x01000193);
60 }
61 hash.to_le_bytes()
62}
63
64/// Extracts the 4-byte function selector from calldata.
65///
66/// The selector is expected to be the first 4 bytes of the calldata.
67/// Returns an error if calldata is shorter than 4 bytes.
68///
69/// # Arguments
70///
71/// * `calldata` - Raw calldata bytes (selector + arguments)
72///
73/// # Returns
74///
75/// * `Ok([u8; 4])` - The extracted selector
76/// * `Err(Error)` - If calldata is too short
77///
78/// # Example
79///
80/// ```ignore
81/// let calldata = context::calldata()?;
82/// let selector = abi::selector(&calldata)?;
83///
84/// if selector == abi::selector_of("transfer") {
85/// // Handle transfer function
86/// }
87/// ```
88pub fn selector(calldata: &[u8]) -> Result<[u8; 4]> {
89 if calldata.len() < 4 {
90 return Err(Error::new(ERR_BAD_CALLDATA));
91 }
92 let mut out = [0u8; 4];
93 out.copy_from_slice(&calldata[..4]);
94 Ok(out)
95}
96
97/// Reads a `u64` value from calldata at the specified byte offset.
98///
99/// Interprets 8 bytes starting at `offset` as a little-endian `u64`.
100/// Returns an error if there aren't enough bytes available.
101///
102/// # Arguments
103///
104/// * `calldata` - Raw calldata bytes
105/// * `offset` - Byte offset where the u64 starts (typically 4+ for arguments after selector)
106///
107/// # Returns
108///
109/// * `Ok(u64)` - The decoded value
110/// * `Err(Error)` - If offset is out of bounds or calldata is too short
111///
112/// # Example
113///
114/// ```ignore
115/// let calldata = context::calldata()?;
116/// let amount = abi::read_u64(&calldata, 4)?; // Read first argument after selector
117/// ```
118pub fn read_u64(calldata: &[u8], offset: usize) -> Result<u64> {
119 let end = offset
120 .checked_add(8)
121 .ok_or_else(|| Error::new(ERR_BAD_CALLDATA))?;
122 if calldata.len() < end {
123 return Err(Error::new(ERR_BAD_CALLDATA));
124 }
125 let mut out = [0u8; 8];
126 out.copy_from_slice(&calldata[offset..end]);
127 Ok(u64::from_le_bytes(out))
128}
129
130/// Reads a `u128` value from calldata at the specified byte offset.
131///
132/// Interprets 16 bytes starting at `offset` as a little-endian `u128`.
133/// Useful for reading large token amounts or 128-bit integers.
134///
135/// # Arguments
136///
137/// * `calldata` - Raw calldata bytes
138/// * `offset` - Byte offset where the u128 starts
139///
140/// # Returns
141///
142/// * `Ok(u128)` - The decoded value
143/// * `Err(Error)` - If offset is out of bounds or calldata is too short
144///
145/// # Example
146///
147/// ```ignore
148/// let calldata = context::calldata()?;
149/// let token_amount = abi::read_u128(&calldata, 4)?;
150/// ```
151pub fn read_u128(calldata: &[u8], offset: usize) -> Result<u128> {
152 let end = offset
153 .checked_add(16)
154 .ok_or_else(|| Error::new(ERR_BAD_CALLDATA))?;
155 if calldata.len() < end {
156 return Err(Error::new(ERR_BAD_CALLDATA));
157 }
158 let mut out = [0u8; 16];
159 out.copy_from_slice(&calldata[offset..end]);
160 Ok(u128::from_le_bytes(out))
161}
162
163/// Reads a 32-byte account ID from calldata at the specified byte offset.
164///
165/// Account IDs in TruthLinked are 32-byte arrays (typically BLAKE3 hashes of public keys).
166///
167/// # Arguments
168///
169/// * `calldata` - Raw calldata bytes
170/// * `offset` - Byte offset where the account ID starts
171///
172/// # Returns
173///
174/// * `Ok([u8; 32])` - The account ID
175/// * `Err(Error)` - If offset is out of bounds or calldata is too short
176///
177/// # Example
178///
179/// ```ignore
180/// let calldata = context::calldata()?;
181/// let recipient = abi::read_account(&calldata, 4)?;
182/// let sender = context::caller()?;
183/// ```
184pub fn read_account(calldata: &[u8], offset: usize) -> Result<[u8; 32]> {
185 let end = offset
186 .checked_add(32)
187 .ok_or_else(|| Error::new(ERR_BAD_CALLDATA))?;
188 if calldata.len() < end {
189 return Err(Error::new(ERR_BAD_CALLDATA));
190 }
191 let mut out = [0u8; 32];
192 out.copy_from_slice(&calldata[offset..end]);
193 Ok(out)
194}