abstract-std 0.26.1

Abstract contract interfaces and storage layouts
Documentation
use std::{fmt, str::FromStr};

use cosmwasm_std::{Addr, Api, StdError};

use crate::{error::AbstractError, AbstractResult};

#[cosmwasm_schema::cw_serde]
#[non_exhaustive]
// Need eq and hash for ans scraper
#[cfg_attr(not(target_arch = "wasm32"), derive(Eq, Hash, PartialOrd, Ord))]
pub enum PoolAddressBase<T> {
    SeparateAddresses { swap: T, liquidity: T },
    Contract(T),
    Id(u64),
}

impl<T> PoolAddressBase<T> {
    pub fn contract<C: Into<T>>(contract: C) -> Self {
        Self::Contract(contract.into())
    }
    pub fn id<N: Into<u64>>(id: N) -> Self {
        Self::Id(id.into())
    }
}

/// Actual instance of a PoolAddress with verified data
pub type PoolAddress = PoolAddressBase<Addr>;

impl PoolAddress {
    pub fn expect_contract(&self) -> AbstractResult<Addr> {
        match self {
            PoolAddress::Contract(addr) => Ok(addr.clone()),
            _ => Err(AbstractError::Assert(
                "Pool address not a contract address.".into(),
            )),
        }
    }

    pub fn expect_id(&self) -> AbstractResult<u64> {
        match self {
            PoolAddress::Id(id) => Ok(*id),
            _ => Err(AbstractError::Assert(
                "Pool address not an numerical ID.".into(),
            )),
        }
    }
}
/// Instance of a PoolAddress passed around messages
pub type UncheckedPoolAddress = PoolAddressBase<String>;

impl FromStr for UncheckedPoolAddress {
    type Err = AbstractError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let words: Vec<&str> = s.split(':').collect();

        match words[0] {
            "contract" => {
                if words.len() != 2 {
                    return Err(AbstractError::FormattingError {
                        object: "unchecked pool address".to_string(),
                        expected: "contract:{{contract_addr}}".to_string(),
                        actual: s.to_string(),
                    });
                }

                Ok(UncheckedPoolAddress::Contract(String::from(words[1])))
            }
            "id" => {
                if words.len() != 2 {
                    return Err(AbstractError::FormattingError {
                        object: "unchecked pool address".to_string(),
                        expected: "id:{{pool_id}}".to_string(),
                        actual: s.to_string(),
                    });
                }
                let parsed_id_res = words[1].parse::<u64>();
                match parsed_id_res {
                    Ok(id) => Ok(UncheckedPoolAddress::Id(id)),
                    Err(err) => Err(StdError::generic_err(err.to_string()).into()),
                }
            }
            _unknown => Err(AbstractError::FormattingError {
                object: "unchecked pool address".to_string(),
                expected: "'contract' or 'id'".to_string(),
                actual: s.to_string(),
            }),
        }
    }
}

impl From<PoolAddress> for UncheckedPoolAddress {
    fn from(pool_info: PoolAddress) -> Self {
        match pool_info {
            PoolAddress::Contract(contract_addr) => {
                UncheckedPoolAddress::Contract(contract_addr.into())
            }
            PoolAddress::Id(denom) => UncheckedPoolAddress::Id(denom),
            PoolAddress::SeparateAddresses { swap, liquidity } => {
                UncheckedPoolAddress::SeparateAddresses {
                    swap: swap.into(),
                    liquidity: liquidity.into(),
                }
            }
        }
    }
}

impl From<&PoolAddress> for UncheckedPoolAddress {
    fn from(pool_id: &PoolAddress) -> Self {
        match pool_id {
            PoolAddress::Contract(contract_addr) => {
                UncheckedPoolAddress::Contract(contract_addr.into())
            }
            PoolAddress::Id(denom) => UncheckedPoolAddress::Id(*denom),
            PoolAddress::SeparateAddresses { swap, liquidity } => {
                UncheckedPoolAddress::SeparateAddresses {
                    swap: swap.into(),
                    liquidity: liquidity.into(),
                }
            }
        }
    }
}

