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_be_bytes(taxon_buf);
267 Result::Ok(taxon)
268 }
269 code => Result::Err(Error::from_code(code)),
270 }
271 }
272
273 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 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 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 #[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 #[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 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 #[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; 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 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 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 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 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 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}