privy_rs/
ethereum.rs

1//! Ethereum wallet operations service.
2//!
3//! This module provides convenient methods for Ethereum wallet operations including
4//! message signing, transaction signing, typed data signing, and more. All methods
5//! are designed to work with Privy's embedded wallet infrastructure.
6
7use crate::{
8    AuthorizationContext, PrivySignedApiError,
9    generated::{
10        Error, ResponseValue,
11        types::{
12            EthereumPersonalSignRpcInput, EthereumPersonalSignRpcInputMethod,
13            EthereumPersonalSignRpcInputParams, EthereumPersonalSignRpcInputParamsEncoding,
14            EthereumSecp256k1SignRpcInput, EthereumSecp256k1SignRpcInputMethod,
15            EthereumSecp256k1SignRpcInputParams, EthereumSendTransactionRpcInput,
16            EthereumSendTransactionRpcInputMethod, EthereumSendTransactionRpcInputParams,
17            EthereumSendTransactionRpcInputParamsTransaction,
18            EthereumSign7702AuthorizationRpcInput, EthereumSign7702AuthorizationRpcInputMethod,
19            EthereumSign7702AuthorizationRpcInputParams, EthereumSignTransactionRpcInput,
20            EthereumSignTransactionRpcInputMethod, EthereumSignTransactionRpcInputParams,
21            EthereumSignTransactionRpcInputParamsTransaction, EthereumSignTypedDataRpcInput,
22            EthereumSignTypedDataRpcInputMethod, EthereumSignTypedDataRpcInputParams,
23            EthereumSignTypedDataRpcInputParamsTypedData, WalletRpcBody, WalletRpcResponse,
24        },
25    },
26};
27
28/// Service for Ethereum-specific wallet operations.
29///
30/// Provides convenient methods for common Ethereum wallet operations such as:
31/// - Personal message signing (UTF-8 strings and raw bytes)
32/// - secp256k1 signature generation
33/// - EIP-712 typed data signing
34/// - Transaction signing and broadcasting
35/// - EIP-7702 authorization signing
36///
37/// # Examples
38///
39/// Basic usage:
40///
41/// ```rust,no_run
42/// # use anyhow::Result;
43/// # async fn example() -> Result<()> {
44/// use privy_rs::{AuthorizationContext, generated::types::*};
45/// # use privy_rs::PrivyClient;
46///
47/// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
48/// let ethereum_service = client.wallets().ethereum();
49/// let auth_ctx = AuthorizationContext::new();
50///
51/// // Sign a UTF-8 message
52/// let result = ethereum_service
53///     .sign_message(
54///         "wallet_id",
55///         "Hello, Ethereum!",
56///         &auth_ctx,
57///         None, // no idempotency key
58///     )
59///     .await?;
60/// # Ok(())
61/// # }
62/// ```
63pub struct EthereumService {
64    wallets_client: crate::subclients::WalletsClient,
65}
66
67impl EthereumService {
68    /// Creates a new [`EthereumService`] instance.
69    ///
70    /// This is typically called internally by `WalletsClient::ethereum()`.
71    pub(crate) fn new(wallets_client: crate::subclients::WalletsClient) -> Self {
72        Self { wallets_client }
73    }
74
75    /// Signs a UTF-8 encoded message for an Ethereum wallet using the `personal_sign` method.
76    ///
77    /// This method signs arbitrary UTF-8 text messages using Ethereum's personal message
78    /// signing standard. The message will be prefixed with the Ethereum signed message
79    /// prefix before signing.
80    ///
81    /// # Parameters
82    ///
83    /// * `wallet_id` - The ID of the wallet to use for signing
84    /// * `message` - The UTF-8 message string to be signed
85    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
86    /// * `idempotency_key` - Optional idempotency key for the request to prevent duplicate operations
87    ///
88    /// # Returns
89    ///
90    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signature data.
91    ///
92    /// # Examples
93    ///
94    /// ```rust,no_run
95    /// # use anyhow::Result;
96    /// # async fn example() -> Result<()> {
97    /// use privy_rs::{AuthorizationContext, generated::types::*};
98    /// # use privy_rs::PrivyClient;
99    ///
100    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
101    /// let ethereum_service = client.wallets().ethereum();
102    /// let auth_ctx = AuthorizationContext::new();
103    ///
104    /// let signature = ethereum_service
105    ///     .sign_message(
106    ///         "clz2rqy4500061234abcd1234",
107    ///         "Hello, Ethereum!",
108    ///         &auth_ctx,
109    ///         Some("unique-request-id-123"),
110    ///     )
111    ///     .await?;
112    ///
113    /// println!("Message signed successfully");
114    /// # Ok(())
115    /// # }
116    /// ```
117    ///
118    /// # Errors
119    ///
120    /// This method will return an error if:
121    /// - The wallet ID is invalid or not found
122    /// - The authorization context is invalid
123    /// - Network communication fails
124    /// - The signing operation fails on the server
125    pub async fn sign_message(
126        &self,
127        wallet_id: &str,
128        message: &str,
129        authorization_context: &AuthorizationContext,
130        idempotency_key: Option<&str>,
131    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
132        let rpc_body = WalletRpcBody::EthereumPersonalSignRpcInput(EthereumPersonalSignRpcInput {
133            address: None,
134            chain_type: None,
135            method: EthereumPersonalSignRpcInputMethod::PersonalSign,
136            params: EthereumPersonalSignRpcInputParams {
137                encoding: EthereumPersonalSignRpcInputParamsEncoding::Utf8,
138                message: message.to_string(),
139            },
140        });
141
142        self.wallets_client
143            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
144            .await
145    }
146
147    /// Signs a raw byte array message for an Ethereum wallet using the `personal_sign` method.
148    ///
149    /// This method signs raw binary data by first encoding it as a hex string (with 0x prefix)
150    /// and then using Ethereum's personal message signing standard.
151    ///
152    /// # Parameters
153    ///
154    /// * `wallet_id` - The ID of the wallet to use for signing
155    /// * `message` - The message byte array to be signed
156    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
157    /// * `idempotency_key` - Optional idempotency key for the request
158    ///
159    /// # Returns
160    ///
161    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signature data.
162    ///
163    /// # Examples
164    ///
165    /// ```rust,no_run
166    /// # use anyhow::Result;
167    /// # async fn example() -> Result<()> {
168    /// use privy_rs::{AuthorizationContext, generated::types::*};
169    /// # use privy_rs::PrivyClient;
170    ///
171    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
172    /// let ethereum_service = client.wallets().ethereum();
173    /// let auth_ctx = AuthorizationContext::new();
174    ///
175    /// let message_bytes = b"Hello, bytes!";
176    /// let signature = ethereum_service
177    ///     .sign_message_bytes("clz2rqy4500061234abcd1234", message_bytes, &auth_ctx, None)
178    ///     .await?;
179    ///
180    /// println!("Byte message signed successfully");
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub async fn sign_message_bytes(
185        &self,
186        wallet_id: &str,
187        message: &[u8],
188        authorization_context: &AuthorizationContext,
189        idempotency_key: Option<&str>,
190    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
191        let hex_message = format!("0x{}", hex::encode(message));
192
193        let rpc_body = WalletRpcBody::EthereumPersonalSignRpcInput(EthereumPersonalSignRpcInput {
194            address: None,
195            chain_type: None,
196            method: EthereumPersonalSignRpcInputMethod::PersonalSign,
197            params: EthereumPersonalSignRpcInputParams {
198                encoding: EthereumPersonalSignRpcInputParamsEncoding::Hex,
199                message: hex_message,
200            },
201        });
202
203        self.wallets_client
204            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
205            .await
206    }
207
208    /// Signs a message using secp256k1 signature algorithm.
209    ///
210    /// This method performs low-level secp256k1 signing on a pre-computed hash.
211    /// The hash should be exactly 32 bytes and is typically the result of keccak256
212    /// hashing of the data to be signed.
213    ///
214    /// # Parameters
215    ///
216    /// * `wallet_id` - The ID of the wallet to use for signing
217    /// * `hash` - The hash to sign (typically 32 bytes as hex string with 0x prefix)
218    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
219    /// * `idempotency_key` - Optional idempotency key for the request
220    ///
221    /// # Returns
222    ///
223    /// Returns a `ResponseValue<WalletRpcResponse>` containing the secp256k1 signature.
224    ///
225    /// # Examples
226    ///
227    /// ```rust,no_run
228    /// # use anyhow::Result;
229    /// # async fn example() -> Result<()> {
230    /// use privy_rs::{AuthorizationContext, generated::types::*};
231    /// # use privy_rs::PrivyClient;
232    ///
233    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
234    /// let ethereum_service = client.wallets().ethereum();
235    /// let auth_ctx = AuthorizationContext::new();
236    ///
237    /// // Pre-computed keccak256 hash
238    /// let hash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
239    /// let signature = ethereum_service
240    ///     .sign_secp256k1("clz2rqy4500061234abcd1234", hash, &auth_ctx, None)
241    ///     .await?;
242    ///
243    /// println!("Hash signed with secp256k1");
244    /// # Ok(())
245    /// # }
246    /// ```
247    ///
248    /// # Notes
249    ///
250    /// This is a lower-level signing method. For most use cases, prefer `sign_message()`
251    /// or `sign_typed_data()` which handle the hashing automatically.
252    pub async fn sign_secp256k1(
253        &self,
254        wallet_id: &str,
255        hash: &str,
256        authorization_context: &AuthorizationContext,
257        idempotency_key: Option<&str>,
258    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
259        let rpc_body =
260            WalletRpcBody::EthereumSecp256k1SignRpcInput(EthereumSecp256k1SignRpcInput {
261                address: None,
262                chain_type: None,
263                method: EthereumSecp256k1SignRpcInputMethod::Secp256k1Sign,
264                params: EthereumSecp256k1SignRpcInputParams {
265                    hash: hash.to_string(),
266                },
267            });
268
269        self.wallets_client
270            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
271            .await
272    }
273
274    /// Signs a 7702 authorization using the eth_sign7702Authorization RPC method.
275    ///
276    /// EIP-7702 introduces account abstraction by allowing EOAs to temporarily delegate
277    /// control to a smart contract. This method signs the authorization that allows
278    /// the delegation to take place.
279    ///
280    /// # Parameters
281    ///
282    /// * `wallet_id` - The ID of the wallet to use for signing
283    /// * `params` - The parameters for the eth_sign7702Authorization RPC method including contract address, chain ID, and nonce
284    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
285    /// * `idempotency_key` - Optional idempotency key for the request
286    ///
287    /// # Returns
288    ///
289    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signed authorization data.
290    ///
291    /// # Examples
292    ///
293    /// ```rust,no_run
294    /// # use anyhow::Result;
295    /// # async fn example() -> Result<()> {
296    /// use privy_rs::{AuthorizationContext, generated::types::*};
297    /// # use privy_rs::PrivyClient;
298    ///
299    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
300    /// let ethereum_service = client.wallets().ethereum();
301    /// let auth_ctx = AuthorizationContext::new();
302    ///
303    /// let params = EthereumSign7702AuthorizationRpcInputParams {
304    ///     chain_id: EthereumSign7702AuthorizationRpcInputParamsChainId::Integer(1),
305    ///     contract: "0x1234567890abcdef1234567890abcdef12345678".to_string(),
306    ///     nonce: None,
307    /// };
308    ///
309    /// let authorization = ethereum_service
310    ///     .sign_7702_authorization("clz2rqy4500061234abcd1234", params, &auth_ctx, None)
311    ///     .await?;
312    ///
313    /// println!("7702 authorization signed successfully");
314    /// # Ok(())
315    /// # }
316    /// ```
317    pub async fn sign_7702_authorization(
318        &self,
319        wallet_id: &str,
320        params: EthereumSign7702AuthorizationRpcInputParams,
321        authorization_context: &AuthorizationContext,
322        idempotency_key: Option<&str>,
323    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
324        let rpc_body = WalletRpcBody::EthereumSign7702AuthorizationRpcInput(
325            EthereumSign7702AuthorizationRpcInput {
326                address: None,
327                chain_type: None,
328                method: EthereumSign7702AuthorizationRpcInputMethod::EthSign7702Authorization,
329                params,
330            },
331        );
332
333        self.wallets_client
334            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
335            .await
336    }
337
338    /// Signs typed data using EIP-712 standard.
339    ///
340    /// EIP-712 defines a standard for typed structured data signing that provides
341    /// better UX and security compared to signing arbitrary strings. This method
342    /// implements the `eth_signTypedData_v4` RPC method.
343    ///
344    /// # Parameters
345    ///
346    /// * `wallet_id` - The ID of the wallet to use for signing
347    /// * `typed_data` - The typed data structure to be signed, conforming to EIP-712 format
348    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
349    /// * `idempotency_key` - Optional idempotency key for the request
350    ///
351    /// # Returns
352    ///
353    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signed typed data.
354    ///
355    /// # Examples
356    ///
357    /// ```rust,no_run
358    /// # use anyhow::Result;
359    /// # async fn example() -> Result<()> {
360    /// use privy_rs::{AuthorizationContext, generated::types::*};
361    /// # use privy_rs::PrivyClient;
362    ///
363    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
364    /// let ethereum_service = client.wallets().ethereum();
365    /// let auth_ctx = AuthorizationContext::new();
366    ///
367    /// // Create EIP-712 typed data structure
368    /// let typed_data = EthereumSignTypedDataRpcInputParamsTypedData {
369    ///     domain: Default::default(),
370    ///     message: Default::default(),
371    ///     primary_type: "Mail".to_string(),
372    ///     types: Default::default(),
373    /// };
374    ///
375    /// let signature = ethereum_service
376    ///     .sign_typed_data("clz2rqy4500061234abcd1234", typed_data, &auth_ctx, None)
377    ///     .await?;
378    ///
379    /// println!("Typed data signed successfully");
380    /// # Ok(())
381    /// # }
382    /// ```
383    ///
384    /// # Notes
385    ///
386    /// The typed data must conform to the EIP-712 specification with proper domain,
387    /// types, primaryType, and message fields. Refer to EIP-712 for the complete
388    /// specification of the required structure.
389    pub async fn sign_typed_data(
390        &self,
391        wallet_id: &str,
392        typed_data: EthereumSignTypedDataRpcInputParamsTypedData,
393        authorization_context: &AuthorizationContext,
394        idempotency_key: Option<&str>,
395    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
396        let rpc_body =
397            WalletRpcBody::EthereumSignTypedDataRpcInput(EthereumSignTypedDataRpcInput {
398                address: None,
399                chain_type: None,
400                method: EthereumSignTypedDataRpcInputMethod::EthSignTypedDataV4,
401                params: EthereumSignTypedDataRpcInputParams { typed_data },
402            });
403
404        self.wallets_client
405            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
406            .await
407    }
408
409    /// Signs a transaction using the eth_signTransaction method.
410    ///
411    /// This method signs an Ethereum transaction but does not broadcast it to the network.
412    /// The signed transaction can be broadcast later using other tools or the `send_transaction` method.
413    ///
414    /// # Parameters
415    ///
416    /// * `wallet_id` - The ID of the wallet to use for signing
417    /// * `transaction` - The transaction object to be signed including to, value, data, gas, etc.
418    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
419    /// * `idempotency_key` - Optional idempotency key for the request
420    ///
421    /// # Returns
422    ///
423    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signed transaction data.
424    ///
425    /// # Examples
426    ///
427    /// ```rust,no_run
428    /// # use anyhow::Result;
429    /// # async fn example() -> Result<()> {
430    /// use privy_rs::{AuthorizationContext, generated::types::*};
431    /// # use privy_rs::PrivyClient;
432    ///
433    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
434    /// let ethereum_service = client.wallets().ethereum();
435    /// let auth_ctx = AuthorizationContext::new();
436    ///
437    /// let transaction = EthereumSignTransactionRpcInputParamsTransaction {
438    ///     to: Some("0x742d35Cc6635C0532925a3b8c17d6d1E9C2F7ca".to_string()),
439    ///     value: None,
440    ///     gas_limit: None,
441    ///     gas_price: None,
442    ///     nonce: None,
443    ///     chain_id: None,
444    ///     data: None,
445    ///     from: None,
446    ///     max_fee_per_gas: None,
447    ///     max_priority_fee_per_gas: None,
448    ///     type_: None,
449    /// };
450    ///
451    /// let signed_tx = ethereum_service
452    ///     .sign_transaction("clz2rqy4500061234abcd1234", transaction, &auth_ctx, None)
453    ///     .await?;
454    ///
455    /// println!("Transaction signed successfully");
456    /// # Ok(())
457    /// # }
458    /// ```
459    pub async fn sign_transaction(
460        &self,
461        wallet_id: &str,
462        transaction: EthereumSignTransactionRpcInputParamsTransaction,
463        authorization_context: &AuthorizationContext,
464        idempotency_key: Option<&str>,
465    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
466        let rpc_body =
467            WalletRpcBody::EthereumSignTransactionRpcInput(EthereumSignTransactionRpcInput {
468                address: None,
469                chain_type: None,
470                method: EthereumSignTransactionRpcInputMethod::EthSignTransaction,
471                params: EthereumSignTransactionRpcInputParams { transaction },
472            });
473
474        self.wallets_client
475            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
476            .await
477    }
478
479    /// Signs and sends a transaction using the eth_sendTransaction method.
480    ///
481    /// This method both signs and broadcasts an Ethereum transaction to the specified network.
482    /// It's a convenience method that combines signing and sending in one operation.
483    ///
484    /// # Parameters
485    ///
486    /// * `wallet_id` - The ID of the wallet used for the transaction
487    /// * `caip2` - The CAIP-2 chain ID of the Ethereum network (e.g., "eip155:1" for Ethereum Mainnet, "eip155:11155111" for Sepolia)
488    /// * `transaction` - The transaction object to be sent
489    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
490    /// * `idempotency_key` - Optional idempotency key for the request
491    ///
492    /// # Returns
493    ///
494    /// Returns a `ResponseValue<WalletRpcResponse>` containing the transaction hash or other relevant data.
495    ///
496    /// # Examples
497    ///
498    /// ```rust,no_run
499    /// # use anyhow::Result;
500    /// # async fn example() -> Result<()> {
501    /// use privy_rs::{AuthorizationContext, generated::types::*};
502    /// # use privy_rs::PrivyClient;
503    ///
504    /// # let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
505    /// let ethereum_service = client.wallets().ethereum();
506    /// let auth_ctx = AuthorizationContext::new();
507    ///
508    /// let transaction = EthereumSendTransactionRpcInputParamsTransaction {
509    ///     to: Some("0x742d35Cc6635C0532925a3b8c17d6d1E9C2F7ca".to_string()),
510    ///     value: None,
511    ///     gas_limit: None,
512    ///     max_fee_per_gas: None,
513    ///     max_priority_fee_per_gas: None,
514    ///     data: Some("0x".to_string()),
515    ///     chain_id: None,
516    ///     from: None,
517    ///     gas_price: None,
518    ///     nonce: None,
519    ///     type_: None,
520    /// };
521    ///
522    /// let result = ethereum_service
523    ///     .send_transaction(
524    ///         "clz2rqy4500061234abcd1234",
525    ///         "eip155:1",
526    ///         transaction,
527    ///         &auth_ctx,
528    ///         None,
529    ///     )
530    ///     .await?;
531    ///
532    /// println!("Transaction sent successfully");
533    /// # Ok(())
534    /// # }
535    /// ```
536    ///
537    /// # Notes
538    ///
539    /// - The transaction will be broadcast to the network specified by the CAIP-2 chain ID
540    /// - This method requires sufficient balance in the wallet to cover gas costs and transfer value
541    /// - The transaction will be mined and included in a block if successful
542    /// - Common CAIP-2 chain IDs: "eip155:1" (Ethereum), "eip155:137" (Polygon), "eip155:11155111" (Sepolia testnet)
543    pub async fn send_transaction(
544        &self,
545        wallet_id: &str,
546        caip2: &str,
547        transaction: EthereumSendTransactionRpcInputParamsTransaction,
548        authorization_context: &AuthorizationContext,
549        idempotency_key: Option<&str>,
550    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
551        let rpc_body =
552            WalletRpcBody::EthereumSendTransactionRpcInput(EthereumSendTransactionRpcInput {
553                address: None,
554                caip2: caip2
555                    .parse()
556                    .map_err(|_| Error::InvalidRequest("Invalid CAIP-2 format".to_string()))?,
557                chain_type: None,
558                method: EthereumSendTransactionRpcInputMethod::EthSendTransaction,
559                params: EthereumSendTransactionRpcInputParams { transaction },
560                sponsor: Some(false),
561            });
562
563        self.wallets_client
564            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
565            .await
566    }
567}