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#[derive(
28 Debug, Eq, PartialEq, Copy, Clone, Serialize_repr, Deserialize_repr, Display, AsRefStr, EnumIter,
29)]
30#[repr(u32)]
31pub enum NFTokenMintFlag {
32 TfBurnable = 0x00000001,
35 TfOnlyXRP = 0x00000002,
39 TfTrustLine = 0x00000004,
42 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#[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 #[serde(flatten)]
103 pub common_fields: CommonFields<'a, NFTokenMintFlag>,
104 #[serde(rename = "NFTokenTaxon")]
107 pub nftoken_taxon: u32,
108 pub issuer: Option<Cow<'a, str>>,
114 pub transfer_fee: Option<u32>,
120 #[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 pub fn with_issuer(mut self, issuer: Cow<'a, str>) -> Self {
259 self.issuer = Some(issuer);
260 self
261 }
262
263 pub fn with_transfer_fee(mut self, transfer_fee: u32) -> Self {
265 self.transfer_fee = Some(transfer_fee);
266 self
267 }
268
269 pub fn with_uri(mut self, uri: Cow<'a, str>) -> Self {
271 self.uri = Some(uri);
272 self
273 }
274
275 pub fn with_flag(mut self, flag: NFTokenMintFlag) -> Self {
277 self.common_fields.flags.0.push(flag);
278 self
279 }
280
281 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 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 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, ..Default::default()
475 }
476 .with_flags(vec![
477 NFTokenMintFlag::TfTransferable,
478 NFTokenMintFlag::TfOnlyXRP,
479 ])
480 .with_transfer_fee(500) .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 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(())), (0x00000009, Err(())), (0x00000000, Err(())), ];
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}