neo3/neo_wallets/
ledger.rs

1use std::{fmt, str::FromStr, sync::Arc};
2
3use crate::{
4	builder::Transaction, neo_clients::JsonRpcProvider, neo_wallets::WalletError, Address,
5	ScriptHashExtension,
6};
7use async_trait::async_trait;
8use coins_ledger::{
9	common::APDUCommand,
10	transports::{Ledger, LedgerAsync},
11};
12use p256::NistP256;
13use primitive_types::{H160, H256};
14use sha2::Digest;
15use signature::hazmat::{PrehashSigner, PrehashVerifier};
16
17/// Neo N3 APDU commands for Ledger devices.
18pub mod apdu {
19	use coins_ledger::common::APDUCommand;
20
21	/// APDU command to get the Neo N3 address for a given derivation path.
22	pub fn get_address(derivation_path: &[u32], display: bool) -> APDUCommand {
23		let mut data = Vec::new();
24		data.push(derivation_path.len() as u8);
25
26		for item in derivation_path.iter() {
27			data.push((*item >> 24) as u8);
28			data.push((*item >> 16) as u8);
29			data.push((*item >> 8) as u8);
30			data.push(*item as u8);
31		}
32
33		APDUCommand {
34			cla: 0x80,
35			ins: 0x02,
36			p1: if display { 0x01 } else { 0x00 },
37			p2: 0x00,
38			data: data.into(),
39			response_len: Some(65),
40		}
41	}
42
43	/// APDU command to sign a Neo N3 transaction.
44	pub fn sign_tx(derivation_path: &[u32], tx_hash: &[u8]) -> APDUCommand {
45		let mut data = Vec::new();
46		data.push(derivation_path.len() as u8);
47
48		for item in derivation_path.iter() {
49			data.push((*item >> 24) as u8);
50			data.push((*item >> 16) as u8);
51			data.push((*item >> 8) as u8);
52			data.push(*item as u8);
53		}
54
55		data.extend_from_slice(tx_hash);
56
57		APDUCommand {
58			cla: 0x80,
59			ins: 0x04,
60			p1: 0x00,
61			p2: 0x00,
62			data: data.into(),
63			response_len: Some(64),
64		}
65	}
66
67	/// APDU command to sign a Neo N3 message.
68	pub fn sign_message(derivation_path: &[u32], message_hash: &[u8]) -> APDUCommand {
69		let mut data = Vec::new();
70		data.push(derivation_path.len() as u8);
71
72		for item in derivation_path.iter() {
73			data.push((*item >> 24) as u8);
74			data.push((*item >> 16) as u8);
75			data.push((*item >> 8) as u8);
76			data.push(*item as u8);
77		}
78
79		data.extend_from_slice(message_hash);
80
81		APDUCommand {
82			cla: 0x80,
83			ins: 0x08,
84			p1: 0x00,
85			p2: 0x00,
86			data: data.into(),
87			response_len: Some(64),
88		}
89	}
90}
91
92/// Represents a hierarchical deterministic path for Neo N3 accounts.
93#[derive(Debug, Clone)]
94pub enum HDPath {
95	/// Ledger Live-style derivation path: m/44'/888'/0'/0/0
96	LedgerLive(u32),
97	/// Legacy derivation path: m/44'/888'/0'/0
98	Legacy(u32),
99	/// Custom derivation path
100	Custom(Vec<u32>),
101}
102
103impl HDPath {
104	/// Converts the HD path to a vector of integers.
105	pub fn to_vec(&self) -> Vec<u32> {
106		match self {
107			HDPath::LedgerLive(index) =>
108				vec![44 + 0x80000000, 888 + 0x80000000, 0 + 0x80000000, 0, *index],
109			HDPath::Legacy(index) =>
110				vec![44 + 0x80000000, 888 + 0x80000000, 0 + 0x80000000, *index],
111			HDPath::Custom(path) => path.clone(),
112		}
113	}
114}
115
116/// A Ledger hardware wallet signer for Neo N3.
117pub struct LedgerWallet<T: LedgerAsync> {
118	/// The Ledger device
119	pub(crate) ledger: Arc<T>,
120	/// The derivation path
121	pub(crate) derivation_path: HDPath,
122	/// The wallet's address
123	pub(crate) address: Option<Address>,
124	/// The network ID
125	pub(crate) network: Option<u64>,
126}
127
128impl<T: LedgerAsync> LedgerWallet<T> {
129	/// Creates a new Ledger wallet with the specified derivation path and account index.
130	pub async fn new(ledger: T, derivation_path: HDPath) -> Result<Self, WalletError> {
131		let ledger = Arc::new(ledger);
132		let mut wallet = Self { ledger, derivation_path, address: None, network: None };
133
134		// Derive the address
135		wallet.address = Some(wallet.get_address().await?);
136
137		Ok(wallet)
138	}
139
140	/// Gets the Neo N3 address for the current derivation path.
141	pub async fn get_address(&self) -> Result<Address, WalletError> {
142		let path = self.derivation_path.to_vec();
143		let command = apdu::get_address(&path, false);
144
145		let response = self
146			.ledger
147			.exchange(&command)
148			.await
149			.map_err(|e| WalletError::LedgerError(format!("Failed to get address: {e}")))?;
150
151		if response.retcode() != 0x9000 {
152			return Err(WalletError::LedgerError(format!("Ledger error: {:x}", response.retcode())));
153		}
154
155		// The response data contains the public key and the Neo N3 address
156		// Extract the public key (first 65 bytes) and derive the address
157		let data = response
158			.data()
159			.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
160		if data.len() < 65 {
161			return Err(WalletError::LedgerError("Invalid response data length".to_string()));
162		}
163
164		let public_key = &data[0..65];
165		// Convert the public key to a Neo N3 address
166		let address =
167			Address::from_str(&format!("0x{}", H160::from_slice(&public_key[1..21]).to_hex()))
168				.map_err(|e| WalletError::LedgerError(format!("Failed to derive address: {e}")))?;
169
170		Ok(address)
171	}
172
173	/// Signs a transaction using the Ledger device.
174	pub async fn sign_transaction<'a, P: JsonRpcProvider + 'static>(
175		&self,
176		tx: &Transaction<'a, P>,
177	) -> Result<k256::ecdsa::Signature, WalletError> {
178		let path = self.derivation_path.to_vec();
179
180		// Get the transaction hash
181		let tx_hash = tx.get_hash_data().await?;
182
183		// Create the APDU command
184		let command = apdu::sign_tx(&path, &tx_hash);
185
186		// Send the command to the Ledger device
187		let response =
188			self.ledger.exchange(&command).await.map_err(|e| {
189				WalletError::LedgerError(format!("Failed to sign transaction: {e}"))
190			})?;
191
192		if response.retcode() != 0x9000 {
193			return Err(WalletError::LedgerError(format!("Ledger error: {:x}", response.retcode())));
194		}
195
196		// Parse the signature from the response
197		let data = response
198			.data()
199			.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
200		if data.len() != 64 {
201			return Err(WalletError::LedgerError("Invalid signature length".to_string()));
202		}
203
204		// Convert the signature to a Signature
205		let r = H256::from_slice(&data[0..32]);
206		let s = H256::from_slice(&data[32..64]);
207
208		// Create a Signature from r and s
209		let r_bytes: [u8; 32] = r.into();
210		let s_bytes: [u8; 32] = s.into();
211		let signature = k256::ecdsa::Signature::from_scalars(r_bytes, s_bytes)
212			.map_err(|e| WalletError::LedgerError(format!("Failed to create signature: {e}")))?;
213
214		Ok(signature)
215	}
216
217	/// Signs a message using the Ledger device.
218	pub async fn sign_message(
219		&self,
220		message: &[u8],
221	) -> Result<k256::ecdsa::Signature, WalletError> {
222		let path = self.derivation_path.to_vec();
223
224		// Hash the message using SHA-256
225		let message_hash = sha2::Sha256::digest(message);
226
227		// Create the APDU command
228		let command = apdu::sign_message(&path, &message_hash);
229
230		// Send the command to the Ledger device
231		let response = self
232			.ledger
233			.exchange(&command)
234			.await
235			.map_err(|e| WalletError::LedgerError(format!("Failed to sign message: {e}")))?;
236
237		if response.retcode() != 0x9000 {
238			return Err(WalletError::LedgerError(format!("Ledger error: {:x}", response.retcode())));
239		}
240
241		// Parse the signature from the response
242		let data = response
243			.data()
244			.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
245		if data.len() != 64 {
246			return Err(WalletError::LedgerError("Invalid signature length".to_string()));
247		}
248
249		// Convert the signature to a Signature
250		let r = H256::from_slice(&data[0..32]);
251		let s = H256::from_slice(&data[32..64]);
252
253		// Create a Signature from r and s
254		let r_bytes: [u8; 32] = r.into();
255		let s_bytes: [u8; 32] = s.into();
256		let signature = k256::ecdsa::Signature::from_scalars(r_bytes, s_bytes)
257			.map_err(|e| WalletError::LedgerError(format!("Failed to create signature: {e}")))?;
258
259		Ok(signature)
260	}
261}
262
263// Implement Debug for LedgerWallet
264impl<T: LedgerAsync> fmt::Debug for LedgerWallet<T> {
265	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266		f.debug_struct("LedgerWallet")
267			.field("derivation_path", &self.derivation_path)
268			.field("address", &self.address)
269			.field("network", &self.network)
270			.finish()
271	}
272}