impl From<Addr> for PoolAddress {
    fn from(contract_addr: Addr) -> Self {
        PoolAddress::Contract(contract_addr)
    }
}

impl UncheckedPoolAddress {
    /// Validate data contained in an _unchecked_ **pool id** instance; return a new _checked_
    /// **pool id** instance:
    /// * For Contract addresses, assert its address is valid
    ///
    ///
    /// ```rust,no_run
    /// use cosmwasm_std::{Addr, Api};
    /// use abstract_std::{objects::pool_id::UncheckedPoolAddress, AbstractResult};
    ///
    /// fn validate_pool_id(api: &dyn Api, pool_id_unchecked: &UncheckedPoolAddress) {
    ///     match pool_id_unchecked.check(api) {
    ///         Ok(info) => println!("pool id is valid: {}", info.to_string()),
    ///         Err(err) => println!("pool id is invalid! reason: {}", err),
    ///     }
    /// }
    /// ```
    pub fn check(&self, api: &dyn Api) -> AbstractResult<PoolAddress> {
        Ok(match self {
            UncheckedPoolAddress::Contract(contract_addr) => {
                PoolAddress::Contract(api.addr_validate(contract_addr)?)
            }
            UncheckedPoolAddress::Id(pool_id) => PoolAddress::Id(*pool_id),
            UncheckedPoolAddress::SeparateAddresses { swap, liquidity } => {
                PoolAddress::SeparateAddresses {
                    swap: api.addr_validate(swap)?,
                    liquidity: api.addr_validate(liquidity)?,
                }
            }
        })
    }
}

impl fmt::Display for PoolAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PoolAddress::Contract(contract_addr) => write!(f, "contract:{contract_addr}"),
            PoolAddress::Id(pool_id) => write!(f, "id:{pool_id}"),
            PoolAddress::SeparateAddresses { swap, liquidity } => {
                write!(f, "swap:{swap}, pair: {liquidity}")
            }
        }
    }
}

#[cfg(test)]
mod test {
    #![allow(clippy::needless_borrows_for_generic_args)]
    use cosmwasm_std::testing::MockApi;

    use super::*;

    #[coverage_helper::test]
    fn test_pool_id_from_str() {
        let api = MockApi::default();
        let contract_addr = api.addr_make("foo");
        let pool_id_str = format!("contract:{contract_addr}");
        let pool_id = UncheckedPoolAddress::from_str(&pool_id_str).unwrap();
        let pool_id = pool_id.check(&api).unwrap();
        assert_eq!(pool_id.to_string(), pool_id_str.to_string());
    }

    #[coverage_helper::test]
    fn test_expect_contract_happy() {
        let api = MockApi::default();
        let contract_addr = api.addr_make("foo");
        let pool_id = PoolAddress::Contract(contract_addr.clone());
        let res = pool_id.expect_contract();
        assert!(res.is_ok());
        assert_eq!(res.unwrap(), contract_addr);
    }

    #[coverage_helper::test]
    fn test_expect_contract_sad() {
        let pool_id = PoolAddress::Id(1);
        let res = pool_id.expect_contract();
        assert!(res.is_err());
    }

    #[coverage_helper::test]
    fn test_expect_id_happy() {
        let pool_id = PoolAddress::Id(1);
        let res = pool_id.expect_id();
        assert!(res.is_ok());
        assert_eq!(res.unwrap(), 1);
    }

    #[coverage_helper::test]
    fn test_expect_id_sad() {
        let api = MockApi::default();
        let contract_addr = api.addr_make("foo");
        let pool_id = PoolAddress::Contract(contract_addr);
        let res = pool_id.expect_id();
        assert!(res.is_err());
    }
}