chio-credit 0.1.2

Chio credit, capital, and bonded execution contracts
Documentation
use chio_core_types::canonical::canonical_json_bytes;
use chio_core_types::capability::scope::MonetaryAmount;
use chio_core_types::crypto::{sha256_hex, Keypair, PublicKey, Signature};
use chio_core_types::receipt::lineage::SignedExportEnvelope;
pub use chio_core_types::{
    CHIO_FACTOR_ASSIGNMENT_ACKNOWLEDGEMENT_V1_SCHEMA, CHIO_FACTOR_ASSIGNMENT_AGREEMENT_V1_SCHEMA,
    CHIO_FACTOR_ASSIGNMENT_BIND_AUTHORIZATION_V1_SCHEMA,
    CHIO_FACTOR_ASSIGNMENT_NOT_APPLIED_V1_SCHEMA,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};

pub const FACTOR_ASSIGNMENT_BIND_ACTION: &str = "factor.assignment_bind";
pub const FACTOR_ASSIGNMENT_BIND_AUTHORIZATION_SCHEMA: &str =
    CHIO_FACTOR_ASSIGNMENT_BIND_AUTHORIZATION_V1_SCHEMA;
pub const FACTOR_NORMALIZED_ASSIGNMENT_REQUEST_SCHEMA: &str =
    "chio.factor.normalized-assignment-request.v1";
pub const FACTOR_RECEIVABLE_CLAIM_SCHEMA: &str = "chio.factor.receivable-claim.v1";
pub const FACTOR_ASSIGNMENT_OFFER_SCHEMA: &str = "chio.factor.assignment-offer.v1";
pub const FACTOR_DISCOUNT_QUOTE_SCHEMA: &str = "chio.factor.discount-quote.v1";
pub const FACTOR_ASSIGNMENT_AGREEMENT_SCHEMA: &str = CHIO_FACTOR_ASSIGNMENT_AGREEMENT_V1_SCHEMA;
pub const FACTOR_ASSIGNMENT_ACKNOWLEDGEMENT_SCHEMA: &str =
    CHIO_FACTOR_ASSIGNMENT_ACKNOWLEDGEMENT_V1_SCHEMA;
pub const FACTOR_ASSIGNMENT_NOT_APPLIED_SCHEMA: &str = CHIO_FACTOR_ASSIGNMENT_NOT_APPLIED_V1_SCHEMA;

const NORMALIZED_REQUEST_DIGEST_DOMAIN: &[u8] =
    b"chio.factor.normalized-assignment-request.digest.v1\0";
const CLAIM_ID_DOMAIN: &[u8] = b"chio.factor.receivable-claim.id.v1\0";
const CLAIM_DIGEST_DOMAIN: &[u8] = b"chio.factor.receivable-claim.digest.v1\0";
const OFFER_ID_DOMAIN: &[u8] = b"chio.factor.assignment-offer.id.v1\0";
const OFFER_DIGEST_DOMAIN: &[u8] = b"chio.factor.assignment-offer.digest.v1\0";
const QUOTE_ID_DOMAIN: &[u8] = b"chio.factor.discount-quote.id.v1\0";
const QUOTE_DIGEST_DOMAIN: &[u8] = b"chio.factor.discount-quote.digest.v1\0";
const AUTHORIZATION_BODY_DIGEST_DOMAIN: &[u8] =
    b"chio.factor.assignment-bind-authorization.body.v1\0";
const AGREEMENT_BODY_DIGEST_DOMAIN: &[u8] = b"chio.factor.assignment-agreement.body.v1\0";
const AGREEMENT_SIGNATURE_DIGEST_DOMAIN: &[u8] = b"chio.factor.assignment-agreement.signature.v1\0";
const AUTHORIZATION_SET_DIGEST_DOMAIN: &[u8] = b"chio.factor.assignment-authorization-set.v1\0";
const ACKNOWLEDGEMENT_ID_DOMAIN: &[u8] = b"chio.factor.assignment-acknowledgement.id.v1\0";
const ACKNOWLEDGEMENT_BODY_DIGEST_DOMAIN: &[u8] =
    b"chio.factor.assignment-acknowledgement.body.v1\0";
