1mod alias_id;
5
6mod chain_id;
7
8mod foundry_id;
9mod inputs_commitment;
10mod native_token;
11mod nft_id;
12mod output_id;
13mod rent;
14mod state_transition;
15mod token_id;
16mod token_scheme;
17mod treasury;
18
19pub mod alias;
21pub mod basic;
23pub mod feature;
25pub mod foundry;
27pub mod nft;
29pub mod unlock_condition;
31
32use core::ops::RangeInclusive;
33
34use derive_more::From;
35use packable::{
36 error::{UnpackError, UnpackErrorExt},
37 packer::Packer,
38 unpacker::Unpacker,
39 Packable, PackableExt,
40};
41
42pub(crate) use self::{
43 alias::StateMetadataLength,
44 feature::{MetadataFeatureLength, TagFeatureLength},
45 native_token::NativeTokenCount,
46 output_id::OutputIndex,
47 unlock_condition::AddressUnlockCondition,
48};
49pub use self::{
50 alias::{AliasOutput, AliasOutputBuilder},
51 alias_id::AliasId,
52 basic::{BasicOutput, BasicOutputBuilder},
53 chain_id::ChainId,
54 feature::{Feature, Features},
55 foundry::{FoundryOutput, FoundryOutputBuilder},
56 foundry_id::FoundryId,
57 inputs_commitment::InputsCommitment,
58 native_token::{NativeToken, NativeTokens, NativeTokensBuilder},
59 nft::{NftOutput, NftOutputBuilder},
60 nft_id::NftId,
61 output_id::OutputId,
62 rent::{Rent, RentStructure, RentStructureBuilder},
63 state_transition::{StateTransitionError, StateTransitionVerifier},
64 token_id::TokenId,
65 token_scheme::{SimpleTokenScheme, TokenScheme},
66 treasury::TreasuryOutput,
67 unlock_condition::{UnlockCondition, UnlockConditions},
68};
69use crate::{address::Address, protocol::ProtocolParameters, semantic::ValidationContext, Error};
70
71pub const OUTPUT_COUNT_MAX: u16 = 128;
73pub const OUTPUT_COUNT_RANGE: RangeInclusive<u16> = 1..=OUTPUT_COUNT_MAX; pub const OUTPUT_INDEX_MAX: u16 = OUTPUT_COUNT_MAX - 1; pub const OUTPUT_INDEX_RANGE: RangeInclusive<u16> = 0..=OUTPUT_INDEX_MAX; #[derive(Clone)]
81pub(crate) enum OutputBuilderAmount {
82 Amount(u64),
83 MinimumStorageDeposit(RentStructure),
84}
85
86#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, From)]
88#[cfg_attr(
89 feature = "serde",
90 derive(serde::Serialize, serde::Deserialize),
91 serde(tag = "type", content = "data")
92)]
93pub enum Output {
94 Treasury(TreasuryOutput),
96 Basic(BasicOutput),
98 Alias(AliasOutput),
100 Foundry(FoundryOutput),
102 Nft(NftOutput),
104}
105
106impl Output {
107 pub const AMOUNT_MIN: u64 = 1;
109
110 pub fn kind(&self) -> u8 {
112 match self {
113 Self::Treasury(_) => TreasuryOutput::KIND,
114 Self::Basic(_) => BasicOutput::KIND,
115 Self::Alias(_) => AliasOutput::KIND,
116 Self::Foundry(_) => FoundryOutput::KIND,
117 Self::Nft(_) => NftOutput::KIND,
118 }
119 }
120
121 pub fn amount(&self) -> u64 {
123 match self {
124 Self::Treasury(output) => output.amount(),
125 Self::Basic(output) => output.amount(),
126 Self::Alias(output) => output.amount(),
127 Self::Foundry(output) => output.amount(),
128 Self::Nft(output) => output.amount(),
129 }
130 }
131
132 pub fn native_tokens(&self) -> Option<&NativeTokens> {
134 match self {
135 Self::Treasury(_) => None,
136 Self::Basic(output) => Some(output.native_tokens()),
137 Self::Alias(output) => Some(output.native_tokens()),
138 Self::Foundry(output) => Some(output.native_tokens()),
139 Self::Nft(output) => Some(output.native_tokens()),
140 }
141 }
142
143 pub fn unlock_conditions(&self) -> Option<&UnlockConditions> {
145 match self {
146 Self::Treasury(_) => None,
147 Self::Basic(output) => Some(output.unlock_conditions()),
148 Self::Alias(output) => Some(output.unlock_conditions()),
149 Self::Foundry(output) => Some(output.unlock_conditions()),
150 Self::Nft(output) => Some(output.unlock_conditions()),
151 }
152 }
153
154 pub fn features(&self) -> Option<&Features> {
156 match self {
157 Self::Treasury(_) => None,
158 Self::Basic(output) => Some(output.features()),
159 Self::Alias(output) => Some(output.features()),
160 Self::Foundry(output) => Some(output.features()),
161 Self::Nft(output) => Some(output.features()),
162 }
163 }
164
165 pub fn immutable_features(&self) -> Option<&Features> {
167 match self {
168 Self::Treasury(_) => None,
169 Self::Basic(_) => None,
170 Self::Alias(output) => Some(output.immutable_features()),
171 Self::Foundry(output) => Some(output.immutable_features()),
172 Self::Nft(output) => Some(output.immutable_features()),
173 }
174 }
175
176 pub fn chain_id(&self) -> Option<ChainId> {
178 match self {
179 Self::Treasury(_) => None,
180 Self::Basic(_) => None,
181 Self::Alias(output) => Some(output.chain_id()),
182 Self::Foundry(output) => Some(output.chain_id()),
183 Self::Nft(output) => Some(output.chain_id()),
184 }
185 }
186
187 pub fn verify_state_transition(
189 current_state: Option<&Output>,
190 next_state: Option<&Output>,
191 context: &ValidationContext,
192 ) -> Result<(), StateTransitionError> {
193 match (current_state, next_state) {
194 (None, Some(Output::Alias(next_state))) => AliasOutput::creation(next_state, context),
196 (None, Some(Output::Foundry(next_state))) => FoundryOutput::creation(next_state, context),
197 (None, Some(Output::Nft(next_state))) => NftOutput::creation(next_state, context),
198
199 (Some(Output::Alias(current_state)), Some(Output::Alias(next_state))) => {
201 AliasOutput::transition(current_state, next_state, context)
202 }
203 (Some(Output::Foundry(current_state)), Some(Output::Foundry(next_state))) => {
204 FoundryOutput::transition(current_state, next_state, context)
205 }
206 (Some(Output::Nft(current_state)), Some(Output::Nft(next_state))) => {
207 NftOutput::transition(current_state, next_state, context)
208 }
209
210 (Some(Output::Alias(current_state)), None) => AliasOutput::destruction(current_state, context),
212 (Some(Output::Foundry(current_state)), None) => FoundryOutput::destruction(current_state, context),
213 (Some(Output::Nft(current_state)), None) => NftOutput::destruction(current_state, context),
214
215 _ => Err(StateTransitionError::UnsupportedStateTransition),
217 }
218 }
219
220 pub fn verify_storage_deposit(&self, rent_structure: RentStructure, token_supply: u64) -> Result<(), Error> {
225 let required_output_amount = self.rent_cost(&rent_structure);
226
227 if self.amount() < required_output_amount {
228 return Err(Error::InsufficientStorageDepositAmount {
229 amount: self.amount(),
230 required: required_output_amount,
231 });
232 }
233
234 if let Some(return_condition) = self
235 .unlock_conditions()
236 .and_then(UnlockConditions::storage_deposit_return)
237 {
238 if return_condition.amount() > self.amount() {
241 return Err(Error::StorageDepositReturnExceedsOutputAmount {
242 deposit: return_condition.amount(),
243 amount: self.amount(),
244 });
245 }
246
247 let minimum_deposit =
248 minimum_storage_deposit(return_condition.return_address(), rent_structure, token_supply);
249
250 if return_condition.amount() < minimum_deposit {
252 return Err(Error::InsufficientStorageDepositReturnAmount {
253 deposit: return_condition.amount(),
254 required: minimum_deposit,
255 });
256 }
257 }
258
259 Ok(())
260 }
261}
262
263impl Packable for Output {
264 type UnpackError = Error;
265 type UnpackVisitor = ProtocolParameters;
266
267 fn pack<P: Packer>(&self, packer: &mut P) -> Result<(), P::Error> {
268 match self {
269 Output::Treasury(output) => {
270 TreasuryOutput::KIND.pack(packer)?;
271 output.pack(packer)
272 }
273 Output::Basic(output) => {
274 BasicOutput::KIND.pack(packer)?;
275 output.pack(packer)
276 }
277 Output::Alias(output) => {
278 AliasOutput::KIND.pack(packer)?;
279 output.pack(packer)
280 }
281 Output::Foundry(output) => {
282 FoundryOutput::KIND.pack(packer)?;
283 output.pack(packer)
284 }
285 Output::Nft(output) => {
286 NftOutput::KIND.pack(packer)?;
287 output.pack(packer)
288 }
289 }?;
290
291 Ok(())
292 }
293
294 fn unpack<U: Unpacker, const VERIFY: bool>(
295 unpacker: &mut U,
296 visitor: &Self::UnpackVisitor,
297 ) -> Result<Self, UnpackError<Self::UnpackError, U::Error>> {
298 Ok(match u8::unpack::<_, VERIFY>(unpacker, &()).coerce()? {
299 TreasuryOutput::KIND => Output::from(TreasuryOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?),
300 BasicOutput::KIND => Output::from(BasicOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?),
301 AliasOutput::KIND => Output::from(AliasOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?),
302 FoundryOutput::KIND => Output::from(FoundryOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?),
303 NftOutput::KIND => Output::from(NftOutput::unpack::<_, VERIFY>(unpacker, visitor).coerce()?),
304 k => return Err(Error::InvalidOutputKind(k)).map_err(UnpackError::Packable),
305 })
306 }
307}
308
309impl Rent for Output {
310 fn weighted_bytes(&self, rent_structure: &RentStructure) -> u64 {
311 self.packed_len() as u64 * rent_structure.v_byte_factor_data as u64
312 }
313}
314
315pub(crate) fn verify_output_amount<const VERIFY: bool>(amount: &u64, token_supply: &u64) -> Result<(), Error> {
316 if VERIFY && (*amount < Output::AMOUNT_MIN || amount > token_supply) {
317 Err(Error::InvalidOutputAmount(*amount))
318 } else {
319 Ok(())
320 }
321}
322
323pub(crate) fn verify_output_amount_packable<const VERIFY: bool>(
324 amount: &u64,
325 protocol_parameters: &ProtocolParameters,
326) -> Result<(), Error> {
327 verify_output_amount::<VERIFY>(amount, &protocol_parameters.token_supply())
328}
329
330fn minimum_storage_deposit(address: &Address, rent_structure: RentStructure, token_supply: u64) -> u64 {
333 let address_condition = UnlockCondition::Address(AddressUnlockCondition::new(*address));
334 BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure)
337 .unwrap()
338 .add_unlock_condition(address_condition)
339 .finish(token_supply)
340 .unwrap()
341 .amount()
342}
343
344#[cfg(feature = "dto")]
345#[allow(missing_docs)]
346pub mod dto {
347 use serde::{Deserialize, Serialize, Serializer};
348 use serde_json::Value;
349
350 use super::*;
351 pub use super::{
352 alias::dto::AliasOutputDto,
353 alias_id::dto::AliasIdDto,
354 basic::dto::BasicOutputDto,
355 foundry::dto::FoundryOutputDto,
356 native_token::dto::NativeTokenDto,
357 nft::dto::NftOutputDto,
358 nft_id::dto::NftIdDto,
359 token_id::dto::TokenIdDto,
360 token_scheme::dto::{SimpleTokenSchemeDto, TokenSchemeDto},
361 treasury::dto::TreasuryOutputDto,
362 };
363 use crate::error::dto::DtoError;
364
365 #[derive(Clone, Debug, Deserialize, From)]
366 pub enum OutputBuilderAmountDto {
367 Amount(String),
368 MinimumStorageDeposit(RentStructure),
369 }
370
371 #[derive(Clone, Debug, Eq, PartialEq, From)]
373 pub enum OutputDto {
374 Treasury(TreasuryOutputDto),
375 Basic(BasicOutputDto),
376 Alias(AliasOutputDto),
377 Foundry(FoundryOutputDto),
378 Nft(NftOutputDto),
379 }
380
381 impl From<&Output> for OutputDto {
382 fn from(value: &Output) -> Self {
383 match value {
384 Output::Treasury(o) => OutputDto::Treasury(o.into()),
385 Output::Basic(o) => OutputDto::Basic(o.into()),
386 Output::Alias(o) => OutputDto::Alias(o.into()),
387 Output::Foundry(o) => OutputDto::Foundry(o.into()),
388 Output::Nft(o) => OutputDto::Nft(o.into()),
389 }
390 }
391 }
392
393 impl Output {
394 pub fn try_from_dto(value: &OutputDto, token_supply: u64) -> Result<Output, DtoError> {
395 Ok(match value {
396 OutputDto::Treasury(o) => Output::Treasury(TreasuryOutput::try_from_dto(o, token_supply)?),
397 OutputDto::Basic(o) => Output::Basic(BasicOutput::try_from_dto(o, token_supply)?),
398 OutputDto::Alias(o) => Output::Alias(AliasOutput::try_from_dto(o, token_supply)?),
399 OutputDto::Foundry(o) => Output::Foundry(FoundryOutput::try_from_dto(o, token_supply)?),
400 OutputDto::Nft(o) => Output::Nft(NftOutput::try_from_dto(o, token_supply)?),
401 })
402 }
403 }
404
405 impl<'de> Deserialize<'de> for OutputDto {
406 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
407 let value = Value::deserialize(d)?;
408 Ok(
409 match value
410 .get("type")
411 .and_then(Value::as_u64)
412 .ok_or_else(|| serde::de::Error::custom("invalid output type"))? as u8
413 {
414 TreasuryOutput::KIND => {
415 OutputDto::Treasury(TreasuryOutputDto::deserialize(value).map_err(|e| {
416 serde::de::Error::custom(format!("cannot deserialize treasury output: {}", e))
417 })?)
418 }
419 BasicOutput::KIND => OutputDto::Basic(
420 BasicOutputDto::deserialize(value)
421 .map_err(|e| serde::de::Error::custom(format!("cannot deserialize basic output: {}", e)))?,
422 ),
423 AliasOutput::KIND => OutputDto::Alias(
424 AliasOutputDto::deserialize(value)
425 .map_err(|e| serde::de::Error::custom(format!("cannot deserialize alias output: {}", e)))?,
426 ),
427 FoundryOutput::KIND => {
428 OutputDto::Foundry(FoundryOutputDto::deserialize(value).map_err(|e| {
429 serde::de::Error::custom(format!("cannot deserialize foundry output: {}", e))
430 })?)
431 }
432 NftOutput::KIND => OutputDto::Nft(
433 NftOutputDto::deserialize(value)
434 .map_err(|e| serde::de::Error::custom(format!("cannot deserialize NFT output: {}", e)))?,
435 ),
436 _ => return Err(serde::de::Error::custom("invalid output type")),
437 },
438 )
439 }
440 }
441
442 impl Serialize for OutputDto {
443 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
444 where
445 S: Serializer,
446 {
447 #[derive(Serialize)]
448 #[serde(untagged)]
449 enum OutputDto_<'a> {
450 T1(&'a TreasuryOutputDto),
451 T2(&'a BasicOutputDto),
452 T3(&'a AliasOutputDto),
453 T4(&'a FoundryOutputDto),
454 T5(&'a NftOutputDto),
455 }
456 #[derive(Serialize)]
457 struct TypedOutput<'a> {
458 #[serde(flatten)]
459 output: OutputDto_<'a>,
460 }
461 let output = match self {
462 OutputDto::Treasury(o) => TypedOutput {
463 output: OutputDto_::T1(o),
464 },
465 OutputDto::Basic(o) => TypedOutput {
466 output: OutputDto_::T2(o),
467 },
468 OutputDto::Alias(o) => TypedOutput {
469 output: OutputDto_::T3(o),
470 },
471 OutputDto::Foundry(o) => TypedOutput {
472 output: OutputDto_::T4(o),
473 },
474 OutputDto::Nft(o) => TypedOutput {
475 output: OutputDto_::T5(o),
476 },
477 };
478 output.serialize(serializer)
479 }
480 }
481}