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                let taxon = u32::from_le_bytes(taxon_buf);
266                Result::Ok(taxon)
267            }
268            code => Result::Err(Error::from_code(code)),
269        }
270    }
271
272    /// Retrieves the token sequence number of this NFToken.
273    ///
274    /// The token sequence number is automatically incremented for each NFToken minted
275    /// by the issuer, based on the `MintedNFTokens` field of the issuer's account.
276    /// This ensures each NFToken has a unique identifier.
277    ///
278    /// # Returns
279    ///
280    /// * `Ok(u32)` - The token sequence number
281    /// * `Err(Error)` - If the host function fails
282    ///
283    pub fn token_sequence(&self) -> Result<u32> {
284        let mut serial_buf = [0u8; 4];
285        let result = unsafe {
286            host::nft_serial(
287                self.as_ptr(),
288                self.len(),
289                serial_buf.as_mut_ptr(),
290                serial_buf.len(),
291            )
292        };
293
294        match result {
295            code if code > 0 => {
296                let serial = u32::from_le_bytes(serial_buf);
297                Result::Ok(serial)
298            }
299            code => Result::Err(Error::from_code(code)),
300        }
301    }
302
303    /// Retrieves the URI of this NFToken for a given owner.
304    ///
305    /// # Arguments
306    ///
307    /// * `owner` - The account that owns this NFToken
308    ///
309    /// # Returns
310    ///
311    /// * `Ok(UriBlob)` - The URI data (variable length, up to 256 bytes)
312    /// * `Err(Error)` - If the NFT is not found or the host function fails
313    ///
314    ///
315    pub fn uri(&self, owner: &AccountID) -> Result<UriBlob> {
316        let mut uri_buf = [0u8; URI_BLOB_SIZE];
317        let result = unsafe {
318            host::nft_uri(
319                owner.0.as_ptr(),
320                owner.0.len(),
321                self.as_ptr(),
322                self.len(),
323                uri_buf.as_mut_ptr(),
324                uri_buf.len(),
325            )
326        };
327
328        match result {
329            code if code > 0 => Result::Ok(UriBlob::from(uri_buf)),
330            code => Result::Err(Error::from_code(code)),
331        }
332    }
333}
334
335impl From<[u8; NFT_ID_SIZE]> for NFToken {
336    fn from(value: [u8; NFT_ID_SIZE]) -> Self {
337        NFToken(value)
338    }
339}
340
341impl AsRef<[u8]> for NFToken {
342    fn as_ref(&self) -> &[u8] {
343        &self.0
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::host::host_bindings_trait::MockHostBindings;
351    use crate::host::setup_mock;
352    use mockall::predicate::{always, eq};
353
354    #[test]
355    fn test_nft_creation() {
356        let nft_id = [0u8; 32];
357        let nft = NFToken::new(nft_id);
358        assert_eq!(nft.as_bytes(), &nft_id);
359        assert_eq!(nft.len(), 32);
360    }
361
362    #[test]
363    fn test_nft_from_array() {
364        let nft_id = [0u8; 32];
365        let nft: NFToken = nft_id.into();
366        assert_eq!(nft.as_bytes(), &nft_id);
367    }
368
369    // NftFlags tests
370    #[test]
371    fn test_nft_flags_no_flags_set() {
372        let nft_flags = NftFlags::new(0);
373        assert!(!nft_flags.is_burnable());
374        assert!(!nft_flags.is_only_xrp());
375        assert!(!nft_flags.is_trust_line());
376        assert!(!nft_flags.is_transferable());
377        assert_eq!(nft_flags.as_u16(), 0);
378    }
379
380    #[test]
381    fn test_nft_flags_burnable() {
382        let nft_flags = NftFlags::new(flags::BURNABLE);
383        assert!(nft_flags.is_burnable());
384        assert!(!nft_flags.is_only_xrp());
385        assert!(!nft_flags.is_trust_line());
386        assert!(!nft_flags.is_transferable());
387        assert_eq!(nft_flags.as_u16(), flags::BURNABLE);
388    }
389
390    #[test]
391    fn test_nft_flags_only_xrp() {
392        let nft_flags = NftFlags::new(flags::ONLY_XRP);
393        assert!(!nft_flags.is_burnable());
394        assert!(nft_flags.is_only_xrp());
395        assert!(!nft_flags.is_trust_line());
396        assert!(!nft_flags.is_transferable());
397        assert_eq!(nft_flags.as_u16(), flags::ONLY_XRP);
398    }
399
400    #[test]
401    fn test_nft_flags_trust_line() {
402        let nft_flags = NftFlags::new(flags::TRUST_LINE);
403        assert!(!nft_flags.is_burnable());
404        assert!(!nft_flags.is_only_xrp());
405        assert!(nft_flags.is_trust_line());
406        assert!(!nft_flags.is_transferable());
407        assert_eq!(nft_flags.as_u16(), flags::TRUST_LINE);
408    }
409
410    #[test]
411    fn test_nft_flags_transferable() {
412        let nft_flags = NftFlags::new(flags::TRANSFERABLE);
413        assert!(!nft_flags.is_burnable());
414        assert!(!nft_flags.is_only_xrp());
415        assert!(!nft_flags.is_trust_line());
416        assert!(nft_flags.is_transferable());
417        assert_eq!(nft_flags.as_u16(), flags::TRANSFERABLE);
418    }
419
420    #[test]
421    fn test_nft_flags_multiple_flags() {
422        let nft_flags = NftFlags::new(flags::BURNABLE | flags::TRANSFERABLE);
423        assert!(nft_flags.is_burnable());
424        assert!(!nft_flags.is_only_xrp());
425        assert!(!nft_flags.is_trust_line());
426        assert!(nft_flags.is_transferable());
427        assert_eq!(nft_flags.as_u16(), flags::BURNABLE | flags::TRANSFERABLE);
428    }
429
430    #[test]
431    fn test_nft_flags_all_flags_set() {
432        let all_flags = flags::BURNABLE | flags::ONLY_XRP | flags::TRUST_LINE | flags::TRANSFERABLE;
433        let nft_flags = NftFlags::new(all_flags);
434        assert!(nft_flags.is_burnable());
435        assert!(nft_flags.is_only_xrp());
436        assert!(nft_flags.is_trust_line());
437        assert!(nft_flags.is_transferable());
438        assert_eq!(nft_flags.as_u16(), all_flags);
439    }
440
441    #[test]
442    fn test_nft_flags_from_u16() {
443        let flags_value: u16 = flags::BURNABLE | flags::ONLY_XRP;
444        let nft_flags: NftFlags = flags_value.into();
445        assert!(nft_flags.is_burnable());
446        assert!(nft_flags.is_only_xrp());
447        assert_eq!(nft_flags.as_u16(), flags_value);
448    }
449
450    #[test]
451    fn test_nft_flags_into_u16() {
452        let nft_flags = NftFlags::new(flags::TRANSFERABLE | flags::TRUST_LINE);
453        let flags_value: u16 = nft_flags.into();
454        assert_eq!(flags_value, flags::TRANSFERABLE | flags::TRUST_LINE);
455    }
456
457    #[test]
458    fn test_nft_flags_equality() {
459        let nft_flags1 = NftFlags::new(flags::BURNABLE);
460        let nft_flags2 = NftFlags::new(flags::BURNABLE);
461        let nft_flags3 = NftFlags::new(flags::ONLY_XRP);
462
463        assert_eq!(nft_flags1, nft_flags2);
464        assert_ne!(nft_flags1, nft_flags3);
465    }
466
467    #[test]
468    fn test_nft_flags_clone() {
469        let nft_flags1 = NftFlags::new(flags::TRANSFERABLE);
470        let nft_flags2 = nft_flags1;
471
472        assert_eq!(nft_flags1, nft_flags2);
473        assert!(nft_flags2.is_transferable());
474    }
475
476    // NFToken additional tests
477    #[test]
478    fn test_nft_as_ptr() {
479        let nft_id = [42u8; 32];
480        let nft = NFToken::new(nft_id);
481
482        let ptr = nft.as_ptr();
483        assert!(!ptr.is_null());
484
485        // Verify the pointer points to the correct data
486        unsafe {
487            assert_eq!(*ptr, 42u8);
488        }
489    }
490
491    #[test]
492    fn test_nft_as_ref() {
493        let nft_id = [7u8; 32];
494        let nft = NFToken::new(nft_id);
495
496        let slice: &[u8] = nft.as_ref();
497        assert_eq!(slice.len(), 32);
498        assert_eq!(slice, &nft_id);
499    }
500
501    #[test]
502    fn test_nft_equality() {
503        let nft_id1 = [5u8; 32];
504        let nft_id2 = [5u8; 32];
505        let nft_id3 = [6u8; 32];
506
507        let nft1 = NFToken::new(nft_id1);
508        let nft2 = NFToken::new(nft_id2);
509        let nft3 = NFToken::new(nft_id3);
510
511        assert_eq!(nft1, nft2);
512        assert_ne!(nft1, nft3);
513    }
514
515    #[test]
516    fn test_nft_clone() {
517        let nft_id = [9u8; 32];
518        let nft1 = NFToken::new(nft_id);
519        let nft2 = nft1;
520
521        assert_eq!(nft1, nft2);
522        assert_eq!(nft1.as_bytes(), nft2.as_bytes());
523    }
524
525    // NFToken method tests
526    #[test]
527    fn test_nft_host_method_error() {
528        let mut mock = MockHostBindings::new();
529
530        mock.expect_nft_flags()
531            .with(always(), eq(NFT_ID_SIZE))
532            .returning(|_, _| crate::host::error_codes::SOME_ERROR);
533
534        let _guard = setup_mock(mock);
535
536        let nft = NFToken::new([0u8; 32]);
537        let result = nft.flags();
538        assert!(result.is_err());
539        assert_eq!(
540            result.err().unwrap().code(),
541            crate::host::error_codes::SOME_ERROR
542        );
543    }
544
545    #[test]
546    fn test_nft_flags_method() {
547        let mut mock = MockHostBindings::new();
548        let nft_id = [0u8; 32];
549        let expected_flags = 0x0001u16; // BURNABLE flag
550
551        // Set up expectations
552        mock.expect_nft_flags()
553            .with(always(), eq(NFT_ID_SIZE))
554            .returning(move |_, _| expected_flags as i32);
555
556        let _guard = setup_mock(mock);
557
558        let nft = NFToken::new(nft_id);
559        let result = nft.flags();
560        assert!(result.is_ok());
561        assert_eq!(result.unwrap().as_u16(), expected_flags);
562    }
563
564    #[test]
565    fn test_nft_transfer_fee_method() {
566        let mut mock = MockHostBindings::new();
567        let nft_id = [0u8; 32];
568        let expected_fee = 1000u16;
569
570        // Set up expectations
571        mock.expect_nft_xfer_fee()
572            .with(always(), eq(NFT_ID_SIZE))
573            .returning(move |_, _| expected_fee as i32);
574
575        let _guard = setup_mock(mock);
576
577        let nft = NFToken::new(nft_id);
578        let result = nft.transfer_fee();
579        assert!(result.is_ok());
580        assert_eq!(result.unwrap(), expected_fee);
581    }
582
583    #[test]
584    fn test_nft_issuer_method() {
585        let mut mock = MockHostBindings::new();
586        let nft_id = [0u8; 32];
587
588        // Set up expectations
589        mock.expect_nft_issuer()
590            .with(always(), eq(NFT_ID_SIZE), always(), eq(ACCOUNT_ID_SIZE))
591            .returning(|_, _, _, _| ACCOUNT_ID_SIZE as i32);
592
593        let _guard = setup_mock(mock);
594
595        let nft = NFToken::new(nft_id);
596        let result = nft.issuer();
597        assert!(result.is_ok());
598        let issuer = result.unwrap();
599        assert_eq!(issuer.0.len(), ACCOUNT_ID_SIZE);
600    }
601
602    #[test]
603    fn test_nft_taxon_method() {
604        let mut mock = MockHostBindings::new();
605        let nft_id = [0u8; 32];
606
607        // Non-palindromic so a byte-order regression changes the decoded value.
608        const TAXON: u32 = 0x0102_0304;
609
610        // Set up expectations - the host writes the taxon as 4 little-endian bytes
611        mock.expect_nft_taxon()
612            .with(always(), eq(NFT_ID_SIZE), always(), eq(4))
613            .returning(|_, _, out_buff_ptr, _| {
614                unsafe { out_buff_ptr.copy_from_nonoverlapping(TAXON.to_le_bytes().as_ptr(), 4) }
615                4
616            });
617
618        let _guard = setup_mock(mock);
619
620        let nft = NFToken::new(nft_id);
621        let result = nft.taxon();
622        assert!(result.is_ok());
623        assert_eq!(result.unwrap(), TAXON);
624    }
625
626    #[test]
627    fn test_nft_token_sequence_method() {
628        let mut mock = MockHostBindings::new();
629        let nft_id = [0u8; 32];
630
631        // Non-palindromic so a byte-order regression changes the decoded value.
632        const SERIAL: u32 = 0x0102_0304;
633
634        // Set up expectations - the host writes the serial as 4 little-endian bytes
635        mock.expect_nft_serial()
636            .with(always(), eq(NFT_ID_SIZE), always(), eq(4))
637            .returning(|_, _, out_buff_ptr, _| {
638                unsafe { out_buff_ptr.copy_from_nonoverlapping(SERIAL.to_le_bytes().as_ptr(), 4) }
639                4
640            });
641
642        let _guard = setup_mock(mock);
643
644        let nft = NFToken::new(nft_id);
645        let result = nft.token_sequence();
646        assert!(result.is_ok());
647        assert_eq!(result.unwrap(), SERIAL);
648    }
649
650    #[test]
651    fn test_nft_uri_method() {
652        let mut mock = MockHostBindings::new();
653        let nft_id = [0u8; 32];
654        let owner = AccountID([0u8; ACCOUNT_ID_SIZE]);
655        let expected_uri_len = 10;
656
657        // Set up expectations
658        mock.expect_nft_uri()
659            .with(
660                always(),
661                eq(ACCOUNT_ID_SIZE),
662                always(),
663                eq(NFT_ID_SIZE),
664                always(),
665                eq(URI_BLOB_SIZE),
666            )
667            .returning(move |_, _, _, _, _, _| expected_uri_len);
668
669        let _guard = setup_mock(mock);
670
671        let nft = NFToken::new(nft_id);
672        let result = nft.uri(&owner);
673        assert!(result.is_ok());
674        let uri = result.unwrap();
675        assert!(uri.len <= URI_BLOB_SIZE);
676    }
677}