use std::sync::OnceLock;
use prometheus::{IntCounterVec, register_int_counter_vec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettlementDirection {
Inbound,
Outbound,
}
impl SettlementDirection {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Inbound => "inbound",
Self::Outbound => "outbound",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AmountReadError {
#[error("the receipt records no amount")]
Absent,
#[error("the recorded amount is not a decimal figure in the settlement currency")]
MalformedDollars,
#[error("the recorded amount is not a whole number of the token's base units")]
MalformedBaseUnits,
}
impl AmountReadError {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Absent => "absent",
Self::MalformedDollars | Self::MalformedBaseUnits => "malformed",
}
}
}
pub fn read_dollar_amount(stored: &str, decimals: u32) -> Result<u128, AmountReadError> {
let trimmed = stored.trim();
if trimmed.is_empty() {
return Err(AmountReadError::Absent);
}
dollars_to_base_units(trimmed, decimals).ok_or(AmountReadError::MalformedDollars)
}
pub fn read_base_unit_amount(stored: &str) -> Result<u128, AmountReadError> {
let trimmed = stored.trim();
if trimmed.is_empty() {
return Err(AmountReadError::Absent);
}
trimmed
.parse::<u128>()
.map_err(|_| AmountReadError::MalformedBaseUnits)
}
fn unreadable_amount_total() -> &'static IntCounterVec {
static V: OnceLock<IntCounterVec> = OnceLock::new();
V.get_or_init(|| {
register_int_counter_vec!(
"polychrome_settlement_amount_unreadable_total",
"Count of settled payment receipts dropped from an accounting read because their \
recorded amount could not be read, by settlement direction and reason.",
&["direction", "reason"]
)
.expect("register polychrome_settlement_amount_unreadable_total")
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AmountReadSite {
DashboardRollup,
CommittedSpendFloor,
WalletHistory,
}
impl AmountReadSite {
#[must_use]
const fn consequence(self) -> &'static str {
match self {
Self::DashboardRollup => {
"settled payment left out of the dashboard settlement rollup; the figure a reader \
sees is below what really settled"
}
Self::CommittedSpendFloor => {
"settled outbound payment missing from the committed-spend floor; the reseeded \
budget is below actual spend for this conversation"
}
Self::WalletHistory => {
"settled payment left out of the caller's wallet history; the payment does not \
appear in their own history at all"
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnreadableAmount<'a> {
pub site: AmountReadSite,
pub direction: SettlementDirection,
pub error: AmountReadError,
pub scope: &'a str,
pub reference: &'a str,
pub tool_call_id: &'a str,
pub approval_pos: &'a str,
pub subject: &'a str,
pub timestamp: &'a str,
}
pub fn record_unreadable_amount(dropped: &UnreadableAmount<'_>) {
unreadable_amount_total()
.with_label_values(&[dropped.direction.label(), dropped.error.label()])
.inc();
tracing::warn!(
scope = %dropped.scope,
direction = dropped.direction.label(),
reference = %dropped.reference,
tool_call_id = %dropped.tool_call_id,
approval_pos = %dropped.approval_pos,
subject = %dropped.subject,
timestamp = %dropped.timestamp,
error = %dropped.error,
"{}",
dropped.site.consequence()
);
}
#[must_use]
pub fn unreadable_amount_count(direction: SettlementDirection, error: AmountReadError) -> u64 {
unreadable_amount_total()
.with_label_values(&[direction.label(), error.label()])
.get()
}
pub(crate) fn force() {
for direction in [SettlementDirection::Inbound, SettlementDirection::Outbound] {
for reason in [AmountReadError::Absent, AmountReadError::MalformedBaseUnits] {
unreadable_amount_total().with_label_values(&[direction.label(), reason.label()]);
}
}
}
#[must_use]
pub fn dollars_to_base_units(s: &str, decimals: u32) -> Option<u128> {
let base_str =
mpp::server::parse_dollar_amount(at_scale(s.trim(), decimals)?, decimals).ok()?;
let amount = mpp::evm::parse_amount(&base_str).ok()?;
u128::try_from(amount).ok()
}
fn at_scale(figure: &str, decimals: u32) -> Option<&str> {
let Some((_, fraction)) = figure.split_once('.') else {
return Some(figure);
};
let scale = decimals as usize;
if fraction.len() <= scale || !fraction.bytes().all(|b| b.is_ascii_digit()) {
return Some(figure);
}
let (kept, below_scale) = fraction.split_at(scale);
if below_scale.bytes().any(|b| b != b'0') {
return None;
}
Some(&figure[..figure.len() - (fraction.len() - kept.len())])
}
pub const SETTLEMENT_SYMBOL: &str = "USD";
pub const DEFAULT_DECIMALS: u32 = 6;
#[must_use]
pub fn format_bare_amount(amount: u128, decimals: u32) -> String {
if decimals == 0 {
return amount.to_string();
}
let scale = 10u128.checked_pow(decimals).unwrap_or(u128::MAX);
let whole = amount / scale;
let frac = amount % scale;
if frac == 0 {
return whole.to_string();
}
let frac_str = format!("{frac:0width$}", width = decimals as usize);
format!("{whole}.{}", frac_str.trim_end_matches('0'))
}
#[must_use]
pub fn format_settled_amount(amount: u128, decimals: u32) -> String {
format!(
"{} {SETTLEMENT_SYMBOL}",
format_bare_amount(amount, decimals)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dollars_to_base_units_parses() {
assert_eq!(dollars_to_base_units("0.10", 6), Some(100_000));
assert_eq!(dollars_to_base_units("1", 6), Some(1_000_000));
assert_eq!(dollars_to_base_units("2.5", 6), Some(2_500_000));
assert_eq!(dollars_to_base_units("0.000001", 6), Some(1));
assert_eq!(dollars_to_base_units("", 6), None);
assert_eq!(dollars_to_base_units("abc", 6), None);
assert_eq!(dollars_to_base_units("1", 2), Some(100));
assert_eq!(dollars_to_base_units("1.5", 2), Some(150));
}
#[test]
fn dollars_to_base_units_accepts_padding_zeros_below_the_scale() {
assert_eq!(dollars_to_base_units("0.1000000", 6), Some(100_000));
assert_eq!(dollars_to_base_units("0.010000000000", 6), Some(10_000));
assert_eq!(dollars_to_base_units("1.000", 0), Some(1));
assert_eq!(dollars_to_base_units("2.500000", 2), Some(250));
assert_eq!(dollars_to_base_units("0.1", 6), Some(100_000));
assert_eq!(dollars_to_base_units("0.100000", 6), Some(100_000));
}
#[test]
fn dollars_to_base_units_still_refuses_precision_it_cannot_hold() {
assert_eq!(dollars_to_base_units("0.1000001", 6), None);
assert_eq!(dollars_to_base_units("0.0000001", 6), None);
assert_eq!(dollars_to_base_units("1.01", 1), None);
assert_eq!(dollars_to_base_units("0.1abcdefg", 6), None);
assert_eq!(dollars_to_base_units("1.2.3", 6), None);
}
#[test]
fn read_dollar_amount_reads_a_padded_stored_figure() {
assert_eq!(
read_dollar_amount("0.1000000", DEFAULT_DECIMALS),
Ok(100_000)
);
assert_eq!(
read_dollar_amount("0.1000001", DEFAULT_DECIMALS),
Err(AmountReadError::MalformedDollars)
);
}
#[test]
fn read_dollar_amount_reads_a_production_shaped_price() {
assert_eq!(read_dollar_amount("0.01", DEFAULT_DECIMALS), Ok(10_000));
assert_eq!(read_dollar_amount("1", DEFAULT_DECIMALS), Ok(1_000_000));
assert_eq!(read_dollar_amount(" 2.5 ", DEFAULT_DECIMALS), Ok(2_500_000));
assert_eq!(
read_dollar_amount("", DEFAULT_DECIMALS),
Err(AmountReadError::Absent)
);
assert_eq!(
read_dollar_amount(" ", DEFAULT_DECIMALS),
Err(AmountReadError::Absent)
);
assert_eq!(
read_dollar_amount("abc", DEFAULT_DECIMALS),
Err(AmountReadError::MalformedDollars)
);
}
#[test]
fn read_base_unit_amount_separates_an_uncaptured_charge_from_a_zero() {
assert_eq!(read_base_unit_amount("10000"), Ok(10_000));
assert_eq!(read_base_unit_amount("0"), Ok(0));
assert_eq!(read_base_unit_amount(""), Err(AmountReadError::Absent));
assert_eq!(read_base_unit_amount(" "), Err(AmountReadError::Absent));
assert_eq!(
read_base_unit_amount("-1"),
Err(AmountReadError::MalformedBaseUnits)
);
}
#[test]
fn read_base_unit_amount_refuses_a_decimal_figure() {
assert_eq!(
read_base_unit_amount("0.01"),
Err(AmountReadError::MalformedBaseUnits)
);
assert_eq!(
read_base_unit_amount("1.50"),
Err(AmountReadError::MalformedBaseUnits)
);
}
#[test]
fn unreadable_amount_labels_are_a_closed_set() {
assert_eq!(SettlementDirection::Inbound.label(), "inbound");
assert_eq!(SettlementDirection::Outbound.label(), "outbound");
assert_eq!(AmountReadError::Absent.label(), "absent");
assert_eq!(AmountReadError::MalformedDollars.label(), "malformed");
assert_eq!(AmountReadError::MalformedBaseUnits.label(), "malformed");
}
#[test]
fn record_unreadable_amount_increments_only_the_recorded_child() {
force();
let before =
unreadable_amount_count(SettlementDirection::Outbound, AmountReadError::Absent);
let other_before = unreadable_amount_count(
SettlementDirection::Inbound,
AmountReadError::MalformedDollars,
);
record_unreadable_amount(&UnreadableAmount {
site: AmountReadSite::CommittedSpendFloor,
direction: SettlementDirection::Outbound,
error: AmountReadError::Absent,
scope: "conv-1",
reference: "tx-1",
tool_call_id: "call-1",
approval_pos: "1",
subject: "persona-1",
timestamp: "2026-07-20T00:00:00Z",
});
assert_eq!(
unreadable_amount_count(SettlementDirection::Outbound, AmountReadError::Absent),
before + 1
);
assert_eq!(
unreadable_amount_count(
SettlementDirection::Inbound,
AmountReadError::MalformedDollars
),
other_before
);
}
#[test]
fn every_read_site_words_a_distinct_consequence() {
let sites = [
AmountReadSite::DashboardRollup,
AmountReadSite::CommittedSpendFloor,
AmountReadSite::WalletHistory,
];
let worded: Vec<&str> = sites.iter().map(|s| s.consequence()).collect();
for phrase in &worded {
assert!(!phrase.is_empty());
}
let distinct: std::collections::BTreeSet<&&str> = worded.iter().collect();
assert_eq!(distinct.len(), sites.len(), "one wording per site");
}
#[test]
fn format_settled_amount_renders_human_with_symbol() {
assert_eq!(format_settled_amount(100_000, 6), "0.1 USD");
assert_eq!(format_settled_amount(1_000_000, 6), "1 USD");
assert_eq!(format_settled_amount(2_500_000, 6), "2.5 USD");
assert_eq!(format_settled_amount(1, 6), "0.000001 USD");
assert_eq!(format_settled_amount(0, 6), "0 USD");
assert_eq!(format_settled_amount(42, 0), "42 USD");
}
#[test]
fn format_bare_amount_renders_the_same_figure_with_no_symbol() {
assert_eq!(format_bare_amount(100_000, 6), "0.1");
assert_eq!(format_bare_amount(1_000_000, 6), "1");
assert_eq!(format_bare_amount(2_500_000, 6), "2.5");
assert_eq!(format_bare_amount(0, 6), "0");
assert_eq!(format_bare_amount(42, 0), "42");
}
#[test]
fn format_bare_amount_round_trips_through_dollars_to_base_units() {
for figure in ["5", "5.00", "0.10", "123.456789"] {
let base = dollars_to_base_units(figure, 6).expect("parses");
let rendered = format_bare_amount(base, 6);
let re_parsed = dollars_to_base_units(&rendered, 6).expect("re-parses");
assert_eq!(
base, re_parsed,
"{figure} must round-trip through base units"
);
}
}
}