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			},
110			HDPath::Legacy(index) => {
111				vec![44 + 0x80000000, 888 + 0x80000000, 0 + 0x80000000, *index]
112			},
113			HDPath::Custom(path) => path.clone(),
114		}
115	}
116}
117
118/// A Ledger hardware wallet signer for Neo N3.
119pub struct LedgerWallet<T: LedgerAsync> {
120	/// The Ledger device
121	pub(crate) ledger: Arc<T>,
122	/// The derivation path
123	pub(crate) derivation_path: HDPath,
124	/// The wallet's address
125	pub(crate) address: Option<Address>,
126	/// The network ID
127	pub(crate) network: Option<u64>,
128}
129
130impl<T: LedgerAsync> LedgerWallet<T> {
131	/// Creates a new Ledger wallet with the specified derivation path and account index.
132	pub async fn new(ledger: T, derivation_path: HDPath) -> Result<Self, WalletError> {
133		let ledger = Arc::new(ledger);
134		let mut wallet = Self { ledger, derivation_path, address: None, network: None };
135
136		// Derive the address
137		wallet.address = Some(wallet.get_address().await?);
138
139		Ok(wallet)
140	}
141
142	/// Gets the Neo N3 address for the current derivation path.
143	pub async fn get_address(&self) -> Result<Address, WalletError> {
144		let path = self.derivation_path.to_vec();
145		let command = apdu::get_address(&path, false);
146
147		let response = self
148			.ledger
149			.exchange(&command)
150			.await
151			.map_err(|e| WalletError::LedgerError(format!("Failed to get address: {e}")))?;
152
153		if response.retcode() != 0x9000 {
154			return Err(WalletError::LedgerError(format!(
155				"Ledger error: {:x}",
156				response.retcode()
157			)));
158		}
159
160		// The response data contains the public key and the Neo N3 address
161		// Extract the public key (first 65 bytes) and derive the address
162		let data = response
163			.data()
164			.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
165		if data.len() < 65 {
166			return Err(WalletError::LedgerError("Invalid response data length".to_string()));
167		}
168
169		let public_key = &data[0..65];
170		// Convert the public key to a Neo N3 address
171		let address =
172			Address::from_str(&format!("0x{}", H160::from_slice(&public_key[1..21]).to_hex()))
173				.map_err(|e| WalletError::LedgerError(format!("Failed to derive address: {e}")))?;
174
175		Ok(address)
176	}
177
178	/// Signs a transaction using the Ledger device.
179	pub async fn sign_transaction<'a, P: JsonRpcProvider + 'static>(
180		&self,
181		tx: &Transaction<'a, P>,
182	) -> Result<k256::ecdsa::Signature, WalletError> {
183		let path = self.derivation_path.to_vec();
184
185		// Get the transaction hash
186		let tx_hash = tx.get_hash_data().await?;
187
188		// Create the APDU command
189		let command = apdu::sign_tx(&path, &tx_hash);
190
191		// Send the command to the Ledger device
192		let response =
193			self.ledger.exchange(&command).await.map_err(|e| {
194				WalletError::LedgerError(format!("Failed to sign transaction: {e}"))
195			})?;
196
197		if response.retcode() != 0x9000 {
198			return Err(WalletError::LedgerError(format!(
199				"Ledger error: {:x}",
200				response.retcode()
201			)));
202		}
203
204		// Parse the signature from the response
205		let data = response
206			.data()
207			.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
208		if data.len() != 64 {
209			return Err(WalletError::LedgerError("Invalid signature length".to_string()));
210		}
211
212		// Convert the signature to a Signature
213		let r = H256::from_slice(&data[0..32]);
214		let s = H256::from_slice(&data[32..64]);
215
216		// Create a Signature from r and s
217		let r_bytes: [u8; 32] = r.into();
218		let s_bytes: [u8; 32] = s.into();
219		let signature = k256::ecdsa::Signature::from_scalars(r_bytes, s_bytes)
220			.map_err(|e| WalletError::LedgerError(format!("Failed to create signature: {e}")))?;
221
222		Ok(signature)
223	}
224
225	/// Signs a message using the Ledger device.
226	pub async fn sign_message(
227		&self,
228		message: &[u8],
229	) -> Result<k256::ecdsa::Signature, WalletError> {
230		let path = self.derivation_path.to_vec();
231
232		// Hash the message using SHA-256
233		let message_hash = sha2::Sha256::digest(message);
234
235		// Create the APDU command
236		let command = apdu::sign_message(&path, &message_hash);
237
238		// Send the command to the Ledger device
239		let response = self
240			.ledger
241			.exchange(&command)
242			.await
243			.map_err(|e| WalletError::LedgerError(format!("Failed to sign message: {e}")))?;
244
245		if response.retcode() != 0x9000 {
246			return Err(WalletError::LedgerError(format!(
247				"Ledger error: {:x}",
248				response.retcode()
249			)));
250		}
251
252		// Parse the signature from the response
253		let data = response
254			.data()
255			.ok_or_else(|| WalletError::LedgerError("No data in response".to_string()))?;
256		if data.len() != 64 {
257			return Err(WalletError::LedgerError("Invalid signature length".to_string()));
258		}
259
260		// Convert the signature to a Signature
261		let r = H256::from_slice(&data[0..32]);
262		let s = H256::from_slice(&data[32..64]);
263
264		// Create a Signature from r and s
265		let r_bytes: [u8; 32] = r.into();
266		let s_bytes: [u8; 32] = s.into();
267		let signature = k256::ecdsa::Signature::from_scalars(r_bytes, s_bytes)
268			.map_err(|e| WalletError::LedgerError(format!("Failed to create signature: {e}")))?;
269
270		Ok(signature)
271	}
272}
273
274// Implement Debug for LedgerWallet
275impl<T: LedgerAsync> fmt::Debug for LedgerWallet<T> {
276	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277		f.debug_struct("LedgerWallet")
278			.field("derivation_path", &self.derivation_path)
279			.field("address", &self.address)
280			.field("network", &self.network)
281			.finish()
282	}
283}