dmsg_protocol 0.4.0

Deterministic encoding, COSE document verification, and approval helpers for dMsg
Documentation
use crate::*;
use candid::Principal;
use dmsg_types::*;
use icrc_ledger_types::icrc1::account::Account;

/// Commit to name-registration ledger charge terms under `dmsg/handle-charge/v1`.
///
/// Binds the ledger Principal, explicit ICRC account representation, amount and
/// network fee. Both amounts are integer ledger base units. This does not check
/// allowances, balances, handle validity, or whether the charge has occurred.
pub fn charge_terms_digest(ledger: Principal, payer: &Account, amount: u128, fee: u128) -> Hash {
    digest(
        "dmsg/handle-charge/v1",
        &(
            ledger,
            dmsg_types::account::account_cbor::value(payer),
            amount,
            fee,
        ),
    )
}

/// Validate a handle and convert ASCII letters to lowercase.
///
/// Accepts 1..19 ASCII letters, digits or underscores, with no leading underscore.
/// The cap keeps handles disjoint from 20-character `AccountId` Xid text.
/// Does not trim whitespace or perform Unicode normalization.
///
/// # Errors
/// Invalid length, prefix or characters return `Error::InvalidInput`.
pub fn normalize_handle(handle: &str) -> Result<String> {
    ensure_valid(
        !handle.is_empty() && handle.len() <= 19 && !handle.starts_with('_'),
        "handle length/prefix",
    )?;
    ensure_valid(
        handle
            .bytes()
            .all(|c| c.is_ascii_alphanumeric() || c == b'_'),
        "handle characters",
    )?;
    Ok(handle.to_ascii_lowercase())
}

/// Leading digest bits that select a handle's certification bucket.
pub const HANDLE_BUCKET_BITS: u32 = 20;

/// Certification bucket of a canonical handle: the leading
/// [`HANDLE_BUCKET_BITS`] bits of `digest("dmsg/handle-bucket/v1", handle)`.
///
/// The registry certifies a handle at the path of one `[0]` or `[1]` label per
/// bucket bit, most significant first, followed by the handle bytes. Call
/// [`normalize_handle`] first; the bucket of a non-canonical spelling differs.
pub fn handle_bucket(handle: &str) -> u32 {
    let d = digest("dmsg/handle-bucket/v1", &handle);
    u32::from_be_bytes(d.as_slice()[..4].try_into().expect("four bytes"))
        >> (32 - HANDLE_BUCKET_BITS)
}

const PANDA: u128 = 100_000_000;

/// Lowest registration price in PANDA base units, charged for 7..19-byte names.
pub const MIN_HANDLE_PRICE: u128 = 100 * PANDA;

/// Return the fixed PANDA registration price in base units (8 decimal places).
///
/// Call [`normalize_handle`] first: this function only examines byte length and
/// will also return a value for invalid handles. Token prices are 1,000,000 for
/// 1 byte, 200,000 for 2, 50,000 for 3..4, 20,000 for 5..6, and 100 otherwise.
/// It does not query a ledger, convert currencies, or quote a network fee.
pub fn price(handle: &str) -> u128 {
    match handle.len() {
        1 => 1_000_000 * PANDA,
        2 => 200_000 * PANDA,
        3 | 4 => 50_000 * PANDA,
        5 | 6 => 20_000 * PANDA,
        _ => MIN_HANDLE_PRICE,
    }
}

#[cfg(test)]
mod tests;