use std::{fmt, str::FromStr};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Api, Coin, CosmosMsg};
use cw_address_like::AddressLike;
use crate::{Asset, AssetBase, AssetError, AssetInfo, AssetUnchecked};
#[cw_serde]
pub struct AssetListBase<T: AddressLike>(Vec<AssetBase<T>>);
#[allow(clippy::derivable_impls)] impl<T: AddressLike> Default for AssetListBase<T> {
fn default() -> Self {
Self(vec![])
}
}
pub type AssetListUnchecked = AssetListBase<String>;
pub type AssetList = AssetListBase<Addr>;
impl FromStr for AssetListUnchecked {
type Err = AssetError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(Self(vec![]));
}
s.split(',').map(AssetUnchecked::from_str).collect::<Result<_, _>>().map(Self)
}
}
impl From<AssetList> for AssetListUnchecked {
fn from(list: AssetList) -> Self {
Self(list.to_vec().iter().cloned().map(|asset| asset.into()).collect())
}
}
impl AssetListUnchecked {
pub fn check(
&self,
api: &dyn Api,
optional_whitelist: Option<&[&str]>,
) -> Result<AssetList, AssetError> {
self.0
.iter()
.map(|asset| asset.check(api, optional_whitelist))
.collect::<Result<Vec<_>, _>>()
.map(AssetList::from)
}
}
impl fmt::Display for AssetList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = if self.is_empty() {
"[]".to_string()
} else {
self.0.iter().map(|asset| asset.to_string()).collect::<Vec<_>>().join(",")
};
write!(f, "{s}")
}
}
impl std::ops::Index<usize> for AssetList {
type Output = Asset;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Index<usize> for &AssetList {
type Output = Asset;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl<'a> IntoIterator for &'a AssetList {
type Item = &'a Asset;
type IntoIter = std::slice::Iter<'a, Asset>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl From<Vec<Asset>> for AssetList {
fn from(vec: Vec<Asset>) -> Self {
Self(vec)
}
}
impl From<&Vec<Asset>> for AssetList {
fn from(vec: &Vec<Asset>) -> Self {
Self(vec.clone())
}
}
impl From<&[Asset]> for AssetList {
fn from(vec: &[Asset]) -> Self {
vec.to_vec().into()
}
}
impl From<Vec<Coin>> for AssetList {
fn from(coins: Vec<Coin>) -> Self {
(&coins).into()
}
}
impl From<&Vec<Coin>> for AssetList {
fn from(coins: &Vec<Coin>) -> Self {
Self(coins.iter().map(|coin| coin.into()).collect())
}
}
impl From<&[Coin]> for AssetList {
fn from(coins: &[Coin]) -> Self {
coins.to_vec().into()
}
}
impl AssetList {
pub fn new() -> Self {
AssetListBase::default()
}
pub fn to_vec(&self) -> Vec<Asset> {
self.0.clone()
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn find(&self, info: &AssetInfo) -> Option<&Asset> {
self.0.iter().find(|asset| asset.info == *info)
}
pub fn apply<F: FnMut(&mut Asset)>(&mut self, f: F) -> &mut Self {
self.0.iter_mut().for_each(f);
self
}
pub fn purge(&mut self) -> &mut Self {
self.0.retain(|asset| !asset.amount.is_zero());
self
}
pub fn add(&mut self, asset_to_add: &Asset) -> Result<&mut Self, AssetError> {
match self.0.iter_mut().find(|asset| asset.info == asset_to_add.info) {
Some(asset) => {
asset.amount = asset.amount.checked_add(asset_to_add.amount)?;
},
None => {
self.0.push(asset_to_add.clone());
},
}
Ok(self.purge())
}
pub fn add_many(&mut self, assets_to_add: &AssetList) -> Result<&mut Self, AssetError> {
for asset in &assets_to_add.0 {
self.add(asset)?;
}
Ok(self)
}
pub fn deduct(&mut self, asset_to_deduct: &Asset) -> Result<&mut Self, AssetError> {
match self.0.iter_mut().find(|asset| asset.info == asset_to_deduct.info) {
Some(asset) => {
asset.amount = asset.amount.checked_sub(asset_to_deduct.amount)?;
},
None => {
return Err(AssetError::NotFoundInList {
info: asset_to_deduct.info.to_string(),
});
},
}
Ok(self.purge())
}
pub fn deduct_many(&mut self, assets_to_deduct: &AssetList) -> Result<&mut Self, AssetError> {
for asset in &assets_to_deduct.0 {
self.deduct(asset)?;
}
Ok(self)
}
pub fn transfer_msgs<A: Into<String> + Clone>(
&self,
to: A,
) -> Result<Vec<CosmosMsg>, AssetError> {
self.0.iter().map(|asset| asset.transfer_msg(to.clone())).collect()
}
}
#[cfg(test)]
mod test_helpers {
use super::*;
use crate::Asset;
pub fn uluna() -> AssetInfo {
AssetInfo::native("uluna")
}
pub fn uusd() -> AssetInfo {
AssetInfo::native("uusd")
}
pub fn mock_token() -> AssetInfo {
AssetInfo::cw20(Addr::unchecked("cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q"))
}
pub fn mock_list() -> AssetList {
AssetList::from(vec![Asset::native("uusd", 69420u128), Asset::new(mock_token(), 88888u128)])
}
}
#[cfg(test)]
mod tests {
use cosmwasm_std::{
testing::MockApi, to_json_binary, BankMsg, Coin, CosmosMsg, OverflowError,
OverflowOperation, StdError, Uint128, WasmMsg,
};
use cw20::Cw20ExecuteMsg;
use super::{
super::asset::{Asset, AssetUnchecked},
test_helpers::{mock_list, mock_token, uluna, uusd},
*,
};
#[test]
fn from_string() {
let s = "";
assert_eq!(AssetListUnchecked::from_str(s).unwrap(), AssetListBase::<String>(vec![]));
let s = "native:uusd:69420,cw20:mock_token";
assert_eq!(
AssetListUnchecked::from_str(s),
Err(AssetError::InvalidAssetFormat {
received: "cw20:mock_token".into(),
}),
);
let s = "native:uusd:69420,cw721:galactic_punk:1";
assert_eq!(
AssetListUnchecked::from_str(s),
Err(AssetError::InvalidAssetType {
ty: "cw721".into(),
}),
);
let s = "native:uusd:69420,cw20:mock_token:ngmi";
assert_eq!(
AssetListUnchecked::from_str(s),
Err(AssetError::InvalidAssetAmount {
amount: "ngmi".into(),
}),
);
let s = "native:uusd:69420,cw20:cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q: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:cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q:88888"
)
);
let list = AssetList::from(vec![] as Vec<Asset>);
assert_eq!(list.to_string(), String::from("[]"));
}
#[test]
fn indexing() {
let list = mock_list();
let vec = list.to_vec();
assert_eq!(list[0], vec[0]);
assert_eq!(list[1], vec[1]);
}
#[test]
fn iterating() {
let list = mock_list();
let strs: Vec<String> = list.into_iter().map(|asset| asset.to_string()).collect();
assert_eq!(
strs,
vec![
String::from("native:uusd:69420"),
String::from("cw20:cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q:88888"),
]
);
}
#[test]
fn checking() {
let api = MockApi::default().with_prefix("cosmos");
let checked = mock_list();
let unchecked: AssetListUnchecked = checked.clone().into();
assert_eq!(unchecked.check(&api, None).unwrap(), checked);
assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna"])).unwrap(), checked);
assert_eq!(
unchecked.check(&api, Some(&["uatom", "uosmo", "uscrt"])),
Err(AssetError::UnacceptedDenom {
denom: "uusd".into(),
whitelist: "uatom|uosmo|uscrt".into(),
}),
);
}
#[test]
fn checking_uppercase() {
let api = MockApi::default();
let mut token_addr = api.addr_make("mock_token");
token_addr = Addr::unchecked(token_addr.into_string().to_uppercase());
let unchecked = AssetListBase(vec![
AssetUnchecked::native("uusd", 69420u128),
AssetUnchecked::cw20(token_addr, 88888u128),
]);
assert_eq!(
unchecked.check(&api, None).unwrap_err(),
StdError::generic_err("Invalid input: address not normalized").into(),
);
}
#[test]
fn finding() {
let list = mock_list();
let asset_option = list.find(&uusd());
assert_eq!(asset_option, Some(&Asset::new(uusd(), 69420u128)));
let asset_option = list.find(&mock_token());
assert_eq!(asset_option, Some(&Asset::new(mock_token(), 88888u128)));
}
#[test]
fn applying() {
let mut list = mock_list();
list.apply(|asset: &mut Asset| asset.amount = asset.amount.multiply_ratio(1u128, 2u128));
assert_eq!(
list,
AssetList::from(vec![
Asset::native("uusd", 34710u128),
Asset::new(mock_token(), 44444u128)
]),
);
}
#[test]
fn adding() {
let mut list = mock_list();
list.add(&Asset::new(uluna(), 12345u128)).unwrap();
let asset = list.find(&uluna()).unwrap();
assert_eq!(asset.amount, Uint128::new(12345));
list.add(&Asset::new(uusd(), 1u128)).unwrap();
let asset = list.find(&uusd()).unwrap();
assert_eq!(asset.amount, Uint128::new(69421));
}
#[test]
fn adding_many() {
let mut list = mock_list();
list.add_many(&mock_list()).unwrap();
let expected = mock_list().apply(|a| a.amount *= Uint128::new(2)).clone();
assert_eq!(list, expected);
}
#[test]
fn deducting() {
let mut list = mock_list();
list.deduct(&Asset::new(uusd(), 12345u128)).unwrap();
let asset = list.find(&uusd()).unwrap();
assert_eq!(asset.amount, Uint128::new(57075));
list.deduct(&Asset::new(uusd(), 57075u128)).unwrap();
let asset_option = list.find(&uusd());
assert_eq!(asset_option, None);
let err = list.deduct(&Asset::new(uusd(), 57075u128));
assert_eq!(
err,
Err(AssetError::NotFoundInList {
info: "native:uusd".into(),
}),
);
list.deduct(&Asset::new(mock_token(), 12345u128)).unwrap();
let asset = list.find(&mock_token()).unwrap();
assert_eq!(asset.amount, Uint128::new(76543));
let err = list.deduct(&Asset::new(mock_token(), 99999u128));
assert_eq!(err, Err(OverflowError::new(OverflowOperation::Sub,).into()),);
}
#[test]
fn deducting_many() {
let mut list = mock_list();
list.deduct_many(&mock_list()).unwrap();
assert_eq!(list, AssetList::new());
}
#[test]
fn creating_messages() {
let list = mock_list();
let msgs = list.transfer_msgs("alice").unwrap();
assert_eq!(
msgs,
vec![
CosmosMsg::Bank(BankMsg::Send {
to_address: String::from("alice"),
amount: vec![Coin::new(69420u128, "uusd")]
}),
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: String::from("cosmos1c4k24jzduc365kywrsvf5ujz4ya6mwymy8vq4q"),
msg: to_json_binary(&Cw20ExecuteMsg::Transfer {
recipient: String::from("alice"),
amount: Uint128::new(88888)
})
.unwrap(),
funds: vec![]
}),
],
);
}
}