Skip to main content

xrpl/models/transactions/
nftoken_mint.rs

1use alloc::borrow::Cow;
2use alloc::vec::Vec;
3use core::convert::TryFrom;
4
5use serde::{Deserialize, Serialize};
6use serde_repr::{Deserialize_repr, Serialize_repr};
7use serde_with::skip_serializing_none;
8use strum_macros::{AsRefStr, Display, EnumIter};
9
10use crate::{
11    constants::{MAX_TRANSFER_FEE, MAX_URI_LENGTH},
12    models::{
13        transactions::{Memo, Signer, Transaction, TransactionType},
14        Model, ValidateCurrencies, XRPLModelException, XRPLModelResult,
15    },
16};
17
18use crate::models::amount::XRPAmount;
19
20use super::{CommonFields, CommonTransactionBuilder, FlagCollection};
21
22/// Transactions of the NFTokenMint type support additional values
23/// in the Flags field. This enum represents those options.
24///
25/// See NFTokenMint flags:
26/// `<https://xrpl.org/docs/references/protocol/transactions/types/nftokenmint>`
27#[derive(
28    Debug, Eq, PartialEq, Copy, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter,
29)]
30#[repr(u32)]
31pub enum NFTokenMintFlag {
32    /// Allow the issuer (or an entity authorized by the issuer) to
33    /// destroy the minted NFToken. (The NFToken's owner can always do so.)
34    TfBurnable = 0x00000001,
35    /// The minted NFToken can only be bought or sold for XRP.
36    /// This can be desirable if the token has a transfer fee and the issuer
37    /// does not want to receive fees in non-XRP currencies.
38    TfOnlyXRP = 0x00000002,
39    /// Allows the issuer (or an entity authorized by the issuer) to
40    /// destroy the minted NFToken even if the NFToken is owned by another account.
41    TfTrustLine = 0x00000004,
42    /// The minted NFToken can be transferred to others. If this flag is not
43    /// enabled, the token can still be transferred from or to the issuer.
44    TfTransferable = 0x00000008,
45}
46
47impl TryFrom<u32> for NFTokenMintFlag {
48    type Error = ();
49
50    fn try_from(value: u32) -> Result<Self, Self::Error> {
51        match value {
52            0x00000001 => Ok(NFTokenMintFlag::TfBurnable),
53            0x00000002 => Ok(NFTokenMintFlag::TfOnlyXRP),
54            0x00000004 => Ok(NFTokenMintFlag::TfTrustLine),
55            0x00000008 => Ok(NFTokenMintFlag::TfTransferable),
56            _ => Err(()),
57        }
58    }
59}
60
61impl NFTokenMintFlag {
62    pub fn from_bits(bits: u32) -> Vec<Self> {
63        let mut flags = Vec::new();
64        if bits & 0x00000001 != 0 {
65            flags.push(NFTokenMintFlag::TfBurnable);
66        }
67        if bits & 0x00000002 != 0 {
68            flags.push(NFTokenMintFlag::TfOnlyXRP);
69        }
70        if bits & 0x00000004 != 0 {
71            flags.push(NFTokenMintFlag::TfTrustLine);
72        }
73        if bits & 0x00000008 != 0 {
74            flags.push(NFTokenMintFlag::TfTransferable);
75        }
76        flags
77    }
78}
79
80/// The NFTokenMint transaction creates a non-fungible token and adds it to
81/// the relevant NFTokenPage object of the NFTokenMinter as an NFToken object.
82///
83/// See NFTokenMint:
84/// `<https://xrpl.org/docs/references/protocol/transactions/types/nftokenmint>`
85#[skip_serializing_none]
86#[derive(
87    Debug,
88    Default,
89    Serialize,
90    Deserialize,
91    PartialEq,
92    Eq,
93    Clone,
94    xrpl_rust_macros::ValidateCurrencies,
95)]
96#[serde(rename_all = "PascalCase")]
97pub struct NFTokenMint<'a> {
98    /// The base fields for all transaction models.
99    ///
100    /// See Transaction Common Fields:
101    /// `<https://xrpl.org/transaction-common-fields.html>`
102    #[serde(flatten)]
103    pub common_fields: CommonFields<'a, NFTokenMintFlag>,
104    /// An arbitrary taxon, or shared identifier, for a series or collection of related NFTs.
105    /// To mint a series of NFTs, give them all the same taxon.
106    #[serde(rename = "NFTokenTaxon")]
107    pub nftoken_taxon: u32,
108    /// The issuer of the token, if the sender of the account is issuing it on behalf of
109    /// another account. This field must be omitted if the account sending the transaction
110    /// is the issuer of the NFToken. If provided, the issuer's AccountRoot object must have
111    /// the NFTokenMinter field set to the sender of this transaction (this transaction's
112    /// Account field).
113    pub issuer: Option<Cow<'a, str>>,
114    /// The value specifies the fee charged by the issuer for secondary sales of the NFToken,
115    /// if such sales are allowed. Valid values for this field are between 0 and 50000
116    /// inclusive, allowing transfer rates of between 0.00% and 50.00% in increments of
117    /// 0.001. If this field is provided, the transaction MUST have the tfTransferable
118    /// flag enabled.
119    pub transfer_fee: Option<u32>,
120    /// Up to 256 bytes of arbitrary data. In JSON, this should be encoded as a string of
121    /// hexadecimal. You can use the xrpl.convertStringToHex utility to convert a URI to
122    /// its hexadecimal equivalent. This is intended to be a URI that points to the data or
123    /// metadata associated with the NFT. The contents could decode to an HTTP or HTTPS URL,
124    /// an IPFS URI, a magnet link, immediate data encoded as an RFC 2379 "data" URL, or
125    /// even an issuer-specific encoding. The URI is NOT checked for validity.
126    #[serde(rename = "URI")]
127    pub uri: Option<Cow<'a, str>>,
128}
129
130impl<'a> Model for NFTokenMint<'a> {
131    fn get_errors(&self) -> XRPLModelResult<()> {
132        self._get_issuer_error()?;
133        self._get_transfer_fee_error()?;
134        self._get_uri_error()?;
135        self.validate_currencies()
136    }
137}
138
139impl<'a> Transaction<'a, NFTokenMintFlag> for NFTokenMint<'a> {
140    fn has_flag(&self, flag: &NFTokenMintFlag) -> bool {
141        self.common_fields.has_flag(flag)
142    }
143
144    fn get_transaction_type(&self) -> &TransactionType {
145        self.common_fields.get_transaction_type()
146    }
147
148    fn get_common_fields(&self) -> &CommonFields<'_, NFTokenMintFlag> {
149        self.common_fields.get_common_fields()
150    }
151
152    fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NFTokenMintFlag> {
153        self.common_fields.get_mut_common_fields()
154    }
155}
156
157impl<'a> CommonTransactionBuilder<'a, NFTokenMintFlag> for NFTokenMint<'a> {
158    fn get_mut_common_fields(&mut self) -> &mut CommonFields<'a, NFTokenMintFlag> {
159        &mut self.common_fields
160    }
161
162    fn into_self(self) -> Self {
163        self
164    }
165}
166
167impl<'a> NFTokenMintError for NFTokenMint<'a> {
168    fn _get_issuer_error(&self) -> XRPLModelResult<()> {
169        if let Some(issuer) = &self.issuer {
170            if issuer == &self.common_fields.account {
171                Err(XRPLModelException::ValueEqualsValue {
172                    field1: "issuer".into(),
173                    field2: "account".into(),
174                })
175            } else {
176                Ok(())
177            }
178        } else {
179            Ok(())
180        }
181    }
182
183    fn _get_transfer_fee_error(&self) -> XRPLModelResult<()> {
184        if let Some(transfer_fee) = self.transfer_fee {
185            if transfer_fee > MAX_TRANSFER_FEE {
186                Err(XRPLModelException::ValueTooHigh {
187                    field: "transfer_fee".into(),
188                    max: MAX_TRANSFER_FEE,
189                    found: transfer_fee,
190                })
191            } else {
192                Ok(())
193            }
194        } else {
195            Ok(())
196        }
197    }
198
199    fn _get_uri_error(&self) -> XRPLModelResult<()> {
200        if let Some(uri) = &self.uri {
201            if uri.len() > MAX_URI_LENGTH {
202                Err(XRPLModelException::ValueTooLong {
203                    field: "uri".into(),
204                    max: MAX_URI_LENGTH,
205                    found: uri.len(),
206                })
207            } else {
208                Ok(())
209            }
210        } else {
211            Ok(())
212        }
213    }
214}
215
216impl<'a> NFTokenMint<'a> {
217    pub fn new(
218        account: Cow<'a, str>,
219        account_txn_id: Option<Cow<'a, str>>,
220        fee: Option<XRPAmount<'a>>,
221        flags: Option<FlagCollection<NFTokenMintFlag>>,
222        last_ledger_sequence: Option<u32>,
223        memos: Option<Vec<Memo>>,
224        sequence: Option<u32>,
225        signers: Option<Vec<Signer>>,
226        source_tag: Option<u32>,
227        ticket_sequence: Option<u32>,
228        nftoken_taxon: u32,
229        issuer: Option<Cow<'a, str>>,
230        transfer_fee: Option<u32>,
231        uri: Option<Cow<'a, str>>,
232    ) -> Self {
233        Self {
234            common_fields: CommonFields::new(
235                account,
236                TransactionType::NFTokenMint,
237                account_txn_id,
238                fee,
239                Some(flags.unwrap_or_default()),
240                last_ledger_sequence,
241                memos,
242                None,
243                sequence,
244                signers,
245                None,
246                source_tag,
247                ticket_sequence,
248                None,
249            ),
250            nftoken_taxon,
251            issuer,
252            transfer_fee,
253            uri,
254        }
255    }
256
257    /// Set issuer
258    pub fn with_issuer(mut self, issuer: Cow<'a, str>) -> Self {
259        self.issuer = Some(issuer);
260        self
261    }
262
263    /// Set transfer fee
264    pub fn with_transfer_fee(mut self, transfer_fee: u32) -> Self {
265        self.transfer_fee = Some(transfer_fee);
266        self
267    }
268
269    /// Set URI
270    pub fn with_uri(mut self, uri: Cow<'a, str>) -> Self {
271        self.uri = Some(uri);
272        self
273    }
274
275    /// Add flag
276    pub fn with_flag(mut self, flag: NFTokenMintFlag) -> Self {
277        self.common_fields.flags.0.push(flag);
278        self
279    }
280
281    /// Set multiple flags
282    pub fn with_flags(mut self, flags: Vec<NFTokenMintFlag>) -> Self {
283        self.common_fields.flags = flags.into();
284        self
285    }
286}
287
288pub trait NFTokenMintError {
289    fn _get_issuer_error(&self) -> XRPLModelResult<()>;
290    fn _get_transfer_fee_error(&self) -> XRPLModelResult<()>;
291    fn _get_uri_error(&self) -> XRPLModelResult<()>;
292}
293
294#[cfg(test)]
295mod tests {
296    use alloc::string::ToString;
297    use alloc::vec;
298    use core::convert::TryFrom;
299
300    use super::*;
301    use crate::models::Model;
302
303    #[test]
304    fn test_issuer_error() {
305        let nftoken_mint = NFTokenMint {
306            common_fields: CommonFields {
307                account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
308                transaction_type: TransactionType::NFTokenMint,
309                ..Default::default()
310            },
311            nftoken_taxon: 0,
312            issuer: Some("rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into()),
313            ..Default::default()
314        };
315
316        assert_eq!(
317            nftoken_mint.validate().unwrap_err().to_string().as_str(),
318            "The value of the field `\"issuer\"` is not allowed to be the same as the value of the field `\"account\"`"
319        );
320    }
321
322    #[test]
323    fn test_transfer_fee_error() {
324        let nftoken_mint = NFTokenMint {
325            common_fields: CommonFields {
326                account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
327                transaction_type: TransactionType::NFTokenMint,
328                ..Default::default()
329            },
330            nftoken_taxon: 0,
331            transfer_fee: Some(50001),
332            ..Default::default()
333        };
334
335        assert_eq!(
336            nftoken_mint.validate().unwrap_err().to_string().as_str(),
337            "The value of the field `\"transfer_fee\"` is defined above its maximum (max 50000, found 50001)"
338        );
339    }
340
341    #[test]
342    fn test_uri_error() {
343        let nftoken_mint = NFTokenMint {
344            common_fields: CommonFields {
345                account: "rU4EE1FskCPJw5QkLx1iGgdWiJa6HeqYyb".into(),
346                transaction_type: TransactionType::NFTokenMint,
347                ..Default::default()
348            },
349            nftoken_taxon: 0,
350            uri: Some("wss://xrplcluster.com/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into()),
351            ..Default::default()
352        };
353
354        assert_eq!(
355            nftoken_mint.validate().unwrap_err().to_string().as_str(),
356            "The value of the field `\"uri\"` exceeds its maximum length of characters (max 512, found 513)"
357        );
358    }
359
360    #[test]
361    fn test_serde() {
362        let default_txn = NFTokenMint {
363            common_fields: CommonFields {
364                account: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
365                transaction_type: TransactionType::NFTokenMint,
366                fee: Some("10".into()),
367                flags: vec![NFTokenMintFlag::TfTransferable].into(),
368                memos: Some(vec![Memo::new(
369                    Some("72656E74".to_string()),
370                    None,
371                    Some("687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963".to_string())
372                )]),
373                signing_pub_key: Some("".into()),
374                ..Default::default()
375            },
376            nftoken_taxon: 0,
377            transfer_fee: Some(314),
378            uri: Some("697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469".into()),
379            ..Default::default()
380        };
381
382        let default_json_str = r#"{"Account":"rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B","TransactionType":"NFTokenMint","Fee":"10","Flags":8,"Memos":[{"Memo":{"MemoData":"72656E74","MemoFormat":null,"MemoType":"687474703A2F2F6578616D706C652E636F6D2F6D656D6F2F67656E65726963"}}],"SigningPubKey":"","NFTokenTaxon":0,"TransferFee":314,"URI":"697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469"}"#;
383
384        // Serialize
385        let default_json_value = serde_json::to_value(default_json_str).unwrap();
386        let serialized_string = serde_json::to_string(&default_txn).unwrap();
387        let serialized_value = serde_json::to_value(&serialized_string).unwrap();
388        assert_eq!(serialized_value, default_json_value);
389
390        // Deserialize
391        let deserialized: NFTokenMint = serde_json::from_str(default_json_str).unwrap();
392        assert_eq!(default_txn, deserialized);
393    }
394
395    #[test]
396    fn test_builder_pattern() {
397        let nftoken_mint = NFTokenMint {
398            common_fields: CommonFields {
399                account: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
400                transaction_type: TransactionType::NFTokenMint,
401                ..Default::default()
402            },
403            nftoken_taxon: 12345,
404            ..Default::default()
405        }
406        .with_issuer("rLsn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK".into())
407        .with_transfer_fee(314)
408        .with_uri("697066733A2F2F62616679626569676479727A74357366703775646D37687537367568377932366E6634646675796C71616266336F636C67747179353566627A6469".into())
409        .with_flags(vec![NFTokenMintFlag::TfTransferable, NFTokenMintFlag::TfBurnable])
410        .with_fee("10".into())
411        .with_sequence(123)
412        .with_last_ledger_sequence(7108682)
413        .with_source_tag(12345)
414        .with_memo(Memo::new(
415            Some("creating NFT".into()),
416            None,
417            Some("text".into())
418        ));
419
420        assert_eq!(nftoken_mint.nftoken_taxon, 12345);
421        assert_eq!(
422            nftoken_mint.issuer.as_ref().unwrap(),
423            "rLsn6Z3T8uCxbcd1oxwfGQN1Fdn5CyGujK"
424        );
425        assert_eq!(nftoken_mint.transfer_fee, Some(314));
426        assert!(nftoken_mint.uri.is_some());
427        assert!(nftoken_mint.has_flag(&NFTokenMintFlag::TfTransferable));
428        assert!(nftoken_mint.has_flag(&NFTokenMintFlag::TfBurnable));
429        assert_eq!(nftoken_mint.common_fields.fee.as_ref().unwrap().0, "10");
430        assert_eq!(nftoken_mint.common_fields.sequence, Some(123));
431        assert_eq!(
432            nftoken_mint.common_fields.last_ledger_sequence,
433            Some(7108682)
434        );
435        assert_eq!(nftoken_mint.common_fields.source_tag, Some(12345));
436        assert_eq!(nftoken_mint.common_fields.memos.as_ref().unwrap().len(), 1);
437    }
438
439    #[test]
440    fn test_default() {
441        let nftoken_mint = NFTokenMint {
442            common_fields: CommonFields {
443                account: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
444                transaction_type: TransactionType::NFTokenMint,
445                ..Default::default()
446            },
447            nftoken_taxon: 0,
448            ..Default::default()
449        };
450
451        assert_eq!(
452            nftoken_mint.common_fields.account,
453            "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
454        );
455        assert_eq!(
456            nftoken_mint.common_fields.transaction_type,
457            TransactionType::NFTokenMint
458        );
459        assert_eq!(nftoken_mint.nftoken_taxon, 0);
460        assert!(nftoken_mint.issuer.is_none());
461        assert!(nftoken_mint.transfer_fee.is_none());
462        assert!(nftoken_mint.uri.is_none());
463    }
464
465    #[test]
466    fn test_collection_minting() {
467        let collection_mint = NFTokenMint {
468            common_fields: CommonFields {
469                account: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
470                transaction_type: TransactionType::NFTokenMint,
471                ..Default::default()
472            },
473            nftoken_taxon: 99999, // Collection identifier
474            ..Default::default()
475        }
476        .with_flags(vec![
477            NFTokenMintFlag::TfTransferable,
478            NFTokenMintFlag::TfOnlyXRP,
479        ])
480        .with_transfer_fee(500) // 0.5%
481        .with_uri("ipfs://collection-metadata-hash".into())
482        .with_fee("15".into())
483        .with_sequence(456);
484
485        assert_eq!(collection_mint.nftoken_taxon, 99999);
486        assert!(collection_mint.has_flag(&NFTokenMintFlag::TfTransferable));
487        assert!(collection_mint.has_flag(&NFTokenMintFlag::TfOnlyXRP));
488        assert_eq!(collection_mint.transfer_fee, Some(500));
489        assert!(collection_mint.uri.is_some());
490        assert!(collection_mint.validate().is_ok());
491    }
492
493    #[test]
494    fn test_ticket_sequence() {
495        let ticket_mint = NFTokenMint {
496            common_fields: CommonFields {
497                account: "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B".into(),
498                transaction_type: TransactionType::NFTokenMint,
499                ..Default::default()
500            },
501            nftoken_taxon: 888,
502            ..Default::default()
503        }
504        .with_ticket_sequence(789)
505        .with_flag(NFTokenMintFlag::TfBurnable)
506        .with_fee("12".into());
507
508        assert_eq!(ticket_mint.common_fields.ticket_sequence, Some(789));
509        assert_eq!(ticket_mint.nftoken_taxon, 888);
510        assert!(ticket_mint.has_flag(&NFTokenMintFlag::TfBurnable));
511        // When using tickets, sequence should be None or 0
512        assert!(ticket_mint.common_fields.sequence.is_none());
513    }
514
515    #[test]
516    fn test_try_from_u32() {
517        let cases = [
518            (0x00000001, Ok(NFTokenMintFlag::TfBurnable)),
519            (0x00000002, Ok(NFTokenMintFlag::TfOnlyXRP)),
520            (0x00000004, Ok(NFTokenMintFlag::TfTrustLine)),
521            (0x00000008, Ok(NFTokenMintFlag::TfTransferable)),
522            (0x00000010, Err(())), // invalid flag
523            (0x00000009, Err(())), // not a single flag
524            (0x00000000, Err(())), // zero is not a valid single flag
525        ];
526
527        for (input, expected) in cases {
528            assert_eq!(
529                NFTokenMintFlag::try_from(input),
530                expected,
531                "try_from({:#X}) failed",
532                input
533            );
534        }
535    }
536
537    #[test]
538    fn test_from_bits() {
539        use NFTokenMintFlag::*;
540        let cases = [
541            (0x00000001, vec![TfBurnable]),
542            (0x00000002, vec![TfOnlyXRP]),
543            (0x00000004, vec![TfTrustLine]),
544            (0x00000008, vec![TfTransferable]),
545            (0x00000009, vec![TfBurnable, TfTransferable]),
546            (0x0000000B, vec![TfBurnable, TfOnlyXRP, TfTransferable]),
547            (
548                0x0000000F,
549                vec![TfBurnable, TfOnlyXRP, TfTrustLine, TfTransferable],
550            ),
551            (0x00000000, vec![]),
552            (0x00000003, vec![TfBurnable, TfOnlyXRP]),
553            (0x00000005, vec![TfBurnable, TfTrustLine]),
554            (0x0000000C, vec![TfTrustLine, TfTransferable]),
555        ];
556
557        for (input, ref expected) in cases {
558            let mut actual = NFTokenMintFlag::from_bits(input);
559            let mut expected_sorted = expected.clone();
560            actual.sort_by_key(|f| *f as u32);
561            expected_sorted.sort_by_key(|f| *f as u32);
562            assert_eq!(actual, expected_sorted, "from_bits({:#X}) failed", input);
563        }
564    }
565}