tx3_sdk/trp/
mod.rs

1use reqwest::header;
2use serde::{de::DeserializeOwned, Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::collections::HashMap;
5use thiserror::Error;
6use uuid::Uuid;
7
8pub mod args;
9
10pub use args::ArgValue;
11
12use crate::trp::args::BytesEnvelope;
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct SearchSpaceDiagnostic {
16    pub matched: Vec<String>,
17    pub by_address_count: Option<usize>,
18    pub by_asset_class_count: Option<usize>,
19    pub by_ref_count: Option<usize>,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct InputQueryDiagnostic {
24    pub address: Option<String>,
25    pub min_amount: HashMap<String, String>,
26    pub refs: Vec<String>,
27    pub support_many: bool,
28    pub collateral: bool,
29}
30
31#[derive(Debug, Serialize, Deserialize, Error)]
32#[error("input `{name}` not resolved")]
33pub struct InputNotResolvedDiagnostic {
34    pub name: String,
35    pub query: InputQueryDiagnostic,
36    pub search_space: SearchSpaceDiagnostic,
37}
38
39#[derive(Debug, Serialize, Deserialize, Error)]
40#[error("TIR version {provided} is not supported, expected {expected}")]
41pub struct UnsupportedTirDiagnostic {
42    pub provided: String,
43    pub expected: String,
44}
45
46#[derive(Debug, Serialize, Deserialize, Error)]
47#[error("tx script returned failure")]
48pub struct TxScriptFailureDiagnostic {
49    pub logs: Vec<String>,
50}
51
52#[derive(Debug, Serialize, Deserialize, Error)]
53#[error("missing argument `{key}` of type {ty}")]
54pub struct MissingTxArgDiagnostic {
55    pub key: String,
56    #[serde(rename = "type")]
57    pub ty: String,
58}
59
60// Custom error type for TRP operations
61#[derive(Debug, Error)]
62pub enum Error {
63    #[error("network error: {0}")]
64    NetworkError(#[from] reqwest::Error),
65
66    #[error("HTTP error {0}: {1}")]
67    HttpError(u16, String),
68
69    #[error("Failed to deserialize response: {0}")]
70    DeserializationError(String),
71
72    #[error("({0}) {1}")]
73    GenericRpcError(i32, String, Option<Value>),
74
75    #[error("Unknown error: {0}")]
76    UnknownError(String),
77
78    #[error(transparent)]
79    UnsupportedTir(UnsupportedTirDiagnostic),
80
81    #[error("invalid TIR envelope")]
82    InvalidTirEnvelope,
83
84    #[error("failed to decode IR bytes")]
85    InvalidTirBytes,
86
87    #[error("only txs from Conway era are supported")]
88    UnsupportedTxEra,
89
90    #[error("node can't resolve txs while running at era {era}")]
91    UnsupportedEra { era: String },
92
93    #[error(transparent)]
94    MissingTxArg(MissingTxArgDiagnostic),
95
96    #[error(transparent)]
97    InputNotResolved(InputNotResolvedDiagnostic),
98
99    #[error(transparent)]
100    TxScriptFailure(TxScriptFailureDiagnostic),
101}
102
103impl Error {
104    fn generic(payload: JsonRpcError) -> Self {
105        Self::GenericRpcError(payload.code, payload.message, payload.data)
106    }
107}
108
109fn expect_json_rpc_error_data<T: DeserializeOwned>(payload: JsonRpcError) -> Result<T, Error> {
110    let Some(data) = payload.data.clone() else {
111        return Err(Error::generic(payload));
112    };
113
114    let Ok(data) = serde_json::from_value(data.clone()) else {
115        return Err(Error::generic(payload));
116    };
117
118    Ok(data)
119}
120
121impl From<JsonRpcError> for Error {
122    fn from(error: JsonRpcError) -> Self {
123        match error.code {
124            -32000 => match expect_json_rpc_error_data(error) {
125                Ok(data) => Error::UnsupportedTir(data),
126                Err(e) => e,
127            },
128            -32001 => match expect_json_rpc_error_data(error) {
129                Ok(data) => Error::MissingTxArg(data),
130                Err(e) => e,
131            },
132            -32002 => match expect_json_rpc_error_data(error) {
133                Ok(data) => Error::InputNotResolved(data),
134                Err(e) => e,
135            },
136            -32003 => match expect_json_rpc_error_data(error) {
137                Ok(data) => Error::TxScriptFailure(data),
138                Err(e) => e,
139            },
140            _ => Error::generic(error),
141        }
142    }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct TirInfo {
147    pub version: String,
148    pub bytecode: String,
149    pub encoding: String, // "base64" | "hex" | other
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct VKeyWitness {
154    pub key: args::BytesEnvelope,
155    pub signature: args::BytesEnvelope,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(tag = "type")]
160pub enum SubmitWitness {
161    #[serde(rename = "vkey")]
162    VKey(VKeyWitness),
163}
164
165#[derive(Deserialize, Debug, Serialize)]
166pub struct SubmitParams {
167    pub tx: args::BytesEnvelope,
168    pub witnesses: Vec<SubmitWitness>,
169}
170
171#[derive(Deserialize, Debug, Serialize)]
172pub struct SubmitResponse {
173    pub hash: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct TxEnvelope {
178    pub tx: String,
179    pub hash: String,
180}
181
182#[derive(Debug, Clone)]
183pub struct ClientOptions {
184    pub endpoint: String,
185    pub headers: Option<HashMap<String, String>>,
186    pub env_args: Option<HashMap<String, ArgValue>>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct JsonRpcRequest {
191    pub jsonrpc: String,
192    pub method: String,
193    pub params: serde_json::Value,
194    pub id: String,
195}
196
197#[derive(Debug, Deserialize)]
198struct JsonRpcResponse {
199    result: Option<serde_json::Value>,
200    error: Option<JsonRpcError>,
201}
202
203#[derive(Debug, Deserialize)]
204struct JsonRpcError {
205    code: i32,
206    message: String,
207    data: Option<Value>,
208}
209
210/// Client for the Transaction Resolve Protocol (TRP)
211pub struct Client {
212    options: ClientOptions,
213    client: reqwest::Client,
214}
215
216pub struct ProtoTxRequest {
217    pub tir: TirInfo,
218    pub args: HashMap<String, ArgValue>,
219}
220
221impl Client {
222    pub fn new(options: ClientOptions) -> Self {
223        Self {
224            options,
225            client: reqwest::Client::new(),
226        }
227    }
228
229    pub async fn call(
230        &self,
231        method: &str,
232        params: serde_json::Value,
233    ) -> Result<serde_json::Value, Error> {
234        // Prepare headers
235        let mut headers = header::HeaderMap::new();
236        headers.insert(
237            header::CONTENT_TYPE,
238            header::HeaderValue::from_static("application/json"),
239        );
240
241        if let Some(user_headers) = &self.options.headers {
242            for (key, value) in user_headers {
243                if let Ok(header_name) = header::HeaderName::from_bytes(key.as_bytes()) {
244                    if let Ok(header_value) = header::HeaderValue::from_str(value) {
245                        headers.insert(header_name, header_value);
246                    }
247                }
248            }
249        }
250
251        // Prepare request body with FlattenedArgs for proper serialization
252        let body = JsonRpcRequest {
253            jsonrpc: "2.0".to_string(),
254            method: method.to_string(),
255            params,
256            id: Uuid::new_v4().to_string(),
257        };
258
259        // Send request
260        let response = self
261            .client
262            .post(&self.options.endpoint)
263            .headers(headers)
264            .json(&serde_json::to_value(body).unwrap())
265            .send()
266            .await
267            .map_err(Error::from)?;
268
269        // If the response at the HTTP level is not successful, return an error
270        if !response.status().is_success() {
271            return Err(Error::HttpError(
272                response.status().as_u16(),
273                response.status().to_string(),
274            ));
275        }
276
277        // Parse response
278        let result: JsonRpcResponse = response
279            .json()
280            .await
281            .map_err(|e| Error::DeserializationError(e.to_string()))?;
282
283        // Handle possible error
284        if let Some(error) = result.error {
285            return Err(Error::from(error));
286        }
287
288        result
289            .result
290            .ok_or_else(|| Error::UnknownError("No result in response".to_string()))
291    }
292
293    pub async fn resolve(&self, proto_tx: ProtoTxRequest) -> Result<TxEnvelope, Error> {
294        let params = json!({
295            "tir": proto_tx.tir,
296            "args": HashMap::<String, serde_json::Value>::from_iter(proto_tx.args.into_iter().map(|(k, v)| (k, args::to_json(v)))),
297            "env": self.options.env_args,
298        });
299
300        let response = self.call("trp.resolve", params).await?;
301
302        // Return result
303        let out = serde_json::from_value(response)
304            .map_err(|e| Error::DeserializationError(e.to_string()))?;
305
306        Ok(out)
307    }
308
309    pub async fn submit(
310        &self,
311        tx: TxEnvelope,
312        witnesses: Vec<SubmitWitness>,
313    ) -> Result<SubmitResponse, Error> {
314        let params = serde_json::to_value(SubmitParams {
315            tx: BytesEnvelope::from_hex(&tx.tx).unwrap(),
316            witnesses,
317        })
318        .unwrap();
319
320        let response = self.call("trp.submit", params).await?;
321
322        let out = serde_json::from_value(response)
323            .map_err(|e| Error::DeserializationError(e.to_string()))?;
324
325        Ok(out)
326    }
327}