Skip to content

Commit 481b7ac

Browse files
committed
refactor(wasm-dot): use bs58 crate for address encoding
Replace manual base58 implementation with official bs58 crate for SS58 address encoding/decoding. This reduces code complexity and improves reliability by using a well-tested library matching the Substrate ecosystem.
1 parent 0d34e8d commit 481b7ac

2 files changed

Lines changed: 55 additions & 179 deletions

File tree

packages/wasm-dot/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ subxt-core = { version = "0.37", default-features = false }
3131

3232
# Crypto
3333
blake2 = "0.10"
34+
bs58 = "0.5"
3435

3536
# WASM random number generation support
3637
getrandom = { version = "0.2", features = ["js"] }

packages/wasm-dot/src/address.rs

Lines changed: 54 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! SS58 address encoding and decoding for Polkadot/Substrate chains
22
//!
3-
//! SS58 is a simple address format designed for Substrate based chains.
3+
//! Uses the official bs58 crate for base58 encoding, matching the Substrate ecosystem.
44
//! See: https://docs.substrate.io/reference/address-formats/
55
66
use crate::error::WasmDotError;
@@ -10,17 +10,11 @@ use blake2::{Blake2b512, Digest};
1010
/// SS58 prefix for checksum calculation
1111
const SS58_PREFIX: &[u8] = b"SS58PRE";
1212

13-
/// Base58 alphabet used by Substrate (same as Bitcoin)
14-
const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
15-
1613
/// Encode a public key to SS58 address format
1714
///
1815
/// # Arguments
1916
/// * `public_key` - 32-byte Ed25519 public key
20-
/// * `prefix` - Network prefix (0 for Polkadot, 42 for generic Substrate)
21-
///
22-
/// # Returns
23-
/// SS58 encoded address string
17+
/// * `prefix` - Network prefix (0 for Polkadot, 2 for Kusama, 42 for generic Substrate)
2418
pub fn encode_ss58(public_key: &[u8], prefix: u16) -> Result<String, WasmDotError> {
2519
if public_key.len() != 32 {
2620
return Err(WasmDotError::InvalidAddress(format!(
@@ -29,70 +23,33 @@ pub fn encode_ss58(public_key: &[u8], prefix: u16) -> Result<String, WasmDotErro
2923
)));
3024
}
3125

32-
// Build the payload: prefix + public key
33-
let mut payload = Vec::new();
34-
35-
if prefix < 64 {
36-
// Single byte prefix
37-
payload.push(prefix as u8);
38-
} else if prefix < 16384 {
39-
// Two byte prefix (encoded as per SS58 spec)
40-
let first = ((prefix & 0b0000_0000_1111_1100) as u8) >> 2 | 0b0100_0000;
41-
let second = ((prefix >> 8) as u8) | ((prefix & 0b0000_0000_0000_0011) as u8) << 6;
42-
payload.push(first);
43-
payload.push(second);
44-
} else {
45-
return Err(WasmDotError::InvalidAddress(format!(
46-
"Invalid prefix: {}",
47-
prefix
48-
)));
49-
}
50-
26+
// Build payload: prefix + public key
27+
let mut payload = encode_prefix(prefix)?;
5128
payload.extend_from_slice(public_key);
5229

53-
// Calculate checksum (first 2 bytes of Blake2b hash of SS58PRE || payload)
30+
// Calculate checksum (first 2 bytes of Blake2b-512 hash)
5431
let checksum = ss58_checksum(&payload);
5532
payload.extend_from_slice(&checksum[..2]);
5633

57-
// Base58 encode
58-
Ok(base58_encode(&payload))
34+
// Base58 encode using bs58 crate
35+
Ok(bs58::encode(&payload).into_string())
5936
}
6037

