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}