#[cfg(feature = "json")]
use std::str::FromStr;
use bincode::{Decode, Encode};
use num_bigint::BigUint;
use num_integer::Integer;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "json")]
use serde_json;
#[cfg(feature = "json")]
use crate::EscrowError;
use crate::error::AssetError;
use crate::{BigNumber, ID, Result};
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, Encode, Decode)]
pub struct Asset {
pub kind: AssetKind,
pub agent_id: Option<ID>,
pub amount: BigNumber,
pub decimals: Option<u8>,
}
#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
pub enum AssetKind {
Native,
Token,
}
impl Asset {
pub fn native(amount: BigNumber) -> Self {
Self {
kind: AssetKind::Native,
agent_id: None,
amount,
decimals: None,
}
}
pub fn token(contract: ID, amount: BigNumber, decimals: u8) -> Self {
Self {
kind: AssetKind::Token,
agent_id: Some(contract),
amount,
decimals: Some(decimals),
}
}
pub fn validate(&self) -> Result<()> {
self.validate_non_zero_amount()
.and_then(|_| self.validate_by_kind())
}
fn validate_non_zero_amount(&self) -> Result<()> {
(self.amount != BigNumber::zero())
.then_some(())
.ok_or_else(|| AssetError::ZeroAmount.into())
}
fn validate_by_kind(&self) -> Result<()> {
match self.kind {
AssetKind::Native => Ok(()),
AssetKind::Token => self.validate_agent_id(),
}
}
fn validate_agent_id(&self) -> Result<()> {
self.agent_id
.as_ref()
.ok_or_else(|| AssetError::MissingId.into())
.and_then(ID::validate)
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
bincode::encode_to_vec(self, bincode::config::standard())
.map_err(|e| AssetError::Serialization(e.to_string()).into())
}
pub fn from_bytes(src: &[u8]) -> Result<Self> {
bincode::decode_from_slice(src, bincode::config::standard())
.map_err(|e| AssetError::Parsing(e.to_string()).into())
.map(|(asset, _)| asset)
}
pub fn format_amount(&self) -> String {
let decimals = self.decimals.unwrap_or(0);
let factor = BigUint::from(10u8).pow(u32::from(decimals));
let (whole, rem) = self.amount.0.div_rem(&factor);
if decimals == 0 {
return whole.to_str_radix(10);
}
format!(
"{}.{:0>width$}",
whole.to_str_radix(10),
rem.to_str_radix(10),
width = usize::from(decimals)
)
}
pub fn amount(&self) -> &BigNumber {
&self.amount
}
}
#[cfg(feature = "json")]
impl std::fmt::Display for Asset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let json = serde_json::to_string(self).map_err(|_| std::fmt::Error)?;
write!(f, "{json}")
}
}
#[cfg(feature = "json")]
impl FromStr for Asset {
type Err = EscrowError;
fn from_str(s: &str) -> Result<Self> {
serde_json::from_str::<Self>(s).map_err(|e| AssetError::Parsing(e.to_string()).into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BigNumber, ID};
fn to_bignum(num: u64) -> BigNumber {
BigNumber::from(num)
}
#[test]
fn native() {
let coin = Asset::native(to_bignum(1));
assert!(coin.validate().is_ok());
let zero_coin = Asset::native(to_bignum(0));
assert!(zero_coin.validate().is_err());
}
#[test]
fn token() {
let token = Asset::token(ID::from(vec![4, 5, 6]), to_bignum(1000), 9);
assert!(token.validate().is_ok());
let empty_agent_id = Asset::token(ID::from(Vec::new()), to_bignum(100), 9);
assert!(empty_agent_id.validate().is_err());
let zero_token = Asset::token(ID::from(vec![1, 2, 3]), to_bignum(0), 6);
assert!(zero_token.validate().is_err());
}
#[test]
fn bincode_roundtrip_native() {
let original = Asset::native(to_bignum(1_000_000_000));
let bytes = original.to_bytes().unwrap();
let decoded = Asset::from_bytes(&bytes).unwrap();
assert!(matches!(decoded.kind, AssetKind::Native));
assert_eq!(decoded.amount, original.amount);
}
#[test]
fn bincode_roundtrip_token() {
let original = Asset::token(ID::from(vec![0xde, 0xad, 0xbe, 0xef]), to_bignum(1_000), 18);
let bytes = original.to_bytes().unwrap();
let decoded = Asset::from_bytes(&bytes).unwrap();
assert!(matches!(decoded.kind, AssetKind::Token));
assert_eq!(decoded.amount, original.amount);
assert_eq!(decoded.decimals, Some(18));
}
#[test]
fn format_amount_with_decimals() {
let asset = Asset::token(ID::from(vec![1]), to_bignum(1_500_000_000), 9);
assert_eq!(asset.format_amount(), "1.500000000");
}
#[test]
fn format_amount_without_decimals() {
let asset = Asset::native(to_bignum(12345));
assert_eq!(asset.format_amount(), "12345");
}
}