use crate::accounts::{derive_eth_key, NATIVE_PREFIX};
use crate::address::bytes_to_bech32;
use crate::error::Result;
use crate::pqc::{pqc_keypair_from_seed, PqcKeypair};
use k256::elliptic_curve::sec1::ToEncodedPoint;
use k256::SecretKey;
use sha3::digest::{ExtendableOutput, Update, XofReader};
use sha3::{Keccak256, Shake256};
pub const HRP: &str = NATIVE_PREFIX;
#[derive(Debug, Clone)]
pub struct UnifiedAccount {
pub private_key: [u8; 32],
pub public_key: Vec<u8>,
pub address_bytes: [u8; 20],
pub cosmos: String,
pub evm: String,
pub svm: String,
pub pqc: PqcKeypair,
}
#[derive(Debug, Clone)]
pub struct UnifiedAddresses {
pub address_bytes: [u8; 20],
pub cosmos: String,
pub evm: String,
pub svm: String,
}
fn keccak256(data: &[u8]) -> [u8; 32] {
use sha3::Digest;
let out = Keccak256::digest(data);
let mut arr = [0u8; 32];
arr.copy_from_slice(&out);
arr
}
fn shake256_32(data: &[u8]) -> [u8; 32] {
let mut hasher = Shake256::default();
hasher.update(data);
let mut reader = hasher.finalize_xof();
let mut out = [0u8; 32];
reader.read(&mut out);
out
}
fn to_eip55(addr20: &[u8; 20]) -> String {
let lower = hex::encode(addr20);
let hash = keccak256(lower.as_bytes());
let mut out = String::with_capacity(42);
out.push_str("0x");
for (i, c) in lower.chars().enumerate() {
if c.is_ascii_digit() {
out.push(c);
continue;
}
let byte = hash[i / 2];
let nibble = if i % 2 == 0 { byte >> 4 } else { byte & 0x0f };
if nibble >= 8 {
out.push(c.to_ascii_uppercase());
} else {
out.push(c);
}
}
out
}
pub fn addresses_from_20(addr20: [u8; 20]) -> Result<UnifiedAddresses> {
let cosmos = bytes_to_bech32(&addr20, HRP)?;
let evm = to_eip55(&addr20);
let mut svm_bytes = [0u8; 32];
svm_bytes[..20].copy_from_slice(&addr20);
let svm = bs58::encode(&svm_bytes).into_string();
Ok(UnifiedAddresses {
address_bytes: addr20,
cosmos,
evm,
svm,
})
}
pub fn qore_addresses(
cosmos: Option<&str>,
evm: Option<&str>,
hex_addr: Option<&str>,
) -> Result<UnifiedAddresses> {
let addr20 = if let Some(e) = evm {
hex20_from_str(e)?
} else if let Some(h) = hex_addr {
hex20_from_str(h)?
} else if let Some(c) = cosmos {
let (_, data) =
bech32::decode(c).map_err(|e| crate::error::Error::Address(e.to_string()))?;
<[u8; 20]>::try_from(data.as_slice())
.map_err(|_| crate::error::Error::Address("bech32 data must be 20 bytes".into()))?
} else {
return Err(crate::error::Error::Address(
"provide one of {cosmos, evm, hex}".into(),
));
};
addresses_from_20(addr20)
}
fn hex20_from_str(s: &str) -> Result<[u8; 20]> {
let trimmed = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(trimmed).map_err(|e| crate::error::Error::Address(e.to_string()))?;
<[u8; 20]>::try_from(bytes.as_slice())
.map_err(|_| crate::error::Error::Address("address must be 20 bytes".into()))
}
fn derive_pqc(cosmos: &str, secret: &str) -> PqcKeypair {
let mut msg = Vec::new();
msg.extend_from_slice(b"qorechain:pqc:v1|");
msg.extend_from_slice(cosmos.as_bytes());
msg.push(b'|');
msg.extend_from_slice(secret.as_bytes());
let seed = shake256_32(&msg);
pqc_keypair_from_seed(&seed)
}
fn build_account(
private_key: [u8; 32],
compressed: Vec<u8>,
uncompressed: &[u8],
pqc_secret: &str,
) -> Result<UnifiedAccount> {
let digest = keccak256(&uncompressed[1..]);
let mut addr20 = [0u8; 20];
addr20.copy_from_slice(&digest[12..]);
let enc = addresses_from_20(addr20)?;
let pqc = derive_pqc(&enc.cosmos, pqc_secret);
Ok(UnifiedAccount {
private_key,
public_key: compressed,
address_bytes: addr20,
cosmos: enc.cosmos,
evm: enc.evm,
svm: enc.svm,
pqc,
})
}
fn pubkeys_from_private(private_key: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>)> {
let sk = SecretKey::from_slice(private_key)
.map_err(|e| crate::error::Error::Derivation(format!("invalid private key: {e}")))?;
let pk = sk.public_key();
let compressed = pk.to_encoded_point(true).as_bytes().to_vec();
let uncompressed = pk.to_encoded_point(false).as_bytes().to_vec();
Ok((compressed, uncompressed))
}
pub fn derive_unified_account(mnemonic: &str, index: u32) -> Result<UnifiedAccount> {
let (private_key, compressed, uncompressed) = derive_eth_key(mnemonic, index)?;
build_account(private_key, compressed, &uncompressed, mnemonic)
}
pub fn unified_account_from_seed(seed32: [u8; 32]) -> Result<UnifiedAccount> {
let (compressed, uncompressed) = pubkeys_from_private(&seed32)?;
build_account(
seed32,
compressed,
&uncompressed,
&format!("seed:{}", hex::encode(seed32)),
)
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_MNEMONIC: &str =
"test test test test test test test test test test test junk";
#[test]
fn kat_mnemonic_index0() {
let acct = derive_unified_account(TEST_MNEMONIC, 0).unwrap();
assert_eq!(acct.cosmos, "qor17w0adeg64ky0daxwd2ugyuneellmjgnxhkv37z");
assert_eq!(acct.evm, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
assert_eq!(acct.svm, "HQ1S8pxTw4YN41GPdWYPQVfweQveXAUtfFnZNmvPfrYf");
assert_eq!(acct.pqc.public_key.len(), 2592);
assert_eq!(acct.pqc.secret_key.len(), 4896);
assert_eq!(hex::encode(&acct.pqc.public_key[..8]), "4a685622f2a99d54");
}
#[test]
fn kat_seed_0x01() {
let acct = unified_account_from_seed([0x01u8; 32]).unwrap();
assert_eq!(acct.cosmos, "qor1rfjz7r3u8t65teavh5utquj3kwvsj983hh5zaj");
assert_eq!(acct.evm, "0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1");
assert_eq!(acct.svm, "2n2Cnc7fib6rm4Azo1mbvMuHB3iCb1J254kEQDcrHyPu");
assert_eq!(acct.pqc.public_key.len(), 2592);
assert_eq!(acct.pqc.secret_key.len(), 4896);
assert_eq!(hex::encode(&acct.pqc.public_key[..8]), "2d7f888fecbe5b24");
}
#[test]
fn addresses_from_20_roundtrip() {
let acct = unified_account_from_seed([0x01u8; 32]).unwrap();
let enc = addresses_from_20(acct.address_bytes).unwrap();
assert_eq!(enc.cosmos, acct.cosmos);
assert_eq!(enc.evm, acct.evm);
assert_eq!(enc.svm, acct.svm);
}
#[test]
fn qore_addresses_from_each_form() {
let acct = unified_account_from_seed([0x01u8; 32]).unwrap();
let from_evm = qore_addresses(None, Some(&acct.evm), None).unwrap();
assert_eq!(from_evm.cosmos, acct.cosmos);
let hex_addr = hex::encode(acct.address_bytes);
let from_hex = qore_addresses(None, None, Some(&hex_addr)).unwrap();
assert_eq!(from_hex.evm, acct.evm);
let from_cosmos = qore_addresses(Some(&acct.cosmos), None, None).unwrap();
assert_eq!(from_cosmos.svm, acct.svm);
}
#[test]
fn pqc_deterministic() {
let a = derive_unified_account(TEST_MNEMONIC, 0).unwrap();
let b = derive_unified_account(TEST_MNEMONIC, 0).unwrap();
assert_eq!(a.pqc.public_key, b.pqc.public_key);
assert_eq!(a.pqc.secret_key, b.pqc.secret_key);
}
}