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};
use tonlib_core::TonAddress;
use crate::{TONCENTER_MAINNET_BASE_URL, TONCENTER_TESTNET_BASE_URL};
pub const TVM_NAMESPACE: &str = "tvm";
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum TvmChainReference {
Mainnet,
Testnet,
}
impl TvmChainReference {
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 => "-239",
Self::Testnet => "-3",
}
}
#[must_use]
pub const fn global_id(self) -> i32 {
match self {
Self::Mainnet => -239,
Self::Testnet => -3,
}
}
#[must_use]
pub const fn default_rpc_url(self) -> &'static str {
match self {
Self::Mainnet => TONCENTER_MAINNET_BASE_URL,
Self::Testnet => TONCENTER_TESTNET_BASE_URL,
}
}
}
impl Debug for TvmChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TvmChainReference({})", self.as_str())
}
}
impl Display for TvmChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for TvmChainReference {
type Err = TvmChainReferenceFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"-239" => Ok(Self::Mainnet),
"-3" => Ok(Self::Testnet),
other => Err(TvmChainReferenceFormatError::InvalidReference(
other.to_owned(),
)),
}
}
}
impl Serialize for TvmChainReference {
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 TvmChainReference {
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<TvmChainReference> for ChainId {
fn from(value: TvmChainReference) -> Self {
Self::new(TVM_NAMESPACE, value.as_str())
}
}
impl TryFrom<ChainId> for TvmChainReference {
type Error = TvmChainReferenceFormatError;
fn try_from(value: ChainId) -> Result<Self, Self::Error> {
let (namespace, reference) = value.into_parts();
if namespace != TVM_NAMESPACE {
return Err(TvmChainReferenceFormatError::InvalidNamespace(namespace));
}
Self::from_str(&reference)
.map_err(|_| TvmChainReferenceFormatError::InvalidReference(reference))
}
}
#[derive(Debug, thiserror::Error)]
pub enum TvmChainReferenceFormatError {
#[error("Invalid namespace {0}, expected tvm")]
InvalidNamespace(String),
#[error("Invalid tvm chain reference {0}")]
InvalidReference(String),
}
#[must_use]
pub fn is_tvm_network(network: &str) -> bool {
network == "tvm:-239" || network == "tvm:-3"
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TvmAddress(CompactString);
impl TvmAddress {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_ton(&self) -> Result<TonAddress, TvmAddressFormatError> {
TonAddress::from_str(self.as_str())
.map_err(|e| TvmAddressFormatError::Invalid(e.to_string()))
}
}
impl Display for TvmAddress {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for TvmAddress {
type Err = TvmAddressFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed =
TonAddress::from_str(s).map_err(|e| TvmAddressFormatError::Invalid(e.to_string()))?;
Ok(Self(CompactString::from(parsed.to_hex())))
}
}
impl TryFrom<&TonAddress> for TvmAddress {
type Error = TvmAddressFormatError;
fn try_from(value: &TonAddress) -> Result<Self, Self::Error> {
Ok(Self(CompactString::from(value.to_hex())))
}
}
impl Serialize for TvmAddress {
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 TvmAddress {
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 TvmAddress {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, thiserror::Error)]
pub enum TvmAddressFormatError {
#[error("invalid tvm address: {0}")]
Invalid(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TvmTokenAmount(CompactString);
impl TvmTokenAmount {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn as_u128(&self) -> Result<u128, TvmTokenAmountFormatError> {
self.0
.parse()
.map_err(|_| TvmTokenAmountFormatError::Invalid(self.0.to_string()))
}
}
impl Display for TvmTokenAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for TvmTokenAmount {
type Err = TvmTokenAmountFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(TvmTokenAmountFormatError::Invalid(s.to_owned()));
}
let _: u128 = s
.parse()
.map_err(|_| TvmTokenAmountFormatError::Invalid(s.to_owned()))?;
Ok(Self(CompactString::from(s)))
}
}
impl Serialize for TvmTokenAmount {
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 TvmTokenAmount {
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<u128> for TvmTokenAmount {
fn from(value: u128) -> Self {
Self(CompactString::from(value.to_string()))
}
}
impl TryFrom<TvmTokenAmount> for u128 {
type Error = TvmTokenAmountFormatError;
fn try_from(value: TvmTokenAmount) -> Result<Self, Self::Error> {
value.as_u128()
}
}
#[derive(Debug, thiserror::Error)]
pub enum TvmTokenAmountFormatError {
#[error("invalid tvm token amount: {0}")]
Invalid(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TvmTokenDeployment {
pub chain_reference: TvmChainReference,
pub address: TvmAddress,
pub decimals: u8,
}
impl TvmTokenDeployment {
#[must_use]
pub const fn new(
chain_reference: TvmChainReference,
address: TvmAddress,
decimals: u8,
) -> Self {
Self {
chain_reference,
address,
decimals,
}
}
#[must_use]
pub fn amount(&self, v: u128) -> DeployedTokenAmount<u128, Self> {
DeployedTokenAmount {
amount: v,
token: self.clone(),
}
}
pub fn parse<V>(&self, v: V) -> Result<DeployedTokenAmount<u128, Self>, MoneyAmountParseError>
where
V: TryInto<MoneyAmount>,
MoneyAmountParseError: From<<V as TryInto<MoneyAmount>>::Error>,
{
let amount: u128 = 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 chain_id_roundtrips_signed_reference() {
let mainnet: ChainId = TvmChainReference::MAINNET.into();
assert_eq!(mainnet.to_string(), "tvm:-239");
assert_eq!(mainnet.namespace(), "tvm");
assert_eq!(mainnet.reference(), "-239");
let back = TvmChainReference::try_from(mainnet).unwrap();
assert_eq!(back, TvmChainReference::MAINNET);
let testnet: ChainId = TvmChainReference::TESTNET.into();
assert_eq!(testnet.to_string(), "tvm:-3");
assert_eq!(testnet.reference(), "-3");
assert!(is_tvm_network("tvm:-239"));
assert!(is_tvm_network("tvm:-3"));
assert!(!is_tvm_network("eip155:1"));
}
#[test]
fn address_normalizes_raw_and_friendly() {
let raw = "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe";
let parsed: TvmAddress = raw.parse().unwrap();
assert_eq!(parsed.as_str(), raw);
let friendly = parsed.to_ton().unwrap().to_base64_url();
let from_friendly: TvmAddress = friendly.parse().unwrap();
assert_eq!(from_friendly.as_str(), raw);
}
#[test]
fn token_amount_decimal_string() {
let amount: TvmTokenAmount = "10000".parse().unwrap();
assert_eq!(amount.as_u128().unwrap(), 10_000);
assert!("".parse::<TvmTokenAmount>().is_err());
assert!("1.0".parse::<TvmTokenAmount>().is_err());
assert!("-1".parse::<TvmTokenAmount>().is_err());
}
}