o402 0.1.4

OpenAI-compatible gateway, paid with x402.
//! eip155 exact and upto accepts.

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

#[cfg(not(feature = "evm"))]
use super::feature_missing;
#[cfg(feature = "evm")]
use super::{
    accept_custom_decimals, custom_asset_address, invalid_asset_address, invalid_network,
    invalid_pay_to, named_ticker, no_deployment,
};
use crate::config::{AcceptConfig, ConfigError};

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

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

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

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

    use alloy_primitives::{Address, U256};
    use r402_evm::chain::ChecksummedAddress;
    use r402_evm::{Eip155Exact, Eip155Upto};

    let pay_to = Address::from_str(pay_to).map_err(|error| invalid_pay_to("EVM", pay_to, error))?;
    let token = token(accept, network)?;
    let asset = token.amount(U256::from(amount));
    match accept.scheme.as_str() {
        "upto" => {
            if accept.transfer_method.is_some() {
                return Err(ConfigError::Validation(
                    "transfer_method is not valid on upto accepts".to_owned(),
                ));
            }
            Ok(Eip155Upto::price_tag(
                ChecksummedAddress::from(pay_to),
                asset,
            ))
        }
        "exact" => {
            let method = parse_transfer_method(accept.transfer_method.as_deref())?;
            Ok(Eip155Exact::price_tag(
                ChecksummedAddress::from(pay_to),
                asset,
                method,
            ))
        }
        other => Err(ConfigError::Validation(format!(
            "payment.accepts scheme '{other}' is not supported"
        ))),
    }
}

#[cfg(feature = "evm")]
fn token(
    accept: &AcceptConfig,
    network: &ChainId,
) -> Result<r402_evm::chain::Eip155TokenDeployment, ConfigError> {
    use std::str::FromStr as _;

    use alloy_primitives::Address;
    use r402_evm::USDC;
    use r402_evm::chain::{Eip155ChainReference, Eip155TokenDeployment, TokenDeploymentEip712};

    let chain = Eip155ChainReference::try_from(network)
        .map_err(|error| invalid_network("eip155", &accept.network, error))?;
    if named_ticker(accept, &["usdc"])?.is_some() {
        return USDC::on(chain)
            .cloned()
            .ok_or_else(|| no_deployment("USDC", &accept.network));
    }
    let address = custom_asset_address(accept)?;
    let address =
        Address::from_str(address).map_err(|error| invalid_asset_address(address, error))?;
    let decimals = accept_custom_decimals(accept)?;
    let eip712 = match (
        accept.eip712_name.as_deref(),
        accept.eip712_version.as_deref(),
    ) {
        (None, None) => None,
        (Some(name), Some(version)) => Some(TokenDeploymentEip712 {
            name: name.to_owned(),
            version: version.to_owned(),
        }),
        _ => {
            return Err(ConfigError::Validation(
                "custom EVM accepts need both eip712_name and eip712_version".to_owned(),
            ));
        }
    };
    Ok(Eip155TokenDeployment {
        chain_reference: chain,
        address,
        decimals,
        eip712,
    })
}

#[cfg(feature = "evm")]
fn parse_transfer_method(
    raw: Option<&str>,
) -> Result<Option<r402_evm::AssetTransferMethod>, ConfigError> {
    match raw {
        None | Some("eip3009") => Ok(None),
        Some("permit2") => Ok(Some(r402_evm::AssetTransferMethod::Permit2)),
        Some(other) => Err(ConfigError::Validation(format!(
            "unsupported transfer_method '{other}'"
        ))),
    }
}