privy_rs/
solana.rs

1//! Solana wallet operations service.
2//!
3//! This module provides convenient methods for Solana wallet operations including
4//! message signing, transaction signing, and transaction broadcasting. All methods
5//! are designed to work with Privy's embedded wallet infrastructure and expect
6//! Base64-encoded data following Solana's standard encoding practices.
7
8use std::str::FromStr;
9
10use crate::{
11    AuthorizationContext, PrivySignedApiError,
12    generated::{
13        Error, ResponseValue,
14        types::{
15            SolanaSignAndSendTransactionRpcInput, SolanaSignAndSendTransactionRpcInputCaip2,
16            SolanaSignAndSendTransactionRpcInputMethod, SolanaSignAndSendTransactionRpcInputParams,
17            SolanaSignAndSendTransactionRpcInputParamsEncoding, SolanaSignMessageRpcInput,
18            SolanaSignMessageRpcInputMethod, SolanaSignMessageRpcInputParams,
19            SolanaSignMessageRpcInputParamsEncoding, SolanaSignTransactionRpcInput,
20            SolanaSignTransactionRpcInputMethod, SolanaSignTransactionRpcInputParams,
21            SolanaSignTransactionRpcInputParamsEncoding, WalletRpcBody, WalletRpcResponse,
22        },
23    },
24};
25
26/// Service for Solana-specific wallet operations.
27///
28/// Provides convenient methods for common Solana wallet operations such as:
29/// - Message signing with Base64 encoding
30/// - Transaction signing for offline use
31/// - Transaction signing and broadcasting in one operation
32///
33/// All Solana operations expect Base64-encoded data as input, following Solana's
34/// standard encoding practices for transactions and messages.
35///
36/// # Examples
37///
38/// Basic usage:
39///
40/// ```rust,no_run
41/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
42/// use privy_rs::{AuthorizationContext, PrivyClient};
43///
44/// let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
45/// let solana_service = client.wallets().solana();
46/// let auth_ctx = AuthorizationContext::new();
47///
48/// // Sign a Base64-encoded message
49/// let result = solana_service
50///     .sign_message(
51///         "wallet_id",
52///         "SGVsbG8sIFNvbGFuYSE=", // "Hello, Solana!" in Base64
53///         &auth_ctx,
54///         None, // no idempotency key
55///     )
56///     .await?;
57/// # Ok(())
58/// # }
59/// ```
60pub struct SolanaService {
61    wallets_client: crate::subclients::WalletsClient,
62}
63
64impl SolanaService {
65    /// Creates a new SolanaService instance.
66    ///
67    /// This is typically called internally by `WalletsClient::solana()`.
68    pub(crate) fn new(wallets_client: crate::subclients::WalletsClient) -> Self {
69        Self { wallets_client }
70    }
71
72    /// Signs a Base64 encoded message for a Solana wallet.
73    ///
74    /// This method signs arbitrary messages using Solana's message signing standard.
75    /// The message must be provided as a Base64-encoded string. This is typically
76    /// used for authentication or verification purposes where you need to prove
77    /// ownership of a Solana wallet.
78    ///
79    /// # Parameters
80    ///
81    /// * `wallet_id` - The ID of the wallet to use for signing
82    /// * `message` - The message string to be signed (expected to be Base64 encoded)
83    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
84    /// * `idempotency_key` - Optional idempotency key for the request to prevent duplicate operations
85    ///
86    /// # Returns
87    ///
88    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signature data.
89    ///
90    /// # Examples
91    ///
92    /// ```rust,no_run
93    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
94    /// use privy_rs::{AuthorizationContext, PrivyClient};
95    ///
96    /// let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
97    /// let solana_service = client.wallets().solana();
98    /// let auth_ctx = AuthorizationContext::new();
99    ///
100    /// // Base64 encode your message first
101    /// let message = base64::encode("Hello, Solana!");
102    /// let signature = solana_service
103    ///     .sign_message(
104    ///         "clz2rqy4500061234abcd1234",
105    ///         &message,
106    ///         &auth_ctx,
107    ///         Some("unique-request-id-456"),
108    ///     )
109    ///     .await?;
110    ///
111    /// println!("Message signed successfully");
112    /// # Ok(())
113    /// # }
114    /// ```
115    ///
116    /// # Errors
117    ///
118    /// This method will return an error if:
119    /// - The wallet ID is invalid or not found
120    /// - The authorization context is invalid
121    /// - The message is not properly Base64 encoded
122    /// - Network communication fails
123    /// - The signing operation fails on the server
124    ///
125    /// # Notes
126    ///
127    /// Unlike Ethereum personal message signing, Solana message signing doesn't add
128    /// any prefixes to the message. The signature is computed directly over the
129    /// decoded message bytes.
130    pub async fn sign_message(
131        &self,
132        wallet_id: &str,
133        message: &str,
134        authorization_context: &AuthorizationContext,
135        idempotency_key: Option<&str>,
136    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
137        let rpc_body = WalletRpcBody::SolanaSignMessageRpcInput(SolanaSignMessageRpcInput {
138            address: None,
139            chain_type: None,
140            method: SolanaSignMessageRpcInputMethod::SignMessage,
141            params: SolanaSignMessageRpcInputParams {
142                encoding: SolanaSignMessageRpcInputParamsEncoding::Base64,
143                message: message.to_string(),
144            },
145        });
146
147        self.wallets_client
148            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
149            .await
150    }
151
152    /// Signs a Solana transaction for a specific wallet.
153    ///
154    /// This method signs a Solana transaction but does not broadcast it to the network.
155    /// The transaction must be provided as a Base64-encoded string representing the
156    /// serialized transaction. The signed transaction can be broadcast later using
157    /// other tools or the `sign_and_send_transaction` method.
158    ///
159    /// # Parameters
160    ///
161    /// * `wallet_id` - The ID of the wallet to use for signing
162    /// * `transaction` - The transaction string to be signed (expected to be Base64 encoded)
163    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
164    /// * `idempotency_key` - Optional idempotency key for the request
165    ///
166    /// # Returns
167    ///
168    /// Returns a `ResponseValue<WalletRpcResponse>` containing the signed transaction data.
169    ///
170    /// # Examples
171    ///
172    /// ```rust,no_run
173    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
174    /// use privy_rs::{AuthorizationContext, PrivyClient};
175    ///
176    /// let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
177    /// let solana_service = client.wallets().solana();
178    /// let auth_ctx = AuthorizationContext::new();
179    ///
180    /// // Base64-encoded Solana transaction (example)
181    /// let transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDArczbMia1tLmq7zz4DinMNN0pJ1JtLdqIJPUw3YrGCzYAMHBsgN27lcgB6H2WQvFgyZuJYHa46puOQo9yQ8CVQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp20C7Wj2aiuk5TReAXo+VTVg8QTHjs0UjNMMKCvpzZ+ABAgEBARU=";
182    ///
183    /// let signed_tx = solana_service.sign_transaction(
184    ///     "clz2rqy4500061234abcd1234",
185    ///     transaction,
186    ///     &auth_ctx,
187    ///     None
188    /// ).await?;
189    ///
190    /// println!("Transaction signed successfully");
191    /// # Ok(())
192    /// # }
193    /// ```
194    ///
195    /// # Notes
196    ///
197    /// - The transaction must be a properly serialized Solana transaction in Base64 format
198    /// - The transaction should include all necessary fields (recent blockhash, instructions, etc.)
199    /// - This method only signs the transaction; use `sign_and_send_transaction` to also broadcast it
200    pub async fn sign_transaction(
201        &self,
202        wallet_id: &str,
203        transaction: &str,
204        authorization_context: &AuthorizationContext,
205        idempotency_key: Option<&str>,
206    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
207        let rpc_body =
208            WalletRpcBody::SolanaSignTransactionRpcInput(SolanaSignTransactionRpcInput {
209                address: None,
210                chain_type: None,
211                method: SolanaSignTransactionRpcInputMethod::SignTransaction,
212                params: SolanaSignTransactionRpcInputParams {
213                    encoding: SolanaSignTransactionRpcInputParamsEncoding::Base64,
214                    transaction: transaction.to_string(),
215                },
216            });
217
218        self.wallets_client
219            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
220            .await
221    }
222
223    /// Signs and sends a Solana transaction.
224    ///
225    /// This method both signs and broadcasts a Solana transaction to the specified network.
226    /// It's a convenience method that combines signing and sending in one operation.
227    /// The transaction will be immediately submitted to the Solana network after signing.
228    ///
229    /// # Parameters
230    ///
231    /// * `wallet_id` - The ID of the wallet used for the transaction
232    /// * `caip2` - The CAIP-2 chain ID of the Solana network (e.g., "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" for mainnet-beta)
233    /// * `transaction` - The transaction string to be signed and sent (expected to be Base64 encoded)
234    /// * `authorization_context` - The authorization context containing JWT or private keys for request signing
235    /// * `idempotency_key` - Optional idempotency key for the request
236    ///
237    /// # Returns
238    ///
239    /// Returns a `ResponseValue<WalletRpcResponse>` containing the transaction signature and other relevant data.
240    ///
241    /// # Examples
242    ///
243    /// ```rust,no_run
244    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
245    /// use privy_rs::{AuthorizationContext, PrivyClient};
246    ///
247    /// let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?;
248    /// let solana_service = client.wallets().solana();
249    /// let auth_ctx = AuthorizationContext::new();
250    ///
251    /// // Base64-encoded Solana transaction
252    /// let transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDRpb0mdmKftapwzzqUtlcDnuWbw8vwlyiyuWyyieQFKESezu52HWNss0SAcb60ftz7DSpgTwUmfUSl1CYHJ91GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAScgJ7J0AXFr1azCEvB1Y5zpiF4eXR+yTW0UB7am+E/MBAgIAAQwCAAAAQEIPAAAAAAA=";
253    ///
254    /// let result = solana_service.sign_and_send_transaction(
255    ///     "clz2rqy4500061234abcd1234",
256    ///     "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", // Solana mainnet-beta
257    ///     transaction,
258    ///     &auth_ctx,
259    ///     None
260    /// ).await?;
261    ///
262    /// println!("Transaction sent successfully");
263    /// # Ok(())
264    /// # }
265    /// ```
266    ///
267    /// # Errors
268    ///
269    /// This method will return an error if:
270    /// - The wallet ID is invalid or not found
271    /// - The CAIP-2 chain ID format is invalid
272    /// - The transaction format is invalid or corrupted
273    /// - The wallet has insufficient balance for the transaction
274    /// - Network communication fails
275    /// - The transaction is rejected by the Solana network
276    ///
277    /// # Notes
278    ///
279    /// - The transaction will be broadcast to the network specified by the CAIP-2 chain ID
280    /// - This method requires sufficient SOL balance in the wallet to cover transaction fees
281    /// - The transaction will be processed by the Solana network and may take time to confirm
282    /// - Common CAIP-2 chain IDs:
283    ///   - "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" (mainnet-beta)
284    ///   - "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z" (testnet)
285    ///   - "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" (devnet)
286    pub async fn sign_and_send_transaction(
287        &self,
288        wallet_id: &str,
289        caip2: &str,
290        transaction: &str,
291        authorization_context: &AuthorizationContext,
292        idempotency_key: Option<&str>,
293    ) -> Result<ResponseValue<WalletRpcResponse>, PrivySignedApiError> {
294        let caip2_parsed = SolanaSignAndSendTransactionRpcInputCaip2::from_str(caip2)
295            .map_err(|_| Error::InvalidRequest("Invalid CAIP-2 format".to_string()))?;
296
297        let rpc_body = WalletRpcBody::SolanaSignAndSendTransactionRpcInput(
298            SolanaSignAndSendTransactionRpcInput {
299                address: None,
300                caip2: caip2_parsed,
301                chain_type: None,
302                method: SolanaSignAndSendTransactionRpcInputMethod::SignAndSendTransaction,
303                params: SolanaSignAndSendTransactionRpcInputParams {
304                    encoding: SolanaSignAndSendTransactionRpcInputParamsEncoding::Base64,
305                    transaction: transaction.to_string(),
306                },
307                sponsor: Some(false),
308            },
309        );
310
311        self.wallets_client
312            .rpc(wallet_id, authorization_context, idempotency_key, &rpc_body)
313            .await
314    }
315}