use std::{fmt, str::FromStr, sync::Arc};
use crate::{
builder::Transaction, neo_clients::JsonRpcProvider, neo_wallets::WalletError, Address,
ScriptHashExtension,
};
use coins_ledger::transports::LedgerAsync;
use primitive_types::{H160, H256};
use sha2::Digest;
mod apdu {
use coins_ledger::common::APDUCommand;
pub(crate) fn get_address(derivation_path: &[u32], display: bool) -> APDUCommand {
let mut data = Vec::new();
data.push(derivation_path.len() as u8);
for item in derivation_path.iter() {
data.push((*item >> 24) as u8);
data.push((*item >> 16) as u8);
data.push((*item >> 8) as u8);
data.push(*item as u8);
}
APDUCommand {
cla: 0x80,
ins: 0x02,
p1: if display { 0x01 } else { 0x00 },
p2: 0x00,
data: data.into(),
response_len: Some(65),
}
}
pub(crate) fn sign_tx(derivation_path: &[u32], tx_hash: &[u8]) -> APDUCommand {
let mut data = Vec::new();
data.push(derivation_path.len() as u8);
for item in derivation_path.iter() {
data.push((*item >> 24) as u8);
data.push((*item >> 16) as u8);
data.push((*item >> 8) as u8);
data.push(*item as u8);
}
data.extend_from_slice(tx_hash);
APDUCommand {
cla: 0x80,
ins: 0x04,
p1: 0x00,
p2: 0x00,
data: data.into(),
response_len: Some(64),
}
}
pub(crate) fn sign_message(derivation_path: &[u32], message_hash: &[u8]) -> APDUCommand {
let mut data = Vec::new();
data.push(derivation_path.len() as u8);
for item in derivation_path.iter() {
data.push((*item >> 24) as u8);
data.push((*item >> 16) as u8);
data.push((*item >> 8) as u8);
data.push(*item as u8);
}
data.extend_from_slice(message_hash);
APDUCommand {
cla: 0x80,
ins: 0x08,
p1: 0x00,
p2: 0x00,
data: data.into(),
response_len: Some(64),
}
}
}
#[derive(Debug, Clone)]
pub enum HDPath {
LedgerLive(u32),
Legacy(u32),
Custom(Vec<u32>),
}
impl HDPath {
pub fn to_vec(&self) -> Vec<u32> {
match self {
HDPath::LedgerLive(index) => {
vec![44 + 0x80000000, 888 + 0x80000000, 0x80000000, 0, *index]
},
HDPath::Legacy(index) => {
vec![44 + 0x80000000, 888 + 0x80000000, 0x80000000, *index]
},
HDPath::Custom(path) => path.clone(),
}
}
}
pub struct LedgerWallet<T: LedgerAsync> {
pub(crate) ledger: Arc<T>,
pub(crate) derivation_path: HDPath,
pub(crate) address: Option<Address>,
pub(crate) network: Option<u64>,
}
impl<T: LedgerAsync> LedgerWallet<T> {
pub async fn new(ledger: T, derivation_path: HDPath) -> Result<Self, WalletError> {
let ledger = Arc::new(ledger);
let mut wallet = Self { ledger, derivation_path, address: None, network: None };
wallet.address = Some(wallet.get_address().await?);
Ok(wallet)
}
pub async fn get_address(&self) -> Result<Address, WalletError> {
let path = self.derivation_path.to_vec();
let command = apdu::get_address(&path, false);
let response = self
.ledger
.exchange(&command)
.await
.map_err(|e| WalletError::LedgerError(format!("Failed to get address: {e}")))?;
if response.retcode() != 0x9000 {
return Err(WalletError::LedgerError(format!(
"Ledger error: {:x}",
response.retcode()
)));
}
let data = response
.data()
.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
if data.len() < 65 {
return Err(WalletError::LedgerError("Invalid response data length".to_string()));
}
let public_key = &data[0..65];
let address =
Address::from_str(&format!("0x{}", H160::from_slice(&public_key[1..21]).to_hex()))
.map_err(|e| WalletError::LedgerError(format!("Failed to derive address: {e}")))?;
Ok(address)
}
pub async fn sign_transaction<'a, P: JsonRpcProvider + 'static>(
&self,
tx: &Transaction<'a, P>,
) -> Result<k256::ecdsa::Signature, WalletError> {
let path = self.derivation_path.to_vec();
let tx_hash = tx.get_hash_data().await?;
let command = apdu::sign_tx(&path, &tx_hash);
let response =
self.ledger.exchange(&command).await.map_err(|e| {
WalletError::LedgerError(format!("Failed to sign transaction: {e}"))
})?;
if response.retcode() != 0x9000 {
return Err(WalletError::LedgerError(format!(
"Ledger error: {:x}",
response.retcode()
)));
}
let data = response
.data()
.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
if data.len() != 64 {
return Err(WalletError::LedgerError("Invalid signature length".to_string()));
}
let r = H256::from_slice(&data[0..32]);
let s = H256::from_slice(&data[32..64]);
let r_bytes: [u8; 32] = r.into();
let s_bytes: [u8; 32] = s.into();
let signature = k256::ecdsa::Signature::from_scalars(r_bytes, s_bytes)
.map_err(|e| WalletError::LedgerError(format!("Failed to create signature: {e}")))?;
Ok(signature)
}
pub async fn sign_message(
&self,
message: &[u8],
) -> Result<k256::ecdsa::Signature, WalletError> {
let path = self.derivation_path.to_vec();
let message_hash = sha2::Sha256::digest(message);
let command = apdu::sign_message(&path, &message_hash);
let response = self
.ledger
.exchange(&command)
.await
.map_err(|e| WalletError::LedgerError(format!("Failed to sign message: {e}")))?;
if response.retcode() != 0x9000 {
return Err(WalletError::LedgerError(format!(
"Ledger error: {:x}",
response.retcode()
)));
}
let data = response
.data()
.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
if data.len() != 64 {
return Err(WalletError::LedgerError("Invalid signature length".to_string()));
}
let r = H256::from_slice(&data[0..32]);
let s = H256::from_slice(&data[32..64]);
let r_bytes: [u8; 32] = r.into();
let s_bytes: [u8; 32] = s.into();
let signature = k256::ecdsa::Signature::from_scalars(r_bytes, s_bytes)
.map_err(|e| WalletError::LedgerError(format!("Failed to create signature: {e}")))?;
Ok(signature)
}
}
impl<T: LedgerAsync> fmt::Debug for LedgerWallet<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LedgerWallet")
.field("derivation_path", &self.derivation_path)
.field("address", &self.address)
.field("network", &self.network)
.finish()
}
}