1use alloc::vec::Vec;
5
6use packable::{
7 error::{UnpackError, UnpackErrorExt},
8 packer::Packer,
9 unpacker::Unpacker,
10 Packable,
11};
12
13use crate::{
14 address::{Address, NftAddress},
15 output::{
16 feature::{verify_allowed_features, Feature, FeatureFlags, Features},
17 unlock_condition::{verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions},
18 verify_output_amount, ChainId, NativeToken, NativeTokens, NftId, Output, OutputBuilderAmount, OutputId, Rent,
19 RentStructure, StateTransitionError, StateTransitionVerifier,
20 },
21 protocol::ProtocolParameters,
22 semantic::{ConflictReason, ValidationContext},
23 unlock::Unlock,
24 Error,
25};
26
27#[derive(Clone)]
29#[must_use]
30pub struct NftOutputBuilder {
31 amount: OutputBuilderAmount,
32 native_tokens: Vec<NativeToken>,
33 nft_id: NftId,
34 unlock_conditions: Vec<UnlockCondition>,
35 features: Vec<Feature>,
36 immutable_features: Vec<Feature>,
37}
38
39impl NftOutputBuilder {
40 pub fn new_with_amount(amount: u64, nft_id: NftId) -> Result<NftOutputBuilder, Error> {
42 Self::new(OutputBuilderAmount::Amount(amount), nft_id)
43 }
44
45 pub fn new_with_minimum_storage_deposit(
48 rent_structure: RentStructure,
49 nft_id: NftId,
50 ) -> Result<NftOutputBuilder, Error> {
51 Self::new(OutputBuilderAmount::MinimumStorageDeposit(rent_structure), nft_id)
52 }
53
54 fn new(amount: OutputBuilderAmount, nft_id: NftId) -> Result<NftOutputBuilder, Error> {
55 Ok(Self {
56 amount,
57 native_tokens: Vec::new(),
58 nft_id,
59 unlock_conditions: Vec::new(),
60 features: Vec::new(),
61 immutable_features: Vec::new(),
62 })
63 }
64
65 #[inline(always)]
67 pub fn with_amount(mut self, amount: u64) -> Result<Self, Error> {
68 self.amount = OutputBuilderAmount::Amount(amount);
69 Ok(self)
70 }
71
72 #[inline(always)]
74 pub fn with_minimum_storage_deposit(mut self, rent_structure: RentStructure) -> Self {
75 self.amount = OutputBuilderAmount::MinimumStorageDeposit(rent_structure);
76 self
77 }
78
79 #[inline(always)]
81 pub fn add_native_token(mut self, native_token: NativeToken) -> Self {
82 self.native_tokens.push(native_token);
83 self
84 }
85
86 #[inline(always)]
88 pub fn with_native_tokens(mut self, native_tokens: impl IntoIterator<Item = NativeToken>) -> Self {
89 self.native_tokens = native_tokens.into_iter().collect();
90 self
91 }
92
93 #[inline(always)]
95 pub fn with_nft_id(mut self, nft_id: NftId) -> Self {
96 self.nft_id = nft_id;
97 self
98 }
99
100 #[inline(always)]
102 pub fn add_unlock_condition(mut self, unlock_condition: UnlockCondition) -> Self {
103 self.unlock_conditions.push(unlock_condition);
104 self
105 }
106
107 #[inline(always)]
109 pub fn with_unlock_conditions(mut self, unlock_conditions: impl IntoIterator<Item = UnlockCondition>) -> Self {
110 self.unlock_conditions = unlock_conditions.into_iter().collect();
111 self
112 }
113
114 pub fn replace_unlock_condition(mut self, unlock_condition: UnlockCondition) -> Result<Self, Error> {
116 match self
117 .unlock_conditions
118 .iter_mut()
119 .find(|u| u.kind() == unlock_condition.kind())
120 {
121 Some(u) => *u = unlock_condition,
122 None => return Err(Error::CannotReplaceMissingField),
123 }
124 Ok(self)
125 }
126
127 #[inline(always)]
129 pub fn add_feature(mut self, feature: Feature) -> Self {
130 self.features.push(feature);
131 self
132 }
133
134 #[inline(always)]
136 pub fn with_features(mut self, features: impl IntoIterator<Item = Feature>) -> Self {
137 self.features = features.into_iter().collect();
138 self
139 }
140
141 pub fn replace_feature(mut self, feature: Feature) -> Result<Self, Error> {
143 match self.features.iter_mut().find(|f| f.kind() == feature.kind()) {
144 Some(f) => *f = feature,
145 None => return Err(Error::CannotReplaceMissingField),
146 }
147 Ok(self)
148 }
149
150 #[inline(always)]
152 pub fn add_immutable_feature(mut self, immutable_feature: Feature) -> Self {
153 self.immutable_features.push(immutable_feature);
154 self
155 }
156
157 #[inline(always)]
159 pub fn with_immutable_features(mut self, immutable_features: impl IntoIterator<Item = Feature>) -> Self {
160 self.immutable_features = immutable_features.into_iter().collect();
161 self
162 }
163
164 pub fn replace_immutable_feature(mut self, immutable_feature: Feature) -> Result<Self, Error> {
166 match self
167 .immutable_features
168 .iter_mut()
169 .find(|f| f.kind() == immutable_feature.kind())
170 {
171 Some(f) => *f = immutable_feature,
172 None => return Err(Error::CannotReplaceMissingField),
173 }
174 Ok(self)
175 }
176
177 pub fn finish(self, token_supply: u64) -> Result<NftOutput, Error> {
179 let unlock_conditions = UnlockConditions::new(self.unlock_conditions)?;
180
181 verify_unlock_conditions(&unlock_conditions, &self.nft_id)?;
182
183 let features = Features::new(self.features)?;
184
185 verify_allowed_features(&features, NftOutput::ALLOWED_FEATURES)?;
186
187 let immutable_features = Features::new(self.immutable_features)?;
188
189 verify_allowed_features(&immutable_features, NftOutput::ALLOWED_IMMUTABLE_FEATURES)?;
190
191 let mut output = NftOutput {
192 amount: 1u64,
193 native_tokens: NativeTokens::new(self.native_tokens)?,
194 nft_id: self.nft_id,
195 unlock_conditions,
196 features,
197 immutable_features,
198 };
199
200 output.amount = match self.amount {
201 OutputBuilderAmount::Amount(amount) => amount,
202 OutputBuilderAmount::MinimumStorageDeposit(rent_structure) => {
203 Output::Nft(output.clone()).rent_cost(&rent_structure)
204 }
205 };
206
207 verify_output_amount::<true>(&output.amount, &token_supply)?;
208
209 Ok(output)
210 }
211
212 pub fn finish_output(self, token_supply: u64) -> Result<Output, Error> {
214 Ok(Output::Nft(self.finish(token_supply)?))
215 }
216}
217
218impl From<&NftOutput> for NftOutputBuilder {
219 fn from(output: &NftOutput) -> Self {
220 NftOutputBuilder {
221 amount: OutputBuilderAmount::Amount(output.amount),
222 native_tokens: output.native_tokens.to_vec(),
223 nft_id: output.nft_id,
224 unlock_conditions: output.unlock_conditions.to_vec(),
225 features: output.features.to_vec(),
226 immutable_features: output.immutable_features.to_vec(),
227 }
228 }
229}
230
231#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct NftOutput {
235 amount: u64,
237 native_tokens: NativeTokens,
239 nft_id: NftId,
241 unlock_conditions: UnlockConditions,
242 features: Features,
243 immutable_features: Features,
244}
245
246impl NftOutput {
247 pub const KIND: u8 = 6;
249 pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS
251 .union(UnlockConditionFlags::STORAGE_DEPOSIT_RETURN)
252 .union(UnlockConditionFlags::TIMELOCK)
253 .union(UnlockConditionFlags::EXPIRATION);
254 pub const ALLOWED_FEATURES: FeatureFlags = FeatureFlags::SENDER
256 .union(FeatureFlags::METADATA)
257 .union(FeatureFlags::TAG);
258 pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags = FeatureFlags::ISSUER.union(FeatureFlags::METADATA);
260
261 #[inline(always)]
263 pub fn new_with_amount(amount: u64, nft_id: NftId, token_supply: u64) -> Result<Self, Error> {
264 NftOutputBuilder::new_with_amount(amount, nft_id)?.finish(token_supply)
265 }
266
267 #[inline(always)]
270 pub fn new_with_minimum_storage_deposit(
271 nft_id: NftId,
272 rent_structure: RentStructure,
273 token_supply: u64,
274 ) -> Result<Self, Error> {
275 NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, nft_id)?.finish(token_supply)
276 }
277
278 #[inline(always)]
280 pub fn build_with_amount(amount: u64, nft_id: NftId) -> Result<NftOutputBuilder, Error> {
281 NftOutputBuilder::new_with_amount(amount, nft_id)
282 }
283
284 #[inline(always)]
287 pub fn build_with_minimum_storage_deposit(
288 rent_structure: RentStructure,
289 nft_id: NftId,
290 ) -> Result<NftOutputBuilder, Error> {
291 NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, nft_id)
292 }
293
294 #[inline(always)]
296 pub fn amount(&self) -> u64 {
297 self.amount
298 }
299
300 #[inline(always)]
302 pub fn native_tokens(&self) -> &NativeTokens {
303 &self.native_tokens
304 }
305
306 #[inline(always)]
308 pub fn nft_id(&self) -> &NftId {
309 &self.nft_id
310 }
311
312 #[inline(always)]
314 pub fn unlock_conditions(&self) -> &UnlockConditions {
315 &self.unlock_conditions
316 }
317
318 #[inline(always)]
320 pub fn features(&self) -> &Features {
321 &self.features
322 }
323
324 #[inline(always)]
326 pub fn immutable_features(&self) -> &Features {
327 &self.immutable_features
328 }
329
330 #[inline(always)]
332 pub fn address(&self) -> &Address {
333 self.unlock_conditions
335 .address()
336 .map(|unlock_condition| unlock_condition.address())
337 .unwrap()
338 }
339
340 #[inline(always)]
342 pub fn chain_id(&self) -> ChainId {
343 ChainId::Nft(self.nft_id)
344 }
345
346 pub fn unlock(
348 &self,
349 output_id: &OutputId,
350 unlock: &Unlock,
351 inputs: &[(OutputId, &Output)],
352 context: &mut ValidationContext,
353 ) -> Result<(), ConflictReason> {
354 self.unlock_conditions()
355 .locked_address(self.address(), context.milestone_timestamp)
356 .unlock(unlock, inputs, context)?;
357
358 let nft_id = if self.nft_id().is_null() {
359 NftId::from(*output_id)
360 } else {
361 *self.nft_id()
362 };
363
364 context
365 .unlocked_addresses
366 .insert(Address::from(NftAddress::from(nft_id)));
367
368 Ok(())
369 }
370}
371
372impl StateTransitionVerifier for NftOutput {
373 fn creation(next_state: &Self, context: &ValidationContext) -> Result<(), StateTransitionError> {
374 if !next_state.nft_id.is_null() {
375 return Err(StateTransitionError::NonZeroCreatedId);
376 }
377
378 if let Some(issuer) = next_state.immutable_features().issuer() {
379 if !context.unlocked_addresses.contains(issuer.address()) {
380 return Err(StateTransitionError::IssuerNotUnlocked);
381 }
382 }
383
384 Ok(())
385 }
386
387 fn transition(
388 current_state: &Self,
389 next_state: &Self,
390 _context: &ValidationContext,
391 ) -> Result<(), StateTransitionError> {
392 if current_state.immutable_features != next_state.immutable_features {
393 return Err(StateTransitionError::MutatedImmutableField);
394 }
395
396 Ok(())
397 }
398
399 fn destruction(_current_state: &Self, _context: &ValidationContext) -> Result<(), StateTransitionError> {
400 Ok(())
401 }
402}
403
404impl Packable for NftOutput {
405 type UnpackError = Error;
406 type UnpackVisitor = ProtocolParameters;
407
408 fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
409 self.amount.pack(packer)?;
410 self.native_tokens.pack(packer)?;
411 self.nft_id.pack(packer)?;
412 self.unlock_conditions.pack(packer)?;
413 self.features.pack(packer)?;
414 self.immutable_features.pack(packer)?;
415
416 Ok(())
417 }
418
419 fn unpack<U: Unpacker, const VERIFY: bool>(
420 unpacker: &mut U,
421 visitor: &Self::UnpackVisitor,
422 ) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
423 let amount = u64::unpack::<_, VERIFY>(unpacker, &()).coerce()?;
424
425 verify_output_amount::<VERIFY>(&amount, &visitor.token_supply()).map_err(UnpackError::Packable)?;
426
427 let native_tokens = NativeTokens::unpack::<_, VERIFY>(unpacker, &())?;
428 let nft_id = NftId::unpack::<_, VERIFY>(unpacker, &()).coerce()?;
429 let unlock_conditions = UnlockConditions::unpack::<_, VERIFY>(unpacker, visitor)?;
430
431 if VERIFY {
432 verify_unlock_conditions(&unlock_conditions, &nft_id).map_err(UnpackError::Packable)?;
433 }
434
435 let features = Features::unpack::<_, VERIFY>(unpacker, &())?;
436
437 if VERIFY {
438 verify_allowed_features(&features, NftOutput::ALLOWED_FEATURES).map_err(UnpackError::Packable)?;
439 }
440
441 let immutable_features = Features::unpack::<_, VERIFY>(unpacker, &())?;
442
443 if VERIFY {
444 verify_allowed_features(&immutable_features, NftOutput::ALLOWED_IMMUTABLE_FEATURES)
445 .map_err(UnpackError::Packable)?;
446 }
447
448 Ok(Self {
449 amount,
450 native_tokens,
451 nft_id,
452 unlock_conditions,
453 features,
454 immutable_features,
455 })
456 }
457}
458
459fn verify_unlock_conditions(unlock_conditions: &UnlockConditions, nft_id: &NftId) -> Result<(), Error> {
460 if let Some(unlock_condition) = unlock_conditions.address() {
461 if let Address::Nft(nft_address) = unlock_condition.address() {
462 if nft_address.nft_id() == nft_id {
463 return Err(Error::SelfDepositNft(*nft_id));
464 }
465 }
466 } else {
467 return Err(Error::MissingAddressUnlockCondition);
468 }
469
470 verify_allowed_unlock_conditions(unlock_conditions, NftOutput::ALLOWED_UNLOCK_CONDITIONS)
471}
472
473#[cfg(feature = "dto")]
474#[allow(missing_docs)]
475pub mod dto {
476 use serde::{Deserialize, Serialize};
477
478 use super::*;
479 use crate::{
480 error::dto::DtoError,
481 output::{
482 dto::OutputBuilderAmountDto, feature::dto::FeatureDto, native_token::dto::NativeTokenDto,
483 nft_id::dto::NftIdDto, unlock_condition::dto::UnlockConditionDto,
484 },
485 };
486
487 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
489 pub struct NftOutputDto {
490 #[serde(rename = "type")]
491 pub kind: u8,
492 pub amount: String,
494 #[serde(rename = "nativeTokens", skip_serializing_if = "Vec::is_empty", default)]
496 pub native_tokens: Vec<NativeTokenDto>,
497 #[serde(rename = "nftId")]
499 pub nft_id: NftIdDto,
500 #[serde(rename = "unlockConditions")]
501 pub unlock_conditions: Vec<UnlockConditionDto>,
502 #[serde(skip_serializing_if = "Vec::is_empty", default)]
503 pub features: Vec<FeatureDto>,
504 #[serde(rename = "immutableFeatures", skip_serializing_if = "Vec::is_empty", default)]
505 pub immutable_features: Vec<FeatureDto>,
506 }
507
508 impl From<&NftOutput> for NftOutputDto {
509 fn from(value: &NftOutput) -> Self {
510 Self {
511 kind: NftOutput::KIND,
512 amount: value.amount().to_string(),
513 native_tokens: value.native_tokens().iter().map(Into::into).collect::<_>(),
514 nft_id: NftIdDto(value.nft_id().to_string()),
515 unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(),
516 features: value.features().iter().map(Into::into).collect::<_>(),
517 immutable_features: value.immutable_features().iter().map(Into::into).collect::<_>(),
518 }
519 }
520 }
521
522 impl NftOutput {
523 pub fn try_from_dto(value: &NftOutputDto, token_supply: u64) -> Result<NftOutput, DtoError> {
524 let mut builder = NftOutputBuilder::new_with_amount(
525 value
526 .amount
527 .parse::<u64>()
528 .map_err(|_| DtoError::InvalidField("amount"))?,
529 (&value.nft_id).try_into()?,
530 )?;
531
532 for t in &value.native_tokens {
533 builder = builder.add_native_token(t.try_into()?);
534 }
535
536 for u in &value.unlock_conditions {
537 builder = builder.add_unlock_condition(UnlockCondition::try_from_dto(u, token_supply)?);
538 }
539
540 for b in &value.features {
541 builder = builder.add_feature(b.try_into()?);
542 }
543
544 for b in &value.immutable_features {
545 builder = builder.add_immutable_feature(b.try_into()?);
546 }
547
548 Ok(builder.finish(token_supply)?)
549 }
550
551 pub fn try_from_dtos(
552 amount: OutputBuilderAmountDto,
553 native_tokens: Option<Vec<NativeTokenDto>>,
554 nft_id: &NftIdDto,
555 unlock_conditions: Vec<UnlockConditionDto>,
556 features: Option<Vec<FeatureDto>>,
557 immutable_features: Option<Vec<FeatureDto>>,
558 token_supply: u64,
559 ) -> Result<NftOutput, DtoError> {
560 let nft_id = NftId::try_from(nft_id)?;
561
562 let mut builder = match amount {
563 OutputBuilderAmountDto::Amount(amount) => NftOutputBuilder::new_with_amount(
564 amount.parse().map_err(|_| DtoError::InvalidField("amount"))?,
565 nft_id,
566 )?,
567 OutputBuilderAmountDto::MinimumStorageDeposit(rent_structure) => {
568 NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, nft_id)?
569 }
570 };
571
572 if let Some(native_tokens) = native_tokens {
573 let native_tokens = native_tokens
574 .iter()
575 .map(NativeToken::try_from)
576 .collect::<Result<Vec<NativeToken>, DtoError>>()?;
577 builder = builder.with_native_tokens(native_tokens);
578 }
579
580 let unlock_conditions = unlock_conditions
581 .iter()
582 .map(|u| UnlockCondition::try_from_dto(u, token_supply))
583 .collect::<Result<Vec<UnlockCondition>, DtoError>>()?;
584 builder = builder.with_unlock_conditions(unlock_conditions);
585
586 if let Some(features) = features {
587 let features = features
588 .iter()
589 .map(Feature::try_from)
590 .collect::<Result<Vec<Feature>, DtoError>>()?;
591 builder = builder.with_features(features);
592 }
593
594 if let Some(immutable_features) = immutable_features {
595 let immutable_features = immutable_features
596 .iter()
597 .map(Feature::try_from)
598 .collect::<Result<Vec<Feature>, DtoError>>()?;
599 builder = builder.with_immutable_features(immutable_features);
600 }
601
602 Ok(builder.finish(token_supply)?)
603 }
604 }
605}