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}