o402 0.1.2

OpenAI-compatible gateway, paid with x402.
//! xrpl exact accepts. Named XRP / RLUSD only.

use r402_protocol::network::ChainId;
use r402_protocol::payment::PriceTag;

#[cfg(not(feature = "xrpl"))]
use super::feature_missing;
#[cfg(feature = "xrpl")]
use super::{
    invalid_network, invalid_pay_to, named_ticker, no_deployment, reject_evm_fields, require_exact,
};
use crate::config::{AcceptConfig, ConfigError};

#[cfg(not(feature = "xrpl"))]
pub(super) fn build(
    _accept: &AcceptConfig,
    _network: &ChainId,
    _amount: u128,
    _pay_to: &str,
) -> Result<PriceTag, ConfigError> {
    Err(feature_missing("xrpl", "xrpl"))
}

#[cfg(not(feature = "xrpl"))]
pub(super) fn decimals(_accept: &AcceptConfig, _network: &ChainId) -> Result<u8, ConfigError> {
    Err(feature_missing("xrpl", "xrpl"))
}

#[cfg(feature = "xrpl")]
pub(super) fn decimals(accept: &AcceptConfig, network: &ChainId) -> Result<u8, ConfigError> {
    Ok(token(accept, network)?.decimals)
}

#[cfg(feature = "xrpl")]
pub(super) fn build(
    accept: &AcceptConfig,
    network: &ChainId,
    amount: u128,
    pay_to: &str,
) -> Result<PriceTag, ConfigError> {
    use std::str::FromStr as _;

    use r402_xrpl::XrplExact;
    use r402_xrpl::chain::XrplClassicAddress;

    require_exact(accept)?;
    reject_evm_fields(accept, "xrpl")?;
    if accept.asset_address.is_some() {
        return Err(ConfigError::Validation(
            "xrpl accepts do not support custom asset_address; use asset = \"xrp\" or \"rlusd\""
                .to_owned(),
        ));
    }
    let pay_to = XrplClassicAddress::from_str(pay_to)
        .map_err(|error| invalid_pay_to("XRPL", pay_to, error))?;
    let token = token(accept, network)?;
    let tag_amount = match accept.asset.as_deref() {
        Some("xrp") => token.amount(amount),
        Some("rlusd") => {
            let encoded = xrpl_iou_amount_string(amount, token.decimals)?;
            token.amount(encoded.as_str())
        }
        _ => {
            return Err(ConfigError::Validation(
                "xrpl accepts do not support custom asset_address; use asset = \"xrp\" or \"rlusd\""
                    .to_owned(),
            ));
        }
    };
    Ok(XrplExact::price_tag(pay_to, tag_amount))
}

#[cfg(feature = "xrpl")]
fn token(
    accept: &AcceptConfig,
    network: &ChainId,
) -> Result<r402_xrpl::chain::XrplTokenDeployment, ConfigError> {
    use r402_xrpl::chain::XrplChainReference;
    use r402_xrpl::{rlusd_deployment, xrp_deployment};

    let chain = XrplChainReference::try_from(network.clone())
        .map_err(|error| invalid_network("xrpl", &accept.network, error))?;
    match named_ticker(accept, &["xrp", "rlusd"])? {
        Some("xrp") => xrp_deployment(chain)
            .cloned()
            .ok_or_else(|| no_deployment("XRP", &accept.network)),
        Some("rlusd") => rlusd_deployment(chain)
            .cloned()
            .ok_or_else(|| no_deployment("RLUSD", &accept.network)),
        Some(other) => Err(ConfigError::Validation(format!(
            "unknown payment.accepts asset '{other}'; use asset = \"xrp\" or \"rlusd\""
        ))),
        None => Err(ConfigError::Validation(
            "xrpl accepts do not support custom asset_address; use asset = \"xrp\" or \"rlusd\""
                .to_owned(),
        )),
    }
}

#[cfg(feature = "xrpl")]
pub(super) fn xrpl_iou_amount_string(atomic: u128, decimals: u8) -> Result<String, ConfigError> {
    let scale = usize::from(decimals);
    let divisor = 10u128
        .checked_pow(u32::from(decimals))
        .ok_or_else(|| ConfigError::Validation("xrpl IOU decimals exceed u128 scale".to_owned()))?;
    let whole = atomic / divisor;
    let frac = atomic % divisor;
    if frac == 0 {
        return Ok(whole.to_string());
    }
    let frac_padded = format!("{frac:0scale$}");
    Ok(format!("{whole}.{}", frac_padded.trim_end_matches('0')))
}