xrpl_common_stdlib/types/
nft.rs1use 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
25pub const NFT_ID_SIZE: usize = 32;
27
28pub mod flags {
30 pub const BURNABLE: u16 = 0x0001;
34
35 pub const ONLY_XRP: u16 = 0x0002;
39
40 pub const TRUST_LINE: u16 = 0x0004;
43
44 pub const TRANSFERABLE: u16 = 0x0008;
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct NftFlags(u16);
58
59impl NftFlags {
60 #[inline]
62 pub const fn new(flags: u16) -> Self {
63 NftFlags(flags)
64 }
65
66 #[inline]
68 pub const fn as_u16(&self) -> u16 {
69 self.0
70 }
71
72 #[inline]
77 pub const fn is_burnable(&self) -> bool {
78 self.0 & flags::BURNABLE != 0
79 }
80
81 #[inline]
85 pub const fn is_only_xrp(&self) -> bool {
86 self.0 & flags::ONLY_XRP != 0
87 }
88
89 #[inline]
94 pub const fn is_trust_line(&self) -> bool {
95 self.0 & flags::TRUST_LINE != 0
96 }
97
98 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141#[repr(C)]
142pub struct NFToken(pub [u8; NFT_ID_SIZE]);
143
144impl NFToken {
145 #[inline]
152 pub const fn new(id: [u8; NFT_ID_SIZE]) -> Self {
153 NFToken(id)
154 }
155
156 #[inline]
159 pub const fn as_bytes(&self) -> &[u8; NFT_ID_SIZE] {
160 &self.0
161 }
162
163 #[inline]
167 pub const fn as_ptr(&self) -> *const u8 {
168 self.0.as_ptr()
169 }
170
171 #[inline]
173 #[allow(clippy::len_without_is_empty)]
174 pub const fn len(&self) -> usize {
175 NFT_ID_SIZE
176 }
177
178 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 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 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 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 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 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 #[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 #[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 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 #[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; 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 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 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 const TAXON: u32 = 0x0102_0304;
609
610 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 const SERIAL: u32 = 0x0102_0304;
633
634 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 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}