ledger_sdk_eth_app/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Ledger Ethereum Application SDK
4//!
5//! This crate provides a comprehensive interface for interacting with the Ethereum application
6//! on Ledger hardware wallets. It implements the full APDU command set as specified in the
7//! Ethereum application technical documentation.
8//!
9//! ## Features
10//!
11//! - **Core Operations**: Get addresses, sign transactions, sign personal messages
12//! - **Configuration**: Query application configuration and capabilities
13//! - **BIP32 Support**: Full BIP32 derivation path support with validation
14//! - **Chunked Operations**: Support for large data transmission via chunked APDU commands
15//! - **Type Safety**: Strongly typed parameters and responses
16//! - **Async/Await**: Fully async API using async-trait
17//!
18//!
19
20use async_trait::async_trait;
21use ledger_sdk_device_base::App;
22use ledger_sdk_transport::Exchange;
23
24// Re-export all public types and traits
25pub mod commands;
26pub mod errors;
27pub mod instructions;
28pub mod types;
29pub mod utils;
30
31pub use commands::*;
32pub use errors::*;
33pub use types::*;
34
35/// Ethereum app marker implementing `App` trait CLA.
36#[derive(Debug, Clone)]
37pub struct EthApp;
38
39impl App for EthApp {
40 /// CLA for Ethereum app on Ledger (0xE0)
41 const CLA: u8 = 0xE0;
42}
43
44/// High-level Ethereum application client
45///
46/// This struct provides a convenient interface for all Ethereum application operations.
47/// It wraps the transport layer and provides type-safe methods for interacting with
48/// the Ledger device.
49#[derive(Debug)]
50pub struct EthereumApp<E: Exchange> {
51 transport: E,
52}
53
54impl<E: Exchange> EthereumApp<E> {
55 /// Create a new Ethereum application client
56 pub fn new(transport: E) -> Self {
57 Self { transport }
58 }
59
60 /// Get a reference to the underlying transport
61 pub fn transport(&self) -> &E {
62 &self.transport
63 }
64}
65
66#[async_trait]
67impl<E> GetAddress<E> for EthereumApp<E>
68where
69 E: Exchange + Send + Sync,
70 E::Error: std::error::Error,
71{
72 async fn get_address(
73 transport: &E,
74 params: GetAddressParams,
75 ) -> EthAppResult<PublicKeyInfo, E::Error> {
76 EthApp::get_address(transport, params).await
77 }
78}
79
80#[async_trait]
81impl<E> GetConfiguration<E> for EthereumApp<E>
82where
83 E: Exchange + Send + Sync,
84 E::Error: std::error::Error,
85{
86 async fn get_configuration(transport: &E) -> EthAppResult<AppConfiguration, E::Error> {
87 EthApp::get_configuration(transport).await
88 }
89}
90
91#[async_trait]
92impl<E> SignPersonalMessage<E> for EthereumApp<E>
93where
94 E: Exchange + Send + Sync,
95 E::Error: std::error::Error,
96{
97 async fn sign_personal_message(
98 transport: &E,
99 params: SignMessageParams,
100 ) -> EthAppResult<Signature, E::Error> {
101 EthApp::sign_personal_message(transport, params).await
102 }
103}
104
105#[async_trait]
106impl<E> SignTransaction<E> for EthereumApp<E>
107where
108 E: Exchange + Send + Sync,
109 E::Error: std::error::Error,
110{
111 async fn sign_transaction(
112 transport: &E,
113 params: SignTransactionParams,
114 ) -> EthAppResult<Signature, E::Error> {
115 EthApp::sign_transaction(transport, params).await
116 }
117
118 async fn sign_transaction_with_mode(
119 transport: &E,
120 params: SignTransactionParams,
121 mode: commands::sign_transaction::TransactionMode,
122 ) -> EthAppResult<Option<Signature>, E::Error> {
123 EthApp::sign_transaction_with_mode(transport, params, mode).await
124 }
125}
126
127#[async_trait]
128impl<E> SignEip712V0<E> for EthereumApp<E>
129where
130 E: Exchange + Send + Sync,
131 E::Error: std::error::Error,
132{
133 async fn sign_eip712_v0(
134 transport: &E,
135 params: SignEip712Params,
136 ) -> EthAppResult<Signature, E::Error> {
137 EthApp::sign_eip712_v0(transport, params).await
138 }
139}
140
141#[async_trait]
142impl<E> SignEip712Full<E> for EthereumApp<E>
143where
144 E: Exchange + Send + Sync,
145 E::Error: std::error::Error,
146{
147 async fn sign_eip712_full(transport: &E, path: &BipPath) -> EthAppResult<Signature, E::Error> {
148 EthApp::sign_eip712_full(transport, path).await
149 }
150}
151
152#[async_trait]
153impl<E> Eip712StructDef<E> for EthereumApp<E>
154where
155 E: Exchange + Send + Sync,
156 E::Error: std::error::Error,
157{
158 async fn send_struct_definition(
159 transport: &E,
160 struct_def: &Eip712StructDefinition,
161 ) -> EthAppResult<(), E::Error> {
162 EthApp::send_struct_definition(transport, struct_def).await
163 }
164}
165
166#[async_trait]
167impl<E> Eip712StructImpl<E> for EthereumApp<E>
168where
169 E: Exchange + Send + Sync,
170 E::Error: std::error::Error,
171{
172 async fn send_struct_implementation(
173 transport: &E,
174 struct_impl: &Eip712StructImplementation,
175 ) -> EthAppResult<(), E::Error> {
176 EthApp::send_struct_implementation(transport, struct_impl).await
177 }
178
179 async fn set_array_size(transport: &E, size: u8) -> EthAppResult<(), E::Error> {
180 EthApp::set_array_size(transport, size).await
181 }
182}
183
184#[async_trait]
185impl<E> Eip712Filtering<E> for EthereumApp<E>
186where
187 E: Exchange + Send + Sync,
188 E::Error: std::error::Error,
189{
190 async fn send_filter_config(
191 transport: &E,
192 filter_params: &Eip712FilterParams,
193 ) -> EthAppResult<(), E::Error> {
194 EthApp::send_filter_config(transport, filter_params).await
195 }
196
197 async fn activate_filtering(transport: &E) -> EthAppResult<(), E::Error> {
198 EthApp::activate_filtering(transport).await
199 }
200}
201
202impl<E> EthereumApp<E>
203where
204 E: Exchange + Send + Sync,
205 E::Error: std::error::Error,
206{
207 /// Get Ethereum public address for the given BIP 32 path
208 ///
209 /// # Arguments
210 ///
211 /// * `params` - Parameters for address retrieval including path, display options, etc.
212 ///
213 /// # Returns
214 ///
215 /// Returns `PublicKeyInfo` containing the public key, address, and optionally chain code.
216 ///
217 ///
218 pub async fn get_address(
219 &self,
220 params: GetAddressParams,
221 ) -> EthAppResult<PublicKeyInfo, E::Error> {
222 EthApp::get_address(&self.transport, params).await
223 }
224
225 /// Get Ethereum application configuration
226 ///
227 /// Returns information about the application's capabilities and version.
228 ///
229 ///
230 pub async fn get_configuration(&self) -> EthAppResult<AppConfiguration, E::Error> {
231 EthApp::get_configuration(&self.transport).await
232 }
233
234 /// Sign an Ethereum personal message
235 ///
236 /// Signs a message using the personal_sign specification. The message will be
237 /// displayed on the device for user confirmation.
238 ///
239 /// # Arguments
240 ///
241 /// * `params` - Parameters including BIP32 path and message data
242 ///
243 ///
244 pub async fn sign_personal_message(
245 &self,
246 params: SignMessageParams,
247 ) -> EthAppResult<Signature, E::Error> {
248 EthApp::sign_personal_message(&self.transport, params).await
249 }
250
251 /// Sign an Ethereum transaction
252 ///
253 /// Signs a transaction using the provided RLP-encoded transaction data.
254 /// The transaction details will be displayed on the device for user confirmation.
255 ///
256 /// # Arguments
257 ///
258 /// * `params` - Parameters including BIP32 path and RLP-encoded transaction data
259 ///
260 ///
261 pub async fn sign_transaction(
262 &self,
263 params: SignTransactionParams,
264 ) -> EthAppResult<Signature, E::Error> {
265 EthApp::sign_transaction(&self.transport, params).await
266 }
267
268 /// Sign an Ethereum transaction with specific processing mode
269 ///
270 /// Provides fine-grained control over transaction processing, allowing for
271 /// operations like storing transaction data without immediate signing.
272 ///
273 /// # Arguments
274 ///
275 /// * `params` - Parameters including BIP32 path and RLP-encoded transaction data
276 /// * `mode` - Processing mode (ProcessAndStart, StoreOnly, or StartFlow)
277 ///
278 /// # Returns
279 ///
280 /// Returns `Some(Signature)` for modes that produce a signature, or `None` for store-only mode.
281 pub async fn sign_transaction_with_mode(
282 &self,
283 params: SignTransactionParams,
284 mode: commands::sign_transaction::TransactionMode,
285 ) -> EthAppResult<Option<Signature>, E::Error> {
286 EthApp::sign_transaction_with_mode(&self.transport, params, mode).await
287 }
288
289 /// Sign an EIP-712 message using v0 implementation (domain hash + message hash)
290 ///
291 /// This is the simpler EIP-712 signing mode where domain and message hashes
292 /// are computed externally and provided directly to the device.
293 ///
294 /// **Version Requirements**: Requires app version >= 1.5.0
295 ///
296 /// # Arguments
297 ///
298 /// * `params` - Parameters including BIP32 path, domain hash, and message hash
299 ///
300 /// # Errors
301 ///
302 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.5.0
303 ///
304 pub async fn sign_eip712_v0(
305 &self,
306 params: SignEip712Params,
307 ) -> EthAppResult<Signature, E::Error> {
308 // Check version requirement for EIP-712 v0 (>= 1.5.0)
309 let config = self.get_configuration().await?;
310 if !config.version.supports_eip712_v0() {
311 return Err(EthAppError::UnsupportedVersion(format!(
312 "EIP-712 v0 requires app version >= 1.5.0, found {}",
313 config.version
314 )));
315 }
316
317 EthApp::sign_eip712_v0(&self.transport, params).await
318 }
319
320 /// Sign an EIP-712 message using full implementation
321 ///
322 /// This mode requires sending struct definitions and implementations before
323 /// calling this final signing method. Use the struct definition and
324 /// implementation methods first to set up the EIP-712 data.
325 ///
326 /// **Version Requirements**: Requires app version >= 1.9.19
327 ///
328 /// # Arguments
329 ///
330 /// * `path` - BIP32 derivation path for the signing key
331 ///
332 /// # Errors
333 ///
334 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
335 ///
336 pub async fn sign_eip712_full(&self, path: &BipPath) -> EthAppResult<Signature, E::Error> {
337 // Check version requirement for EIP-712 full (>= 1.9.19)
338 let config = self.get_configuration().await?;
339 if !config.version.supports_eip712_full() {
340 return Err(EthAppError::UnsupportedVersion(format!(
341 "EIP-712 full implementation requires app version >= 1.9.19, found {}",
342 config.version
343 )));
344 }
345
346 EthApp::sign_eip712_full(&self.transport, path).await
347 }
348
349 /// Send EIP-712 struct definition to the device
350 ///
351 /// This method sends type definitions for EIP-712 structures. Must be called
352 /// before sending struct implementations in full EIP-712 mode.
353 ///
354 /// **Version Requirements**: Requires app version >= 1.9.19
355 ///
356 /// # Arguments
357 ///
358 /// * `struct_def` - The struct definition including name and field types
359 ///
360 /// # Errors
361 ///
362 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
363 ///
364 pub async fn send_struct_definition(
365 &self,
366 struct_def: &Eip712StructDefinition,
367 ) -> EthAppResult<(), E::Error> {
368 // Check version requirement for EIP-712 full implementation
369 let config = self.get_configuration().await?;
370 if !config.version.supports_eip712_full() {
371 return Err(EthAppError::UnsupportedVersion(format!(
372 "EIP-712 struct definitions require app version >= 1.9.19, found {}",
373 config.version
374 )));
375 }
376
377 EthApp::send_struct_definition(&self.transport, struct_def).await
378 }
379
380 /// Send EIP-712 struct implementation to the device
381 ///
382 /// This method sends the actual data values for EIP-712 structures.
383 /// Must be called after sending struct definitions.
384 ///
385 /// **Version Requirements**: Requires app version >= 1.9.19
386 ///
387 /// # Arguments
388 ///
389 /// * `struct_impl` - The struct implementation with field values
390 /// * `complete` - Whether this is a complete send or partial
391 ///
392 /// # Errors
393 ///
394 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
395 ///
396 pub async fn send_struct_implementation(
397 &self,
398 struct_impl: &Eip712StructImplementation,
399 ) -> EthAppResult<(), E::Error> {
400 // Check version requirement for EIP-712 full implementation
401 let config = self.get_configuration().await?;
402 if !config.version.supports_eip712_full() {
403 return Err(EthAppError::UnsupportedVersion(format!(
404 "EIP-712 struct implementations require app version >= 1.9.19, found {}",
405 config.version
406 )));
407 }
408
409 EthApp::send_struct_implementation(&self.transport, struct_impl).await
410 }
411
412 /// Set array size for upcoming array fields in EIP-712 implementation
413 ///
414 /// **Version Requirements**: Requires app version >= 1.9.19
415 ///
416 /// # Arguments
417 ///
418 /// * `size` - The size of the array
419 ///
420 /// # Errors
421 ///
422 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
423 ///
424 pub async fn set_array_size(&self, size: u8) -> EthAppResult<(), E::Error> {
425 // Check version requirement for EIP-712 full implementation
426 let config = self.get_configuration().await?;
427 if !config.version.supports_eip712_full() {
428 return Err(EthAppError::UnsupportedVersion(format!(
429 "EIP-712 array operations require app version >= 1.9.19, found {}",
430 config.version
431 )));
432 }
433
434 EthApp::set_array_size(&self.transport, size).await
435 }
436
437 /// Send EIP-712 filtering configuration
438 ///
439 /// Configure how EIP-712 data should be filtered and displayed on the device.
440 ///
441 /// **Version Requirements**: Requires app version >= 1.9.19
442 ///
443 /// # Arguments
444 ///
445 /// * `filter_params` - Filtering parameters and configuration
446 ///
447 /// # Errors
448 ///
449 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
450 ///
451 pub async fn send_filter_config(
452 &self,
453 filter_params: &Eip712FilterParams,
454 ) -> EthAppResult<(), E::Error> {
455 // Check version requirement for EIP-712 full implementation
456 let config = self.get_configuration().await?;
457 if !config.version.supports_eip712_full() {
458 return Err(EthAppError::UnsupportedVersion(format!(
459 "EIP-712 filtering requires app version >= 1.9.19, found {}",
460 config.version
461 )));
462 }
463
464 EthApp::send_filter_config(&self.transport, filter_params).await
465 }
466
467 /// Activate EIP-712 filtering on the device
468 ///
469 /// Must be called to enable filtering before sending struct definitions.
470 ///
471 /// **Version Requirements**: Requires app version >= 1.9.19
472 ///
473 /// # Errors
474 ///
475 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
476 ///
477 pub async fn activate_filtering(&self) -> EthAppResult<(), E::Error> {
478 // Check version requirement for EIP-712 full implementation
479 let config = self.get_configuration().await?;
480 if !config.version.supports_eip712_full() {
481 return Err(EthAppError::UnsupportedVersion(format!(
482 "EIP-712 filtering requires app version >= 1.9.19, found {}",
483 config.version
484 )));
485 }
486
487 EthApp::activate_filtering(&self.transport).await
488 }
489
490 /// Sign EIP-712 typed data using the high-level API (matching viem interface)
491 ///
492 /// This method provides a simple interface for EIP-712 signing that matches the viem
493 /// interface. It automatically handles the conversion from high-level typed data to
494 /// the low-level struct definitions and implementations required by the Ledger device.
495 ///
496 /// **Version Requirements**: Requires app version >= 1.9.19
497 ///
498 /// # Arguments
499 ///
500 /// * `path` - BIP32 derivation path for the signing key
501 /// * `typed_data` - EIP-712 typed data structure matching viem interface
502 ///
503 /// # Example
504 ///
505 /// ```rust,ignore
506 /// use ledger_eth_app::{Eip712Domain, Eip712Field, Eip712Struct, Eip712Types, Eip712TypedData};
507 /// use serde_json::json;
508 /// use std::collections::HashMap;
509 ///
510 /// let domain = Eip712Domain::new()
511 /// .with_name("Ether Mail".to_string())
512 /// .with_version("1".to_string())
513 /// .with_chain_id(1);
514 ///
515 /// let mut types = Eip712Types::new();
516 /// types.insert(
517 /// "Person".to_string(),
518 /// Eip712Struct::new()
519 /// .with_field(Eip712Field::new("name".to_string(), "string".to_string()))
520 /// .with_field(Eip712Field::new("wallet".to_string(), "address".to_string())),
521 /// );
522 ///
523 /// let message = json!({
524 /// "from": {
525 /// "name": "Cow",
526 /// "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
527 /// },
528 /// "to": {
529 /// "name": "Bob",
530 /// "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
531 /// },
532 /// "contents": "Hello, Bob!"
533 /// });
534 ///
535 /// let typed_data = Eip712TypedData::new(domain, types, "Mail".to_string(), message);
536 /// // let signature = app.sign_eip712_typed_data(&path, &typed_data).await?;
537 /// ```
538 ///
539 /// # Errors
540 ///
541 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
542 ///
543 pub async fn sign_eip712_typed_data(
544 &self,
545 path: &BipPath,
546 typed_data: &Eip712TypedData,
547 ) -> EthAppResult<crate::types::Signature, E::Error> {
548 // Check version requirement for EIP-712 full implementation
549 let config = self.get_configuration().await?;
550 if !config.version.supports_eip712_full() {
551 return Err(EthAppError::UnsupportedVersion(format!(
552 "EIP-712 typed data signing requires app version >= 1.9.19, found {}",
553 config.version
554 )));
555 }
556
557 EthApp::sign_eip712_typed_data(&self.transport, path, typed_data).await
558 }
559
560 /// Sign EIP-712 typed data from JSON string
561 ///
562 /// This method accepts a JSON string containing EIP-712 typed data and automatically
563 /// parses, validates, and signs it. The JSON format should match the standard EIP-712
564 /// structure with domain, types, primaryType, and message fields.
565 ///
566 /// **Version Requirements**: Requires app version >= 1.9.19
567 ///
568 /// # Arguments
569 ///
570 /// * `path` - BIP32 derivation path for the signing key
571 /// * `json_str` - JSON string containing EIP-712 typed data
572 ///
573 /// # Example
574 ///
575 /// ```rust,ignore
576 /// let json_str = r#"{
577 /// "domain": {
578 /// "name": "USD Coin",
579 /// "verifyingContract": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
580 /// "chainId": 1,
581 /// "version": "2"
582 /// },
583 /// "primaryType": "Permit",
584 /// "message": {
585 /// "deadline": 1718992051,
586 /// "nonce": 0,
587 /// "spender": "0x111111125421ca6dc452d289314280a0f8842a65",
588 /// "owner": "0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d",
589 /// "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
590 /// },
591 /// "types": {
592 /// "EIP712Domain": [
593 /// {"name": "name", "type": "string"},
594 /// {"name": "version", "type": "string"},
595 /// {"name": "chainId", "type": "uint256"},
596 /// {"name": "verifyingContract", "type": "address"}
597 /// ],
598 /// "Permit": [
599 /// {"name": "owner", "type": "address"},
600 /// {"name": "spender", "type": "address"},
601 /// {"name": "value", "type": "uint256"},
602 /// {"name": "nonce", "type": "uint256"},
603 /// {"name": "deadline", "type": "uint256"}
604 /// ]
605 /// }
606 /// }"#;
607 ///
608 /// // let signature = app.sign_eip712_from_json(&path, json_str).await?;
609 /// ```
610 ///
611 /// # Errors
612 ///
613 /// Returns `EthAppError::UnsupportedVersion` if app version is below 1.9.19
614 /// Returns `EthAppError::InvalidEip712Data` if JSON format is invalid
615 ///
616 pub async fn sign_eip712_from_json(
617 &self,
618 path: &BipPath,
619 json_str: &str,
620 ) -> EthAppResult<crate::types::Signature, E::Error> {
621 // Check version requirement for EIP-712 full implementation
622 let config = self.get_configuration().await?;
623 if !config.version.supports_eip712_full() {
624 return Err(EthAppError::UnsupportedVersion(format!(
625 "EIP-712 JSON signing requires app version >= 1.9.19, found {}",
626 config.version
627 )));
628 }
629
630 EthApp::sign_eip712_from_json(&self.transport, path, json_str).await
631 }
632}