scale_value/string_impls/custom_parsers/
ss58.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Copyright (C) 2022-2023 Parity Technologies (UK) Ltd. (admin@parity.io)
// This file is a part of the scale-value crate.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//         http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::prelude::*;
use crate::{stringify::ParseError, Value};

/// Attempt to parse an ss58 address into a [`Value<()>`] (or more specifically,
/// an unnamed composite wrapped in a newtype which represents an AccountId32).
///
/// - Returns `None` if we can't parse the address.
/// - Returns `Some(value)` if parsing was successful. In this case, the string
///   reference given is wound forwards to consume what was parsed.
pub fn parse_ss58(s: &mut &str) -> Option<Result<Value<()>, ParseError>> {
    let bytes = parse_ss58_bytes(s)?;
    Some(Ok(Value::from_bytes(bytes)))
}

fn parse_ss58_bytes(s: &mut &str) -> Option<Vec<u8>> {
    const CHECKSUM_LEN: usize = 2;

    // ss58 addresses are base58 encoded. Base58 is all alphanumeric chars
    // minus a few that look potentially similar. So, gather alphanumeric chars
    // first.
    let end_idx = s.find(|c: char| !c.is_ascii_alphanumeric()).unwrap_or(s.len());
    let maybe_ss58 = &s[0..end_idx];
    let rest = &s[end_idx..];

    if maybe_ss58.is_empty() {
        return None;
    }

    // Break early on obvious non-addresses that we want to parse elsewise, ie true, false or numbers.
    // This is mostly an optimisation but also eliminates some potential weird edge cases.
    if maybe_ss58 == "true"
        || maybe_ss58 == "false"
        || maybe_ss58.chars().all(|c: char| c.is_ascii_digit())
    {
        return None;
    }

    // If what we are parsing is a variant ident, a `{` or `(` will follow
    // (eg `Foo { hi: 1 }` or `Foo (1)`). In this case, don't try to parse
    // as an ss58 address, since it would definitely be wrong to do so.
    if rest.trim_start().starts_with(['(', '{']) {
        return None;
    }

    // Attempt to base58-decode these chars.
    use base58::FromBase58;
    let Ok(bytes) = maybe_ss58.from_base58() else { return None };

    // decode length of address prefix.
    let prefix_len = match bytes.first() {
        Some(0..=63) => 1,
        Some(64..=127) => 2,
        _ => return None,
    };

    if bytes.len() < prefix_len + CHECKSUM_LEN {
        return None;
    }

    // The checksum is the last 2 bytes:
    let checksum_start_idx = bytes.len() - CHECKSUM_LEN;

    // Check that the checksum lines up with the rest of the address; if not,
    // this isn't a valid address.
    let hash = ss58hash(&bytes[0..checksum_start_idx]);
    let checksum = &hash[0..CHECKSUM_LEN];
    if &bytes[checksum_start_idx..] != checksum {
        return None;
    }

    // Everything checks out; wind the string cursor forwards and
    // return the bytes representing the address provided.
    *s = rest;
    Some(bytes[prefix_len..checksum_start_idx].to_vec())
}

fn ss58hash(data: &[u8]) -> Vec<u8> {
    use blake2::{Blake2b512, Digest};
    const PREFIX: &[u8] = b"SS58PRE";
    let mut ctx = Blake2b512::new();
    ctx.update(PREFIX);
    ctx.update(data);
    ctx.finalize().to_vec()
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn can_parse_ss58_address() {
        // hex keys obtained via `subkey`, so we're comparing our decoding against that.
        // We simultaneously check that things after the address aren't consumed.
        let expected = [
            // Alice
            (
                "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY ",
                "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
                " ",
            ),
            // Bob
            (
                "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty-100",
                "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48",
                "-100",
            ),
            // Charlie
            (
                "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y,1,2,3",
                "90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22",
                ",1,2,3",
            ),
            // Eve
            (
                "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw }",
                "e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e",
                " }",
            ),
        ];

        for (ss58, expected_hex, expected_remaining) in expected {
            let cursor = &mut &*ss58;
            let bytes = parse_ss58_bytes(cursor).expect("address should parse OK");
            let expected = hex::decode(expected_hex).expect("hex should decode OK");

            assert_eq!(bytes, expected);
            assert_eq!(*cursor, expected_remaining);
        }
    }

    #[test]
    fn invalid_addresses_will_error() {
        let invalids = [
            // An otherwise valid address in a variant "ident" position will not parse:
            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY { hi: 1 }",
            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY     \t\n (1)",
            // Invalid addresses will return None:
            "Foo",
            "",
        ];

        for invalid in invalids {
            assert!(parse_ss58_bytes(&mut &*invalid).is_none());
        }
    }
}