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 NEAR_NAMESPACE: &str = "near";
#[allow(clippy::doc_markdown, reason = "FastNEAR is a product name")]
pub const NEAR_MAINNET_RPC_URL: &str = "https://rpc.mainnet.fastnear.com";
#[allow(clippy::doc_markdown, reason = "FastNEAR is a product name")]
pub const NEAR_TESTNET_RPC_URL: &str = "https://rpc.testnet.fastnear.com";
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum NearChainReference {
Mainnet,
Testnet,
}
impl NearChainReference {
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 => "mainnet",
Self::Testnet => "testnet",
}
}
#[must_use]
pub const fn default_rpc_url(self) -> &'static str {
match self {
Self::Mainnet => NEAR_MAINNET_RPC_URL,
Self::Testnet => NEAR_TESTNET_RPC_URL,
}
}
}
impl Debug for NearChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "NearChainReference({})", self.as_str())
}
}
impl Display for NearChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for NearChainReference {
type Err = NearChainReferenceFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mainnet" => Ok(Self::Mainnet),
"testnet" => Ok(Self::Testnet),
other => Err(NearChainReferenceFormatError::InvalidReference(
other.to_owned(),
)),
}
}
}
impl Serialize for NearChainReference {
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 NearChainReference {
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<NearChainReference> for ChainId {
fn from(value: NearChainReference) -> Self {
Self::new(NEAR_NAMESPACE, value.as_str())
}
}
impl TryFrom<ChainId> for NearChainReference {
type Error = NearChainReferenceFormatError;
fn try_from(value: ChainId) -> Result<Self, Self::Error> {
let (namespace, reference) = value.into_parts();
if namespace != NEAR_NAMESPACE {
return Err(NearChainReferenceFormatError::InvalidNamespace(namespace));
}
Self::from_str(&reference)
.map_err(|_| NearChainReferenceFormatError::InvalidReference(reference))
}
}
#[derive(Debug, thiserror::Error)]
pub enum NearChainReferenceFormatError {
#[error("Invalid namespace {0}, expected near")]
InvalidNamespace(String),
#[error("Invalid near chain reference {0}")]
InvalidReference(String),
}
#[must_use]
pub fn is_near_network(network: &str) -> bool {
network == "near:mainnet" || network == "near:testnet"
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct NearAddress(CompactString);
impl NearAddress {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Display for NearAddress {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for NearAddress {
type Err = NearAddressFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate_account_id(s)?;
Ok(Self(CompactString::from(s)))
}
}
impl Serialize for NearAddress {
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 NearAddress {
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 NearAddress {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Debug, thiserror::Error)]
pub enum NearAddressFormatError {
#[error("invalid near account id: {0}")]
Invalid(String),
}
fn validate_account_id(s: &str) -> Result<(), NearAddressFormatError> {
let len = s.len();
if !(2..=64).contains(&len) {
return Err(NearAddressFormatError::Invalid(s.to_owned()));
}
if s.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
&& len == 64
{
return Ok(());
}
let bytes = s.as_bytes();
let mut prev_separator = true;
for &b in bytes {
let is_separator = matches!(b, b'.' | b'-' | b'_');
if is_separator {
if prev_separator {
return Err(NearAddressFormatError::Invalid(s.to_owned()));
}
prev_separator = true;
continue;
}
if !b.is_ascii_lowercase() && !b.is_ascii_digit() {
return Err(NearAddressFormatError::Invalid(s.to_owned()));
}
prev_separator = false;
}
if prev_separator {
return Err(NearAddressFormatError::Invalid(s.to_owned()));
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NearTokenAmount(CompactString);
impl NearTokenAmount {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn as_u128(&self) -> Result<u128, NearTokenAmountFormatError> {
self.0
.parse()
.map_err(|_| NearTokenAmountFormatError::Invalid(self.0.to_string()))
}
}
impl Display for NearTokenAmount {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for NearTokenAmount {
type Err = NearTokenAmountFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return Err(NearTokenAmountFormatError::Invalid(s.to_owned()));
}
let _: u128 = s
.parse()
.map_err(|_| NearTokenAmountFormatError::Invalid(s.to_owned()))?;
Ok(Self(CompactString::from(s)))
}
}
impl Serialize for NearTokenAmount {
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 NearTokenAmount {
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 NearTokenAmount {
fn from(value: u128) -> Self {
Self(CompactString::from(value.to_string()))
}
}
impl TryFrom<NearTokenAmount> for u128 {
type Error = NearTokenAmountFormatError;
fn try_from(value: NearTokenAmount) -> Result<Self, Self::Error> {
value.as_u128()
}
}
#[derive(Debug, thiserror::Error)]
pub enum NearTokenAmountFormatError {
#[error("invalid near token amount: {0}")]
Invalid(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NearTokenDeployment {
pub chain_reference: NearChainReference,
pub address: NearAddress,
pub decimals: u8,
}
impl NearTokenDeployment {
#[must_use]
pub const fn new(
chain_reference: NearChainReference,
address: NearAddress,
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 named_and_implicit_addresses() {
assert!("alice.testnet".parse::<NearAddress>().is_ok());
assert!(
"17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1"
.parse::<NearAddress>()
.is_ok()
);
assert!("Alice.testnet".parse::<NearAddress>().is_err());
assert!("a".parse::<NearAddress>().is_err());
assert!(".alice".parse::<NearAddress>().is_err());
assert!("alice.".parse::<NearAddress>().is_err());
assert!("alice..testnet".parse::<NearAddress>().is_err());
}
#[test]
fn token_amount_decimal_string() {
let amount: NearTokenAmount = "1000000".parse().unwrap();
assert_eq!(amount.as_u128().unwrap(), 1_000_000);
assert!("".parse::<NearTokenAmount>().is_err());
assert!("1.0".parse::<NearTokenAmount>().is_err());
assert!("-1".parse::<NearTokenAmount>().is_err());
}
#[test]
fn chain_reference_roundtrip() {
let chain_id: ChainId = NearChainReference::TESTNET.into();
assert_eq!(chain_id.to_string(), "near:testnet");
let back = NearChainReference::try_from(chain_id).unwrap();
assert_eq!(back, NearChainReference::TESTNET);
assert!(is_near_network("near:mainnet"));
assert!(!is_near_network("eip155:1"));
}
#[test]
fn token_deployment_parse() {
let addr: NearAddress = "usdc.testnet".parse().unwrap();
let deployment = NearTokenDeployment::new(NearChainReference::TESTNET, addr, 6);
let parsed = deployment.parse("10.50").unwrap();
assert_eq!(parsed.amount, 10_500_000);
}
}