use std::fmt::{Debug, Display, Formatter};
use std::str::FromStr;
use compact_str::CompactString;
use r402_core::amount::{MoneyAmount, MoneyAmountParseError};
use r402_core::chain::{ChainId, DeployedTokenAmount};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub const APTOS_NAMESPACE: &str = "aptos";
pub const APTOS_MAINNET_FULLNODE_URL: &str = "https://fullnode.mainnet.aptoslabs.com/v1";
pub const APTOS_TESTNET_FULLNODE_URL: &str = "https://fullnode.testnet.aptoslabs.com/v1";
pub const USDC_MAINNET_FA: &str =
"0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b";
pub const USDC_TESTNET_FA: &str =
"0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832";
const ADDRESS_LEN: usize = 66;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum AptosChainReference {
Mainnet,
Testnet,
}
impl AptosChainReference {
pub const MAINNET: Self = Self::Mainnet;
pub const TESTNET: Self = Self::Testnet;
pub const ALL: &'static [Self] = &[Self::Mainnet, Self::Testnet];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Mainnet => "1",
Self::Testnet => "2",
}
}
#[must_use]
pub const fn chain_id(self) -> u8 {
match self {
Self::Mainnet => 1,
Self::Testnet => 2,
}
}
#[must_use]
pub const fn default_fullnode_url(self) -> &'static str {
match self {
Self::Mainnet => APTOS_MAINNET_FULLNODE_URL,
Self::Testnet => APTOS_TESTNET_FULLNODE_URL,
}
}
}
impl Debug for AptosChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "AptosChainReference({})", self.as_str())
}
}
impl Display for AptosChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for AptosChainReference {
type Err = AptosChainReferenceFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"1" => Ok(Self::Mainnet),
"2" => Ok(Self::Testnet),
other => Err(AptosChainReferenceFormatError::InvalidReference(
other.to_owned(),
)),
}
}
}
impl Serialize for AptosChainReference {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for AptosChainReference {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl From<AptosChainReference> for ChainId {
fn from(value: AptosChainReference) -> Self {
Self::new(APTOS_NAMESPACE, value.as_str())
}
}
impl TryFrom<ChainId> for AptosChainReference {
type Error = AptosChainReferenceFormatError;
fn try_from(value: ChainId) -> Result<Self, Self::Error> {
let (namespace, reference) = value.into_parts();
if namespace != APTOS_NAMESPACE {
return Err(AptosChainReferenceFormatError::InvalidNamespace(namespace));
}
Self::from_str(&reference)
.map_err(|_| AptosChainReferenceFormatError::InvalidReference(reference))
}
}
#[derive(Debug, thiserror::Error)]
pub enum AptosChainReferenceFormatError {
#[error("Invalid namespace {0}, expected aptos")]
InvalidNamespace(String),
#[error("Invalid aptos chain reference {0}")]
InvalidReference(String),
}
#[must_use]
pub fn is_aptos_network(network: &str) -> bool {
network == "aptos:1" || network == "aptos:2"
}
#[must_use]
pub fn is_aptos_address(s: &str) -> bool {
if s.len() != ADDRESS_LEN || !s.starts_with("0x") {
return false;
}
s.as_bytes()
.get(2..)
.is_some_and(|hex| hex.iter().all(u8::is_ascii_hexdigit))
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct AptosAddress(CompactString);
impl AptosAddress {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Display for AptosAddress {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for AptosAddress {
type Err = AptosAddressFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !is_aptos_address(s) {
return Err(AptosAddressFormatError::Invalid(s.to_owned()));
}
Ok(Self(CompactString::from(s)))
}
}
impl Serialize for AptosAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for AptosAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl AsRef<str> for AptosAddress {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, thiserror::Error)]
pub enum AptosAddressFormatError {
#[error("invalid aptos address: {0}")]
Invalid(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AptosTokenAmount(CompactString);
impl AptosTokenAmount {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn as_u64(&self) -> Result<u64, AptosTokenAmountFormatError> {
self.0
.parse()
.map_err(|_| AptosTokenAmountFormatError::Invalid(self.0.to_string()))
}
}
impl Display for AptosTokenAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for AptosTokenAmount {
type Err = AptosTokenAmountFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(AptosTokenAmountFormatError::Invalid(s.to_owned()));
}
let _: u64 = s
.parse()
.map_err(|_| AptosTokenAmountFormatError::Invalid(s.to_owned()))?;
Ok(Self(CompactString::from(s)))
}
}
impl Serialize for AptosTokenAmount {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for AptosTokenAmount {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl From<u64> for AptosTokenAmount {
fn from(value: u64) -> Self {
Self(CompactString::from(value.to_string()))
}
}
impl TryFrom<AptosTokenAmount> for u64 {
type Error = AptosTokenAmountFormatError;
fn try_from(value: AptosTokenAmount) -> Result<Self, Self::Error> {
value.as_u64()
}
}
#[derive(Debug, thiserror::Error)]
pub enum AptosTokenAmountFormatError {
#[error("invalid aptos token amount: {0}")]
Invalid(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AptosTokenDeployment {
pub chain_reference: AptosChainReference,
pub address: AptosAddress,
pub decimals: u8,
}
impl AptosTokenDeployment {
#[must_use]
pub const fn new(
chain_reference: AptosChainReference,
address: AptosAddress,
decimals: u8,
) -> Self {
Self {
chain_reference,
address,
decimals,
}
}
#[must_use]
pub fn amount(&self, v: u64) -> DeployedTokenAmount<u64, Self> {
DeployedTokenAmount {
amount: v,
token: self.clone(),
}
}
pub fn parse<V>(&self, v: V) -> Result<DeployedTokenAmount<u64, Self>, MoneyAmountParseError>
where
V: TryInto<MoneyAmount>,
MoneyAmountParseError: From<<V as TryInto<MoneyAmount>>::Error>,
{
let amount: u64 = v.try_into()?.to_token_amount(self.decimals)?;
Ok(DeployedTokenAmount {
amount,
token: self.clone(),
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "test assertions")]
mod tests {
use super::*;
#[test]
fn address_requires_long_hex() {
assert!(USDC_TESTNET_FA.parse::<AptosAddress>().is_ok());
assert!("0x1".parse::<AptosAddress>().is_err());
assert!("".parse::<AptosAddress>().is_err());
assert!(
"0x0000000000000000000000000000000000000000000000000000000000000001"
.parse::<AptosAddress>()
.is_ok()
);
assert!(!is_aptos_address(
"0xG000000000000000000000000000000000000000000000000000000000000001"
));
}
#[test]
fn token_amount_decimal_string() {
let amount: AptosTokenAmount = "1000".parse().unwrap();
assert_eq!(amount.as_u64().unwrap(), 1000);
assert!("".parse::<AptosTokenAmount>().is_err());
assert!("1.0".parse::<AptosTokenAmount>().is_err());
assert!("-1".parse::<AptosTokenAmount>().is_err());
}
#[test]
fn chain_reference_roundtrip() {
let chain_id: ChainId = AptosChainReference::TESTNET.into();
assert_eq!(chain_id.to_string(), "aptos:2");
let back = AptosChainReference::try_from(chain_id).unwrap();
assert_eq!(back, AptosChainReference::TESTNET);
assert_eq!(AptosChainReference::MAINNET.chain_id(), 1);
assert!(is_aptos_network("aptos:1"));
assert!(!is_aptos_network("eip155:1"));
}
#[test]
fn token_deployment_parse() {
let addr: AptosAddress = USDC_TESTNET_FA.parse().unwrap();
let deployment = AptosTokenDeployment::new(AptosChainReference::TESTNET, addr, 6);
let parsed = deployment.parse("10.50").unwrap();
assert_eq!(parsed.amount, 10_500_000);
}
}