tx3_sdk/trp/
mod.rs

1//! Transaction Resolve Protocol (TRP) Client
2//!
3//! This SDK provides tools for interacting with TX3 services.
4//! Currently includes support for the Transaction Resolve Protocol (TRP).
5//!
6//! ## Usage Example
7//!
8//! ```
9//! use tx3_sdk::trp::{Client, ClientOptions};
10//!
11//! // Create TRP client
12//! let client = Client::new(ClientOptions {
13//!     endpoint: "https://trp.example.com".to_string(),
14//!     headers: None,
15//! });
16//! ```
17//!
18
19use reqwest::header;
20use serde::{de::DeserializeOwned, Deserialize, Serialize};
21use serde_json::Value;
22use std::collections::HashMap;
23use thiserror::Error;
24use uuid::Uuid;
25
26pub use crate::trp::spec::{
27    InputNotResolvedDiagnostic, MissingTxArgDiagnostic, ResolveParams, SubmitParams,
28    SubmitResponse, SubmitWitness, TxEnvelope, TxScriptFailureDiagnostic, UnsupportedTirDiagnostic,
29};
30
31mod spec;
32
33// Custom error type for TRP operations
34#[derive(Debug, Error)]
35pub enum Error {
36    #[error("network error: {0}")]
37    NetworkError(#[from] reqwest::Error),
38
39    #[error("HTTP error {0}: {1}")]
40    HttpError(u16, String),
41
42    #[error("Failed to deserialize response: {0}")]
43    DeserializationError(String),
44
45    #[error("({0}) {1}")]
46    GenericRpcError(i32, String, Option<Value>),
47
48    #[error("Unknown error: {0}")]
49    UnknownError(String),
50
51    #[error("TIR version {provided} is not supported, expected {expected}", provided = .0.provided, expected = .0.expected)]
52    UnsupportedTir(UnsupportedTirDiagnostic),
53
54    #[error("invalid TIR envelope")]
55    InvalidTirEnvelope,
56
57    #[error("failed to decode IR bytes")]
58    InvalidTirBytes,
59
60    #[error("only txs from Conway era are supported")]
61    UnsupportedTxEra,
62
63    #[error("node can't resolve txs while running at era {era}")]
64    UnsupportedEra { era: String },
65
66    #[error("missing argument `{key}` of type {ty}", key = .0.key, ty = .0.ty)]
67    MissingTxArg(MissingTxArgDiagnostic),
68
69    #[error("input `{name}` not resolved", name = .0.name)]
70    InputNotResolved(InputNotResolvedDiagnostic),
71
72    #[error("tx script returned failure")]
73    TxScriptFailure(TxScriptFailureDiagnostic),
74}
75
76impl Error {
77    fn generic(payload: JsonRpcError) -> Self {
78        Self::GenericRpcError(payload.code, payload.message, payload.data)
79    }
80}
81
82fn expect_json_rpc_error_data<T: DeserializeOwned>(payload: JsonRpcError) -> Result<T, Error> {
83    let Some(data) = payload.data.clone() else {
84        return Err(Error::generic(payload));
85    };
86
87    let Ok(data) = serde_json::from_value(data.clone()) else {
88        return Err(Error::generic(payload));
89    };
90
91    Ok(data)
92}
93
94impl From<JsonRpcError> for Error {
95    fn from(error: JsonRpcError) -> Self {
96        match error.code {
97            -32000 => match expect_json_rpc_error_data(error) {
98                Ok(data) => Error::UnsupportedTir(data),
99                Err(e) => e,
100            },
101            -32001 => match expect_json_rpc_error_data(error) {
102                Ok(data) => Error::MissingTxArg(data),
103                Err(e) => e,
104            },
105            -32002 => match expect_json_rpc_error_data(error) {
106                Ok(data) => Error::InputNotResolved(data),
107                Err(e) => e,
108            },
109            -32003 => match expect_json_rpc_error_data(error) {
110                Ok(data) => Error::TxScriptFailure(data),
111                Err(e) => e,
112            },
113            _ => Error::generic(error),
114        }
115    }
116}
117
118#[derive(Debug, Clone)]
119pub struct ClientOptions {
120    pub endpoint: String,
121    pub headers: Option<HashMap<String, String>>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct JsonRpcRequest {
126    pub jsonrpc: String,
127    pub method: String,
128    pub params: serde_json::Value,
129    pub id: String,
130}
131
132#[derive(Debug, Deserialize)]
133struct JsonRpcResponse {
134    result: Option<serde_json::Value>,
135    error: Option<JsonRpcError>,
136}
137
138#[derive(Debug, Deserialize)]
139struct JsonRpcError {
140    code: i32,
141    message: String,
142    data: Option<Value>,
143}
144
145/// Client for the Transaction Resolve Protocol (TRP)
146#[derive(Clone)]
147pub struct Client {
148    options: ClientOptions,
149    client: reqwest::Client,
150}
151
152impl Client {
153    pub fn new(options: ClientOptions) -> Self {
154        Self {
155            options,
156            client: reqwest::Client::new(),
157        }
158    }
159
160    pub async fn call(
161        &self,
162        method: &str,
163        params: serde_json::Value,
164    ) -> Result<serde_json::Value, Error> {
165        // Prepare headers
166        let mut headers = header::HeaderMap::new();
167        headers.insert(
168            header::CONTENT_TYPE,
169            header::HeaderValue::from_static("application/json"),
170        );
171
172        if let Some(user_headers) = &self.options.headers {
173            for (key, value) in user_headers {
174                if let Ok(header_name) = header::HeaderName::from_bytes(key.as_bytes()) {
175                    if let Ok(header_value) = header::HeaderValue::from_str(value) {
176                        headers.insert(header_name, header_value);
177                    }
178                }
179            }
180        }
181
182        // Prepare request body with FlattenedArgs for proper serialization
183        let body = JsonRpcRequest {
184            jsonrpc: "2.0".to_string(),
185            method: method.to_string(),
186            params,
187            id: Uuid::new_v4().to_string(),
188        };
189
190        // Send request
191        let response = self
192            .client
193            .post(&self.options.endpoint)
194            .headers(headers)
195            .json(&serde_json::to_value(body).unwrap())
196            .send()
197            .await
198            .map_err(Error::from)?;
199
200        // If the response at the HTTP level is not successful, return an error
201        if !response.status().is_success() {
202            return Err(Error::HttpError(
203                response.status().as_u16(),
204                response.status().to_string(),
205            ));
206        }
207
208        // Parse response
209        let result: JsonRpcResponse = response
210            .json()
211            .await
212            .map_err(|e| Error::DeserializationError(e.to_string()))?;
213
214        // Handle possible error
215        if let Some(error) = result.error {
216            return Err(Error::from(error));
217        }
218
219        result
220            .result
221            .ok_or_else(|| Error::UnknownError("No result in response".to_string()))
222    }
223
224    pub async fn resolve(&self, request: ResolveParams) -> Result<TxEnvelope, Error> {
225        let params = serde_json::to_value(request).unwrap();
226
227        let response = self.call("trp.resolve", params).await?;
228
229        // Return result
230        let out = serde_json::from_value(response)
231            .map_err(|e| Error::DeserializationError(e.to_string()))?;
232
233        Ok(out)
234    }
235
236    pub async fn submit(&self, request: SubmitParams) -> Result<SubmitResponse, Error> {
237        let params = serde_json::to_value(request).unwrap();
238
239        let response = self.call("trp.submit", params).await?;
240
241        let out = serde_json::from_value(response)
242            .map_err(|e| Error::DeserializationError(e.to_string()))?;
243
244        Ok(out)
245    }
246}