use alloc::vec::Vec;
use core::fmt::{self, Display, Formatter};
#[cfg(feature = "datasize")]
use datasize::DataSize;
#[cfg(any(feature = "testing", test))]
use rand::Rng;
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(doc)]
use super::Transaction;
use super::{
serialization::CalltableSerializationEnvelope, InvalidTransaction, InvalidTransactionV1,
TransactionEntryPoint,
};
#[cfg(any(feature = "testing", test))]
use crate::testing::TestRng;
use crate::{
bytesrepr::{
Error::{self, Formatting},
FromBytes, ToBytes,
},
transaction::serialization::CalltableSerializationEnvelopeBuilder,
Digest,
};
#[cfg(any(feature = "std", test))]
use crate::{Chainspec, Gas, Motes, AUCTION_LANE_ID, MINT_LANE_ID, U512};
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "datasize", derive(DataSize))]
#[cfg_attr(
feature = "json-schema",
derive(JsonSchema),
schemars(description = "Pricing mode of a Transaction.")
)]
#[serde(deny_unknown_fields)]
pub enum PricingMode {
PaymentLimited {
payment_amount: u64,
gas_price_tolerance: u8,
standard_payment: bool,
},
Fixed {
additional_computation_factor: u8,
gas_price_tolerance: u8,
},
Prepaid {
receipt: Digest,
},
}
impl PricingMode {
#[cfg(any(feature = "testing", test))]
pub fn random(rng: &mut TestRng) -> Self {
match rng.gen_range(0..=2) {
0 => PricingMode::PaymentLimited {
payment_amount: rng.gen(),
gas_price_tolerance: 1,
standard_payment: true,
},
1 => PricingMode::Fixed {
gas_price_tolerance: rng.gen(),
additional_computation_factor: 1,
},
2 => PricingMode::Prepaid { receipt: rng.gen() },
_ => unreachable!(),
}
}
pub fn is_standard_payment(&self) -> bool {
match self {
PricingMode::PaymentLimited {
standard_payment, ..
} => *standard_payment,
PricingMode::Fixed { .. } => true,
PricingMode::Prepaid { .. } => true,
}
}
fn serialized_field_lengths(&self) -> Vec<usize> {
match self {
PricingMode::PaymentLimited {
payment_amount,
gas_price_tolerance,
standard_payment,
} => {
vec![
crate::bytesrepr::U8_SERIALIZED_LENGTH,
payment_amount.serialized_length(),
gas_price_tolerance.serialized_length(),
standard_payment.serialized_length(),
]
}
PricingMode::Fixed {
gas_price_tolerance,
additional_computation_factor,
} => {
vec![
crate::bytesrepr::U8_SERIALIZED_LENGTH,
gas_price_tolerance.serialized_length(),
additional_computation_factor.serialized_length(),
]
}
PricingMode::Prepaid { receipt } => {
vec![
crate::bytesrepr::U8_SERIALIZED_LENGTH,
receipt.serialized_length(),
]
}
}
}
#[cfg(any(feature = "std", test))]
pub fn gas_limit(
&self,
chainspec: &Chainspec,
entry_point: &TransactionEntryPoint,
lane_id: u8,
) -> Result<Gas, PricingModeError> {
let costs = chainspec.system_costs_config;
let gas = match self {
PricingMode::PaymentLimited { payment_amount, .. } => Gas::new(*payment_amount),
PricingMode::Fixed { .. } => {
let computation_limit = {
if lane_id == MINT_LANE_ID {
costs.mint_costs().transfer as u64
} else if lane_id == AUCTION_LANE_ID {
let amount = match entry_point {
TransactionEntryPoint::Call => {
return Err(PricingModeError::EntryPointCannotBeCall)
}
TransactionEntryPoint::Custom(_) | TransactionEntryPoint::Transfer => {
return Err(PricingModeError::EntryPointCannotBeCustom {
entry_point: entry_point.clone(),
});
}
TransactionEntryPoint::AddBid | TransactionEntryPoint::ActivateBid => {
costs.auction_costs().add_bid
}
TransactionEntryPoint::WithdrawBid => {
costs.auction_costs().withdraw_bid.into()
}
TransactionEntryPoint::Delegate => {
costs.auction_costs().delegate.into()
}
TransactionEntryPoint::Undelegate => {
costs.auction_costs().undelegate.into()
}
TransactionEntryPoint::Redelegate => {
costs.auction_costs().redelegate.into()
}
TransactionEntryPoint::ChangeBidPublicKey => {
costs.auction_costs().change_bid_public_key
}
TransactionEntryPoint::AddReservations => {
costs.auction_costs().add_reservations.into()
}
TransactionEntryPoint::CancelReservations => {
costs.auction_costs().cancel_reservations.into()
}
};
amount
} else {
chainspec.get_max_gas_limit_by_category(lane_id)
}
};
Gas::new(U512::from(computation_limit))
}
PricingMode::Prepaid { receipt } => {
return Err(PricingModeError::InvalidPricingMode {
price_mode: PricingMode::Prepaid { receipt: *receipt },
});
}
};
Ok(gas)
}
#[cfg(any(feature = "std", test))]
pub fn gas_cost(
&self,
chainspec: &Chainspec,
entry_point: &TransactionEntryPoint,
lane_id: u8,
gas_price: u8,
) -> Result<Motes, PricingModeError> {
let gas_limit = self.gas_limit(chainspec, entry_point, lane_id)?;
let motes = match self {
PricingMode::PaymentLimited { .. } | PricingMode::Fixed { .. } => {
Motes::from_gas(gas_limit, gas_price)
.ok_or(PricingModeError::UnableToCalculateGasCost)?
}
PricingMode::Prepaid { .. } => {
Motes::zero() }
};
Ok(motes)
}
pub fn additional_computation_factor(&self) -> u8 {
match self {
PricingMode::PaymentLimited { .. } => 0,
PricingMode::Fixed {
additional_computation_factor,
..
} => *additional_computation_factor,
PricingMode::Prepaid { .. } => 0,
}
}
}
pub enum PricingModeError {
EntryPointCannotBeCall,
EntryPointCannotBeCustom {
entry_point: TransactionEntryPoint,
},
InvalidPricingMode {
price_mode: PricingMode,
},
UnableToCalculateGasCost,
}
impl From<PricingModeError> for InvalidTransaction {
fn from(err: PricingModeError) -> Self {
InvalidTransaction::V1(err.into())
}
}
impl From<PricingModeError> for InvalidTransactionV1 {
fn from(err: PricingModeError) -> Self {
match err {
PricingModeError::EntryPointCannotBeCall => {
InvalidTransactionV1::EntryPointCannotBeCall
}
PricingModeError::EntryPointCannotBeCustom { entry_point } => {
InvalidTransactionV1::EntryPointCannotBeCustom { entry_point }
}
PricingModeError::InvalidPricingMode { price_mode } => {
InvalidTransactionV1::InvalidPricingMode { price_mode }
}
PricingModeError::UnableToCalculateGasCost => {
InvalidTransactionV1::UnableToCalculateGasCost
}
}
}
}
const TAG_FIELD_INDEX: u16 = 0;
const PAYMENT_LIMITED_VARIANT_TAG: u8 = 0;
const PAYMENT_LIMITED_PAYMENT_AMOUNT_INDEX: u16 = 1;
const PAYMENT_LIMITED_GAS_PRICE_TOLERANCE_INDEX: u16 = 2;
const PAYMENT_LIMITED_STANDARD_PAYMENT_INDEX: u16 = 3;
const FIXED_VARIANT_TAG: u8 = 1;
const FIXED_GAS_PRICE_TOLERANCE_INDEX: u16 = 1;
const FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX: u16 = 2;
const RESERVED_VARIANT_TAG: u8 = 2;
const RESERVED_RECEIPT_INDEX: u16 = 1;
impl ToBytes for PricingMode {
fn to_bytes(&self) -> Result<Vec<u8>, Error> {
match self {
PricingMode::PaymentLimited {
payment_amount,
gas_price_tolerance,
standard_payment,
} => CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
.add_field(TAG_FIELD_INDEX, &PAYMENT_LIMITED_VARIANT_TAG)?
.add_field(PAYMENT_LIMITED_PAYMENT_AMOUNT_INDEX, &payment_amount)?
.add_field(
PAYMENT_LIMITED_GAS_PRICE_TOLERANCE_INDEX,
&gas_price_tolerance,
)?
.add_field(PAYMENT_LIMITED_STANDARD_PAYMENT_INDEX, &standard_payment)?
.binary_payload_bytes(),
PricingMode::Fixed {
gas_price_tolerance,
additional_computation_factor,
} => CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
.add_field(TAG_FIELD_INDEX, &FIXED_VARIANT_TAG)?
.add_field(FIXED_GAS_PRICE_TOLERANCE_INDEX, &gas_price_tolerance)?
.add_field(
FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX,
&additional_computation_factor,
)?
.binary_payload_bytes(),
PricingMode::Prepaid { receipt } => {
CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
.add_field(TAG_FIELD_INDEX, &RESERVED_VARIANT_TAG)?
.add_field(RESERVED_RECEIPT_INDEX, &receipt)?
.binary_payload_bytes()
}
}
}
fn serialized_length(&self) -> usize {
CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths())
}
}
impl FromBytes for PricingMode {
fn from_bytes(bytes: &[u8]) -> Result<(PricingMode, &[u8]), Error> {
let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(4, bytes)?;
let window = binary_payload.start_consuming()?.ok_or(Formatting)?;
window.verify_index(TAG_FIELD_INDEX)?;
let (tag, window) = window.deserialize_and_maybe_next::<u8>()?;
let to_ret = match tag {
PAYMENT_LIMITED_VARIANT_TAG => {
let window = window.ok_or(Formatting)?;
window.verify_index(PAYMENT_LIMITED_PAYMENT_AMOUNT_INDEX)?;
let (payment_amount, window) = window.deserialize_and_maybe_next::<u64>()?;
let window = window.ok_or(Formatting)?;
window.verify_index(PAYMENT_LIMITED_GAS_PRICE_TOLERANCE_INDEX)?;
let (gas_price_tolerance, window) = window.deserialize_and_maybe_next::<u8>()?;
let window = window.ok_or(Formatting)?;
window.verify_index(PAYMENT_LIMITED_STANDARD_PAYMENT_INDEX)?;
let (standard_payment, window) = window.deserialize_and_maybe_next::<bool>()?;
if window.is_some() {
return Err(Formatting);
}
Ok(PricingMode::PaymentLimited {
payment_amount,
gas_price_tolerance,
standard_payment,
})
}
FIXED_VARIANT_TAG => {
let window = window.ok_or(Formatting)?;
window.verify_index(FIXED_GAS_PRICE_TOLERANCE_INDEX)?;
let (gas_price_tolerance, window) = window.deserialize_and_maybe_next::<u8>()?;
let window = window.ok_or(Formatting)?;
window.verify_index(FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX)?;
let (additional_computation_factor, window) =
window.deserialize_and_maybe_next::<u8>()?;
if window.is_some() {
return Err(Formatting);
}
Ok(PricingMode::Fixed {
gas_price_tolerance,
additional_computation_factor,
})
}
RESERVED_VARIANT_TAG => {
let window = window.ok_or(Formatting)?;
window.verify_index(RESERVED_RECEIPT_INDEX)?;
let (receipt, window) = window.deserialize_and_maybe_next::<Digest>()?;
if window.is_some() {
return Err(Formatting);
}
Ok(PricingMode::Prepaid { receipt })
}
_ => Err(Formatting),
};
to_ret.map(|endpoint| (endpoint, remainder))
}
}
impl Display for PricingMode {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
match self {
PricingMode::PaymentLimited {
payment_amount,
gas_price_tolerance: gas_price,
standard_payment,
} => {
write!(
formatter,
"payment amount {}, gas price multiplier {} standard_payment {}",
payment_amount, gas_price, standard_payment
)
}
PricingMode::Prepaid { receipt } => write!(formatter, "reserved: {}", receipt),
PricingMode::Fixed {
gas_price_tolerance,
additional_computation_factor,
} => write!(
formatter,
"fixed pricing {} {}",
gas_price_tolerance, additional_computation_factor
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bytesrepr;
#[test]
fn test_to_bytes_and_from_bytes() {
let classic = PricingMode::PaymentLimited {
payment_amount: 100,
gas_price_tolerance: 1,
standard_payment: true,
};
match classic {
PricingMode::PaymentLimited { .. } => {}
PricingMode::Fixed { .. } => {}
PricingMode::Prepaid { .. } => {}
}
bytesrepr::test_serialization_roundtrip(&classic);
bytesrepr::test_serialization_roundtrip(&PricingMode::Fixed {
gas_price_tolerance: 2,
additional_computation_factor: 1,
});
bytesrepr::test_serialization_roundtrip(&PricingMode::Prepaid {
receipt: Digest::hash(b"prepaid"),
});
}
use crate::gens::pricing_mode_arb;
use proptest::prelude::*;
proptest! {
#[test]
fn generative_bytesrepr_roundtrip(val in pricing_mode_arb()) {
bytesrepr::test_serialization_roundtrip(&val);
}
}
}