6138
/// Decode an SS58 address to public key and prefix
62-
///
63-
/// # Arguments
64-
/// * `address` - SS58 encoded address string
65-
///
66-
/// # Returns
67-
/// Tuple of (public_key, prefix)
6839
pub fn decode_ss58(address: &str) -> Result<(Vec<u8>, u16), WasmDotError> {
69-
let decoded = base58_decode(address)?;
40+
// Base58 decode using bs58 crate
41+
let decoded = bs58::decode(address)
42+
.into_vec()
43+
.map_err(|e| WasmDotError::InvalidAddress(format!("Invalid base58: {}", e)))?;
7044

7145
if decoded.len() < 35 {
72-
// minimum: 1 byte prefix + 32 byte key + 2 byte checksum
7346
return Err(WasmDotError::InvalidAddress(
7447
"Address too short".to_string(),
7548
));
7649
}
7750

78-
// Extract prefix
79-
let (prefix, prefix_len) = if decoded[0] < 64 {
80-
(decoded[0] as u16, 1)
81-
} else if decoded[0] < 128 {
82-
if decoded.len() < 36 {
83-
return Err(WasmDotError::InvalidAddress(
84-
"Address too short for two-byte prefix".to_string(),
85-
));
86-
}
87-
let lower = (decoded[0] & 0b0011_1111) << 2 | (decoded[1] >> 6);
88-
let upper = decoded[1] & 0b0011_1111;
89-
(((upper as u16) << 8) | (lower as u16), 2)
90-
} else {
91-
return Err(WasmDotError::InvalidAddress(format!(
92-
"Invalid prefix byte: {}",
93-
decoded[0]
94-
)));
95-
};
51+
// Decode prefix
52+
let (prefix, prefix_len) = decode_prefix(&decoded)?;
9653

9754
// Extract public key and checksum
9855
let checksum_start = decoded.len() - 2;
@@ -118,24 +75,14 @@ pub fn decode_ss58(address: &str) -> Result<(Vec<u8>, u16), WasmDotError> {
11875
}
11976

12077
/// Validate an SS58 address
121-
///
122-
/// # Arguments
123-
/// * `address` - SS58 encoded address string
124-
/// * `expected_prefix` - Optional expected network prefix
125-
///
126-
/// # Returns
127-
/// true if valid, false otherwise
12878
pub fn validate_address(address: &str, expected_prefix: Option<u16>) -> bool {
12979
match decode_ss58(address) {
130-
Ok((_, prefix)) => match expected_prefix {
131-
Some(expected) => prefix == expected,
132-
None => true,
133-
},
80+
Ok((_, prefix)) => expected_prefix.map_or(true, |expected| prefix == expected),
13481
Err(_) => false,
13582
}
13683
}
13784

138-
/// Get address format from address string by decoding and checking prefix
85+
/// Get address format from address string
13986
pub fn get_address_format(address: &str) -> Result<AddressFormat, WasmDotError> {
14087
let (_, prefix) = decode_ss58(address)?;
14188
Ok(match prefix {
@@ -145,7 +92,45 @@ pub fn get_address_format(address: &str) -> Result<AddressFormat, WasmDotError>
14592
})
14693
}
14794

148-
/// Calculate SS58 checksum
95+
/// Encode SS58 prefix (supports single and two-byte prefixes)
96+
fn encode_prefix(prefix: u16) -> Result<Vec<u8>, WasmDotError> {
97+
if prefix < 64 {
98+
Ok(vec![prefix as u8])
99+
} else if prefix < 16384 {
100+
// Two-byte encoding per SS58 spec
101+
let first = ((prefix & 0b0000_0000_1111_1100) as u8) >> 2 | 0b0100_0000;
102+
let second = ((prefix >> 8) as u8) | ((prefix & 0b0000_0000_0000_0011) as u8) << 6;
103+
Ok(vec![first, second])
104+
} else {
105+
Err(WasmDotError::InvalidAddress(format!(
106+
"Invalid prefix: {}",
107+
prefix
108+
)))
109+
}
110+
}
111+
112+
/// Decode SS58 prefix from raw bytes
113+
fn decode_prefix(data: &[u8]) -> Result<(u16, usize), WasmDotError> {
114+
if data[0] < 64 {
115+
Ok((data[0] as u16, 1))
116+
} else if data[0] < 128 {
117+
if data.len() < 2 {
118+
return Err(WasmDotError::InvalidAddress(
119+
"Address too short for two-byte prefix".to_string(),
120+
));
121+
}
122+
let lower = (data[0] & 0b0011_1111) << 2 | (data[1] >> 6);
123+
let upper = data[1] & 0b0011_1111;
124+
Ok((((upper as u16) << 8) | (lower as u16), 2))
125+
} else {
126+
Err(WasmDotError::InvalidAddress(format!(
127+
"Invalid prefix byte: {}",
128+
data[0]
129+
)))
130+
}
131+
}
132+
133+
/// Calculate SS58 checksum (Blake2b-512 of "SS58PRE" || payload)
149134
fn ss58_checksum(payload: &[u8]) -> [u8; 64] {
150135
let mut hasher = Blake2b512::new();
151136
hasher.update(SS58_PREFIX);
@@ -156,122 +141,12 @@ fn ss58_checksum(payload: &[u8]) -> [u8; 64] {
156141
checksum
157142
}
158143

159-
/// Base58 encode bytes
160-
fn base58_encode(data: &[u8]) -> String {
161-
if data.is_empty() {
162-
return String::new();
163-
}
164-
165-
// Count leading zeros
166-
let zeros = data.iter().take_while(|&&b| b == 0).count();
167-
168-
// Allocate enough space
169-
let size = data.len() * 138 / 100 + 1;
170-
let mut buffer = vec![0u8; size];
171-
172-
let mut length = 0;
173-
for &byte in data {
174-
let mut carry = byte as u32;
175-
let mut i = 0;
176-
for j in (0..size).rev() {
177-
if carry == 0 && i >= length {
178-
break;
179-
}
180-
carry += 256 * buffer[j] as u32;
181-
buffer[j] = (carry % 58) as u8;
182-
carry /= 58;
183-
i += 1;
184-
}
185-
length = i;
186-
}
187-
188-
// Skip leading zeros in buffer
189-
let mut start = size - length;
190-
while start < size && buffer[start] == 0 {
191-
start += 1;
192-
}
193-
194-
// Build result
195-
let mut result = String::with_capacity(zeros + size - start);
196-
for _ in 0..zeros {
197-
result.push('1');
198-
}
199-
for &b in &buffer[start..] {
200-
result.push(ALPHABET[b as usize] as char);
201-
}
202-
203-
result
204-
}
205-
206-
/// Base58 decode string
207-
fn base58_decode(input: &str) -> Result<Vec<u8>, WasmDotError> {
208-
if input.is_empty() {
209-
return Ok(Vec::new());
210-
}
211-
212-
// Build reverse lookup table
213-
let mut alphabet_map = [255u8; 128];
214-
for (i, &c) in ALPHABET.iter().enumerate() {
215-
alphabet_map[c as usize] = i as u8;
216-
}
217-
218-
// Count leading '1's (zeros)
219-
let zeros = input.chars().take_while(|&c| c == '1').count();
220-
221-
// Allocate space
222-
let size = input.len() * 733 / 1000 + 1;
223-
let mut buffer = vec![0u8; size];
224-
225-
let mut length = 0;
226-
for c in input.chars() {
227-
if c as usize >= 128 {
228-
return Err(WasmDotError::InvalidAddress(format!(
229-
"Invalid character: {}",
230-
c
231-
)));
232-
}
233-
let digit = alphabet_map[c as usize];
234-
if digit == 255 {
235-
return Err(WasmDotError::InvalidAddress(format!(
236-
"Invalid character: {}",
237-
c
238-
)));
239-
}
240-
241-
let mut carry = digit as u32;
242-
let mut i = 0;
243-
for j in (0..size).rev() {
244-
if carry == 0 && i >= length {
245-
break;
246-
}
247-
carry += 58 * buffer[j] as u32;
248-
buffer[j] = (carry % 256) as u8;
249-
carry /= 256;
250-
i += 1;
251-
}
252-
length = i;
253-
}
254-
255-
// Skip leading zeros in buffer
256-
let mut start = size - length;
257-
while start < size && buffer[start] == 0 {
258-
start += 1;
259-
}
260-
261-
// Build result with leading zeros
262-
let mut result = vec![0u8; zeros];
263-
result.extend_from_slice(&buffer[start..]);
264-
265-
Ok(result)
266-
}
267-
268144
#[cfg(test)]
269145
mod tests {
270146
use super::*;
271147

272148
#[test]
273149
fn test_encode_decode_roundtrip() {
274-
// Test with a known public key
275150
let pubkey =
276151
hex::decode("61b18c6dc02ddcabdeac56cb4f21a971cc41cc97640f6f85b073480008c53a0d")
277152
.unwrap();

0 commit comments

Comments
 (0)