1use base64::{engine::general_purpose::STANDARD, Engine};
2use serde::{Deserialize, Serialize};
3use solana_sdk::transaction::{Transaction, VersionedTransaction};
4use thiserror::Error;
5use utoipa::ToSchema;
6
7#[derive(Debug, Error, Deserialize, Serialize)]
8#[allow(clippy::enum_variant_names)]
9pub enum SolanaEncodingError {
10 #[error("Failed to serialize transaction: {0}")]
11 Serialization(String),
12 #[error("Failed to decode base64: {0}")]
13 Decode(String),
14 #[error("Failed to deserialize transaction: {0}")]
15 Deserialize(String),
16}
17
18#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
19pub struct EncodedSerializedTransaction(String);
20
21impl EncodedSerializedTransaction {
22 pub fn new(encoded: String) -> Self {
23 Self(encoded)
24 }
25
26 pub fn into_inner(self) -> String {
27 self.0
28 }
29}
30
31impl TryFrom<&solana_sdk::transaction::Transaction> for EncodedSerializedTransaction {
32 type Error = SolanaEncodingError;
33
34 fn try_from(transaction: &Transaction) -> Result<Self, Self::Error> {
35 let serialized = bincode::serialize(transaction)
36 .map_err(|e| SolanaEncodingError::Serialization(e.to_string()))?;
37
38 Ok(Self(STANDARD.encode(serialized)))
39 }
40}
41
42impl TryFrom<EncodedSerializedTransaction> for solana_sdk::transaction::Transaction {
43 type Error = SolanaEncodingError;
44
45 fn try_from(encoded: EncodedSerializedTransaction) -> Result<Self, Self::Error> {
46 let tx_bytes = STANDARD
47 .decode(encoded.0)
48 .map_err(|e| SolanaEncodingError::Decode(e.to_string()))?;
49
50 let decoded_tx: Transaction = bincode::deserialize(&tx_bytes)
51 .map_err(|e| SolanaEncodingError::Deserialize(e.to_string()))?;
52
53 Ok(decoded_tx)
54 }
55}
56
57impl TryFrom<&VersionedTransaction> for EncodedSerializedTransaction {
59 type Error = SolanaEncodingError;
60
61 fn try_from(transaction: &VersionedTransaction) -> Result<Self, Self::Error> {
62 let serialized = bincode::serialize(transaction)
63 .map_err(|e| SolanaEncodingError::Serialization(e.to_string()))?;
64
65 Ok(Self(STANDARD.encode(serialized)))
66 }
67}
68
69impl TryFrom<EncodedSerializedTransaction> for VersionedTransaction {
71 type Error = SolanaEncodingError;
72
73 fn try_from(encoded: EncodedSerializedTransaction) -> Result<Self, Self::Error> {
74 let tx_bytes = STANDARD
75 .decode(&encoded.0)
76 .map_err(|e| SolanaEncodingError::Decode(e.to_string()))?;
77
78 bincode::deserialize(&tx_bytes).map_err(|e| SolanaEncodingError::Deserialize(e.to_string()))
79 }
80}
81
82#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
84#[serde(deny_unknown_fields)]
85pub struct FeeEstimateRequestParams {
86 pub transaction: EncodedSerializedTransaction,
87 pub fee_token: String,
88}
89
90#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
91pub struct FeeEstimateResult {
92 pub estimated_fee: String,
93 pub conversion_rate: String,
94}
95
96#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
98#[serde(deny_unknown_fields)]
99pub struct TransferTransactionRequestParams {
100 pub amount: u64,
101 pub token: String,
102 pub source: String,
103 pub destination: String,
104}
105
106#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
107pub struct TransferTransactionResult {
108 pub transaction: EncodedSerializedTransaction,
109 pub fee_in_spl: String,
110 pub fee_in_lamports: String,
111 pub fee_token: String,
112 pub valid_until_blockheight: u64,
113}
114
115#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
117#[serde(deny_unknown_fields)]
118pub struct PrepareTransactionRequestParams {
119 pub transaction: EncodedSerializedTransaction,
120 pub fee_token: String,
121}
122
123#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
124pub struct PrepareTransactionResult {
125 pub transaction: EncodedSerializedTransaction,
126 pub fee_in_spl: String,
127 pub fee_in_lamports: String,
128 pub fee_token: String,
129 pub valid_until_blockheight: u64,
130}
131
132#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
134#[serde(deny_unknown_fields)]
135pub struct SignTransactionRequestParams {
136 pub transaction: EncodedSerializedTransaction,
137}
138
139#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ToSchema)]
140pub struct SignTransactionResult {
141 pub transaction: EncodedSerializedTransaction,
142 pub signature: String,
143}
144
145#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
147#[serde(deny_unknown_fields)]
148pub struct SignAndSendTransactionRequestParams {
149 pub transaction: EncodedSerializedTransaction,
150}
151
152#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
153pub struct SignAndSendTransactionResult {
154 pub transaction: EncodedSerializedTransaction,
155 pub signature: String,
156 pub id: String,
157}
158
159#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
161#[serde(deny_unknown_fields)]
162pub struct GetSupportedTokensRequestParams {}
163
164#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
165pub struct GetSupportedTokensItem {
166 pub mint: String,
167 pub symbol: String,
168 pub decimals: u8,
169 #[schema(nullable = false)]
170 pub max_allowed_fee: Option<u64>,
171 #[schema(nullable = false)]
172 pub conversion_slippage_percentage: Option<f32>,
173}
174
175#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
176pub struct GetSupportedTokensResult {
177 pub tokens: Vec<GetSupportedTokensItem>,
178}
179
180#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
182#[serde(deny_unknown_fields)]
183pub struct GetFeaturesEnabledRequestParams {}
184
185#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
186pub struct GetFeaturesEnabledResult {
187 pub features: Vec<String>,
188}
189
190pub enum SolanaRpcMethod {
191 FeeEstimate,
192 TransferTransaction,
193 PrepareTransaction,
194 SignTransaction,
195 SignAndSendTransaction,
196 GetSupportedTokens,
197 GetFeaturesEnabled,
198 Generic(String),
199}
200
201impl SolanaRpcMethod {
202 pub fn from_string(method: &str) -> Option<Self> {
203 match method {
204 "feeEstimate" => Some(SolanaRpcMethod::FeeEstimate),
205 "transferTransaction" => Some(SolanaRpcMethod::TransferTransaction),
206 "prepareTransaction" => Some(SolanaRpcMethod::PrepareTransaction),
207 "signTransaction" => Some(SolanaRpcMethod::SignTransaction),
208 "signAndSendTransaction" => Some(SolanaRpcMethod::SignAndSendTransaction),
209 "getSupportedTokens" => Some(SolanaRpcMethod::GetSupportedTokens),
210 "getFeaturesEnabled" => Some(SolanaRpcMethod::GetFeaturesEnabled),
211 _ => Some(SolanaRpcMethod::Generic(method.to_string())),
212 }
213 }
214}
215
216#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
217#[serde(tag = "method", content = "params")]
218#[schema(as = SolanaRpcRequest)]
219pub enum SolanaRpcRequest {
220 #[serde(rename = "feeEstimate")]
221 #[schema(example = "feeEstimate")]
222 FeeEstimate(FeeEstimateRequestParams),
223 #[serde(rename = "transferTransaction")]
224 #[schema(example = "transferTransaction")]
225 TransferTransaction(TransferTransactionRequestParams),
226 #[serde(rename = "prepareTransaction")]
227 #[schema(example = "prepareTransaction")]
228 PrepareTransaction(PrepareTransactionRequestParams),
229 #[serde(rename = "signTransaction")]
230 #[schema(example = "signTransaction")]
231 SignTransaction(SignTransactionRequestParams),
232 #[serde(rename = "signAndSendTransaction")]
233 #[schema(example = "signAndSendTransaction")]
234 SignAndSendTransaction(SignAndSendTransactionRequestParams),
235 #[serde(rename = "getSupportedTokens")]
236 #[schema(example = "getSupportedTokens")]
237 GetSupportedTokens(GetSupportedTokensRequestParams),
238 #[serde(rename = "getFeaturesEnabled")]
239 #[schema(example = "getFeaturesEnabled")]
240 GetFeaturesEnabled(GetFeaturesEnabledRequestParams),
241 #[serde(rename = "rawRpcRequest")]
242 #[schema(example = "rawRpcRequest")]
243 RawRpcRequest {
244 method: String,
245 params: serde_json::Value,
246 },
247}
248
249#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
250#[serde(tag = "method", rename_all = "camelCase")]
251pub enum SolanaRpcResult {
252 FeeEstimate(FeeEstimateResult),
253 TransferTransaction(TransferTransactionResult),
254 PrepareTransaction(PrepareTransactionResult),
255 SignTransaction(SignTransactionResult),
256 SignAndSendTransaction(SignAndSendTransactionResult),
257 GetSupportedTokens(GetSupportedTokensResult),
258 GetFeaturesEnabled(GetFeaturesEnabledResult),
259 RawRpc(serde_json::Value),
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use solana_sdk::{
266 hash::Hash,
267 message::Message,
268 pubkey::Pubkey,
269 signature::{Keypair, Signer},
270 };
271 use solana_system_interface::instruction;
272
273 fn create_test_transaction() -> Transaction {
274 let payer = Keypair::new();
275
276 let recipient = Pubkey::new_unique();
277 let instruction = instruction::transfer(
278 &payer.pubkey(),
279 &recipient,
280 1000, );
282 let message = Message::new(&[instruction], Some(&payer.pubkey()));
283 Transaction::new(&[&payer], message, Hash::default())
284 }
285
286 #[test]
287 fn test_transaction_to_encoded() {
288 let transaction = create_test_transaction();
289
290 let result = EncodedSerializedTransaction::try_from(&transaction);
291 assert!(result.is_ok(), "Failed to encode transaction");
292
293 let encoded = result.unwrap();
294 assert!(
295 !encoded.into_inner().is_empty(),
296 "Encoded string should not be empty"
297 );
298 }
299
300 #[test]
301 fn test_encoded_to_transaction() {
302 let original_tx = create_test_transaction();
303 let encoded = EncodedSerializedTransaction::try_from(&original_tx).unwrap();
304
305 let result = solana_sdk::transaction::Transaction::try_from(encoded);
306
307 assert!(result.is_ok(), "Failed to decode transaction");
308 let decoded_tx = result.unwrap();
309 assert_eq!(
310 original_tx.message.account_keys, decoded_tx.message.account_keys,
311 "Account keys should match"
312 );
313 assert_eq!(
314 original_tx.message.instructions, decoded_tx.message.instructions,
315 "Instructions should match"
316 );
317 }
318
319 #[test]
320 fn test_invalid_base64_decode() {
321 let invalid_encoded = EncodedSerializedTransaction("invalid base64".to_string());
322 let result = Transaction::try_from(invalid_encoded);
323 assert!(matches!(
324 result.unwrap_err(),
325 SolanaEncodingError::Decode(_)
326 ));
327 }
328
329 #[test]
330 fn test_invalid_transaction_deserialize() {
331 let invalid_data = STANDARD.encode("not a transaction");
333 let invalid_encoded = EncodedSerializedTransaction(invalid_data);
334
335 let result = Transaction::try_from(invalid_encoded);
336 assert!(matches!(
337 result.unwrap_err(),
338 SolanaEncodingError::Deserialize(_)
339 ));
340 }
341
342 #[test]
343 fn test_deserialize_fee_estimate_request() {
344 let params = serde_json::json!({
345 "transaction": EncodedSerializedTransaction::new("dGVzdA==".to_string()),
346 "fee_token": "TOKEN".to_string()
347 });
348
349 let json = serde_json::json!({
350 "method": "feeEstimate",
351 "params": params
352 });
353
354 let deserialized: SolanaRpcRequest =
355 serde_json::from_value(json).expect("Should deserialize");
356
357 match deserialized {
358 SolanaRpcRequest::FeeEstimate(p) => {
359 assert_eq!(p.fee_token, "TOKEN");
360 }
361 _ => panic!("Expected FeeEstimate variant"),
362 }
363 }
364
365 #[test]
366 fn test_deserialize_raw_rpc_request_wrapper() {
367 let inner = serde_json::json!({
369 "method": "customMethod",
370 "params": { "foo": "bar" }
371 });
372
373 let json = serde_json::json!({
374 "method": "rawRpcRequest",
375 "params": inner
376 });
377
378 let deserialized: SolanaRpcRequest =
379 serde_json::from_value(json).expect("Should deserialize raw wrapper");
380
381 match deserialized {
382 SolanaRpcRequest::RawRpcRequest { method, params } => {
383 assert_eq!(method, "customMethod");
384 assert_eq!(params["foo"], "bar");
385 }
386 _ => panic!("Expected RawRpcRequest variant"),
387 }
388 }
389
390 #[test]
391 fn test_solana_rpc_method_from_string_generic() {
392 let known = SolanaRpcMethod::from_string("feeEstimate");
393 assert!(matches!(known, Some(SolanaRpcMethod::FeeEstimate)));
394
395 let other = SolanaRpcMethod::from_string("someUnknownMethod");
396 match other {
397 Some(SolanaRpcMethod::Generic(s)) => assert_eq!(s, "someUnknownMethod"),
398 _ => panic!("Expected Generic variant"),
399 }
400 }
401}