diff --git a/CHANGELOG.md b/CHANGELOG.md index 75136e9c10..15ce5df96d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ and this project adheres to - cosmwasm-vm: Add `Cache::save_wasm_unchecked` to save Wasm blobs that have been checked before. This is useful for state-sync where we know the Wasm code was checked when it was first uploaded. ([#1635]) +- cosmwasm-std: Add `FromStr` impl for `Coin`. ([#1684]) [#1635]: https://github.com/CosmWasm/cosmwasm/pull/1635 +[#1684]: https://github.com/CosmWasm/cosmwasm/pull/1684 ### Changed diff --git a/packages/std/src/coin.rs b/packages/std/src/coin.rs index 289911deef..b88a6da3ee 100644 --- a/packages/std/src/coin.rs +++ b/packages/std/src/coin.rs @@ -1,8 +1,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::{fmt, str::FromStr}; -use crate::math::Uint128; +use crate::{errors::CoinFromStrError, math::Uint128}; #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] pub struct Coin { @@ -19,6 +19,26 @@ impl Coin { } } +impl FromStr for Coin { + type Err = CoinFromStrError; + + fn from_str(s: &str) -> Result { + let pos = s + .find(|c: char| !c.is_ascii_digit()) + .ok_or(CoinFromStrError::MissingDenom)?; + let (amount, denom) = s.split_at(pos); + + if amount.is_empty() { + return Err(CoinFromStrError::MissingAmount); + } + + Ok(Coin { + amount: amount.parse::()?.into(), + denom: denom.to_string(), + }) + } +} + impl fmt::Display for Coin { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // We use the formatting without a space between amount and denom, @@ -166,4 +186,53 @@ mod tests { // less than same type assert!(has_coins(&wallet, &coin(777, "ETH"))); } + + #[test] + fn parse_coin() { + let expected = Coin::new(123, "ucosm"); + assert_eq!("123ucosm".parse::().unwrap(), expected); + // leading zeroes should be ignored + assert_eq!("00123ucosm".parse::().unwrap(), expected); + // 0 amount parses correctly + assert_eq!("0ucosm".parse::().unwrap(), Coin::new(0, "ucosm")); + // ibc denom should work + let ibc_str = "11111ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"; + let ibc_coin = Coin::new( + 11111, + "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2", + ); + assert_eq!(ibc_str.parse::().unwrap(), ibc_coin); + + // error cases + assert_eq!( + Coin::from_str("123").unwrap_err(), + CoinFromStrError::MissingDenom + ); + assert_eq!( + Coin::from_str("ucosm").unwrap_err(), // no amount + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("-123ucosm").unwrap_err(), // negative amount + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("").unwrap_err(), // empty input + CoinFromStrError::MissingDenom + ); + assert_eq!( + Coin::from_str(" 1ucosm").unwrap_err(), // unsupported whitespace + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("�1ucosm").unwrap_err(), // other broken data + CoinFromStrError::MissingAmount + ); + assert_eq!( + Coin::from_str("340282366920938463463374607431768211456ucosm") + .unwrap_err() + .to_string(), + "Invalid amount: number too large to fit in target type" + ); + } } diff --git a/packages/std/src/errors/mod.rs b/packages/std/src/errors/mod.rs index 705382b732..5535479bdf 100644 --- a/packages/std/src/errors/mod.rs +++ b/packages/std/src/errors/mod.rs @@ -6,7 +6,7 @@ mod verification_error; pub use recover_pubkey_error::RecoverPubkeyError; pub use std_error::{ CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, - ConversionOverflowError, DivideByZeroError, OverflowError, OverflowOperation, + CoinFromStrError, ConversionOverflowError, DivideByZeroError, OverflowError, OverflowOperation, RoundUpOverflowError, StdError, StdResult, }; pub use system_error::SystemError; diff --git a/packages/std/src/errors/std_error.rs b/packages/std/src/errors/std_error.rs index 79b6a82f27..d90171c74c 100644 --- a/packages/std/src/errors/std_error.rs +++ b/packages/std/src/errors/std_error.rs @@ -590,6 +590,28 @@ pub enum CheckedFromRatioError { #[error("Round up operation failed because of overflow")] pub struct RoundUpOverflowError; +#[derive(Error, Debug, PartialEq, Eq)] +pub enum CoinFromStrError { + #[error("Missing denominator")] + MissingDenom, + #[error("Missing amount or non-digit characters in amount")] + MissingAmount, + #[error("Invalid amount: {0}")] + InvalidAmount(std::num::ParseIntError), +} + +impl From for CoinFromStrError { + fn from(value: std::num::ParseIntError) -> Self { + Self::InvalidAmount(value) + } +} + +impl From for StdError { + fn from(value: CoinFromStrError) -> Self { + Self::generic_err(format!("Parsing Coin: {}", value)) + } +} + #[cfg(test)] mod tests { use super::*;