Skip to main content

xrpl_common_stdlib/types/
issue.rs

1use crate::fields::decoder::{FieldDecoder, FromLedger};
2use crate::host::Result;
3use crate::types::account_id::AccountID;
4use crate::types::currency::Currency;
5use crate::types::decode_error::DecodeError;
6use crate::types::mpt_id::MptId;
7
8/// Struct to represent an Issue of type XRP. Exists so that other structs can restrict type
9/// information to XRP in their declarations (this is not possible with just the `Issue` enum below).
10///
11/// ## Derived Traits
12///
13/// - `Copy`: Efficient for this zero-sized type
14/// - `PartialEq, Eq`: Enable comparisons
15/// - `Debug, Clone`: Standard traits for development and consistency
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(C)]
18pub struct XrpIssue {}
19
20/// Defines an issue for IOUs (40 bytes: 20-byte currency + 20-byte issuer).
21///
22/// ## Derived Traits
23///
24/// - `PartialEq, Eq`: Enable comparisons and use in collections
25/// - `Debug, Clone`: Standard traits for development and consistency
26///
27/// Note: `Copy` is intentionally not derived due to the struct's size (40 bytes).
28#[derive(Debug, Clone, PartialEq, Eq)]
29#[repr(C)]
30pub struct IouIssue {
31    issuer: AccountID,
32    currency: Currency,
33    _bytes: [u8; 40],
34}
35
36impl IouIssue {
37    pub fn new(issuer: AccountID, currency: Currency) -> Self {
38        let mut bytes = [0u8; 40];
39        bytes[..20].copy_from_slice(currency.as_bytes());
40        bytes[20..].copy_from_slice(&issuer.0);
41        Self {
42            issuer,
43            currency,
44            _bytes: bytes,
45        }
46    }
47
48    pub fn as_bytes(&self) -> &[u8] {
49        &self._bytes
50    }
51}
52
53/// Struct to represent an Issue of type MPT. Exists so that other structs can restrict type
54/// information to MPT in their declarations (this is not possible with just the `Issue` enum below).
55///
56/// ## Derived Traits
57///
58/// - `Copy`: Efficient for this 24-byte struct, enabling implicit copying
59/// - `PartialEq, Eq`: Enable comparisons
60/// - `Debug, Clone`: Standard traits for development and consistency
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[repr(C)]
63pub struct MptIssue {
64    mpt_id: MptId,
65}
66
67impl MptIssue {
68    pub fn new(mpt_id: MptId) -> Self {
69        Self { mpt_id }
70    }
71
72    pub fn mpt_id(&self) -> MptId {
73        self.mpt_id
74    }
75}
76
77/// Represents an issue without a value, such as reading `Asset1` and `Asset2` in AMM ledger
78/// objects.
79///
80/// ## Derived Traits
81///
82/// - `PartialEq, Eq`: Enable comparisons and use in collections
83/// - `Debug, Clone`: Standard traits for development and consistency
84///
85/// Note: `Copy` is intentionally not derived because the `IOU` variant is 40 bytes.
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[repr(C)]
88pub enum Issue {
89    XRP(XrpIssue),
90    IOU(IouIssue),
91    MPT(MptIssue),
92}
93
94impl Issue {
95    pub fn as_bytes(&self) -> &[u8] {
96        match self {
97            Issue::XRP(_) => {
98                static XRP_BUF: [u8; 20] = [0; 20];
99                &XRP_BUF
100            }
101            Issue::IOU(iou) => iou.as_bytes(),
102            Issue::MPT(mpt) => mpt.mpt_id.as_bytes(),
103        }
104    }
105
106    /// Creates an Issue from a buffer and length, detecting the type based on the byte count.
107    ///
108    /// # Arguments
109    ///
110    /// * `buffer` - A 40-byte buffer containing the issue data
111    /// * `len` - The actual number of bytes written to the buffer
112    ///
113    /// # Returns
114    ///
115    /// Returns `Result<Issue>` where:
116    /// * `Ok(Issue::XRP(...))` - If len is 20 (XRP issue)
117    /// * `Ok(Issue::MPT(...))` - If len is 24 (MPT issue)
118    /// * `Ok(Issue::IOU(...))` - If len is 40 (IOU issue)
119    /// * `Err(Error)` - If len is not one of the expected values
120    #[inline]
121    pub fn from_buffer(buffer: [u8; 40], len: usize) -> Result<Self> {
122        match len {
123            20 => Result::Ok(Issue::XRP(XrpIssue {})),
124            24 => {
125                let mpt_bytes: [u8; 24] = buffer[..24].try_into().unwrap_or([0u8; 24]);
126                let mpt_id = MptId::from(mpt_bytes);
127                Result::Ok(Issue::MPT(MptIssue::new(mpt_id)))
128            }
129            40 => {
130                let currency_bytes: [u8; 20] = buffer[..20].try_into().unwrap_or([0u8; 20]);
131                let issuer_bytes: [u8; 20] = buffer[20..40].try_into().unwrap_or([0u8; 20]);
132                let currency = Currency::from(currency_bytes);
133                let issuer = AccountID::from(issuer_bytes);
134                Result::Ok(Issue::IOU(IouIssue::new(issuer, currency)))
135            }
136            _ => Result::Err(crate::host::Error::from_code(len as i32)),
137        }
138    }
139}
140
141/// `FieldDecoder` for XRPL issues. The host writes a variable number of bytes into the fixed
142/// 40-byte buffer — 20 for XRP, 24 for MPT, 40 for IOU — and the variant is detected from
143/// `bytes_written` (see [`Issue::from_buffer`]); a count that matches none of those is a decode
144/// error.
145impl FieldDecoder for Issue {
146    type Buffer = [u8; 40];
147
148    #[inline]
149    fn empty_buffer() -> Self::Buffer {
150        [0u8; 40]
151    }
152
153    #[inline]
154    fn decode(buf: Self::Buffer, bytes_written: usize) -> core::result::Result<Self, DecodeError> {
155        match Issue::from_buffer(buf, bytes_written) {
156            Result::Ok(issue) => core::result::Result::Ok(issue),
157            Result::Err(_) => core::result::Result::Err(DecodeError),
158        }
159    }
160}
161
162impl FromLedger for Issue {}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    // Test IouIssue byte layout
169    #[test]
170    fn test_iou_issue_creation() {
171        let issuer = AccountID::from([1u8; 20]);
172        let currency = Currency::from([2u8; 20]);
173        let iou = IouIssue::new(issuer, currency);
174
175        // Verify bytes structure (currency first, then issuer)
176        let bytes = iou.as_bytes();
177        assert_eq!(bytes.len(), 40);
178        assert_eq!(&bytes[..20], currency.as_bytes());
179        assert_eq!(&bytes[20..], &issuer.0);
180    }
181
182    #[test]
183    fn test_iou_issue_with_standard_currency() {
184        let issuer = AccountID::from([0xAB; 20]);
185        let currency = Currency::from(*b"USD");
186        let iou = IouIssue::new(issuer, currency);
187
188        let bytes = iou.as_bytes();
189        // First 20 bytes are currency
190        assert_eq!(&bytes[..20], currency.as_bytes());
191        // Last 20 bytes are issuer
192        assert_eq!(&bytes[20..], &issuer.0);
193    }
194
195    #[test]
196    fn test_iou_issue_different_issuers_not_equal() {
197        let issuer1 = AccountID::from([1u8; 20]);
198        let issuer2 = AccountID::from([3u8; 20]);
199        let currency = Currency::from([2u8; 20]);
200
201        let iou1 = IouIssue::new(issuer1, currency);
202        let iou2 = IouIssue::new(issuer2, currency);
203
204        assert_ne!(iou1, iou2);
205    }
206
207    // Test MptIssue accessor
208    #[test]
209    fn test_mpt_issue_creation() {
210        let issuer = AccountID::from([1u8; 20]);
211        let mpt_id = MptId::new(12345, issuer);
212        let mpt = MptIssue::new(mpt_id);
213
214        assert_eq!(mpt.mpt_id(), mpt_id);
215    }
216
217    // Test Issue::from_buffer parsing logic
218    #[test]
219    fn test_issue_from_buffer_xrp() {
220        let buffer = [0u8; 40];
221        let result = Issue::from_buffer(buffer, 20);
222        assert!(matches!(result, Result::Ok(Issue::XRP(_))));
223    }
224
225    #[test]
226    fn test_issue_from_buffer_mpt() {
227        // MPT buffer: 4 bytes sequence + 20 bytes issuer = 24 bytes
228        let mut buffer = [0u8; 40];
229        // Set sequence number (first 4 bytes, big-endian)
230        buffer[0..4].copy_from_slice(&12345u32.to_be_bytes());
231        // Set issuer (next 20 bytes)
232        buffer[4..24].copy_from_slice(&[0xAB; 20]);
233
234        let result = Issue::from_buffer(buffer, 24);
235        match result {
236            Result::Ok(Issue::MPT(mpt)) => {
237                assert_eq!(mpt.mpt_id().get_sequence_num(), 12345);
238                assert_eq!(mpt.mpt_id().get_issuer(), AccountID::from([0xAB; 20]));
239            }
240            _ => panic!("Expected MPT issue"),
241        }
242    }
243
244    #[test]
245    fn test_issue_from_buffer_iou() {
246        // IOU buffer: 20 bytes currency + 20 bytes issuer = 40 bytes
247        let mut buffer = [0u8; 40];
248        // Set currency (first 20 bytes)
249        buffer[..20].copy_from_slice(&[0xCC; 20]);
250        // Set issuer (last 20 bytes)
251        buffer[20..40].copy_from_slice(&[0xDD; 20]);
252
253        let result = Issue::from_buffer(buffer, 40);
254        match result {
255            Result::Ok(Issue::IOU(iou)) => {
256                let bytes = iou.as_bytes();
257                assert_eq!(&bytes[..20], &[0xCC; 20]); // currency
258                assert_eq!(&bytes[20..], &[0xDD; 20]); // issuer
259            }
260            _ => panic!("Expected IOU issue"),
261        }
262    }
263
264    #[test]
265    fn test_issue_as_bytes() {
266        let xrp = Issue::XRP(XrpIssue {});
267        assert_eq!(xrp.as_bytes(), &[0u8; 20]);
268
269        let issuer = AccountID::from([0xAA; 20]);
270        let currency = Currency::from([0xBB; 20]);
271        let iou = Issue::IOU(IouIssue::new(issuer, currency));
272        assert_eq!(iou.as_bytes().len(), 40);
273
274        let mpt_id = MptId::new(1, AccountID::from([0xCC; 20]));
275        let mpt = Issue::MPT(MptIssue::new(mpt_id));
276        assert_eq!(mpt.as_bytes(), mpt_id.as_bytes());
277    }
278
279    #[test]
280    fn test_issue_from_buffer_invalid_length() {
281        let buffer = [0u8; 40];
282        // Invalid lengths should return error
283        let result = Issue::from_buffer(buffer, 10);
284        assert!(matches!(result, Result::Err(_)));
285
286        let result = Issue::from_buffer(buffer, 30);
287        assert!(matches!(result, Result::Err(_)));
288
289        let result = Issue::from_buffer(buffer, 0);
290        assert!(matches!(result, Result::Err(_)));
291    }
292}