use reqwest::header;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
pub mod args;
pub use args::ArgValue;
use crate::trp::args::BytesEnvelope;
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchSpaceDiagnostic {
pub matched: Vec<String>,
pub by_address_count: Option<usize>,
pub by_asset_class_count: Option<usize>,
pub by_ref_count: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InputQueryDiagnostic {
pub address: Option<String>,
pub min_amount: HashMap<String, String>,
pub refs: Vec<String>,
pub support_many: bool,
pub collateral: bool,
}
#[derive(Debug, Serialize, Deserialize, Error)]
#[error("input `{name}` not resolved")]
pub struct InputNotResolvedDiagnostic {
pub name: String,
pub query: InputQueryDiagnostic,
pub search_space: SearchSpaceDiagnostic,
}
#[derive(Debug, Serialize, Deserialize, Error)]
#[error("TIR version {provided} is not supported, expected {expected}")]
pub struct UnsupportedTirDiagnostic {
pub provided: String,
pub expected: String,
}
#[derive(Debug, Serialize, Deserialize, Error)]
#[error("tx script returned failure")]
pub struct TxScriptFailureDiagnostic {
pub logs: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Error)]
#[error("missing argument `{key}` of type {ty}")]
pub struct MissingTxArgDiagnostic {
pub key: String,
#[serde(rename = "type")]
pub ty: String,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("network error: {0}")]
NetworkError(#[from] reqwest::Error),
#[error("HTTP error {0}: {1}")]
HttpError(u16, String),
#[error("Failed to deserialize response: {0}")]
DeserializationError(String),
#[error("({0}) {1}")]
GenericRpcError(i32, String, Option<Value>),
#[error("Unknown error: {0}")]
UnknownError(String),
#[error(transparent)]
UnsupportedTir(UnsupportedTirDiagnostic),
#[error("invalid TIR envelope")]
InvalidTirEnvelope,
#[error("failed to decode IR bytes")]
InvalidTirBytes,
#[error("only txs from Conway era are supported")]
UnsupportedTxEra,
#[error("node can't resolve txs while running at era {era}")]
UnsupportedEra { era: String },
#[error(transparent)]
MissingTxArg(MissingTxArgDiagnostic),
#[error(transparent)]
InputNotResolved(InputNotResolvedDiagnostic),
#[error(transparent)]
TxScriptFailure(TxScriptFailureDiagnostic),
}
impl Error {
fn generic(payload: JsonRpcError) -> Self {
Self::GenericRpcError(payload.code, payload.message, payload.data)
}
}
fn expect_json_rpc_error_data<T: DeserializeOwned>(payload: JsonRpcError) -> Result<T, Error> {
let Some(data) = payload.data.clone() else {
return Err(Error::generic(payload));
};
let Ok(data) = serde_json::from_value(data.clone()) else {
return Err(Error::generic(payload));
};
Ok(data)
}
impl From<JsonRpcError> for Error {
fn from(error: JsonRpcError) -> Self {
match error.code {
-32000 => match expect_json_rpc_error_data(error) {
Ok(data) => Error::UnsupportedTir(data),
Err(e) => e,
},
-32001 => match expect_json_rpc_error_data(error) {
Ok(data) => Error::MissingTxArg(data),
Err(e) => e,
},
-32002 => match expect_json_rpc_error_data(error) {
Ok(data) => Error::InputNotResolved(data),
Err(e) => e,
},
-32003 => match expect_json_rpc_error_data(error) {
Ok(data) => Error::TxScriptFailure(data),
Err(e) => e,
},
_ => Error::generic(error),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TirInfo {
pub version: String,
pub bytecode: String,
pub encoding: String, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VKeyWitness {
pub key: args::BytesEnvelope,
pub signature: args::BytesEnvelope,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SubmitWitness {
#[serde(rename = "vkey")]
VKey(VKeyWitness),
}
#[derive(Deserialize, Debug, Serialize)]
pub struct SubmitParams {
pub tx: args::BytesEnvelope,
pub witnesses: Vec<SubmitWitness>,
}
#[derive(Deserialize, Debug, Serialize)]
pub struct SubmitResponse {
pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxEnvelope {
pub tx: String,
pub hash: String,
}
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub endpoint: String,
pub headers: Option<HashMap<String, String>>,
pub env_args: Option<HashMap<String, ArgValue>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub method: String,
pub params: serde_json::Value,
pub id: String,
}
#[derive(Debug, Deserialize)]
struct JsonRpcResponse {
result: Option<serde_json::Value>,
error: Option<JsonRpcError>,
}
#[derive(Debug, Deserialize)]
struct JsonRpcError {
code: i32,
message: String,
data: Option<Value>,
}
pub struct Client {
options: ClientOptions,
client: reqwest::Client,
}
pub struct ProtoTxRequest {
pub tir: TirInfo,
pub args: HashMap<String, ArgValue>,
}
impl Client {
pub fn new(options: ClientOptions) -> Self {
Self {
options,
client: reqwest::Client::new(),
}
}
pub async fn call(
&self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
if let Some(user_headers) = &self.options.headers {
for (key, value) in user_headers {
if let Ok(header_name) = header::HeaderName::from_bytes(key.as_bytes()) {
if let Ok(header_value) = header::HeaderValue::from_str(value) {
headers.insert(header_name, header_value);
}
}
}
}
let body = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: method.to_string(),
params,
id: Uuid::new_v4().to_string(),
};
let response = self
.client
.post(&self.options.endpoint)
.headers(headers)
.json(&serde_json::to_value(body).unwrap())
.send()
.await
.map_err(Error::from)?;
if !response.status().is_success() {
return Err(Error::HttpError(
response.status().as_u16(),
response.status().to_string(),
));
}
let result: JsonRpcResponse = response
.json()
.await
.map_err(|e| Error::DeserializationError(e.to_string()))?;
if let Some(error) = result.error {
return Err(Error::from(error));
}
result
.result
.ok_or_else(|| Error::UnknownError("No result in response".to_string()))
}
pub async fn resolve(&self, proto_tx: ProtoTxRequest) -> Result<TxEnvelope, Error> {
let params = json!({
"tir": proto_tx.tir,
"args": HashMap::<String, serde_json::Value>::from_iter(proto_tx.args.into_iter().map(|(k, v)| (k, args::to_json(v)))),
"env": self.options.env_args,
});
let response = self.call("trp.resolve", params).await?;
let out = serde_json::from_value(response)
.map_err(|e| Error::DeserializationError(e.to_string()))?;
Ok(out)
}
pub async fn submit(
&self,
tx: TxEnvelope,
witnesses: Vec<SubmitWitness>,
) -> Result<SubmitResponse, Error> {
let params = serde_json::to_value(SubmitParams {
tx: BytesEnvelope::from_hex(&tx.tx).unwrap(),
witnesses,
})
.unwrap();
let response = self.call("trp.submit", params).await?;
let out = serde_json::from_value(response)
.map_err(|e| Error::DeserializationError(e.to_string()))?;
Ok(out)
}
}