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
66use crate :: error:: WasmDotError ;
@@ -10,17 +10,11 @@ use blake2::{Blake2b512, Digest};
1010/// SS58 prefix for checksum calculation
1111const 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)
2418pub 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)
6839pub 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
12878pub 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
13986pub 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)
149134fn 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) ]
269145mod 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