Skip to main content

xrpl_common_stdlib/types/
nft.rs

1//! NFToken (Non-Fungible Token) type for XRPL.
2//!
3//! Provides a high-level interface for working with NFTokens on the XRP Ledger.
4//!
5//! ## NFTokenID Structure
6//!
7//! An NFTokenID is a 32-byte identifier with the following structure:
8//!
9//! ```text
10//! 000B 0539 C35B55AA096BA6D87A6E6C965A6534150DC56E5E 12C5D09E 0000000C
11//! +--- +--- +--------------------------------------- +------- +-------
12//! |    |    |                                        |        |
13//! |    |    |                                        |        └─> Sequence (32 bits)
14//! |    |    |                                        └─> Scrambled Taxon (32 bits)
15//! |    |    └─> Issuer Address (160 bits / 20 bytes)
16//! |    └─> Transfer Fee (16 bits)
17//! └─> Flags (16 bits)
18//! ```
19
20use crate::host;
21use crate::host::{Error, Result};
22use crate::types::account_id::{ACCOUNT_ID_SIZE, AccountID};
23use crate::types::blob::{URI_BLOB_SIZE, UriBlob};
24
25/// Size of an NFTokenID in bytes (256 bits)
26pub const NFT_ID_SIZE: usize = 32;
27
28/// NFToken flags - see [NFToken documentation](https://xrpl.org/docs/references/protocol/data-types/nftoken)
29pub mod flags {
30    /// The issuer (or an entity authorized by the issuer) may destroy the object.
31    /// If this flag is set, the object may be burned by the issuer even if the issuer
32    /// does not currently hold the object. The object's owner can always burn it.
33    pub const BURNABLE: u16 = 0x0001;
34
35    /// If set, indicates that the minted token may only be bought or sold for XRP.
36    /// This can be useful for compliance purposes if the issuer wants to avoid
37    /// other tokens.
38    pub const ONLY_XRP: u16 = 0x0002;
39
40    /// If set, automatically create trust lines to hold transfer fees as specified
41    /// in the TransferFee field.
42    pub const TRUST_LINE: u16 = 0x0004;
43
44    /// If set, indicates that the minted token may be transferred to others.
45    /// If not set, the token can only be transferred back to the issuer.
46    pub const TRANSFERABLE: u16 = 0x0008;
47}
48
49/// A wrapper around NFToken flags that provides efficient helper methods.
50///
51/// ## Derived Traits
52///
53/// - `Copy`: Efficient for this 2-byte struct, enabling implicit copying
54/// - `PartialEq, Eq`: Enable comparisons
55/// - `Debug, Clone`: Standard traits for development and consistency
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct NftFlags(u16);
58
59impl NftFlags {
60    /// Creates a new NftFlags from a raw flags value.
61    #[inline]
62    pub const fn new(flags: u16) -> Self {
63        NftFlags(flags)
64    }
65
66    /// Returns the raw flags value.
67    #[inline]
68    pub const fn as_u16(&self) -> u16 {
69        self.0
70    }
71
72    /// Checks if the NFToken has the `BURNABLE` flag set.
73    ///
74    /// If this flag is set, the issuer (or an entity authorized by the issuer)
75    /// may destroy the token even if they don't currently hold it.
76    #[inline]
77    pub const fn is_burnable(&self) -> bool {
78        self.0 & flags::BURNABLE != 0
79    }
80
81    /// Checks if the NFToken has the `ONLY_XRP` flag set.
82    ///
83    /// If this flag is set, the token may only be bought or sold for XRP.
84    #[inline]
85    pub const fn is_only_xrp(&self) -> bool {
86        self.0 & flags::ONLY_XRP != 0
87    }
88
89    /// Checks if the NFToken has the `TRUST_LINE` flag set.
90    ///
91    /// If this flag is set, trust lines are automatically created to hold
92    /// transfer fees.
93    #[inline]
94    pub const fn is_trust_line(&self) -> bool {
95        self.0 & flags::TRUST_LINE != 0
96    }
97
98    /// Checks if the NFToken has the `TRANSFERABLE` flag set.
99    ///
100    /// If this flag is set, the token may be transferred to others.
101    /// If not set, the token can only be transferred back to the issuer.
102    #[inline]
103    pub const fn is_transferable(&self) -> bool {
104        self.0 & flags::TRANSFERABLE != 0
105    }
106}
107
108impl From<u16> for NftFlags {
109    fn from(value: u16) -> Self {
110        NftFlags(value)
111    }
112}
113
114impl From<NftFlags> for u16 {
115    fn from(value: NftFlags) -> Self {
116        value.0
117    }
118}
119
120/// Represents an NFToken (Non-Fungible Token) on the XRP Ledger.
121///
122/// The `NFToken` type wraps a 32-byte NFTokenID and provides methods to extract
123/// all fields encoded within the identifier, as well as retrieve associated
124/// metadata like the NFT's URI.
125///
126/// # NFTokenID Encoding
127///
128/// The 32-byte identifier contains:
129/// - **Bytes 0-1**: Flags (16 bits, big-endian)
130/// - **Bytes 2-3**: Transfer fee (16 bits, big-endian, in 1/100,000 units)
131/// - **Bytes 4-23**: Issuer account address (160 bits)
132/// - **Bytes 24-27**: Scrambled taxon (32 bits, big-endian)
133/// - **Bytes 28-31**: Sequence number (32 bits, big-endian)
134///
135/// ## Derived Traits
136///
137/// - `Copy`: Efficient for this 32-byte struct, enabling implicit copying
138/// - `PartialEq, Eq`: Enable comparisons and use in collections
139/// - `Debug, Clone`: Standard traits for development and consistency
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141#[repr(C)]
142pub struct NFToken(pub [u8; NFT_ID_SIZE]);
143
144impl NFToken {
145    /// Creates a new NFToken from a 32-byte identifier.
146    ///
147    /// # Arguments
148    ///
149    /// * `id` - The 32-byte NFTokenID
150    ///
151    #[inline]
152    pub const fn new(id: [u8; NFT_ID_SIZE]) -> Self {
153        NFToken(id)
154    }
155
156    /// Returns the raw NFTokenID as a byte array.
157    ///
158    #[inline]
159    pub const fn as_bytes(&self) -> &[u8; NFT_ID_SIZE] {
160        &self.0
161    }
162
163    /// Returns a pointer to the NFTokenID data.
164    ///
165    /// This is primarily used internally for FFI calls to host functions.
166    #[inline]
167    pub const fn as_ptr(&self) -> *const u8 {
168        self.0.as_ptr()
169    }
170
171    /// Returns the length of the NFTokenID (always 32 bytes).
172    #[inline]
173    #[allow(clippy::len_without_is_empty)]
174    pub const fn len(&self) -> usize {
175        NFT_ID_SIZE
176    }
177
178    /// Retrieves the flags associated with this NFToken.
179    ///
180    /// Flags are stored in the first 2 bytes of the NFTokenID (big-endian).
181    ///
182    /// # Returns
183    ///
184    /// * `Ok(NftFlags)` - A flags wrapper with helper methods
185    /// * `Err(Error)` - If the host function fails
186    ///
187    pub fn flags(&self) -> Result<NftFlags> {
188        let result = unsafe { host::nft_flags(self.as_ptr(), self.len()) };
189
190        match result {
191            code if code >= 0 => Result::Ok(NftFlags::new(code as u16)),
192            code => Result::Err(Error::from_code(code)),
193        }
194    }
195
196    /// Retrieves the transfer fee for this NFToken.
197    ///
198    /// The transfer fee is expressed in 1/100,000 units, meaning:
199    /// - A value of 1 represents 0.001% (1/10 of a basis point)
200    /// - A value of 100 represents 0.1% (10 basis points)
201    /// - A value of 1000 represents 1% (100 basis points)
202    /// - Maximum allowed value is 50,000 (representing 50%)
203    ///
204    /// # Returns
205    ///
206    /// * `Ok(u16)` - The transfer fee (0-50,000)
207    /// * `Err(Error)` - If the host function fails
208    ///
209    pub fn transfer_fee(&self) -> Result<u16> {
210        let result = unsafe { host::nft_xfer_fee(self.as_ptr(), self.len()) };
211
212        match result {
213            code if code >= 0 => Result::Ok(code as u16),
214            code => Result::Err(Error::from_code(code)),
215        }
216    }
217
218    /// Retrieves the issuer account of this NFToken.
219    ///
220    /// The issuer is encoded in bytes 4-23 of the NFTokenID (160 bits / 20 bytes).
221    ///
222    /// # Returns
223    ///
224    /// * `Ok(AccountID)` - The issuer's account identifier
225    /// * `Err(Error)` - If the host function fails
226    ///
227    pub fn issuer(&self) -> Result<AccountID> {
228        let mut account_buf = [0u8; ACCOUNT_ID_SIZE];
229        let result = unsafe {
230            host::nft_issuer(
231                self.as_ptr(),
232                self.len(),
233                account_buf.as_mut_ptr(),
234                account_buf.len(),
235            )
236        };
237
238        match result {
239            code if code > 0 => Result::Ok(AccountID(account_buf)),
240            code => Result::Err(Error::from_code(code)),
241        }
242    }
243
244    /// Retrieves the taxon of this NFToken.
245    ///
246    /// The taxon is an issuer-defined value that groups related NFTs together.
247    /// # Returns
248    ///
249    /// * `Ok(u32)` - The taxon value
250    /// * `Err(Error)` - If the host function fails
251    ///
252    pub fn taxon(&self) -> Result<u32> {
253        let mut taxon_buf = [0u8; 4];
254        let result = unsafe {
255            host::nft_taxon(
256                self.as_ptr(),
257                self.len(),
258                taxon_buf.as_mut_ptr(),
259                taxon_buf.len(),
260            )
261        };
262
263        match result {
264            code if code > 0 => {
265                // Convert big-endian bytes to u32
266                let taxon = u32::from_be_bytes(taxon_buf);
267                Result::Ok(taxon)
268            }
269            code => Result::Err(Error::from_code(code)),
270        }
271    }
272
273    /// Retrieves the token sequence number of this NFToken.
274    ///
275    /// The token sequence number is automatically incremented for each NFToken minted
276    /// by the issuer, based on the `MintedNFTokens` field of the issuer's account.
277    /// This ensures each NFToken has a unique identifier.
278    ///
279    /// # Returns
280    ///
281    /// * `Ok(u32)` - The token sequence number
282    /// * `Err(Error)` - If the host function fails
283    ///
284    pub fn token_sequence(&self) -> Result<u32> {
285        let mut serial_buf = [0u8; 4];
286        let result = unsafe {
287            host::nft_serial(
288                self.as_ptr(),
289                self.len(),
290                serial_buf.as_mut_ptr(),
291                serial_buf.len(),
292            )
293        };
294
295        match result {
296            code if code > 0 => {
297                // Convert big-endian bytes to u32
298                let serial = u32::from_be_bytes(serial_buf);
299                Result::Ok(serial)
300            }
301            code => Result::Err(Error::from_code(code)),
302        }
303    }
304
305    /// Retrieves the URI of this NFToken for a given owner.
306    ///
307    /// # Arguments
308    ///
309    /// * `owner` - The account that owns this NFToken
310    ///
311    /// # Returns
312    ///
313    /// * `Ok(UriBlob)` - The URI data (variable length, up to 256 bytes)
314    /// * `Err(Error)` - If the NFT is not found or the host function fails
315    ///
316    ///
317    pub fn uri(&self, owner: &AccountID) -> Result<UriBlob> {
318        let mut uri_buf = [0u8; URI_BLOB_SIZE];
319        let result = unsafe {
320            host::nft_uri(
321                owner.0.as_ptr(),
322                owner.0.len(),
323                self.as_ptr(),
324                self.len(),
325                uri_buf.as_mut_ptr(),
326                uri_buf.len(),
327            )
328        };
329
330        match result {
331            code if code > 0 => Result::Ok(UriBlob::from(uri_buf)),
332            code => Result::Err(Error::from_code(code)),
333        }
334    }
335}
336
337impl From<[u8; NFT_ID_SIZE]> for NFToken {
338    fn from(value: [u8; NFT_ID_SIZE]) -> Self {
339        NFToken(value)
340    }
341}
342
343impl AsRef<[u8]> for NFToken {
344    fn as_ref(&self) -> &[u8] {
345        &self.0
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::host::host_bindings_trait::MockHostBindings;
353    use crate::host::setup_mock;
354    use mockall::predicate::{always, eq};
355
356    #[test]
357    fn test_nft_creation() {
358        let nft_id = [0u8; 32];
359        let nft = NFToken::new(nft_id);
360        assert_eq!(nft.as_bytes(), &nft_id);
361        assert_eq!(nft.len(), 32);
362    }
363
364    #[test]
365    fn test_nft_from_array() {
366        let nft_id = [0u8; 32];
367        let nft: NFToken = nft_id.into();
368        assert_eq!(nft.as_bytes(), &nft_id);
369    }
370
371    // NftFlags tests
372    #[test]
373    fn test_nft_flags_no_flags_set() {
374        let nft_flags = NftFlags::new(0);
375        assert!(!nft_flags.is_burnable());
376        assert!(!nft_flags.is_only_xrp());
377        assert!(!nft_flags.is_trust_line());
378        assert!(!nft_flags.is_transferable());
379        assert_eq!(nft_flags.as_u16(), 0);
380    }
381
382    #[test]
383    fn test_nft_flags_burnable() {
384        let nft_flags = NftFlags::new(flags::BURNABLE);
385        assert!(nft_flags.is_burnable());
386        assert!(!nft_flags.is_only_xrp());
387        assert!(!nft_flags.is_trust_line());
388        assert!(!nft_flags.is_transferable());
389        assert_eq!(nft_flags.as_u16(), flags::BURNABLE);
390    }
391
392    #[test]
393    fn test_nft_flags_only_xrp() {
394        let nft_flags = NftFlags::new(flags::ONLY_XRP);
395        assert!(!nft_flags.is_burnable());
396        assert!(nft_flags.is_only_xrp());
397        assert!(!nft_flags.is_trust_line());
398        assert!(!nft_flags.is_transferable());
399        assert_eq!(nft_flags.as_u16(), flags::ONLY_XRP);
400    }
401
402    #[test]
403    fn test_nft_flags_trust_line() {
404        let nft_flags = NftFlags::new(flags::TRUST_LINE);
405        assert!(!nft_flags.is_burnable());
406        assert!(!nft_flags.is_only_xrp());
407        assert!(nft_flags.is_trust_line());
408        assert!(!nft_flags.is_transferable());
409        assert_eq!(nft_flags.as_u16(), flags::TRUST_LINE);
410    }
411
412    #[test]
413    fn test_nft_flags_transferable() {
414        let nft_flags = NftFlags::new(flags::TRANSFERABLE);
415        assert!(!nft_flags.is_burnable());
416        assert!(!nft_flags.is_only_xrp());
417        assert!(!nft_flags.is_trust_line());
418        assert!(nft_flags.is_transferable());
419        assert_eq!(nft_flags.as_u16(), flags::TRANSFERABLE);
420    }
421
422    #[test]
423    fn test_nft_flags_multiple_flags() {
424        let nft_flags = NftFlags::new(flags::BURNABLE | flags::TRANSFERABLE);
425        assert!(nft_flags.is_burnable());
426        assert!(!nft_flags.is_only_xrp());
427        assert!(!nft_flags.is_trust_line());
428        assert!(nft_flags.is_transferable());
429        assert_eq!(nft_flags.as_u16(), flags::BURNABLE | flags::TRANSFERABLE);
430    }
431
432    #[test]
433    fn test_nft_flags_all_flags_set() {
434        let all_flags = flags::BURNABLE | flags::ONLY_XRP | flags::TRUST_LINE | flags::TRANSFERABLE;
435        let nft_flags = NftFlags::new(all_flags);
436        assert!(nft_flags.is_burnable());
437        assert!(nft_flags.is_only_xrp());
438        assert!(nft_flags.is_trust_line());
439        assert!(nft_flags.is_transferable());
440        assert_eq!(nft_flags.as_u16(), all_flags);
441    }
442
443    #[test]
444    fn test_nft_flags_from_u16() {
445        let flags_value: u16 = flags::BURNABLE | flags::ONLY_XRP;
446        let nft_flags: NftFlags = flags_value.into();
447        assert!(nft_flags.is_burnable());
448        assert!(nft_flags.is_only_xrp());
449        assert_eq!(nft_flags.as_u16(), flags_value);
450    }
451
452    #[test]
453    fn test_nft_flags_into_u16() {
454        let nft_flags = NftFlags::new(flags::TRANSFERABLE | flags::TRUST_LINE);
455        let flags_value: u16 = nft_flags.into();
456        assert_eq!(flags_value, flags::TRANSFERABLE | flags::TRUST_LINE);
457    }
458
459    #[test]
460    fn test_nft_flags_equality() {
461        let nft_flags1 = NftFlags::new(flags::BURNABLE);
462        let nft_flags2 = NftFlags::new(flags::BURNABLE);
463        let nft_flags3 = NftFlags::new(flags::ONLY_XRP);
464
465        assert_eq!(nft_flags1, nft_flags2);
466        assert_ne!(nft_flags1, nft_flags3);
467    }
468
469    #[test]
470    fn test_nft_flags_clone() {
471        let nft_flags1 = NftFlags::new(flags::TRANSFERABLE);
472        let nft_flags2 = nft_flags1;
473
474        assert_eq!(nft_flags1, nft_flags2);
475        assert!(nft_flags2.is_transferable());
476    }
477
478    // NFToken additional tests
479    #[test]
480    fn test_nft_as_ptr() {
481        let nft_id = [42u8; 32];
482        let nft = NFToken::new(nft_id);
483
484        let ptr = nft.as_ptr();
485        assert!(!ptr.is_null());
486
487        // Verify the pointer points to the correct data
488        unsafe {
489            assert_eq!(*ptr, 42u8);
490        }
491    }
492
493    #[test]
494    fn test_nft_as_ref() {
495        let nft_id = [7u8; 32];
496        let nft = NFToken::new(nft_id);
497
498        let slice: &[u8] = nft.as_ref();
499        assert_eq!(slice.len(), 32);
500        assert_eq!(slice, &nft_id);
501    }
502
503    #[test]
504    fn test_nft_equality() {
505        let nft_id1 = [5u8; 32];
506        let nft_id2 = [5u8; 32];
507        let nft_id3 = [6u8; 32];
508
509        let nft1 = NFToken::new(nft_id1);
510        let nft2 = NFToken::new(nft_id2);
511        let nft3 = NFToken::new(nft_id3);
512
513        assert_eq!(nft1, nft2);
514        assert_ne!(nft1, nft3);
515    }
516
517    #[test]
518    fn test_nft_clone() {
519        let nft_id = [9u8; 32];
520        let nft1 = NFToken::new(nft_id);
521        let nft2 = nft1;
522
523        assert_eq!(nft1, nft2);
524        assert_eq!(nft1.as_bytes(), nft2.as_bytes());
525    }
526
527    // NFToken method tests
528    #[test]
529    fn test_nft_host_method_error() {
530        let mut mock = MockHostBindings::new();
531
532        mock.expect_nft_flags()
533            .with(always(), eq(NFT_ID_SIZE))
534            .returning(|_, _| crate::host::error_codes::SOME_ERROR);
535
536        let _guard = setup_mock(mock);
537
538        let nft = NFToken::new([0u8; 32]);
539        let result = nft.flags();
540        assert!(result.is_err());
541        assert_eq!(
542            result.err().unwrap().code(),
543            crate::host::error_codes::SOME_ERROR
544        );
545    }
546
547    #[test]
548    fn test_nft_flags_method() {
549        let mut mock = MockHostBindings::new();
550        let nft_id = [0u8; 32];
551        let expected_flags = 0x0001u16; // BURNABLE flag
552
553        // Set up expectations
554        mock.expect_nft_flags()
555            .with(always(), eq(NFT_ID_SIZE))
556            .returning(move |_, _| expected_flags as i32);
557
558        let _guard = setup_mock(mock);
559
560        let nft = NFToken::new(nft_id);
561        let result = nft.flags();
562        assert!(result.is_ok());
563        assert_eq!(result.unwrap().as_u16(), expected_flags);
564    }
565
566    #[test]
567    fn test_nft_transfer_fee_method() {
568        let mut mock = MockHostBindings::new();
569        let nft_id = [0u8; 32];
570        let expected_fee = 1000u16;
571
572        // Set up expectations
573        mock.expect_nft_xfer_fee()
574            .with(always(), eq(NFT_ID_SIZE))
575            .returning(move |_, _| expected_fee as i32);
576
577        let _guard = setup_mock(mock);
578
579        let nft = NFToken::new(nft_id);
580        let result = nft.transfer_fee();
581        assert!(result.is_ok());
582        assert_eq!(result.unwrap(), expected_fee);
583    }
584
585    #[test]
586    fn test_nft_issuer_method() {
587        let mut mock = MockHostBindings::new();
588        let nft_id = [0u8; 32];
589
590        // Set up expectations
591        mock.expect_nft_issuer()
592            .with(always(), eq(NFT_ID_SIZE), always(), eq(ACCOUNT_ID_SIZE))
593            .returning(|_, _, _, _| ACCOUNT_ID_SIZE as i32);
594
595        let _guard = setup_mock(mock);
596
597        let nft = NFToken::new(nft_id);
598        let result = nft.issuer();
599        assert!(result.is_ok());
600        let issuer = result.unwrap();
601        assert_eq!(issuer.0.len(), ACCOUNT_ID_SIZE);
602    }
603
604    #[test]
605    fn test_nft_taxon_method() {
606        let mut mock = MockHostBindings::new();
607        let nft_id = [0u8; 32];
608
609        // Set up expectations - taxon is a u32 (4 bytes)
610        mock.expect_nft_taxon()
611            .with(always(), eq(NFT_ID_SIZE), always(), eq(4))
612            .returning(|_, _, _, _| 4);
613
614        let _guard = setup_mock(mock);
615
616        let nft = NFToken::new(nft_id);
617        let result = nft.taxon();
618        assert!(result.is_ok());
619        assert_eq!(result.unwrap(), 0);
620    }
621
622    #[test]
623    fn test_nft_token_sequence_method() {
624        let mut mock = MockHostBindings::new();
625        let nft_id = [0u8; 32];
626
627        // Set up expectations - serial is a u32 (4 bytes)
628        mock.expect_nft_serial()
629            .with(always(), eq(NFT_ID_SIZE), always(), eq(4))
630            .returning(|_, _, _, _| 4);
631
632        let _guard = setup_mock(mock);
633
634        let nft = NFToken::new(nft_id);
635        let result = nft.token_sequence();
636        assert!(result.is_ok());
637        assert_eq!(result.unwrap(), 0);
638    }
639
640    #[test]
641    fn test_nft_uri_method() {
642        let mut mock = MockHostBindings::new();
643        let nft_id = [0u8; 32];
644        let owner = AccountID([0u8; ACCOUNT_ID_SIZE]);
645        let expected_uri_len = 10;
646
647        // Set up expectations
648        mock.expect_nft_uri()
649            .with(
650                always(),
651                eq(ACCOUNT_ID_SIZE),
652                always(),
653                eq(NFT_ID_SIZE),
654                always(),
655                eq(URI_BLOB_SIZE),
656            )
657            .returning(move |_, _, _, _, _, _| expected_uri_len);
658
659        let _guard = setup_mock(mock);
660
661        let nft = NFToken::new(nft_id);
662        let result = nft.uri(&owner);
663        assert!(result.is_ok());
664        let uri = result.unwrap();
665        assert!(uri.len <= URI_BLOB_SIZE);
666    }
667}