Skip to main content

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}