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}
157
158#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
160#[serde(deny_unknown_fields)]
161pub struct GetSupportedTokensRequestParams {}
162
163#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
164pub struct GetSupportedTokensItem {
165 pub mint: String,
166 pub symbol: String,
167 pub decimals: u8,
168 #[schema(nullable = false)]
169 pub max_allowed_fee: Option<u64>,
170 #[schema(nullable = false)]
171 pub conversion_slippage_percentage: Option<f32>,
172}
173
174#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
175pub struct GetSupportedTokensResult {
176 pub tokens: Vec<GetSupportedTokensItem>,
177}
178
179#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
181#[serde(deny_unknown_fields)]
182pub struct GetFeaturesEnabledRequestParams {}
183
184#[derive(Debug, Deserialize, Serialize, PartialEq, ToSchema)]
185pub struct GetFeaturesEnabledResult {
186 pub features: Vec<String>,
187}
188
189pub enum SolanaRpcMethod {
190 FeeEstimate,
191 TransferTransaction,
192 PrepareTransaction,
193 SignTransaction,
194 SignAndSendTransaction,
195 GetSupportedTokens,
196 GetFeaturesEnabled,
197}
198
199impl SolanaRpcMethod {
200 pub fn from_string(method: &str) -> Option<Self> {
201 match method {
202 "feeEstimate" => Some(SolanaRpcMethod::FeeEstimate),
203 "transferTransaction" => Some(SolanaRpcMethod::TransferTransaction),
204 "prepareTransaction" => Some(SolanaRpcMethod::PrepareTransaction),
205 "signTransaction" => Some(SolanaRpcMethod::SignTransaction),
206 "signAndSendTransaction" => Some(SolanaRpcMethod::SignAndSendTransaction),
207 "getSupportedTokens" => Some(SolanaRpcMethod::GetSupportedTokens),
208 "getFeaturesEnabled" => Some(SolanaRpcMethod::GetFeaturesEnabled),
209 _ => None,
210 }
211 }
212}
213
214#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
215#[serde(tag = "method", content = "params")]
216#[schema(as = SolanaRpcRequest)]
217pub enum SolanaRpcRequest {
218 #[serde(rename = "feeEstimate")]
219 #[schema(example = "feeEstimate")]
220 FeeEstimate(FeeEstimateRequestParams),
221 #[serde(rename = "transferTransaction")]
222 #[schema(example = "transferTransaction")]
223 TransferTransaction(TransferTransactionRequestParams),
224 #[serde(rename = "prepareTransaction")]
225 #[schema(example = "prepareTransaction")]
226 PrepareTransaction(PrepareTransactionRequestParams),
227 #[serde(rename = "signTransaction")]
228 #[schema(example = "signTransaction")]
229 SignTransaction(SignTransactionRequestParams),
230 #[serde(rename = "signAndSendTransaction")]
231 #[schema(example = "signAndSendTransaction")]
232 SignAndSendTransaction(SignAndSendTransactionRequestParams),
233 #[serde(rename = "getSupportedTokens")]
234 #[schema(example = "getSupportedTokens")]
235 GetSupportedTokens(GetSupportedTokensRequestParams),
236 #[serde(rename = "getFeaturesEnabled")]
237 #[schema(example = "getFeaturesEnabled")]
238 GetFeaturesEnabled(GetFeaturesEnabledRequestParams),
239}
240
241#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
242#[serde(untagged)]
243pub enum SolanaRpcResult {
244 FeeEstimate(FeeEstimateResult),
245 TransferTransaction(TransferTransactionResult),
246 PrepareTransaction(PrepareTransactionResult),
247 SignTransaction(SignTransactionResult),
248 SignAndSendTransaction(SignAndSendTransactionResult),
249 GetSupportedTokens(GetSupportedTokensResult),
250 GetFeaturesEnabled(GetFeaturesEnabledResult),
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use solana_sdk::{
257 hash::Hash,
258 message::Message,
259 pubkey::Pubkey,
260 signature::{Keypair, Signer},
261 system_instruction,
262 };
263
264 fn create_test_transaction() -> Transaction {
265 let payer = Keypair::new();
266
267 let recipient = Pubkey::new_unique();
268 let instruction = system_instruction::transfer(
269 &payer.pubkey(),
270 &recipient,
271 1000, );
273 let message = Message::new(&[instruction], Some(&payer.pubkey()));
274 Transaction::new(&[&payer], message, Hash::default())
275 }
276
277 #[test]
278 fn test_transaction_to_encoded() {
279 let transaction = create_test_transaction();
280
281 let result = EncodedSerializedTransaction::try_from(&transaction);
282 assert!(result.is_ok(), "Failed to encode transaction");
283
284 let encoded = result.unwrap();
285 assert!(
286 !encoded.into_inner().is_empty(),
287 "Encoded string should not be empty"
288 );
289 }
290
291 #[test]
292 fn test_encoded_to_transaction() {
293 let original_tx = create_test_transaction();
294 let encoded = EncodedSerializedTransaction::try_from(&original_tx).unwrap();
295
296 let result = solana_sdk::transaction::Transaction::try_from(encoded);
297
298 assert!(result.is_ok(), "Failed to decode transaction");
299 let decoded_tx = result.unwrap();
300 assert_eq!(
301 original_tx.message.account_keys, decoded_tx.message.account_keys,
302 "Account keys should match"
303 );
304 assert_eq!(
305 original_tx.message.instructions, decoded_tx.message.instructions,
306 "Instructions should match"
307 );
308 }
309
310 #[test]
311 fn test_invalid_base64_decode() {
312 let invalid_encoded = EncodedSerializedTransaction("invalid base64".to_string());
313 let result = Transaction::try_from(invalid_encoded);
314 assert!(matches!(
315 result.unwrap_err(),
316 SolanaEncodingError::Decode(_)
317 ));
318 }
319
320 #[test]
321 fn test_invalid_transaction_deserialize() {
322 let invalid_data = STANDARD.encode("not a transaction");
324 let invalid_encoded = EncodedSerializedTransaction(invalid_data);
325
326 let result = Transaction::try_from(invalid_encoded);
327 assert!(matches!(
328 result.unwrap_err(),
329 SolanaEncodingError::Deserialize(_)
330 ));
331 }
332}