bee_block/payload/milestone/option/receipt/
mod.rs1mod 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#[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 pub const KIND: u8 = 0;
47
48 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 pub fn migrated_at(&self) -> MilestoneIndex {
72 self.migrated_at
73 }
74
75 pub fn last(&self) -> bool {
77 self.last
78 }
79
80 pub fn funds(&self) -> &[MigratedFundsEntry] {
82 &self.funds
83 }
84
85 pub fn transaction(&self) -> &TreasuryTransactionPayload {
88 if let Payload::TreasuryTransaction(ref transaction) = self.transaction {
89 transaction
90 } else {
91 unreachable!()
93 }
94 }
95
96 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 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 #[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}