1#![forbid(unsafe_code)]
7
8use aelf_proto::aelf::{Address, Hash, Transaction};
9use coins_bip32::{
10 path::DerivationPath,
11 prelude::{RecoveryId, Signature, SigningKey, VerifyingKey},
12 xkeys::XPriv,
13};
14use coins_bip39::{English, Mnemonic, MnemonicError};
15use prost::Message;
16use rand::rng;
17use sha2::{Digest, Sha256};
18use std::fmt;
19use std::str::FromStr;
20use thiserror::Error;
21use zeroize::{Zeroize, Zeroizing};
22
23pub const DEFAULT_BIP44_PATH: &str = "m/44'/1616'/0'/0/0";
25
26#[derive(Debug, Error)]
28pub enum CryptoError {
29 #[error("invalid hex string: {0}")]
30 Hex(#[from] hex::FromHexError),
31 #[error("invalid mnemonic: {0}")]
32 Mnemonic(#[from] MnemonicError),
33 #[error("invalid derivation path: {0}")]
34 DerivationPath(#[from] coins_bip32::Bip32Error),
35 #[error("invalid private key")]
36 InvalidPrivateKey,
37 #[error("invalid address")]
38 InvalidAddress,
39 #[error("invalid signature")]
40 InvalidSignature,
41 #[error("protobuf encode error: {0}")]
42 ProtobufEncode(#[from] prost::EncodeError),
43 #[error("protobuf decode error: {0}")]
44 ProtobufDecode(#[from] prost::DecodeError),
45}
46
47#[derive(Clone, PartialEq, Eq)]
51pub struct Wallet {
52 mnemonic: String,
53 bip44_path: String,
54 private_key: String,
55 public_key: String,
56 address: String,
57}
58
59impl Wallet {
60 pub fn create() -> Result<Self, CryptoError> {
62 Self::create_with_path(DEFAULT_BIP44_PATH)
63 }
64
65 pub fn create_with_path(path: &str) -> Result<Self, CryptoError> {
67 let mut rng = rng();
68 let mnemonic = Mnemonic::<English>::from_rng_with_count(&mut rng, 12)?;
69 Self::from_mnemonic_with_path(&mnemonic.to_phrase(), path)
70 }
71
72 pub fn from_mnemonic(mnemonic: &str) -> Result<Self, CryptoError> {
74 Self::from_mnemonic_with_path(mnemonic, DEFAULT_BIP44_PATH)
75 }
76
77 pub fn from_mnemonic_with_path(mnemonic: &str, path: &str) -> Result<Self, CryptoError> {
79 let mnemonic = Mnemonic::<English>::new_from_phrase(mnemonic)?;
80 let path = DerivationPath::from_str(path)?;
81 let derived: XPriv = mnemonic.derive_key(path.clone(), None)?;
82 let signing_key: SigningKey = <XPriv as AsRef<SigningKey>>::as_ref(&derived).clone();
83
84 Ok(Self::from_signing_key(
85 signing_key,
86 mnemonic.to_phrase(),
87 path.derivation_string(),
88 ))
89 }
90
91 pub fn from_private_key(private_key_hex: &str) -> Result<Self, CryptoError> {
93 let bytes = Zeroizing::new(normalize_private_key_hex(private_key_hex)?);
94 let signing_key =
95 SigningKey::from_bytes((&*bytes).into()).map_err(|_| CryptoError::InvalidPrivateKey)?;
96
97 Ok(Self::from_signing_key(
98 signing_key,
99 String::new(),
100 DEFAULT_BIP44_PATH.to_owned(),
101 ))
102 }
103
104 pub fn mnemonic(&self) -> &str {
106 &self.mnemonic
107 }
108
109 pub fn bip44_path(&self) -> &str {
111 &self.bip44_path
112 }
113
114 pub fn private_key(&self) -> &str {
116 &self.private_key
117 }
118
119 pub fn public_key(&self) -> &str {
121 &self.public_key
122 }
123
124 pub fn address(&self) -> &str {
126 &self.address
127 }
128
129 pub fn signing_key(&self) -> Result<SigningKey, CryptoError> {
131 let bytes = Zeroizing::new(normalize_private_key_hex(&self.private_key)?);
132 SigningKey::from_bytes((&*bytes).into()).map_err(|_| CryptoError::InvalidPrivateKey)
133 }
134
135 pub fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
137 sign_payload(&self.signing_key()?, payload)
138 }
139
140 fn from_signing_key(signing_key: SigningKey, mnemonic: String, bip44_path: String) -> Self {
141 let private_key = hex::encode(signing_key.to_bytes());
142 let public_key_bytes = signing_key.verifying_key().to_encoded_point(false);
143 let public_key = hex::encode(public_key_bytes.as_bytes());
144 let address = address_from_public_key(public_key_bytes.as_bytes());
145
146 Self {
147 mnemonic,
148 bip44_path,
149 private_key,
150 public_key,
151 address,
152 }
153 }
154}
155
156impl fmt::Debug for Wallet {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.debug_struct("Wallet")
159 .field("mnemonic", &"<redacted>")
160 .field("bip44_path", &self.bip44_path)
161 .field("private_key", &"<redacted>")
162 .field("public_key", &self.public_key)
163 .field("address", &self.address)
164 .finish()
165 }
166}
167
168impl Drop for Wallet {
169 fn drop(&mut self) {
170 self.mnemonic.zeroize();
171 self.private_key.zeroize();
172 }
173}
174
175pub fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
177 Sha256::digest(bytes).into()
178}
179
180pub fn address_from_public_key(public_key: &[u8]) -> String {
182 let first = sha256_bytes(public_key);
183 let second = sha256_bytes(&first);
184 base58check_encode(&second)
185}
186
187pub fn address_to_pb(address: &str) -> Result<Address, CryptoError> {
189 Ok(Address {
190 value: decode_address(address)?,
191 })
192}
193
194pub fn pb_to_address(address: &Address) -> String {
196 base58check_encode(&address.value)
197}
198
199pub fn hash_to_pb(bytes: impl AsRef<[u8]>) -> Hash {
201 Hash {
202 value: bytes.as_ref().to_vec(),
203 }
204}
205
206pub fn parse_aelf_address(value: &str) -> &str {
209 let mut parts = value.split('_');
210 match (parts.next(), parts.next(), parts.next(), parts.next()) {
211 (Some(_prefix), Some(address), Some(_chain_id), None) if !address.is_empty() => address,
212 _ => value,
213 }
214}
215
216pub fn decode_address(address: &str) -> Result<Vec<u8>, CryptoError> {
218 base58check_decode(parse_aelf_address(address))
219}
220
221pub fn base58check_encode(payload: &[u8]) -> String {
223 let checksum = sha256_bytes(&sha256_bytes(payload));
224 let mut bytes = payload.to_vec();
225 bytes.extend_from_slice(&checksum[..4]);
226 bs58::encode(bytes).into_string()
227}
228
229pub fn base58check_decode(value: &str) -> Result<Vec<u8>, CryptoError> {
231 let bytes = bs58::decode(value)
232 .into_vec()
233 .map_err(|_| CryptoError::InvalidAddress)?;
234 if bytes.len() < 4 {
235 return Err(CryptoError::InvalidAddress);
236 }
237
238 let (payload, checksum) = bytes.split_at(bytes.len() - 4);
239 let expected = sha256_bytes(&sha256_bytes(payload));
240 if checksum != &expected[..4] {
241 return Err(CryptoError::InvalidAddress);
242 }
243
244 Ok(payload.to_vec())
245}
246
247pub fn chain_id_to_base58(chain_id: i32) -> String {
249 bs58::encode(chain_id.to_le_bytes()).into_string()
250}
251
252pub fn base58_to_chain_id(chain_id: &str) -> Result<i32, CryptoError> {
254 let bytes = bs58::decode(chain_id)
255 .into_vec()
256 .map_err(|_| CryptoError::InvalidAddress)?;
257 let bytes: [u8; 4] = bytes
258 .as_slice()
259 .try_into()
260 .map_err(|_| CryptoError::InvalidAddress)?;
261 Ok(i32::from_le_bytes(bytes))
262}
263
264pub fn transaction_hash(transaction: &Transaction) -> [u8; 32] {
266 sha256_bytes(&transaction.encode_to_vec())
267}
268
269pub fn sign_transaction(
271 wallet: &Wallet,
272 transaction: &Transaction,
273) -> Result<Vec<u8>, CryptoError> {
274 sign_payload(&wallet.signing_key()?, &transaction.encode_to_vec())
275}
276
277pub fn sign_transaction_with_private_key(
279 private_key_hex: &str,
280 transaction: &Transaction,
281) -> Result<Vec<u8>, CryptoError> {
282 let wallet = Wallet::from_private_key(private_key_hex)?;
283 sign_transaction(&wallet, transaction)
284}
285
286pub fn verify_signature(
288 public_key: &[u8],
289 payload: &[u8],
290 signature_bytes: &[u8],
291) -> Result<bool, CryptoError> {
292 if signature_bytes.len() != 65 {
293 return Err(CryptoError::InvalidSignature);
294 }
295
296 let verifying_key =
297 VerifyingKey::from_sec1_bytes(public_key).map_err(|_| CryptoError::InvalidSignature)?;
298 let recovery_id =
299 RecoveryId::from_byte(signature_bytes[64]).ok_or(CryptoError::InvalidSignature)?;
300 let signature =
301 Signature::from_slice(&signature_bytes[..64]).map_err(|_| CryptoError::InvalidSignature)?;
302
303 let digest = Sha256::new_with_prefix(payload);
304 let recovered = VerifyingKey::recover_from_digest(digest, &signature, recovery_id)
305 .map_err(|_| CryptoError::InvalidSignature)?;
306
307 Ok(recovered == verifying_key)
308}
309
310fn sign_payload(signing_key: &SigningKey, payload: &[u8]) -> Result<Vec<u8>, CryptoError> {
311 let digest = Sha256::new_with_prefix(payload);
312 let (signature, recovery_id) = signing_key
313 .sign_digest_recoverable(digest)
314 .map_err(|_| CryptoError::InvalidSignature)?;
315
316 let mut bytes = Vec::with_capacity(65);
317 bytes.extend_from_slice(&signature.to_bytes());
318 bytes.push(recovery_id.to_byte());
319 Ok(bytes)
320}
321
322fn normalize_private_key_hex(private_key_hex: &str) -> Result<[u8; 32], CryptoError> {
323 let trimmed = private_key_hex
324 .strip_prefix("0x")
325 .unwrap_or(private_key_hex);
326 let normalized = if trimmed.len() >= 64 {
327 trimmed.to_owned()
328 } else {
329 format!("{trimmed:0>64}")
330 };
331
332 let bytes = hex::decode(normalized)?;
333 bytes
334 .as_slice()
335 .try_into()
336 .map_err(|_| CryptoError::InvalidPrivateKey)
337}
338
339#[cfg(test)]
340mod tests {
341 use super::{
342 base58_to_chain_id, chain_id_to_base58, decode_address, parse_aelf_address,
343 sign_transaction, verify_signature, Wallet,
344 };
345 use aelf_proto::aelf::Transaction;
346 use prost::Message;
347
348 const JS_ADDRESS_MNEMONIC: &str =
349 "history segment pizza all time regret robust animal loud gasp razor gadget";
350 const JS_EXPECTED_ADDRESS: &str = "CTqD1M6Kt2v2jS8QLR6tcTq7vv9dHsKibUr6BEaN3BZ94i92m";
351 const JS_SIGN_PRIVATE_KEY: &str =
352 "03bd0cea9730bcfc8045248fd7f4841ea19315995c44801a3dfede0ca872f808";
353 const JS_SIGN_EXPECTED: &str =
354 "276aa36fcab0ac3d4071a4bfb868f636d1a9639916afe4ec329529014f923a372b688b4eb59d6587481bc15e4a1684e1d92b7598967767713d1504dcea83dadb01";
355 const TEST_MNEMONIC: &str =
356 "orange learn result add snack curtain double state expose bless also clarify";
357 const TEST_PRIVATE_KEY: &str =
358 "cc2895b46707a34eefd3c61bd4a8487266e0398a93309a9910a2b88e587b6582";
359
360 #[test]
361 fn derives_known_private_key_from_mnemonic() {
362 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
363 assert_eq!(wallet.private_key(), TEST_PRIVATE_KEY);
364 }
365
366 #[test]
367 fn derives_same_address_from_private_key_and_mnemonic() {
368 let from_mnemonic = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
369 let from_private_key = Wallet::from_private_key(TEST_PRIVATE_KEY).expect("wallet");
370 assert_eq!(from_mnemonic.address(), from_private_key.address());
371 }
372
373 #[test]
374 fn chain_id_roundtrip() {
375 let encoded = chain_id_to_base58(9_992_731);
376 let decoded = base58_to_chain_id(&encoded).expect("chain id");
377 assert_eq!(decoded, 9_992_731);
378 }
379
380 #[test]
381 fn transaction_signature_is_recoverable() {
382 let wallet = Wallet::from_private_key(TEST_PRIVATE_KEY).expect("wallet");
383 let transaction = Transaction {
384 from: None,
385 to: None,
386 ref_block_number: 1,
387 ref_block_prefix: vec![1, 2, 3, 4],
388 method_name: "Test".to_owned(),
389 params: vec![1, 2, 3],
390 signature: Vec::new(),
391 };
392
393 let signature = sign_transaction(&wallet, &transaction).expect("signature");
394 let public_key = hex::decode(wallet.public_key()).expect("public key");
395
396 assert!(
397 verify_signature(&public_key, &transaction.encode_to_vec(), &signature)
398 .expect("verify")
399 );
400 }
401
402 #[test]
403 fn derives_known_address_from_js_mnemonic_fixture() {
404 let wallet = Wallet::from_mnemonic(JS_ADDRESS_MNEMONIC).expect("wallet");
405 assert_eq!(wallet.address(), JS_EXPECTED_ADDRESS);
406 }
407
408 #[test]
409 fn signs_payload_compatible_with_js_fixture() {
410 let wallet = Wallet::from_private_key(JS_SIGN_PRIVATE_KEY).expect("wallet");
411 let signature = wallet.sign(b"hello world").expect("signature");
412 assert_eq!(hex::encode(signature), JS_SIGN_EXPECTED);
413 }
414
415 #[test]
416 fn parses_formatted_aelf_address() {
417 let formatted = format!("ELF_{JS_EXPECTED_ADDRESS}_AELF");
418 assert_eq!(parse_aelf_address(&formatted), JS_EXPECTED_ADDRESS);
419 assert_eq!(
420 decode_address(&formatted).expect("formatted address"),
421 decode_address(JS_EXPECTED_ADDRESS).expect("raw address")
422 );
423 }
424
425 #[test]
426 fn debug_redacts_wallet_secrets() {
427 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC).expect("wallet");
428 let debug = format!("{wallet:?}");
429 assert!(!debug.contains(TEST_MNEMONIC));
430 assert!(!debug.contains(TEST_PRIVATE_KEY));
431 assert!(debug.contains(wallet.address()));
432 }
433}