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