use crate::error::{Error, Result};
use ripemd::Ripemd160;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
pub fn encode(data: &[u8]) -> String {
bs58::encode(data).into_string()
}
pub fn decode(s: &str) -> Result<Vec<u8>> {
if s.is_empty() {
return Err(Error::Base58("empty string".into()));
}
Ok(bs58::decode(s).into_vec()?)
}
fn sha256d(data: &[u8]) -> [u8; 32] {
let first = Sha256::digest(data);
Sha256::digest(first).into()
}
fn ripemd160(data: &[u8]) -> [u8; 20] {
Ripemd160::digest(data).into()
}
pub fn encode_check(payload: &[u8]) -> String {
let mut out = Vec::with_capacity(payload.len() + 4);
out.extend_from_slice(payload);
out.extend_from_slice(&sha256d(payload)[..4]);
encode(&out)
}
pub fn decode_check(s: &str) -> Result<Vec<u8>> {
let raw = decode(s)?;
if raw.len() < 5 {
return Err(Error::Base58(format!(
"base58check payload too short: {} bytes",
raw.len()
)));
}
let (payload, checksum) = raw.split_at(raw.len() - 4);
let expected = sha256d(payload);
if checksum.ct_eq(&expected[..4]).unwrap_u8() != 1 {
return Err(Error::Checksum("base58check (sha256d)"));
}
Ok(payload.to_vec())
}
pub fn decode_check_version(s: &str, version: u8) -> Result<Vec<u8>> {
let payload = decode_check(s)?;
let (&got, rest) = payload
.split_first()
.ok_or_else(|| Error::Base58("base58check payload has no version byte".into()))?;
if got != version {
return Err(Error::Key(format!(
"unexpected version byte 0x{got:02x}, expected 0x{version:02x}"
)));
}
Ok(rest.to_vec())
}
pub fn encode_gph_check(payload: &[u8]) -> String {
let mut out = Vec::with_capacity(payload.len() + 4);
out.extend_from_slice(payload);
out.extend_from_slice(&ripemd160(payload)[..4]);
encode(&out)
}
pub fn decode_gph_check(s: &str) -> Result<Vec<u8>> {
let raw = decode(s)?;
if raw.len() < 5 {
return Err(Error::Base58(format!(
"graphene base58 payload too short: {} bytes",
raw.len()
)));
}
let (payload, checksum) = raw.split_at(raw.len() - 4);
let expected = ripemd160(payload);
if checksum.ct_eq(&expected[..4]).unwrap_u8() != 1 {
return Err(Error::Checksum("graphene base58 (ripemd160)"));
}
Ok(payload.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_characters_outside_the_alphabet() {
for bad in ["0OIl", "STM0", "5J*", "abc def"] {
assert!(decode(bad).is_err(), "should reject {bad:?}");
}
}
#[test]
fn rejects_empty() {
assert!(decode("").is_err());
}
#[test]
fn check_roundtrip() {
let payload = [0x80u8; 33];
let s = encode_check(&payload);
assert_eq!(decode_check(&s).unwrap(), payload);
}
#[test]
fn gph_check_roundtrip() {
let payload = [7u8; 33];
let s = encode_gph_check(&payload);
assert_eq!(decode_gph_check(&s).unwrap(), payload);
}
#[test]
fn detects_corrupted_checksum() {
let payload = [1u8; 33];
let mut raw = payload.to_vec();
raw.extend_from_slice(&[0, 0, 0, 0]);
let s = encode(&raw);
assert!(matches!(decode_check(&s), Err(Error::Checksum(_))));
}
#[test]
fn version_byte_is_enforced() {
let mut payload = vec![0xefu8];
payload.extend_from_slice(&[9u8; 32]);
let s = encode_check(&payload);
assert_eq!(decode_check(&s).unwrap().len(), 33);
assert!(decode_check_version(&s, 0x80).is_err());
assert!(decode_check_version(&s, 0xef).is_ok());
}
#[test]
fn known_wif_vector() {
let mut payload = vec![0x80u8];
payload.extend_from_slice(&[0u8; 32]);
let wif = encode_check(&payload);
assert!(wif.starts_with('5'));
assert_eq!(decode_check_version(&wif, 0x80).unwrap(), vec![0u8; 32]);
}
}