scale_value/string_impls/custom_parsers/ss58.rs
1// Copyright (C) 2022-2023 Parity Technologies (UK) Ltd. (admin@parity.io)
2// This file is a part of the scale-value crate.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::prelude::*;
17use crate::{stringify::ParseError, Value};
18
19/// Attempt to parse an ss58 address into a [`Value<()>`] (or more specifically,
20/// an unnamed composite wrapped in a newtype which represents an AccountId32).
21///
22/// - Returns `None` if we can't parse the address.
23/// - Returns `Some(value)` if parsing was successful. In this case, the string
24/// reference given is wound forwards to consume what was parsed.
25pub fn parse_ss58(s: &mut &str) -> Option<Result<Value<()>, ParseError>> {
26 let bytes = parse_ss58_bytes(s)?;
27 Some(Ok(Value::from_bytes(bytes)))
28}
29
30fn parse_ss58_bytes(s: &mut &str) -> Option<Vec<u8>> {
31 const CHECKSUM_LEN: usize = 2;
32
33 // ss58 addresses are base58 encoded. Base58 is all alphanumeric chars
34 // minus a few that look potentially similar. So, gather alphanumeric chars
35 // first.
36 let end_idx = s.find(|c: char| !c.is_ascii_alphanumeric()).unwrap_or(s.len());
37 let maybe_ss58 = &s[0..end_idx];
38 let rest = &s[end_idx..];
39
40 if maybe_ss58.is_empty() {
41 return None;
42 }
43
44 // Break early on obvious non-addresses that we want to parse elsewise, ie true, false or numbers.
45 // This is mostly an optimisation but also eliminates some potential weird edge cases.
46 if maybe_ss58 == "true"
47 || maybe_ss58 == "false"
48 || maybe_ss58.chars().all(|c: char| c.is_ascii_digit())
49 {
50 return None;
51 }
52
53 // If what we are parsing is a variant ident, a `{` or `(` will follow
54 // (eg `Foo { hi: 1 }` or `Foo (1)`). In this case, don't try to parse
55 // as an ss58 address, since it would definitely be wrong to do so.
56 if rest.trim_start().starts_with(['(', '{']) {
57 return None;
58 }
59
60 // Attempt to base58-decode these chars.
61 use base58::FromBase58;
62 let Ok(bytes) = maybe_ss58.from_base58() else { return None };
63
64 // decode length of address prefix.
65 let prefix_len = match bytes.first() {
66 Some(0..=63) => 1,
67 Some(64..=127) => 2,
68 _ => return None,
69 };
70
71 if bytes.len() < prefix_len + CHECKSUM_LEN {
72 return None;
73 }
74
75 // The checksum is the last 2 bytes:
76 let checksum_start_idx = bytes.len() - CHECKSUM_LEN;
77
78 // Check that the checksum lines up with the rest of the address; if not,
79 // this isn't a valid address.
80 let hash = ss58hash(&bytes[0..checksum_start_idx]);
81 let checksum = &hash[0..CHECKSUM_LEN];
82 if &bytes[checksum_start_idx..] != checksum {
83 return None;
84 }
85
86 // Everything checks out; wind the string cursor forwards and
87 // return the bytes representing the address provided.
88 *s = rest;
89 Some(bytes[prefix_len..checksum_start_idx].to_vec())
90}
91
92fn ss58hash(data: &[u8]) -> Vec<u8> {
93 use blake2::{Blake2b512, Digest};
94 const PREFIX: &[u8] = b"SS58PRE";
95 let mut ctx = Blake2b512::new();
96 ctx.update(PREFIX);
97 ctx.update(data);
98 ctx.finalize().to_vec()
99}
100
101#[cfg(test)]
102mod test {
103 use super::*;
104
105 #[test]
106 fn can_parse_ss58_address() {
107 // hex keys obtained via `subkey`, so we're comparing our decoding against that.
108 // We simultaneously check that things after the address aren't consumed.
109 let expected = [
110 // Alice
111 (
112 "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY ",
113 "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
114 " ",
115 ),
116 // Bob
117 (
118 "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty-100",
119 "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48",
120 "-100",
121 ),
122 // Charlie
123 (
124 "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y,1,2,3",
125 "90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22",
126 ",1,2,3",
127 ),
128 // Eve
129 (
130 "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw }",
131 "e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e",
132 " }",
133 ),
134 ];
135
136 for (ss58, expected_hex, expected_remaining) in expected {
137 let cursor = &mut &*ss58;
138 let bytes = parse_ss58_bytes(cursor).expect("address should parse OK");
139 let expected = hex::decode(expected_hex).expect("hex should decode OK");
140
141 assert_eq!(bytes, expected);
142 assert_eq!(*cursor, expected_remaining);
143 }
144 }
145
146 #[test]
147 fn invalid_addresses_will_error() {
148 let invalids = [
149 // An otherwise valid address in a variant "ident" position will not parse:
150 "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY { hi: 1 }",
151 "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \t\n (1)",
152 // Invalid addresses will return None:
153 "Foo",
154 "",
155 ];
156
157 for invalid in invalids {
158 assert!(parse_ss58_bytes(&mut &*invalid).is_none());
159 }
160 }
161}