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)]
85#[derive(Clone)]
86#[schema(as = SolanaFeeEstimateRequestParams)]
87pub struct FeeEstimateRequestParams {
88 pub transaction: EncodedSerializedTransaction,
89 pub fee_token: String,
90}
91
92#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ToSchema)]
93#[schema(as = SolanaFeeEstimateResult)]
94pub struct FeeEstimateResult {
95 pub estimated_fee: String,
96 pub conversion_rate: String,
97}
98
99#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
101#[serde(deny_unknown_fields)]
102#[derive(Clone)]
103pub struct TransferTransactionRequestParams {
104 pub amount: u64,
105 pub token: String,
106 pub source: String,
107 pub destination: String,
108}
109
110#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
111pub struct TransferTransactionResult {
112 pub transaction: EncodedSerializedTransaction,
113 pub fee_in_spl: String,
114 pub fee_in_lamports: String,
115 pub fee_token: String,
116 pub valid_until_blockheight: u64,
117}
118
119#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
121#[serde(deny_unknown_fields)]
122#[derive(Clone)]
123#[schema(as = SolanaPrepareTransactionRequestParams)]
124pub struct PrepareTransactionRequestParams {
125 pub transaction: EncodedSerializedTransaction,
126 pub fee_token: String,
127}
128
129#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ToSchema)]
130#[schema(as = SolanaPrepareTransactionResult)]
131pub struct PrepareTransactionResult {
132 pub transaction: EncodedSerializedTransaction,
133 pub fee_in_spl: String,
134 pub fee_in_lamports: String,
135 pub fee_token: String,
136 pub valid_until_blockheight: u64,
137}
138
139#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
141#[serde(deny_unknown_fields)]
142#[derive(Clone)]
143pub struct SignTransactionRequestParams {
144 pub transaction: EncodedSerializedTransaction,
145}
146
147#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ToSchema)]
148pub struct SignTransactionResult {
149 pub transaction: EncodedSerializedTransaction,
150 pub signature: String,
151}
152
153#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
155#[serde(deny_unknown_fields)]
156#[derive(Clone)]
157pub struct SignAndSendTransactionRequestParams {
158 pub transaction: EncodedSerializedTransaction,
159}
160
161#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
162pub struct SignAndSendTransactionResult {
163 pub transaction: EncodedSerializedTransaction,
164 pub signature: String,
165 pub id: String,
166}
167
168#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
170#[serde(deny_unknown_fields)]
171#[derive(Clone)]
172pub struct GetSupportedTokensRequestParams {}
173
174#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
175pub struct GetSupportedTokensItem {
176 pub mint: String,
177 pub symbol: String,
178 pub decimals: u8,
179 #[schema(nullable = false)]
180 pub max_allowed_fee: Option<u64>,
181 #[schema(nullable = false)]
182 pub conversion_slippage_percentage: Option<f32>,
183}
184
185#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
186pub struct GetSupportedTokensResult {
187 pub tokens: Vec<GetSupportedTokensItem>,
188}
189
190#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
192#[serde(deny_unknown_fields)]
193#[derive(Clone)]
194pub struct GetFeaturesEnabledRequestParams {}
195
196#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
197pub struct GetFeaturesEnabledResult {
198 pub features: Vec<String>,
199}
200
201pub enum SolanaRpcMethod {
202 FeeEstimate,
203 TransferTransaction,
204 PrepareTransaction,
205 SignTransaction,
206 SignAndSendTransaction,
207 GetSupportedTokens,
208 GetFeaturesEnabled,
209 Generic(String),
210}
211
212impl SolanaRpcMethod {
213 pub fn from_string(method: &str) -> Option<Self> {
214 match method {
215 "feeEstimate" => Some(SolanaRpcMethod::FeeEstimate),
216 "transferTransaction" => Some(SolanaRpcMethod::TransferTransaction),
217 "prepareTransaction" => Some(SolanaRpcMethod::PrepareTransaction),
218 "signTransaction" => Some(SolanaRpcMethod::SignTransaction),
219 "signAndSendTransaction" => Some(SolanaRpcMethod::SignAndSendTransaction),
220 "getSupportedTokens" => Some(SolanaRpcMethod::GetSupportedTokens),
221 "getFeaturesEnabled" => Some(SolanaRpcMethod::GetFeaturesEnabled),
222 _ => Some(SolanaRpcMethod::Generic(method.to_string())),
223 }
224 }
225}
226
227#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Clone)]
228#[serde(tag = "method", content = "params")]
229#[schema(as = SolanaRpcRequest)]
230pub enum SolanaRpcRequest {
231 #[serde(rename = "feeEstimate")]
232 #[schema(example = "feeEstimate")]
233 FeeEstimate(FeeEstimateRequestParams),
234 #[serde(rename = "transferTransaction")]
235 #[schema(example = "transferTransaction")]
236 TransferTransaction(TransferTransactionRequestParams),
237 #[serde(rename = "prepareTransaction")]
238 #[schema(example = "prepareTransaction")]
239 PrepareTransaction(PrepareTransactionRequestParams),
240 #[serde(rename = "signTransaction")]
241 #[schema(example = "signTransaction")]
242 SignTransaction(SignTransactionRequestParams),
243 #[serde(rename = "signAndSendTransaction")]
244 #[schema(example = "signAndSendTransaction")]
245 SignAndSendTransaction(SignAndSendTransactionRequestParams),
246 #[serde(rename = "getSupportedTokens")]
247 #[schema(example = "getSupportedTokens")]
248 GetSupportedTokens(GetSupportedTokensRequestParams),
249 #[serde(rename = "getFeaturesEnabled")]
250 #[schema(example = "getFeaturesEnabled")]
251 GetFeaturesEnabled(GetFeaturesEnabledRequestParams),
252 #[serde(rename = "rawRpcRequest")]
253 #[schema(example = "rawRpcRequest")]
254 RawRpcRequest {
255 method: String,
256 params: serde_json::Value,
257 },
258}
259
260#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
261#[serde(tag = "method", rename_all = "camelCase")]
262pub enum SolanaRpcResult {
263 FeeEstimate(FeeEstimateResult),
264 TransferTransaction(TransferTransactionResult),
265 PrepareTransaction(PrepareTransactionResult),
266 SignTransaction(SignTransactionResult),
267 SignAndSendTransaction(SignAndSendTransactionResult),
268 GetSupportedTokens(GetSupportedTokensResult),
269 GetFeaturesEnabled(GetFeaturesEnabledResult),
270 RawRpc(serde_json::Value),
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use solana_sdk::{
277 hash::Hash,
278 message::Message,
279 pubkey::Pubkey,
280 signature::{Keypair, Signer},
281 };
282 use solana_system_interface::instruction;
283
284 fn create_test_transaction() -> Transaction {
285 let payer = Keypair::new();
286
287 let recipient = Pubkey::new_unique();
288 let instruction = instruction::transfer(
289 &payer.pubkey(),
290 &recipient,
291 1000, );
293 let message = Message::new(&[instruction], Some(&payer.pubkey()));
294 Transaction::new(&[&payer], message, Hash::default())
295 }
296
297 #[test]
298 fn test_transaction_to_encoded() {
299 let transaction = create_test_transaction();
300
301 let result = EncodedSerializedTransaction::try_from(&transaction);
302 assert!(result.is_ok(), "Failed to encode transaction");
303
304 let encoded = result.unwrap();
305 assert!(
306 !encoded.into_inner().is_empty(),
307 "Encoded string should not be empty"
308 );
309 }
310
311 #[test]
312 fn test_encoded_to_transaction() {
313 let original_tx = create_test_transaction();
314 let encoded = EncodedSerializedTransaction::try_from(&original_tx).unwrap();
315
316 let result = solana_sdk::transaction::Transaction::try_from(encoded);
317
318 assert!(result.is_ok(), "Failed to decode transaction");
319 let decoded_tx = result.unwrap();
320 assert_eq!(
321 original_tx.message.account_keys, decoded_tx.message.account_keys,
322 "Account keys should match"
323 );
324 assert_eq!(
325 original_tx.message.instructions, decoded_tx.message.instructions,
326 "Instructions should match"
327 );
328 }
329
330 #[test]
331 fn test_invalid_base64_decode() {
332 let invalid_encoded = EncodedSerializedTransaction("invalid base64".to_string());
333 let result = Transaction::try_from(invalid_encoded);
334 assert!(matches!(
335 result.unwrap_err(),
336 SolanaEncodingError::Decode(_)
337 ));
338 }
339
340 #[test]
341 fn test_invalid_transaction_deserialize() {
342 let invalid_data = STANDARD.encode("not a transaction");
344 let invalid_encoded = EncodedSerializedTransaction(invalid_data);
345
346 let result = Transaction::try_from(invalid_encoded);
347 assert!(matches!(
348 result.unwrap_err(),
349 SolanaEncodingError::Deserialize(_)
350 ));
351 }
352
353 #[test]
354 fn test_deserialize_fee_estimate_request() {
355 let params = serde_json::json!({
356 "transaction": EncodedSerializedTransaction::new("dGVzdA==".to_string()),
357 "fee_token": "TOKEN".to_string()
358 });
359
360 let json = serde_json::json!({
361 "method": "feeEstimate",
362 "params": params
363 });
364
365 let deserialized: SolanaRpcRequest =
366 serde_json::from_value(json).expect("Should deserialize");
367
368 match deserialized {
369 SolanaRpcRequest::FeeEstimate(p) => {
370 assert_eq!(p.fee_token, "TOKEN");
371 }
372 _ => panic!("Expected FeeEstimate variant"),
373 }
374 }
375
376 #[test]
377 fn test_deserialize_raw_rpc_request_wrapper() {
378 let inner = serde_json::json!({
380 "method": "customMethod",
381 "params": { "foo": "bar" }
382 });
383
384 let json = serde_json::json!({
385 "method": "rawRpcRequest",
386 "params": inner
387 });
388
389 let deserialized: SolanaRpcRequest =
390 serde_json::from_value(json).expect("Should deserialize raw wrapper");
391
392 match deserialized {
393 SolanaRpcRequest::RawRpcRequest { method, params } => {
394 assert_eq!(method, "customMethod");
395 assert_eq!(params["foo"], "bar");
396 }
397 _ => panic!("Expected RawRpcRequest variant"),
398 }
399 }
400
401 #[test]
402 fn test_solana_rpc_method_from_string_generic() {
403 let known = SolanaRpcMethod::from_string("feeEstimate");
404 assert!(matches!(known, Some(SolanaRpcMethod::FeeEstimate)));
405
406 let other = SolanaRpcMethod::from_string("someUnknownMethod");
407 match other {
408 Some(SolanaRpcMethod::Generic(s)) => assert_eq!(s, "someUnknownMethod"),
409 _ => panic!("Expected Generic variant"),
410 }
411 }
412}