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}
157
158// getSupportedTokens
159#[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// getFeaturesEnabled
180#[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, // lamports
272        );
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        // Create valid base64 but invalid transaction data
323        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}