use crate::MAX_TX_EXECUTION_CYCLES;
use crate::asset::AssetAmount;
use crate::block::FeeParameters;
use crate::errors::AssetError;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TransactionFeeError {
#[error("transaction fee inputs require a non-zero total cycle count")]
ZeroTotalCycles,
#[error("total cycle count {0} exceeds the maximum of {MAX_TX_EXECUTION_CYCLES} cycles")]
TotalCyclesExceedsMax(u32),
#[error("computed fee exceeds the maximum asset amount")]
FeeExceedsMaxAssetAmount(#[source] AssetError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransactionFee {
log_verification_cycles: u32,
}
impl TransactionFee {
pub fn new(total_cycles: u32) -> Result<Self, TransactionFeeError> {
if total_cycles == 0 {
return Err(TransactionFeeError::ZeroTotalCycles);
}
if total_cycles > MAX_TX_EXECUTION_CYCLES {
return Err(TransactionFeeError::TotalCyclesExceedsMax(total_cycles));
}
Ok(Self {
log_verification_cycles: total_cycles.ilog2() + 1,
})
}
pub fn log_verification_cycles(&self) -> u32 {
self.log_verification_cycles
}
pub fn with_safety_margin(self, extra_verification_cycles: u32) -> Self {
Self {
log_verification_cycles: self
.log_verification_cycles
.saturating_add(extra_verification_cycles),
}
}
pub fn compute_fee(
&self,
fee_parameters: &FeeParameters,
) -> Result<AssetAmount, TransactionFeeError> {
let fee_amount = u64::from(fee_parameters.verification_base_fee())
* u64::from(self.log_verification_cycles);
AssetAmount::new(fee_amount).map_err(TransactionFeeError::FeeExceedsMaxAssetAmount)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::account::AccountId;
use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
fn fee_parameters(verification_base_fee: u32) -> FeeParameters {
let fee_faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
.expect("testing faucet ID should be valid");
FeeParameters::new(fee_faucet_id, verification_base_fee)
}
#[test]
fn log_verification_cycles_formula() {
let log_verification_cycles = |total_cycles: u32| {
TransactionFee::new(total_cycles).unwrap().log_verification_cycles()
};
assert_eq!(log_verification_cycles(1), 1);
assert_eq!(log_verification_cycles(2), 2);
assert_eq!(log_verification_cycles(3), 2);
assert_eq!(log_verification_cycles(4), 3);
assert_eq!(log_verification_cycles(65_536), 17);
assert_eq!(log_verification_cycles(MAX_TX_EXECUTION_CYCLES), 30);
}
#[test]
fn zero_cycles_are_rejected() {
assert!(matches!(TransactionFee::new(0), Err(TransactionFeeError::ZeroTotalCycles)));
}
#[test]
fn cycles_above_the_kernel_maximum_are_rejected() {
assert!(matches!(
TransactionFee::new(MAX_TX_EXECUTION_CYCLES + 1),
Err(TransactionFeeError::TotalCyclesExceedsMax(_))
));
}
#[test]
fn compute_fee_does_not_wrap_at_the_maximal_base_fee() {
let fee = TransactionFee::new(MAX_TX_EXECUTION_CYCLES)
.unwrap()
.compute_fee(&fee_parameters(u32::MAX))
.unwrap();
assert_eq!(fee.as_u64(), u64::from(u32::MAX) * 30);
}
#[test]
fn safety_margin_adds_verification_cycles() {
let fee = TransactionFee::new(1 << 16)
.unwrap()
.with_safety_margin(3)
.compute_fee(&fee_parameters(500))
.unwrap();
assert_eq!(fee.as_u64(), 500 * (17 + 3));
}
#[test]
fn fee_exceeding_max_asset_amount_is_rejected() {
let result = TransactionFee::new(1)
.unwrap()
.with_safety_margin(u32::MAX - 1)
.compute_fee(&fee_parameters(u32::MAX));
assert!(matches!(result, Err(TransactionFeeError::FeeExceedsMaxAssetAmount(_))));
}
}