1
1
//! Defines coin types; the objects that are being transferred.
2
2
3
3
use core:: fmt:: { Display , Error as FmtError , Formatter } ;
4
- use core:: str:: { from_utf8 , FromStr } ;
4
+ use core:: str:: FromStr ;
5
5
6
6
use ibc_proto:: cosmos:: base:: v1beta1:: Coin as ProtoCoin ;
7
- use safe_regex:: regex;
8
7
9
8
use super :: amount:: Amount ;
10
9
use super :: denom:: { BaseDenom , PrefixedDenom } ;
@@ -19,6 +18,9 @@ pub type BaseCoin = Coin<BaseDenom>;
19
18
20
19
pub type RawCoin = Coin < String > ;
21
20
21
+ /// Allowed characters in string representation of a denomination.
22
+ const VALID_DENOM_CHARACTERS : & str = "/:._-" ;
23
+
22
24
/// Coin defines a token with a denomination and an amount.
23
25
#[ cfg_attr( feature = "serde" , derive( serde:: Serialize , serde:: Deserialize ) ) ]
24
26
#[ cfg_attr( feature = "schema" , derive( schemars:: JsonSchema ) ) ]
@@ -58,25 +60,31 @@ where
58
60
// Denominations can be 3 ~ 128 characters long and support letters, followed by either
59
61
// a letter, a number or a separator ('/', ':', '.', '_' or '-').
60
62
// Loosely copy the regex from here:
61
- // https://github.com/cosmos/cosmos-sdk/blob/v0.45.5/types/coin.go#L760-L762
62
- let matcher = regex ! ( br"([0-9]+)([a-zA-Z0-9/:\\._\x2d]+)" ) ;
63
-
64
- let ( m1, m2) = matcher. match_slices ( coin_str. as_bytes ( ) ) . ok_or_else ( || {
65
- TokenTransferError :: InvalidCoin {
63
+ // https://github.com/cosmos/cosmos-sdk/blob/v0.47.5/types/coin.go#L838-L840
64
+ //
65
+ // equivalent regex code in rust:
66
+ // let re = Regex::new(r"^(?<amount>[0-9]+)(?<denom>[a-zA-Z0-9/:._-]+)$")?;
67
+ // let cap = re.captures("123stake")?;
68
+ // let (amount, denom) = (cap.name("amount")?.as_str(), cap.name("denom")?.as_str());
69
+
70
+ let ( amount, denom) = coin_str
71
+ . chars ( )
72
+ . position ( |x| !x. is_numeric ( ) )
73
+ . map ( |index| coin_str. split_at ( index) )
74
+ . filter ( |( amount, _) | !amount. is_empty ( ) )
75
+ . filter ( |( _, denom) | {
76
+ denom
77
+ . chars ( )
78
+ . all ( |x| x. is_alphanumeric ( ) || VALID_DENOM_CHARACTERS . contains ( x) )
79
+ } )
80
+ . ok_or_else ( || TokenTransferError :: InvalidCoin {
66
81
coin : coin_str. to_string ( ) ,
67
- }
68
- } ) ?;
82
+ } ) ?;
69
83
70
- let amount = from_utf8 ( m1)
71
- . map_err ( TokenTransferError :: Utf8Decode ) ?
72
- . parse ( ) ?;
73
-
74
- let denom = from_utf8 ( m2)
75
- . map_err ( TokenTransferError :: Utf8Decode ) ?
76
- . parse ( )
77
- . map_err ( Into :: into) ?;
78
-
79
- Ok ( Coin { amount, denom } )
84
+ Ok ( Coin {
85
+ amount : amount. parse ( ) ?,
86
+ denom : denom. parse ( ) . map_err ( Into :: into) ?,
87
+ } )
80
88
}
81
89
}
82
90
@@ -119,53 +127,77 @@ impl<D: Display> Display for Coin<D> {
119
127
120
128
#[ cfg( test) ]
121
129
mod tests {
122
- use super :: * ;
130
+ use primitive_types:: U256 ;
131
+ use rstest:: rstest;
123
132
124
- #[ test]
125
- fn test_parse_raw_coin ( ) -> Result < ( ) , TokenTransferError > {
126
- {
127
- let coin = RawCoin :: from_str ( "123stake" ) ?;
128
- assert_eq ! ( coin. denom, "stake" ) ;
129
- assert_eq ! ( coin. amount, 123u64 . into( ) ) ;
130
- }
131
-
132
- {
133
- let coin = RawCoin :: from_str ( "1a1" ) ?;
134
- assert_eq ! ( coin. denom, "a1" ) ;
135
- assert_eq ! ( coin. amount, 1u64 . into( ) ) ;
136
- }
137
-
138
- {
139
- let coin = RawCoin :: from_str ( "0x1/:.\\ _-" ) ?;
140
- assert_eq ! ( coin. denom, "x1/:.\\ _-" ) ;
141
- assert_eq ! ( coin. amount, 0u64 . into( ) ) ;
142
- }
133
+ use super :: * ;
143
134
144
- {
145
- // `!` is not allowed
146
- let res = RawCoin :: from_str ( "0x!" ) ;
147
- assert ! ( res. is_err( ) ) ;
148
- }
135
+ #[ rstest]
136
+ #[ case:: nat( "123stake" , 123 , "stake" ) ]
137
+ #[ case:: zero( "0stake" , 0 , "stake" ) ]
138
+ #[ case:: u256_max(
139
+ "115792089237316195423570985008687907853269984665640564039457584007913129639935stake" ,
140
+ U256 :: MAX ,
141
+ "stake"
142
+ ) ]
143
+ #[ case:: digit_in_denom( "1a1" , 1 , "a1" ) ]
144
+ #[ case:: chars_in_denom( "0x1/:._-" , 0 , "x1/:._-" ) ]
145
+ #[ case:: ibc_denom( "1234ibc/a0B1C" , 1234 , "ibc/a0B1C" ) ]
146
+ fn test_parse_raw_coin (
147
+ #[ case] parsed : RawCoin ,
148
+ #[ case] amount : impl Into < Amount > ,
149
+ #[ case] denom : & str ,
150
+ ) {
151
+ assert_eq ! (
152
+ parsed,
153
+ RawCoin {
154
+ denom: denom. into( ) ,
155
+ amount: amount. into( )
156
+ }
157
+ ) ;
158
+ }
149
159
160
+ #[ rstest]
161
+ #[ case:: pos( "+123stake" ) ]
162
+ #[ case:: pos_zero( "+0stake" ) ]
163
+ #[ case:: neg( "-123stake" ) ]
164
+ #[ case:: neg_zero( "-0stake" ) ]
165
+ #[ case:: u256_max_plus_1(
166
+ "115792089237316195423570985008687907853269984665640564039457584007913129639936stake"
167
+ ) ]
168
+ #[ case:: invalid_char_in_denom( "0x!" ) ]
169
+ #[ case:: blackslash_in_denom( "0x1/:.\\ _-" ) ]
170
+ #[ should_panic]
171
+ fn test_failed_parse_raw_coin ( #[ case] _raw : RawCoin ) { }
172
+
173
+ #[ rstest]
174
+ #[ case:: nomal( "123stake,1a1,999den0m" , & [ ( 123 , "stake" ) , ( 1 , "a1" ) , ( 999 , "den0m" ) ] ) ]
175
+ #[ case:: tricky( "123stake,1a1-999den0m" , & [ ( 123 , "stake" ) , ( 1 , "a1-999den0m" ) ] ) ]
176
+ #[ case:: colon_delimiter( "123stake:1a1:999den0m" , & [ ( 123 , "stake:1a1:999den0m" ) ] ) ]
177
+ #[ case:: dash_delimiter( "123stake-1a1-999den0m" , & [ ( 123 , "stake-1a1-999den0m" ) ] ) ]
178
+ #[ case:: slash_delimiter( "123stake/1a1/999den0m" , & [ ( 123 , "stake/1a1/999den0m" ) ] ) ]
179
+ fn test_parse_raw_coin_list (
180
+ #[ case] coins_str : & str ,
181
+ #[ case] coins : & [ ( u64 , & str ) ] ,
182
+ ) -> Result < ( ) , TokenTransferError > {
183
+ assert_eq ! (
184
+ RawCoin :: from_string_list( coins_str) ?,
185
+ coins
186
+ . iter( )
187
+ . map( |& ( amount, denom) | RawCoin {
188
+ denom: denom. to_string( ) ,
189
+ amount: amount. into( ) ,
190
+ } )
191
+ . collect:: <Vec <_>>( )
192
+ ) ;
150
193
Ok ( ( ) )
151
194
}
152
195
153
- #[ test]
154
- fn test_parse_raw_coin_list ( ) -> Result < ( ) , TokenTransferError > {
155
- {
156
- let coins = RawCoin :: from_string_list ( "123stake,1a1,999den0m" ) ?;
157
- assert_eq ! ( coins. len( ) , 3 ) ;
158
-
159
- assert_eq ! ( coins[ 0 ] . denom, "stake" ) ;
160
- assert_eq ! ( coins[ 0 ] . amount, 123u64 . into( ) ) ;
161
-
162
- assert_eq ! ( coins[ 1 ] . denom, "a1" ) ;
163
- assert_eq ! ( coins[ 1 ] . amount, 1u64 . into( ) ) ;
164
-
165
- assert_eq ! ( coins[ 2 ] . denom, "den0m" ) ;
166
- assert_eq ! ( coins[ 2 ] . amount, 999u64 . into( ) ) ;
167
- }
168
-
169
- Ok ( ( ) )
196
+ #[ rstest]
197
+ #[ case:: semicolon_delimiter( "123stake;1a1;999den0m" ) ]
198
+ #[ case:: mixed_delimiter( "123stake,1a1;999den0m" ) ]
199
+ #[ should_panic( expected = "parsing failure in test" ) ]
200
+ fn test_failed_parse_raw_coin_list ( #[ case] coins_str : & str ) {
201
+ RawCoin :: from_string_list ( coins_str) . expect ( "parsing failure in test" ) ;
170
202
}
171
203
}
0 commit comments