Skip to main content

sof_tx/submit/
rpc.rs

1//! JSON-RPC submit transport implementation.
2
3use std::time::Duration;
4
5use async_trait::async_trait;
6use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
7use serde::{Deserialize, Serialize};
8
9use super::{RpcSubmitConfig, RpcSubmitTransport, SubmitTransportError};
10
11/// JSON-RPC transport that submits encoded transactions via `sendTransaction`.
12#[derive(Debug, Clone)]
13pub struct JsonRpcTransport {
14    /// HTTP client used for RPC calls.
15    client: reqwest::Client,
16    /// Target JSON-RPC endpoint URL.
17    rpc_url: String,
18}
19
20impl JsonRpcTransport {
21    /// Creates a JSON-RPC transport.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`SubmitTransportError::Config`] when HTTP client creation fails.
26    pub fn new(rpc_url: impl Into<String>) -> Result<Self, SubmitTransportError> {
27        let client = reqwest::Client::builder()
28            .timeout(Duration::from_secs(10))
29            .build()
30            .map_err(|error| SubmitTransportError::Config {
31                message: error.to_string(),
32            })?;
33        Ok(Self {
34            client,
35            rpc_url: rpc_url.into(),
36        })
37    }
38}
39
40/// JSON-RPC envelope.
41#[derive(Debug, Deserialize)]
42struct JsonRpcResponse {
43    /// Result value for successful calls.
44    result: Option<String>,
45    /// Error payload for failed calls.
46    error: Option<JsonRpcError>,
47}
48
49/// JSON-RPC error object.
50#[derive(Debug, Deserialize)]
51struct JsonRpcError {
52    /// JSON-RPC error code.
53    code: i64,
54    /// Human-readable message.
55    message: String,
56}
57
58#[async_trait]
59impl RpcSubmitTransport for JsonRpcTransport {
60    async fn submit_rpc(
61        &self,
62        tx_bytes: &[u8],
63        config: &RpcSubmitConfig,
64    ) -> Result<String, SubmitTransportError> {
65        #[derive(Debug, Serialize)]
66        struct RpcConfig<'config> {
67            /// Transaction encoding format.
68            encoding: &'config str,
69            /// Optional preflight skip flag.
70            #[serde(rename = "skipPreflight")]
71            skip_preflight: bool,
72            /// Optional preflight commitment.
73            #[serde(
74                rename = "preflightCommitment",
75                skip_serializing_if = "Option::is_none"
76            )]
77            preflight_commitment: Option<&'config str>,
78        }
79
80        let encoded_tx = BASE64_STANDARD.encode(tx_bytes);
81        let payload = serde_json::json!({
82            "jsonrpc": "2.0",
83            "id": 1,
84            "method": "sendTransaction",
85            "params": [
86                encoded_tx,
87                RpcConfig {
88                    encoding: "base64",
89                    skip_preflight: config.skip_preflight,
90                    preflight_commitment: config.preflight_commitment.as_deref(),
91                }
92            ]
93        });
94
95        let response = self
96            .client
97            .post(&self.rpc_url)
98            .json(&payload)
99            .send()
100            .await
101            .map_err(|error| SubmitTransportError::Failure {
102                message: error.to_string(),
103            })?;
104
105        let response =
106            response
107                .error_for_status()
108                .map_err(|error| SubmitTransportError::Failure {
109                    message: error.to_string(),
110                })?;
111
112        let parsed: JsonRpcResponse =
113            response
114                .json()
115                .await
116                .map_err(|error| SubmitTransportError::Failure {
117                    message: error.to_string(),
118                })?;
119
120        if let Some(signature) = parsed.result {
121            return Ok(signature);
122        }
123        if let Some(error) = parsed.error {
124            return Err(SubmitTransportError::Failure {
125                message: format!("rpc error {}: {}", error.code, error.message),
126            });
127        }
128
129        Err(SubmitTransportError::Failure {
130            message: "rpc returned neither result nor error".to_owned(),
131        })
132    }
133}