diff --git a/src/asset.rs b/src/asset.rs index 57db5f1..df288e6 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,5 +1,6 @@ use std::convert::TryFrom; use std::fmt; +use std::str::FromStr; use cosmwasm_std::{ to_binary, Addr, Api, BankMsg, Binary, Coin, CosmosMsg, StdError, StdResult, Uint128, WasmMsg, @@ -9,7 +10,7 @@ use cw20::Cw20ExecuteMsg; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use super::asset_info::{AssetInfo, AssetInfoBase}; +use super::asset_info::{AssetInfo, AssetInfoBase, AssetInfoUnchecked}; /// Represents a fungible asset with a known amount /// @@ -85,6 +86,28 @@ pub type AssetUnchecked = AssetBase; // Represents an **asset** instance containing only verified data; to be saved in contract storage pub type Asset = AssetBase; +impl FromStr for AssetUnchecked { + type Err = StdError; + + fn from_str(s: &str) -> Result { + let words: Vec<&str> = s.split(":").collect(); + if words.len() != 3 { + return Err(StdError::generic_err( + format!("invalid asset format `{}`; must be in format `native:{{denom}}:{{amount}}` or `cw20:{{contract_addr}}:{{amount}}`", s) + )); + } + + let info = AssetInfoUnchecked::from_str(&format!("{}:{}", words[0], words[1]))?; + let amount = Uint128::from_str(words[2]).map_err( + |_| StdError::generic_err( + format!("invalid asset amount `{}`; must be a 128-bit unsigned integer", words[2]) + ) + )?; + + Ok(AssetUnchecked { info, amount }) + } +} + impl From for AssetUnchecked { fn from(asset: Asset) -> Self { AssetUnchecked { @@ -441,7 +464,40 @@ mod tests { } #[test] - fn displaying() { + fn from_string() { + let s = "native:uusd:12345:67890"; + assert_eq!( + AssetUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset format `native:uusd:12345:67890`; must be in format `native:{denom}:{amount}` or `cw20:{contract_addr}:{amount}`")), + ); + + let s = "cw721:galactic_punk:1"; + assert_eq!( + AssetUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset type `cw721`; must be `native` or `cw20`")), + ); + + let s = "native:uusd:ngmi"; + assert_eq!( + AssetUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset amount `ngmi`; must be a 128-bit unsigned integer")), + ); + + let s = "native:uusd:12345"; + assert_eq!( + AssetUnchecked::from_str(s).unwrap(), + AssetUnchecked::native("uusd", 12345u128), + ); + + let s = "cw20:mock_token:12345"; + assert_eq!( + AssetUnchecked::from_str(s).unwrap(), + AssetUnchecked::cw20("mock_token", 12345u128), + ); + } + + #[test] + fn to_string() { let asset = Asset::native("uusd", 69420u128); assert_eq!(asset.to_string(), String::from("native:uusd:69420")); diff --git a/src/asset_info.rs b/src/asset_info.rs index 2a53847..704f2c2 100644 --- a/src/asset_info.rs +++ b/src/asset_info.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::str::FromStr; use cosmwasm_std::{ to_binary, Addr, Api, BalanceResponse, BankQuery, QuerierWrapper, QueryRequest, StdError, @@ -56,6 +57,27 @@ pub type AssetInfoUnchecked = AssetInfoBase; /// Represents an **asset info** instance containing only verified data; to be saved in contract storage pub type AssetInfo = AssetInfoBase; +impl FromStr for AssetInfoUnchecked { + type Err = StdError; + + fn from_str(s: &str) -> Result { + let words: Vec<&str> = s.split(":").collect(); + if words.len() != 2 { + return Err(StdError::generic_err( + format!("invalid asset info format `{}`; must be in format `native:{{denom}}` or `cw20:{{contract_addr}}`", s) + )); + } + + match words[0] { + "native" => Ok(AssetInfoUnchecked::Native(String::from(words[1]))), + "cw20" => Ok(AssetInfoUnchecked::Cw20(String::from(words[1]))), + ty => Err(StdError::generic_err( + format!("invalid asset type `{}`; must be `native` or `cw20`", ty) + )) + } + } +} + impl From for AssetInfoUnchecked { fn from(asset_info: AssetInfo) -> Self { match &asset_info { @@ -260,7 +282,34 @@ mod test { } #[test] - fn displaying() { + fn from_string() { + let s = "native:uusd:12345"; + assert_eq!( + AssetInfoUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset info format `native:uusd:12345`; must be in format `native:{denom}` or `cw20:{contract_addr}`")), + ); + + let s = "cw721:galactic_punk"; + assert_eq!( + AssetInfoUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset type `cw721`; must be `native` or `cw20`")), + ); + + let s = "native:uusd"; + assert_eq!( + AssetInfoUnchecked::from_str(s).unwrap(), + AssetInfoUnchecked::native("uusd"), + ); + + let s = "cw20:mock_token"; + assert_eq!( + AssetInfoUnchecked::from_str(s).unwrap(), + AssetInfoUnchecked::cw20("mock_token"), + ); + } + + #[test] + fn to_string() { let info = AssetInfo::native("uusd"); assert_eq!(info.to_string(), String::from("native:uusd")); diff --git a/src/asset_list.rs b/src/asset_list.rs index b441f57..7734dae 100644 --- a/src/asset_list.rs +++ b/src/asset_list.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::str::FromStr; #[cfg(feature = "legacy")] use std::convert::TryInto; @@ -7,7 +8,7 @@ use cosmwasm_std::{Addr, Api, Coin, CosmosMsg, StdError, StdResult}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use super::asset::{Asset, AssetBase}; +use super::asset::{Asset, AssetBase, AssetUnchecked}; use super::asset_info::AssetInfo; /// Represents a list of fungible tokens, each with a known amount @@ -26,6 +27,25 @@ pub type AssetListUnchecked = AssetListBase; /// Represents an **asset list** instance containing only verified data; to be used in contract storage pub type AssetList = AssetListBase; +impl FromStr for AssetListUnchecked { + type Err = StdError; + + fn from_str(s: &str) -> Result { + if s.len() == 0 { + return Ok(Self(vec![])); + } + + Ok(Self( + s + .split(",") + .collect::>() + .iter() + .map(|s| AssetUnchecked::from_str(s)) + .collect::, Self::Err>>()? + )) + } +} + impl From for AssetListUnchecked { fn from(list: AssetList) -> Self { Self(list.to_vec().iter().cloned().map(|asset| asset.into()).collect()) @@ -462,7 +482,34 @@ mod tests { use cw20::Cw20ExecuteMsg; #[test] - fn displaying() { + fn from_string() { + let s = ""; + assert_eq!(AssetListUnchecked::from_str(s).unwrap(), AssetListBase::(vec![])); + + let s = "native:uusd:69420,cw20:mock_token"; + assert_eq!( + AssetListUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset format `cw20:mock_token`; must be in format `native:{denom}:{amount}` or `cw20:{contract_addr}:{amount}`")), + ); + + let s = "native:uusd:69420,cw721:galactic_punk:1"; + assert_eq!( + AssetListUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset type `cw721`; must be `native` or `cw20`")), + ); + + let s = "native:uusd:69420,cw20:mock_token:ngmi"; + assert_eq!( + AssetListUnchecked::from_str(s), + Err(StdError::generic_err("invalid asset amount `ngmi`; must be a 128-bit unsigned integer")), + ); + + let s = "native:uusd:69420,cw20:mock_token:88888"; + assert_eq!(AssetListUnchecked::from_str(s).unwrap(), AssetListUnchecked::from(mock_list())); + } + + #[test] + fn to_string() { let list = mock_list(); assert_eq!(list.to_string(), String::from("native:uusd:69420,cw20:mock_token:88888")); }