pub mod generate;
pub mod ledger;
pub mod store;
use aleph_types::account::{Account, EvmAccount, SignError, SolanaAccount};
use aleph_types::chain::{Address, Chain, Signature};
use anyhow::{Context, Result, bail};
use zeroize::Zeroizing;
pub enum CliAccount {
Evm(EvmAccount),
Sol(SolanaAccount),
LedgerEvm(ledger::LedgerEvmAccount),
}
impl std::fmt::Debug for CliAccount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CliAccount::Evm(a) => write!(f, "CliAccount::Evm({})", a.address()),
CliAccount::Sol(a) => write!(f, "CliAccount::Sol({})", a.address()),
CliAccount::LedgerEvm(a) => write!(f, "CliAccount::LedgerEvm({})", a.address()),
}
}
}
impl Account for CliAccount {
fn chain(&self) -> Chain {
match self {
CliAccount::Evm(a) => a.chain(),
CliAccount::Sol(a) => a.chain(),
CliAccount::LedgerEvm(a) => a.chain(),
}
}
fn address(&self) -> &Address {
match self {
CliAccount::Evm(a) => a.address(),
CliAccount::Sol(a) => a.address(),
CliAccount::LedgerEvm(a) => a.address(),
}
}
fn sign_raw(&self, buffer: &[u8]) -> Result<Signature, SignError> {
match self {
CliAccount::Evm(a) => a.sign_raw(buffer),
CliAccount::Sol(a) => a.sign_raw(buffer),
CliAccount::LedgerEvm(a) => a.sign_raw(buffer),
}
}
}
pub fn load_account(private_key: Option<&str>, chain: Chain) -> Result<CliAccount> {
let key_hex = Zeroizing::new(match private_key {
Some(k) => k.to_string(),
None => std::env::var("ALEPH_PRIVATE_KEY")
.context("no private key provided; use --private-key or set ALEPH_PRIVATE_KEY")?,
});
let key_hex = key_hex.strip_prefix("0x").unwrap_or(&key_hex);
let key_bytes = Zeroizing::new(hex::decode(key_hex).context("invalid hex in private key")?);
if chain.is_evm() {
let account = EvmAccount::new(chain, &key_bytes).map_err(|e| anyhow::anyhow!(e))?;
Ok(CliAccount::Evm(account))
} else if chain.is_svm() {
let account = SolanaAccount::new(chain, &key_bytes).map_err(|e| anyhow::anyhow!(e))?;
Ok(CliAccount::Sol(account))
} else {
bail!("chain {chain} is not supported for signing (only EVM and SVM chains)")
}
}
pub fn load_account_by_name(store: &store::AccountStore, name: &str) -> Result<CliAccount> {
let entry = store
.get_account(name)
.map_err(|e| anyhow::anyhow!("{e}"))?;
match entry.kind {
store::AccountKind::Local => {
let key_hex = Zeroizing::new(
store
.get_private_key(name)
.map_err(|e| anyhow::anyhow!("{e}"))?,
);
load_account(Some(&key_hex), entry.chain)
}
store::AccountKind::Ledger => {
let path_str = entry
.derivation_path
.as_deref()
.ok_or_else(|| anyhow::anyhow!("ledger account '{name}' has no derivation path"))?;
let path = ledger::DerivationPath::parse(path_str)
.map_err(|e| anyhow::anyhow!("invalid derivation path for '{name}': {e}"))?;
let address = Address::from(entry.address);
if entry.chain.is_evm() {
Ok(CliAccount::LedgerEvm(ledger::LedgerEvmAccount::new(
address,
entry.chain,
path,
)))
} else if entry.chain.is_svm() {
bail!("Solana Ledger signing is not supported. Use a local Solana key instead.")
} else {
bail!("chain {} is not supported for Ledger signing", entry.chain)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_KEY_HEX: &str = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efbba0f2d1db744ce06";
#[test]
fn load_evm_account() {
let account = load_account(Some(TEST_KEY_HEX), Chain::Ethereum).unwrap();
assert!(matches!(account, CliAccount::Evm(_)));
assert_eq!(account.chain(), Chain::Ethereum);
assert!(account.address().as_str().starts_with("0x"));
}
#[test]
fn load_evm_account_with_0x_prefix() {
let prefixed = format!("0x{TEST_KEY_HEX}");
let account = load_account(Some(&prefixed), Chain::Ethereum).unwrap();
let account_no_prefix = load_account(Some(TEST_KEY_HEX), Chain::Ethereum).unwrap();
assert_eq!(account.address(), account_no_prefix.address());
}
#[test]
fn load_evm_account_other_chain() {
let account = load_account(Some(TEST_KEY_HEX), Chain::Base).unwrap();
assert!(matches!(account, CliAccount::Evm(_)));
assert_eq!(account.chain(), Chain::Base);
}
#[test]
fn load_sol_account() {
let account = load_account(Some(TEST_KEY_HEX), Chain::Sol).unwrap();
assert!(matches!(account, CliAccount::Sol(_)));
assert_eq!(account.chain(), Chain::Sol);
assert!(!account.address().as_str().starts_with("0x"));
}
#[test]
fn load_account_invalid_hex() {
let err = load_account(Some("not-valid-hex!"), Chain::Ethereum).unwrap_err();
assert!(err.to_string().contains("invalid hex"));
}
#[test]
fn load_account_wrong_key_length() {
let err = load_account(Some("abcd"), Chain::Ethereum).unwrap_err();
assert!(err.to_string().contains("expected 32 bytes"));
}
#[test]
fn load_account_unsupported_chain() {
let err = load_account(Some(TEST_KEY_HEX), Chain::Tezos).unwrap_err();
assert!(err.to_string().contains("not supported for signing"));
}
#[test]
fn load_account_no_key_no_env() {
if std::env::var("ALEPH_PRIVATE_KEY").is_err() {
let err = load_account(None, Chain::Ethereum).unwrap_err();
assert!(err.to_string().contains("no private key provided"));
}
}
#[test]
fn cli_account_can_sign() {
let account = load_account(Some(TEST_KEY_HEX), Chain::Ethereum).unwrap();
let sig = account.sign_raw(b"test message").unwrap();
assert!(sig.as_str().starts_with("0x"));
}
#[test]
fn load_account_by_name_not_found() {
let dir = tempfile::tempdir().unwrap();
let store = store::AccountStore::with_manifest_path(dir.path().join("accounts.toml"));
let err = load_account_by_name(&store, "nonexistent").unwrap_err();
assert!(err.to_string().contains("not found"));
}
}