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());
}
}
}