use std::fmt::{Debug, Display, Formatter};
use std::str::FromStr;
use r402::amount::{MoneyAmount, MoneyAmountParseError};
use r402::chain::{ChainId, DeployedTokenAmount};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use solana_pubkey::Pubkey;
pub const SOLANA_NAMESPACE: &str = "solana";
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SolanaChainReference([u8; 32]);
impl SolanaChainReference {
pub const SOLANA: Self = Self::new(*b"5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
pub const SOLANA_DEVNET: Self = Self::new(*b"EtWTRABZaYq6iMfeYKouRu166VU2xqa1");
#[must_use]
pub const fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
#[allow(
clippy::expect_used,
reason = "construction validates ASCII, UTF-8 conversion is infallible"
)]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).expect("SolanaChainReference contains valid ASCII")
}
}
impl Debug for SolanaChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("SolanaChainReference(")?;
f.write_str(self.as_str())?;
f.write_str(")")
}
}
impl FromStr for SolanaChainReference {
type Err = SolanaChainReferenceFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !(s.is_ascii() && s.len() == 32) {
return Err(SolanaChainReferenceFormatError::InvalidReference(
s.to_owned(),
));
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(s.as_bytes());
Ok(Self(bytes))
}
}
impl Display for SolanaChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for SolanaChainReference {
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 SolanaChainReference {
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<SolanaChainReference> for ChainId {
fn from(value: SolanaChainReference) -> Self {
Self::new(SOLANA_NAMESPACE, value.as_str())
}
}
impl TryFrom<ChainId> for SolanaChainReference {
type Error = SolanaChainReferenceFormatError;
fn try_from(value: ChainId) -> Result<Self, Self::Error> {
let (namespace, reference) = value.into_parts();
if namespace != SOLANA_NAMESPACE {
return Err(SolanaChainReferenceFormatError::InvalidNamespace(namespace));
}
let solana_chain_reference = Self::from_str(&reference)
.map_err(|_| SolanaChainReferenceFormatError::InvalidReference(reference))?;
Ok(solana_chain_reference)
}
}
#[derive(Debug, thiserror::Error)]
pub enum SolanaChainReferenceFormatError {
#[error("Invalid namespace {0}, expected solana")]
InvalidNamespace(String),
#[error("Invalid solana chain reference {0}")]
InvalidReference(String),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct SolanaTokenDeployment {
pub chain_reference: SolanaChainReference,
pub address: Address,
pub decimals: u8,
}
impl SolanaTokenDeployment {
#[must_use]
pub const fn new(
chain_reference: SolanaChainReference,
address: Address,
decimals: u8,
) -> Self {
Self {
chain_reference,
address,
decimals,
}
}
#[must_use]
pub const fn amount(&self, v: u64) -> DeployedTokenAmount<u64, Self> {
DeployedTokenAmount {
amount: v,
token: *self,
}
}
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 = v.try_into()?.to_token_amount(self.decimals)?;
Ok(DeployedTokenAmount {
amount,
token: *self,
})
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct Address(Pubkey);
impl Address {
#[must_use]
pub const fn new(pubkey: Pubkey) -> Self {
Self(pubkey)
}
#[must_use]
pub const fn pubkey(&self) -> &Pubkey {
&self.0
}
}
impl From<Pubkey> for Address {
fn from(pubkey: Pubkey) -> Self {
Self(pubkey)
}
}
impl From<Address> for Pubkey {
fn from(address: Address) -> Self {
address.0
}
}
impl AsRef<[u8]> for Address {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let base58_string = self.0.to_string();
serializer.serialize_str(&base58_string)
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let pubkey = Pubkey::from_str(&s)
.map_err(|_| serde::de::Error::custom("Failed to decode Solana address"))?;
Ok(Self(pubkey))
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Address {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let pubkey =
Pubkey::from_str(s).map_err(|_| format!("Failed to decode Solana address: {s}"))?;
Ok(Self(pubkey))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_deployment(decimals: u8) -> SolanaTokenDeployment {
let chain_ref = SolanaChainReference::SOLANA;
let address = Address::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZ5nc4pb").unwrap();
SolanaTokenDeployment::new(chain_ref, address, decimals)
}
#[test]
fn test_parse_whole_number() {
let deployment = create_test_deployment(6); let result = deployment.parse("100");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 100_000_000); }
#[test]
fn test_parse_with_decimals() {
let deployment = create_test_deployment(6);
let result = deployment.parse("1.50");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 1_500_000); }
#[test]
fn test_parse_zero_decimals() {
let deployment = create_test_deployment(0);
let result = deployment.parse("42");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 42);
}
#[test]
fn test_parse_precision_too_high() {
let deployment = create_test_deployment(2); let result = deployment.parse("1.234"); assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, MoneyAmountParseError::WrongPrecision { .. }));
}
#[test]
fn test_parse_exact_precision() {
let deployment = create_test_deployment(9); let result = deployment.parse("0.123456789");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 123_456_789);
}
#[test]
fn test_parse_smallest_amount() {
let deployment = create_test_deployment(6);
let result = deployment.parse("0.000001");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 1);
}
#[test]
fn test_parse_with_currency_symbol() {
let deployment = create_test_deployment(6);
let result = deployment.parse("$10.50");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 10_500_000);
}
#[test]
fn test_parse_with_commas() {
let deployment = create_test_deployment(6);
let result = deployment.parse("1,000");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 1_000_000_000);
}
#[test]
fn test_parse_large_amount() {
let deployment = create_test_deployment(6);
let result = deployment.parse("999999999");
assert!(result.is_ok());
assert_eq!(result.unwrap().amount, 999_999_999_000_000);
}
#[test]
fn test_parse_overflow_returns_error() {
let deployment = create_test_deployment(19);
let result = deployment.parse("999999999");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
MoneyAmountParseError::OutOfRange
));
}
}