openzeppelin_relayer/models/rpc/solana/
mod.rs

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
57// Implement conversion from versioned transaction
58impl 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
69// Implement conversion to versioned transaction
70impl 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// feeEstimate
83#[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// transferTransaction
97#[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// prepareTransaction
116#[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// signTransaction
133#[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// signAndSendTransaction
146#[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// getSupportedTokens
160#[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// getFeaturesEnabled
181#[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, // lamports
281        );
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        // Create valid base64 but invalid transaction data
332        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        // rawRpcRequest wraps an inner object with method and params fields
368        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}