bee_block/payload/milestone/option/receipt/
mod.rs

1// Copyright 2020-2021 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Module describing the receipt milestone option.
5
6mod migrated_funds_entry;
7mod tail_transaction_hash;
8
9use alloc::vec::Vec;
10use core::ops::RangeInclusive;
11
12use hashbrown::HashMap;
13use iterator_sorted::is_unique_sorted;
14use packable::{bounded::BoundedU16, prefix::VecPrefix, Packable, PackableExt};
15
16pub use self::{migrated_funds_entry::MigratedFundsEntry, tail_transaction_hash::TailTransactionHash};
17use crate::{
18    output::OUTPUT_COUNT_RANGE,
19    payload::{milestone::MilestoneIndex, Payload, TreasuryTransactionPayload},
20    protocol::ProtocolParameters,
21    Error,
22};
23
24const MIGRATED_FUNDS_ENTRY_RANGE: RangeInclusive<u16> = OUTPUT_COUNT_RANGE;
25
26pub(crate) type ReceiptFundsCount =
27    BoundedU16<{ *MIGRATED_FUNDS_ENTRY_RANGE.start() }, { *MIGRATED_FUNDS_ENTRY_RANGE.end() }>;
28
29/// Receipt is a listing of migrated funds.
30#[derive(Clone, Debug, Eq, PartialEq, Packable)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[packable(unpack_error = Error)]
33#[packable(unpack_visitor = ProtocolParameters)]
34pub struct ReceiptMilestoneOption {
35    migrated_at: MilestoneIndex,
36    last: bool,
37    #[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidReceiptFundsCount(p.into())))]
38    #[packable(verify_with = verify_funds_packable)]
39    funds: VecPrefix<MigratedFundsEntry, ReceiptFundsCount>,
40    #[packable(verify_with = verify_transaction_packable)]
41    transaction: Payload,
42}
43
44impl ReceiptMilestoneOption {
45    /// The milestone option kind of a [`ReceiptMilestoneOption`].
46    pub const KIND: u8 = 0;
47
48    /// Creates a new [`ReceiptMilestoneOption`].
49    pub fn new(
50        migrated_at: MilestoneIndex,
51        last: bool,
52        funds: Vec<MigratedFundsEntry>,
53        transaction: TreasuryTransactionPayload,
54        token_supply: u64,
55    ) -> Result<Self, Error> {
56        let funds = VecPrefix::<MigratedFundsEntry, ReceiptFundsCount>::try_from(funds)
57            .map_err(Error::InvalidReceiptFundsCount)?;
58
59        verify_funds::<true>(&funds, &token_supply)?;
60
61        Ok(Self {
62            migrated_at,
63            last,
64            funds,
65            transaction: transaction.into(),
66        })
67    }
68
69    /// Returns the milestone index at which the funds of a [`ReceiptMilestoneOption`] were migrated at in the legacy
70    /// network.
71    pub fn migrated_at(&self) -> MilestoneIndex {
72        self.migrated_at
73    }
74
75    /// Returns whether a [`ReceiptMilestoneOption`] is the final one for a given migrated at index.
76    pub fn last(&self) -> bool {
77        self.last
78    }
79
80    /// The funds which were migrated with a [`ReceiptMilestoneOption`].
81    pub fn funds(&self) -> &[MigratedFundsEntry] {
82        &self.funds
83    }
84
85    /// The [`TreasuryTransactionPayload`](crate::payload::treasury_transaction::TreasuryTransactionPayload) used to
86    /// fund the funds of a [`ReceiptMilestoneOption`].
87    pub fn transaction(&self) -> &TreasuryTransactionPayload {
88        if let Payload::TreasuryTransaction(ref transaction) = self.transaction {
89            transaction
90        } else {
91            // It has already been validated at construction that `transaction` is a `TreasuryTransactionPayload`.
92            unreachable!()
93        }
94    }
95
96    /// Returns the sum of all [`MigratedFundsEntry`] items within a [`ReceiptMilestoneOption`].
97    pub fn amount(&self) -> u64 {
98        self.funds.iter().map(|f| f.amount()).sum()
99    }
100}
101
102fn verify_funds<const VERIFY: bool>(funds: &[MigratedFundsEntry], token_supply: &u64) -> Result<(), Error> {
103    if VERIFY {
104        // Funds must be lexicographically sorted and unique in their serialised forms.
105        if !is_unique_sorted(funds.iter().map(PackableExt::pack_to_vec)) {
106            return Err(Error::ReceiptFundsNotUniqueSorted);
107        }
108
109        let mut tail_transaction_hashes = HashMap::with_capacity(funds.len());
110        let mut funds_sum: u64 = 0;
111
112        for (index, funds) in funds.iter().enumerate() {
113            if let Some(previous) = tail_transaction_hashes.insert(funds.tail_transaction_hash().as_ref(), index) {
114                return Err(Error::TailTransactionHashNotUnique {
115                    previous,
116                    current: index,
117                });
118            }
119
120            funds_sum = funds_sum
121                .checked_add(funds.amount())
122                .ok_or_else(|| Error::InvalidReceiptFundsSum(funds_sum as u128 + funds.amount() as u128))?;
123
124            if funds_sum > *token_supply {
125                return Err(Error::InvalidReceiptFundsSum(funds_sum as u128));
126            }
127        }
128    }
129
130    Ok(())
131}
132
133fn verify_funds_packable<const VERIFY: bool>(
134    funds: &[MigratedFundsEntry],
135    protocol_parameters: &ProtocolParameters,
136) -> Result<(), Error> {
137    verify_funds::<VERIFY>(funds, &protocol_parameters.token_supply())
138}
139
140fn verify_transaction<const VERIFY: bool>(transaction: &Payload) -> Result<(), Error> {
141    if VERIFY && !matches!(transaction, Payload::TreasuryTransaction(_)) {
142        Err(Error::InvalidPayloadKind(transaction.kind()))
143    } else {
144        Ok(())
145    }
146}
147
148fn verify_transaction_packable<const VERIFY: bool>(transaction: &Payload, _: &ProtocolParameters) -> Result<(), Error> {
149    verify_transaction::<VERIFY>(transaction)
150}
151
152#[cfg(feature = "dto")]
153#[allow(missing_docs)]
154pub mod dto {
155    use serde::{Deserialize, Serialize};
156
157    pub use super::migrated_funds_entry::dto::MigratedFundsEntryDto;
158    use super::*;
159    use crate::{
160        error::dto::DtoError,
161        payload::dto::{PayloadDto, TreasuryTransactionPayloadDto},
162    };
163
164    ///
165    #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
166    pub struct ReceiptMilestoneOptionDto {
167        #[serde(rename = "type")]
168        pub kind: u8,
169        #[serde(rename = "migratedAt")]
170        pub migrated_at: u32,
171        pub funds: Vec<MigratedFundsEntryDto>,
172        pub transaction: PayloadDto,
173        #[serde(rename = "final")]
174        pub last: bool,
175    }
176
177    impl From<&ReceiptMilestoneOption> for ReceiptMilestoneOptionDto {
178        fn from(value: &ReceiptMilestoneOption) -> Self {
179            ReceiptMilestoneOptionDto {
180                kind: ReceiptMilestoneOption::KIND,
181                migrated_at: *value.migrated_at(),
182                last: value.last(),
183                funds: value.funds().iter().map(Into::into).collect::<_>(),
184                transaction: PayloadDto::TreasuryTransaction(
185                    TreasuryTransactionPayloadDto::from(value.transaction()).into(),
186                ),
187            }
188        }
189    }
190
191    impl ReceiptMilestoneOption {
192        pub fn try_from_dto(
193            value: &ReceiptMilestoneOptionDto,
194            token_supply: u64,
195        ) -> Result<ReceiptMilestoneOption, DtoError> {
196            Ok(ReceiptMilestoneOption::new(
197                MilestoneIndex(value.migrated_at),
198                value.last,
199                value
200                    .funds
201                    .iter()
202                    .map(|f| MigratedFundsEntry::try_from_dto(f, token_supply))
203                    .collect::<Result<_, _>>()?,
204                if let PayloadDto::TreasuryTransaction(ref transaction) = value.transaction {
205                    TreasuryTransactionPayload::try_from_dto(transaction.as_ref(), token_supply)?
206                } else {
207                    return Err(DtoError::InvalidField("transaction"));
208                },
209                token_supply,
210            )?)
211        }
212    }
213}