const NOT_APPLIED_ID_DOMAIN: &[u8] = b"chio.factor.assignment-not-applied.id.v1\0";
const NOT_APPLIED_BODY_DIGEST_DOMAIN: &[u8] = b"chio.factor.assignment-not-applied.body.v1\0";
const MAX_TEXT_CHARS: usize = 2_048;
const I_JSON_MAX_SAFE_INTEGER: u64 = (1_u64 << 53) - 1;
const BASIS_POINTS_DENOMINATOR: u16 = 10_000;

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum FactorError {
    #[error("invalid factor field '{0}'")]
    InvalidField(&'static str),
    #[error("factor binding does not match")]
    BindingMismatch,
    #[error("factor authority verification failed")]
    AuthorityVerification,
    #[error("factor artifact is not current")]
    NotCurrent,
    #[error("factor arithmetic overflow")]
    ArithmeticOverflow,
    #[error("factor canonicalization failed: {0}")]
    Canonicalization(String),
}

pub type FactorAuthorizationError = FactorError;

mod authorization;
mod claim;
mod request;
mod result;

pub use authorization::*;
pub use claim::*;
pub use request::*;
pub use result::*;

fn discounted_amount(
    face_value: &MonetaryAmount,
    discount_bps: u16,
) -> Result<MonetaryAmount, FactorError> {
    validate_amount("discount_face_value", face_value, false)?;
    validate_basis_points(discount_bps)?;
    let retained_bps = BASIS_POINTS_DENOMINATOR
        .checked_sub(discount_bps)
        .ok_or(FactorError::ArithmeticOverflow)?;
    let numerator = u128::from(face_value.units)
        .checked_mul(u128::from(retained_bps))
        .ok_or(FactorError::ArithmeticOverflow)?;
    let units = numerator / u128::from(BASIS_POINTS_DENOMINATOR);
    Ok(MonetaryAmount {
        units: u64::try_from(units).map_err(|_| FactorError::ArithmeticOverflow)?,
        currency: face_value.currency.clone(),
    })
}

fn canonical_bytes(value: &impl Serialize) -> Result<Vec<u8>, FactorError> {
    canonical_json_bytes(value).map_err(|error| FactorError::Canonicalization(error.to_string()))
}

fn parse_canonical<T>(bytes: &[u8], artifact: &str) -> Result<T, FactorError>
where
    T: DeserializeOwned + Serialize,
{
    let value: T = serde_json::from_slice(bytes)
        .map_err(|error| FactorError::Canonicalization(error.to_string()))?;
    if canonical_bytes(&value)?.as_slice() != bytes {
        return Err(FactorError::Canonicalization(format!(
            "{artifact} is not canonical"
        )));
    }
    Ok(value)
}

fn domain_digest(domain: &[u8], value: &impl Serialize) -> Result<String, FactorError> {
    let bytes = canonical_bytes(value)?;
    let mut preimage = Vec::with_capacity(domain.len() + bytes.len());
    preimage.extend_from_slice(domain);
    preimage.extend_from_slice(&bytes);
    Ok(sha256_hex(&preimage))
}

fn validate_text(field: &'static str, value: &str) -> Result<(), FactorError> {
    let disallowed_control =
        |character: char| matches!(character as u32, 0x00..=0x1f | 0x7f..=0x9f);
    if value.is_empty()
        || value.trim() != value
        || value.chars().count() > MAX_TEXT_CHARS
        || value.chars().any(disallowed_control)
    {
        Err(FactorError::InvalidField(field))
    } else {
        Ok(())
    }
}

fn validate_digest(field: &'static str, value: &str) -> Result<(), FactorError> {
    if value.len() == 64
        && value
            .bytes()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    {
        Ok(())
    } else {
        Err(FactorError::InvalidField(field))
    }
}

fn validate_positive(field: &'static str, value: u64) -> Result<(), FactorError> {
    if value == 0 || value > I_JSON_MAX_SAFE_INTEGER {
        Err(FactorError::InvalidField(field))
    } else {
        Ok(())
    }
}

fn validate_basis_points(value: u16) -> Result<(), FactorError> {
    if value > BASIS_POINTS_DENOMINATOR {
        Err(FactorError::InvalidField("discount_bps"))
    } else {
        Ok(())
    }
}

fn validate_amount(
    field: &'static str,
    amount: &MonetaryAmount,
    allow_zero: bool,
) -> Result<(), FactorError> {
    if (!allow_zero && amount.units == 0)
        || amount.units > I_JSON_MAX_SAFE_INTEGER
        || amount.currency.len() != 3
        || !amount
            .currency
            .bytes()
            .all(|byte| byte.is_ascii_uppercase())
    {
        Err(FactorError::InvalidField(field))
    } else {
        Ok(())
    }
}