1#[cfg(feature = "json")]
2use std::str::FromStr;
3
4use bincode::{Decode, Encode};
5use num_bigint::BigUint;
6use num_integer::Integer;
7#[cfg(feature = "json")]
8use serde::{Deserialize, Serialize};
9#[cfg(feature = "json")]
10use serde_json;
11
12use crate::error::AssetError;
13#[cfg(feature = "json")]
14use crate::EscrowError;
15use crate::{BigNumber, Result, ID};
16
17#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
19#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
20#[derive(Debug, Clone, Encode, Decode)]
21pub struct Asset {
22 pub kind: AssetKind,
24
25 pub id: Option<ID>,
27
28 pub agent_id: Option<ID>,
30
31 pub amount: BigNumber,
33
34 pub decimals: Option<u8>,
36
37 pub total_supply: Option<BigNumber>,
39}
40
41#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
43#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
44#[derive(Debug, Clone, Encode, Decode)]
45pub enum AssetKind {
46 Native,
48 Token,
50 Nft,
52 MultiToken,
54 LpShare,
56}
57
58impl Asset {
59 pub fn native(amount: BigNumber) -> Self {
61 Self {
62 kind: AssetKind::Native,
63 id: None,
64 agent_id: None,
65 amount,
66 decimals: None,
67 total_supply: None,
68 }
69 }
70
71 pub fn token(contract: ID, amount: BigNumber, total_supply: BigNumber, decimals: u8) -> Self {
73 Self {
74 kind: AssetKind::Token,
75 id: None,
76 agent_id: Some(contract),
77 amount,
78 decimals: Some(decimals),
79 total_supply: Some(total_supply),
80 }
81 }
82
83 pub fn nft(contract: ID, token_id: ID) -> Self {
85 Self {
86 kind: AssetKind::Nft,
87 id: Some(token_id),
88 agent_id: Some(contract),
89 amount: BigNumber::from(1u64),
90 decimals: None,
91 total_supply: None,
92 }
93 }
94
95 pub fn multi_token(contract: ID, token_id: ID, amount: BigNumber) -> Self {
97 Self {
98 kind: AssetKind::MultiToken,
99 id: Some(token_id),
100 agent_id: Some(contract),
101 amount,
102 decimals: None,
103 total_supply: None,
104 }
105 }
106
107 pub fn pool_share(
109 pool_id: ID,
110 share: BigNumber,
111 total_supply: BigNumber,
112 decimals: u8,
113 ) -> Self {
114 Self {
115 kind: AssetKind::LpShare,
116 id: Some(pool_id),
117 agent_id: None,
118 amount: share,
119 decimals: Some(decimals),
120 total_supply: Some(total_supply),
121 }
122 }
123
124 pub fn validate(&self) -> Result<()> {
132 if self.amount == BigNumber::zero() {
133 return Err(AssetError::ZeroAmount.into());
134 }
135
136 match self.kind {
137 AssetKind::Native => Ok(()),
138
139 AssetKind::Token => self
140 .agent_id
141 .as_ref()
142 .ok_or(AssetError::MissingId)?
143 .validate(),
144
145 AssetKind::Nft | AssetKind::MultiToken => {
146 self.agent_id
147 .as_ref()
148 .ok_or(AssetError::MissingId)?
149 .validate()?;
150
151 self.id.as_ref().ok_or(AssetError::MissingId)?.validate()
152 }
153
154 AssetKind::LpShare => {
155 let pool = self.id.as_ref().ok_or(AssetError::MissingId)?;
156 pool.validate()?;
157
158 let total_supply = self
159 .total_supply
160 .as_ref()
161 .ok_or(AssetError::MissingTotalSupply)?;
162 if *total_supply == BigNumber::zero() {
163 return Err(AssetError::ZeroAmount.into());
164 }
165 if self.amount > *total_supply {
166 return Err(AssetError::InvalidShare(
167 self.amount.clone(),
168 total_supply.clone(),
169 )
170 .into());
171 }
172
173 Ok(())
174 }
175 }
176 }
177
178 pub fn to_bytes(&self) -> Result<Vec<u8>> {
194 bincode::encode_to_vec(self, bincode::config::standard())
195 .map_err(|e| AssetError::Serialization(e.to_string()).into())
196 }
197
198 pub fn from_bytes(src: &[u8]) -> Result<Self> {
217 bincode::decode_from_slice(src, bincode::config::standard())
218 .map_err(|e| AssetError::Parsing(e.to_string()).into())
219 .map(|(asset, _)| asset)
220 }
221
222 pub fn format_amount(&self) -> Result<String> {
224 let decimals = self.decimals.unwrap_or(0);
225 let factor = BigUint::from(10u8).pow(decimals as u32);
226 let (whole, rem) = self.amount.0.div_rem(&factor);
227
228 let rem_str = if decimals > 0 {
229 let s = rem.to_str_radix(10);
230 format!("{:0>width$}", s, width = decimals as usize)
231 } else {
232 String::new()
233 };
234
235 Ok(if decimals > 0 {
236 format!("{}.{}", whole.to_str_radix(10), rem_str)
237 } else {
238 whole.to_str_radix(10)
239 })
240 }
241
242 pub fn amount(&self) -> &BigNumber {
244 &self.amount
245 }
246}
247
248#[cfg(feature = "json")]
249impl std::fmt::Display for Asset {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 let json = serde_json::to_string(self).map_err(|_| std::fmt::Error)?;
253 write!(f, "{json}")
254 }
255}
256
257#[cfg(feature = "json")]
258impl FromStr for Asset {
259 type Err = EscrowError;
260
261 fn from_str(s: &str) -> Result<Self> {
263 serde_json::from_str::<Self>(s).map_err(|e| AssetError::Parsing(e.to_string()).into())
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::{BigNumber, ID};
271
272 fn to_bignum(num: u64) -> BigNumber {
273 BigNumber::from(num)
274 }
275
276 #[test]
277 fn native() {
278 let coin = Asset::native(to_bignum(1));
279 assert!(coin.validate().is_ok());
280
281 let zero_coin = Asset::native(to_bignum(0));
282 assert!(zero_coin.validate().is_err());
283 }
284
285 #[test]
286 fn token() {
287 let token = Asset::token(ID::from(vec![4, 5, 6]), to_bignum(1000), to_bignum(1000), 9);
289 assert!(token.validate().is_ok());
290
291 let empty_agent_id = Asset::token(ID::from(Vec::new()), to_bignum(100), to_bignum(100), 9);
293 assert!(empty_agent_id.validate().is_err());
294
295 let zero_token = Asset::token(ID::from(vec![1, 2, 3]), to_bignum(0), to_bignum(100), 6);
297 assert!(zero_token.validate().is_err());
298 }
299
300 #[test]
301 fn nft() {
302 let nft = Asset::nft(ID::from(vec![7, 8, 9]), ID::from("zescrowNFT".as_bytes()));
304 assert!(nft.validate().is_ok());
305
306 let empty_token_id = Asset::nft(ID::from(vec![7, 8, 9]), ID::from(Vec::new()));
308 assert!(empty_token_id.validate().is_err());
309
310 let empty_contract_id = Asset::nft(ID::from(Vec::new()), ID::from("zescrowNFT".as_bytes()));
312 assert!(empty_contract_id.validate().is_err());
313 }
314
315 #[test]
316 fn multi_token() {
317 let asset = Asset::multi_token(
319 ID::from(vec![1]),
320 ID::from("zescrowToken".as_bytes()),
321 to_bignum(500),
322 );
323 assert!(asset.validate().is_ok());
324
325 let zero_amt = Asset::multi_token(
327 ID::from(vec![1]),
328 ID::from("zescrowToken".as_bytes()),
329 to_bignum(0),
330 );
331 assert!(zero_amt.validate().is_err());
332
333 let bad_id = Asset::multi_token(ID::from(vec![1]), ID::from(Vec::new()), to_bignum(10));
335 assert!(bad_id.validate().is_err());
336
337 let bad_contract = Asset::multi_token(
339 ID::from(Vec::new()),
340 ID::from("zescrowToken".as_bytes()),
341 to_bignum(10),
342 );
343 assert!(bad_contract.validate().is_err());
344 }
345
346 #[test]
347 fn pool_share() {
348 let share = to_bignum(50);
350 let total = to_bignum(100);
351 let valid = Asset::pool_share(ID::from(vec![1]), share.clone(), total.clone(), 0);
352 assert!(valid.validate().is_ok());
353
354 let zero_share = Asset::pool_share(ID::from(vec![1]), to_bignum(0), total.clone(), 0);
356 assert!(zero_share.validate().is_err());
357
358 let zero_total = Asset::pool_share(ID::from(vec![1]), share.clone(), to_bignum(0), 0);
360 assert!(zero_total.validate().is_err());
361
362 let bad_pool = Asset::pool_share(ID::from(Vec::new()), share.clone(), total.clone(), 0);
364 assert!(bad_pool.validate().is_err());
365
366 let too_many = Asset::pool_share(ID::from(vec![1]), to_bignum(150), to_bignum(100), 0);
368 assert!(too_many.validate().is_err());
369 }
370}