Skip to main content

sov_universal_wallet/ty/
byte_display.rs

1use std::num::ParseIntError;
2
3use bech32::{Bech32, Bech32m, Hrp};
4use borsh::{BorshDeserialize, BorshSerialize};
5use hex::FromHexError;
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Debug, Error, PartialEq)]
11pub enum ByteParseError {
12    #[error("The input could not be decoded as bech32: {0}")]
13    InvalidBech32(#[from] bech32::DecodeError),
14    #[error("The input could not be decoded as base58: {0}")]
15    InvalidBase58(#[from] bs58::decode::Error),
16    #[error("The input {0} could not be decoded as a decimal array")]
17    InvalidDecimal(String),
18    #[error("The input contained elments that could not be parsed as integers: {0}")]
19    InvalidNumber(#[from] ParseIntError),
20    #[error("The input could not be decoded as a hex string: {0}")]
21    InvalidHex(#[from] FromHexError),
22    #[error("Invalid bech32 prefix. Expected {expected}, input contained prefix {actual}")]
23    InvalidBech32Prefix { expected: String, actual: String },
24    #[error("Invalid length: expected {expected} {encoding}-encoded bytes, but the input - once decoded - contained {actual} bytes")]
25    InvalidLength {
26        expected: usize,
27        encoding: String,
28        actual: usize,
29    },
30}
31
32#[derive(Debug, Error, Clone)]
33pub enum ByteFormatError {
34    #[error("Core error: {0}")]
35    Core(#[from] core::fmt::Error),
36    #[error("The input could not be displayed as bech32: {0}")]
37    InvalidBech32(#[from] bech32::EncodeError),
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, BorshDeserialize, BorshSerialize)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42pub enum ByteDisplay {
43    #[default]
44    Hex,
45    Decimal,
46    Bech32 {
47        #[cfg_attr(feature = "serde", serde(with = "hrp_serde"))]
48        #[borsh(
49            serialize_with = "hrp_borsh::borsh_serialize",
50            deserialize_with = "hrp_borsh::borsh_deserialize"
51        )]
52        prefix: Hrp,
53    },
54    Bech32m {
55        #[cfg_attr(feature = "serde", serde(with = "hrp_serde"))]
56        #[borsh(
57            serialize_with = "hrp_borsh::borsh_serialize",
58            deserialize_with = "hrp_borsh::borsh_deserialize"
59        )]
60        prefix: Hrp,
61    },
62    Base58,
63}
64
65mod hrp_borsh {
66    use bech32::Hrp;
67    use borsh::{BorshDeserialize, BorshSerialize};
68
69    pub fn borsh_serialize<W: borsh::io::Write>(
70        hrp: &Hrp,
71        w: &mut W,
72    ) -> Result<(), borsh::io::Error> {
73        let s = hrp.as_str();
74        BorshSerialize::serialize(&s, w)
75    }
76
77    pub fn borsh_deserialize<R: borsh::io::Read>(r: &mut R) -> Result<Hrp, borsh::io::Error> {
78        let s: String = BorshDeserialize::deserialize_reader(r)?;
79        Hrp::parse(&s).map_err(borsh::io::Error::other)
80    }
81}
82
83#[cfg(feature = "serde")]
84mod hrp_serde {
85    use bech32::Hrp;
86    use serde::de::{self, Unexpected};
87    use serde::{Deserialize, Deserializer, Serializer};
88
89    pub fn serialize<S>(hrp: &Hrp, ser: S) -> Result<S::Ok, S::Error>
90    where
91        S: Serializer,
92    {
93        let s = hrp.as_str();
94        ser.serialize_str(s)
95    }
96
97    pub fn deserialize<'de, D>(d: D) -> Result<Hrp, D::Error>
98    where
99        D: Deserializer<'de>,
100    {
101        let s = <&str>::deserialize(d)?;
102        Hrp::parse(s).map_err(|_| de::Error::invalid_value(Unexpected::Str(s), &"a valid HRP"))
103    }
104}
105impl ByteDisplay {
106    pub fn format(
107        &self,
108        input: &[u8],
109        f: &mut impl core::fmt::Write,
110    ) -> Result<(), ByteFormatError> {
111        match self {
112            ByteDisplay::Hex => {
113                f.write_str("0x")?;
114                for byte in input {
115                    write!(f, "{byte:02x}")?;
116                }
117            }
118            ByteDisplay::Decimal => {
119                write!(f, "{input:?}")?;
120            }
121            ByteDisplay::Bech32 { prefix } => {
122                bech32::encode_to_fmt::<Bech32, _>(f, *prefix, input)?
123            }
124            ByteDisplay::Bech32m { prefix } => {
125                bech32::encode_to_fmt::<Bech32m, _>(f, *prefix, input)?
126            }
127            ByteDisplay::Base58 => {
128                let out = bs58::encode(input).into_string();
129                f.write_str(&out)?;
130            }
131        }
132        Ok(())
133    }
134
135    pub fn parse(&self, input: &str) -> Result<Vec<u8>, ByteParseError> {
136        match self {
137            ByteDisplay::Hex => {
138                let input = input.trim_start_matches("0x");
139                Ok(hex::decode(input)?)
140            }
141            ByteDisplay::Decimal => {
142                let Some(inner) = input
143                    .strip_prefix('[')
144                    .and_then(|input| input.strip_suffix(']'))
145                else {
146                    return Err(ByteParseError::InvalidDecimal(input.to_string()));
147                };
148                let ret: Result<Vec<u8>, _> = inner
149                    .split_terminator(',')
150                    .map(|s| s.trim().parse())
151                    .collect();
152                Ok(ret?)
153            }
154            ByteDisplay::Bech32 { prefix } | ByteDisplay::Bech32m { prefix } => {
155                let (parsed_prefix, bytes) = bech32::decode(input)?;
156                if parsed_prefix != *prefix {
157                    return Err(ByteParseError::InvalidBech32Prefix {
158                        expected: prefix.to_string(),
159                        actual: parsed_prefix.to_string(),
160                    });
161                }
162                Ok(bytes)
163            }
164            ByteDisplay::Base58 => Ok(bs58::decode(input).into_vec()?),
165        }
166    }
167
168    pub fn parse_const<const N: usize>(&self, input: &str) -> Result<[u8; N], ByteParseError> {
169        let parsed_bytes = self.parse(input)?;
170        let encoding = match self {
171            ByteDisplay::Hex => "hex",
172            ByteDisplay::Decimal => "decimal",
173            ByteDisplay::Bech32 { .. } => "bech32",
174            ByteDisplay::Bech32m { .. } => "bech32m",
175            ByteDisplay::Base58 => "base58",
176        }
177        .to_string();
178        <Vec<u8> as TryInto<[u8; N]>>::try_into(parsed_bytes).map_err(|bytes| {
179            ByteParseError::InvalidLength {
180                expected: N,
181                encoding,
182                actual: bytes.len(),
183            }
184        })
185    }
186}
187#[cfg(test)]
188mod byte_display_tests {
189    use bech32::Hrp;
190
191    use super::{ByteDisplay, ByteParseError};
192
193    // These tests would be a lot more concise with either a) the paste! crate, or b) a proc macro.
194    // But paste! is archived/unmaintained, and writing a proc macro is probably overkill
195
196    macro_rules! test_display_passes {
197        ($display:expr, $str:literal) => {
198            let bytes = [12u8; 10];
199            let mut out = String::new();
200            let display = $display;
201
202            display.format(&bytes, &mut out).unwrap();
203            assert_eq!(out, $str);
204        };
205    }
206
207    macro_rules! test_parse_passes {
208        ($display:expr, $str:literal) => {
209            let input = $str;
210            let bytes = [12u8; 10];
211            let display = $display;
212
213            // vec parsing
214            assert_eq!(display.parse(input).unwrap(), bytes.to_vec());
215            // array parsing
216            assert_eq!(display.parse_const(input).unwrap(), bytes);
217        };
218    }
219
220    macro_rules! test_parse_rejects {
221        ($display:expr, $str:literal, $encoding_for_err:literal) => {
222            let input = $str;
223            let display = $display;
224
225            let result = display.parse_const::<11>(input);
226            assert!(result.is_err());
227            assert_eq!(
228                result.err().unwrap(),
229                ByteParseError::InvalidLength {
230                    expected: 11,
231                    encoding: $encoding_for_err.to_string(),
232                    actual: 10
233                }
234            )
235        };
236    }
237
238    #[test]
239    fn test_hex_display() {
240        test_display_passes!(ByteDisplay::Hex, "0x0c0c0c0c0c0c0c0c0c0c");
241    }
242
243    #[test]
244    fn test_hex_parse_passes() {
245        test_parse_passes!(ByteDisplay::Hex, "0x0c0c0c0c0c0c0c0c0c0c");
246    }
247
248    #[test]
249    fn test_hex_parse_failures() {
250        test_parse_rejects!(ByteDisplay::Hex, "0x0c0c0c0c0c0c0c0c0c0c", "hex");
251    }
252
253    #[test]
254    fn test_decimal_display() {
255        test_display_passes!(
256            ByteDisplay::Decimal,
257            "[12, 12, 12, 12, 12, 12, 12, 12, 12, 12]"
258        );
259    }
260
261    #[test]
262    fn test_decimal_parse_passes() {
263        test_parse_passes!(
264            ByteDisplay::Decimal,
265            "[12, 12, 12, 12, 12, 12, 12, 12, 12, 12]"
266        );
267    }
268
269    #[test]
270    fn test_decimal_parse_failures() {
271        test_parse_rejects!(
272            ByteDisplay::Decimal,
273            "[12, 12, 12, 12, 12, 12, 12, 12, 12, 12]",
274            "decimal"
275        );
276    }
277
278    #[test]
279    fn test_bech32_display() {
280        test_display_passes!(
281            ByteDisplay::Bech32 {
282                prefix: Hrp::parse("aa").unwrap()
283            },
284            "aa1psxqcrqvpsxqcrqv80wveu"
285        );
286    }
287
288    #[test]
289    fn test_bech32_parse_passes() {
290        test_parse_passes!(
291            ByteDisplay::Bech32 {
292                prefix: Hrp::parse("aa").unwrap()
293            },
294            "aa1psxqcrqvpsxqcrqv80wveu"
295        );
296    }
297
298    #[test]
299    fn test_bech32_parse_failures() {
300        test_parse_rejects!(
301            ByteDisplay::Bech32 {
302                prefix: Hrp::parse("aa").unwrap()
303            },
304            "aa1psxqcrqvpsxqcrqv80wveu",
305            "bech32"
306        );
307    }
308
309    #[test]
310    fn test_bech32m_display() {
311        test_display_passes!(
312            ByteDisplay::Bech32m {
313                prefix: Hrp::parse("aa").unwrap()
314            },
315            "aa1psxqcrqvpsxqcrqvjn7qu7"
316        );
317    }
318
319    #[test]
320    fn test_bech32m_parse_passes() {
321        test_parse_passes!(
322            ByteDisplay::Bech32m {
323                prefix: Hrp::parse("aa").unwrap()
324            },
325            "aa1psxqcrqvpsxqcrqvjn7qu7"
326        );
327    }
328
329    #[test]
330    fn test_bech32m_parse_failures() {
331        test_parse_rejects!(
332            ByteDisplay::Bech32m {
333                prefix: Hrp::parse("aa").unwrap()
334            },
335            "aa1psxqcrqvpsxqcrqvjn7qu7",
336            "bech32m"
337        );
338    }
339
340    #[test]
341    fn test_base58_display() {
342        test_display_passes!(ByteDisplay::Base58, "gFqoeNwi4sf1M");
343    }
344
345    #[test]
346    fn test_base58_parse_passes() {
347        test_parse_passes!(ByteDisplay::Base58, "gFqoeNwi4sf1M");
348    }
349
350    #[test]
351    fn test_base58_parse_failures() {
352        test_parse_rejects!(ByteDisplay::Base58, "gFqoeNwi4sf1M", "base58");
353    }
354}