mod migrated_funds_entry;
mod tail_transaction_hash;
use alloc::vec::Vec;
use core::ops::RangeInclusive;
use hashbrown::HashMap;
use iterator_sorted::is_unique_sorted;
use packable::{bounded::BoundedU16, prefix::VecPrefix, Packable, PackableExt};
pub use self::{migrated_funds_entry::MigratedFundsEntry, tail_transaction_hash::TailTransactionHash};
use crate::{
output::OUTPUT_COUNT_RANGE,
payload::{milestone::MilestoneIndex, Payload, TreasuryTransactionPayload},
protocol::ProtocolParameters,
Error,
};
const MIGRATED_FUNDS_ENTRY_RANGE: RangeInclusive<u16> = OUTPUT_COUNT_RANGE;
pub(crate) type ReceiptFundsCount =
BoundedU16<{ *MIGRATED_FUNDS_ENTRY_RANGE.start() }, { *MIGRATED_FUNDS_ENTRY_RANGE.end() }>;
#[derive(Clone, Debug, Eq, PartialEq, Packable)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[packable(unpack_error = Error)]
#[packable(unpack_visitor = ProtocolParameters)]
pub struct ReceiptMilestoneOption {
migrated_at: MilestoneIndex,
last: bool,
#[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidReceiptFundsCount(p.into())))]
#[packable(verify_with = verify_funds_packable)]
funds: VecPrefix<MigratedFundsEntry, ReceiptFundsCount>,
#[packable(verify_with = verify_transaction_packable)]
transaction: Payload,
}
impl ReceiptMilestoneOption {
pub const KIND: u8 = 0;
pub fn new(
migrated_at: MilestoneIndex,
last: bool,
funds: Vec<MigratedFundsEntry>,
transaction: TreasuryTransactionPayload,
token_supply: u64,
) -> Result<Self, Error> {
let funds = VecPrefix::<MigratedFundsEntry, ReceiptFundsCount>::try_from(funds)
.map_err(Error::InvalidReceiptFundsCount)?;
verify_funds::<true>(&funds, &token_supply)?;
Ok(Self {
migrated_at,
last,
funds,
transaction: transaction.into(),
})
}
pub fn migrated_at(&self) -> MilestoneIndex {
self.migrated_at
}
pub fn last(&self) -> bool {
self.last
}
pub fn funds(&self) -> &[MigratedFundsEntry] {
&self.funds
}
pub fn transaction(&self) -> &TreasuryTransactionPayload {
if let Payload::TreasuryTransaction(ref transaction) = self.transaction {
transaction
} else {
unreachable!()
}
}
pub fn amount(&self) -> u64 {
self.funds.iter().map(|f| f.amount()).sum()
}
}
fn verify_funds<const VERIFY: bool>(funds: &[MigratedFundsEntry], token_supply: &u64) -> Result<(), Error> {
if VERIFY {
if !is_unique_sorted(funds.iter().map(PackableExt::pack_to_vec)) {
return Err(Error::ReceiptFundsNotUniqueSorted);
}
let mut tail_transaction_hashes = HashMap::with_capacity(funds.len());
let mut funds_sum: u64 = 0;
for (index, funds) in funds.iter().enumerate() {
if let Some(previous) = tail_transaction_hashes.insert(funds.tail_transaction_hash().as_ref(), index) {
return Err(Error::TailTransactionHashNotUnique {
previous,
current: index,
});
}
funds_sum = funds_sum
.checked_add(funds.amount())
.ok_or_else(|| Error::InvalidReceiptFundsSum(funds_sum as u128 + funds.amount() as u128))?;
if funds_sum > *token_supply {
return Err(Error::InvalidReceiptFundsSum(funds_sum as u128));
}
}
}
Ok(())
}
fn verify_funds_packable<const VERIFY: bool>(
funds: &[MigratedFundsEntry],
protocol_parameters: &ProtocolParameters,
) -> Result<(), Error> {
verify_funds::<VERIFY>(funds, &protocol_parameters.token_supply())
}
fn verify_transaction<const VERIFY: bool>(transaction: &Payload) -> Result<(), Error> {
if VERIFY && !matches!(transaction, Payload::TreasuryTransaction(_)) {
Err(Error::InvalidPayloadKind(transaction.kind()))
} else {
Ok(())
}
}
fn verify_transaction_packable<const VERIFY: bool>(transaction: &Payload, _: &ProtocolParameters) -> Result<(), Error> {
verify_transaction::<VERIFY>(transaction)
}
#[cfg(feature = "dto")]
#[allow(missing_docs)]
pub mod dto {
use serde::{Deserialize, Serialize};
pub use super::migrated_funds_entry::dto::MigratedFundsEntryDto;
use super::*;
use crate::{
error::dto::DtoError,
payload::dto::{PayloadDto, TreasuryTransactionPayloadDto},
};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReceiptMilestoneOptionDto {
#[serde(rename = "type")]
pub kind: u8,
#[serde(rename = "migratedAt")]
pub migrated_at: u32,
pub funds: Vec<MigratedFundsEntryDto>,
pub transaction: PayloadDto,
#[serde(rename = "final")]
pub last: bool,
}
impl From<&ReceiptMilestoneOption> for ReceiptMilestoneOptionDto {
fn from(value: &ReceiptMilestoneOption) -> Self {
ReceiptMilestoneOptionDto {
kind: ReceiptMilestoneOption::KIND,
migrated_at: *value.migrated_at(),
last: value.last(),
funds: value.funds().iter().map(Into::into).collect::<_>(),
transaction: PayloadDto::TreasuryTransaction(
TreasuryTransactionPayloadDto::from(value.transaction()).into(),
),
}
}
}
impl ReceiptMilestoneOption {
pub fn try_from_dto(
value: &ReceiptMilestoneOptionDto,
token_supply: u64,
) -> Result<ReceiptMilestoneOption, DtoError> {
Ok(ReceiptMilestoneOption::new(
MilestoneIndex(value.migrated_at),
value.last,
value
.funds
.iter()
.map(|f| MigratedFundsEntry::try_from_dto(f, token_supply))
.collect::<Result<_, _>>()?,
if let PayloadDto::TreasuryTransaction(ref transaction) = value.transaction {
TreasuryTransactionPayload::try_from_dto(transaction.as_ref(), token_supply)?
} else {
return Err(DtoError::InvalidField("transaction"));
},
token_supply,
)?)
}
}
}