use crate::{authenticated, digest, ensure_valid};
use dmsg_types::{membership::*, *};
use num_bigint::BigUint;
use num_traits::ToPrimitive;
pub const MAX_LEASE_MS: u64 = 60 * MINUTE;
pub const MIN_COOLING_MS: u64 = MAX_LEASE_MS + 5 * MINUTE;
pub fn mul_div(a: u128, b: u128, denominator: u128, round_up: bool) -> Result<u128> {
ensure_valid(denominator > 0, "zero denominator")?;
if let Some(n) = a.checked_mul(b) {
return Ok(n / denominator + u128::from(round_up && n % denominator != 0));
}
let n = BigUint::from(a) * BigUint::from(b);
let d = BigUint::from(denominator);
let value = if round_up { (n + &d - 1u8) / d } else { n / d };
value.to_u128().ok_or(Error::QuotaExceeded)
}
pub fn required_panda(threshold: &Threshold, decimals: u8) -> Result<u128> {
ensure(decimals == 8, Error::UnsupportedProtocol)?;
match threshold {
Threshold::FixedPanda { atomic } => {
ensure_valid(*atomic > 0, "zero threshold")?;
Ok(*atomic)
}
Threshold::AnnualPrice {
price_cents,
r_num,
r_den,
} => {
ensure_valid(*price_cents > 0 && *r_num > 0 && *r_den > 0, "price / R")?;
let n =
BigUint::from(*price_cents) * BigUint::from(*r_num) * BigUint::from(100_000_000u64);
let d = BigUint::from(*r_den) * BigUint::from(100u8);
((n + &d - 1u8) / d).to_u128().ok_or(Error::QuotaExceeded)
}
}
}
pub fn validate_beneficiary(b: &Beneficiary) -> Result<()> {
authenticated(b.authority_canister)?;
ensure_valid(
!b.product_id.is_empty()
&& b.product_id.len() <= 32
&& !b.subject_schema.is_empty()
&& b.subject_schema.len() <= 64
&& !b.subject_bytes.is_empty()
&& b.subject_bytes.len() <= 64,
"beneficiary",
)
}
pub fn membership_intent_digest(intent: &MembershipIntent) -> Hash {
digest("dmsg/commerce/intent/v1", intent)
}
pub fn claim_action_digest(request: &ClaimRequest) -> Hash {
digest(
"membership/claim-action/v1",
&(
request.neuron_id,
request.policy_version,
request.benefit_id,
request.expected_business_revision,
&request.term,
&request.change,
),
)
}
pub fn claim_id(service: candid::Principal, intent: &MembershipIntent) -> Hash {
digest(
"membership/claim-id/v1",
&(service, intent.actor, intent.application_id),
)
}
pub fn claim_key(id: Hash) -> Hash {
digest("membership/claim-key/v1", &id)
}
pub fn neuron_key(governance: candid::Principal, neuron: Hash) -> Hash {
digest("membership/neuron-key/v1", &(governance, neuron))
}
pub fn decision_digest(decision: &MembershipDecision) -> Hash {
digest("membership/decision/v1", decision)
}
pub fn policy_key(version: u64) -> Hash {
digest("membership/policy-key/v1", &version)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integer_fast_path_and_wide_intermediates_have_identical_rounding() {
let values = [0, 1, 3, 10_000, 1_000_001, u64::MAX as u128, u128::MAX];
for a in values {
for b in values {
for denominator in values {
for up in [false, true] {
if denominator == 0 {
assert!(matches!(
mul_div(a, b, denominator, up),
Err(Error::InvalidInput(_))
));
continue;
}
let n = BigUint::from(a) * BigUint::from(b);
let d = BigUint::from(denominator);
let mut expected = &n / &d;
if up && &n % &d != BigUint::from(0u8) {
expected += 1u8;
}
assert_eq!(
mul_div(a, b, denominator, up),
expected.to_u128().ok_or(Error::QuotaExceeded)
);
}
}
}
}
}
#[test]
fn thresholds_require_positive_terms_and_the_pinned_asset() {
assert_eq!(
required_panda(&Threshold::FixedPanda { atomic: 1 }, 8),
Ok(1)
);
assert_eq!(
required_panda(&Threshold::FixedPanda { atomic: 1 }, 6),
Err(Error::UnsupportedProtocol)
);
assert!(required_panda(&Threshold::FixedPanda { atomic: 0 }, 8).is_err());
for (price_cents, r_num, r_den) in [(0, 1, 1), (1, 0, 1), (1, 1, 0)] {
assert!(matches!(
required_panda(
&Threshold::AnnualPrice {
price_cents,
r_num,
r_den
},
8
),
Err(Error::InvalidInput(_))
));
}
assert_eq!(
required_panda(
&Threshold::AnnualPrice {
price_cents: 1,
r_num: 1,
r_den: 3
},
8
),
Ok(333_334)
);
assert_eq!(
required_panda(
&Threshold::AnnualPrice {
price_cents: u64::MAX,
r_num: u128::MAX,
r_den: 1
},
8
),
Err(Error::QuotaExceeded)
);
}
#[test]
fn beneficiary_fields_are_bounded_without_implying_authorization() {
let valid = Beneficiary {
product_id: "p".repeat(32),
authority_canister: candid::Principal::from_slice(&[1]),
subject_schema: "s".repeat(64),
subject_bytes: vec![1; 64].into(),
};
assert_eq!(validate_beneficiary(&valid), Ok(()));
let changes: &[fn(&mut Beneficiary)] = &[
|b| b.product_id.clear(),
|b| b.product_id.push('p'),
|b| b.subject_schema.clear(),
|b| b.subject_schema.push('s'),
|b| b.subject_bytes.clear(),
|b| b.subject_bytes.push(1),
|b| b.authority_canister = candid::Principal::anonymous(),
|b| b.authority_canister = candid::Principal::management_canister(),
];
for change in changes {
let mut b = valid.clone();
change(&mut b);
assert!(validate_beneficiary(&b).is_err());
}
}
}