use std::fmt::Debug; use thiserror::Error; use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde_json::json; use reqwest; use crate::transaction::Transaction; #[derive(Deserialize, Debug)] pub struct EmptyResponse {} #[derive(Deserialize, Debug)] pub struct JsonRpcResponse { result: T } #[derive(Error, Debug)] pub enum RpcError { #[error("internal error ({0})")] InternalError(String), #[error("connection error")] ConnectionError, #[error("transaction not found (expected {1}, got {0})")] TransactionsNotFound(usize, usize), #[error("invalid point ({0})")] InvalidPoint(String), #[error("invalid transaction")] InvalidTransaction } pub struct Rpc(String); fn rpc_hex(value: &str) -> Result, RpcError> { hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string())) } fn rpc_point(point: &str) -> Result { CompressedEdwardsY( rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))? ).decompress().ok_or(RpcError::InvalidPoint(point.to_string())) } impl Rpc { pub fn new(daemon: String) -> Rpc { Rpc(daemon) } pub async fn rpc_call< Params: Serialize + Debug, Response: DeserializeOwned + Debug >(&self, method: &str, params: Option) -> Result { let client = reqwest::Client::new(); let mut builder = client.post(&(self.0.clone() + "/" + method)); if let Some(params) = params.as_ref() { builder = builder.json(params); } self.call_tail(method, builder).await } pub async fn bin_call< Response: DeserializeOwned + Debug >(&self, method: &str, params: Vec) -> Result { let client = reqwest::Client::new(); let builder = client.post(&(self.0.clone() + "/" + method)).body(params); self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await } async fn call_tail< Response: DeserializeOwned + Debug >(&self, method: &str, builder: reqwest::RequestBuilder) -> Result { let res = builder .send() .await .map_err(|_| RpcError::ConnectionError)?; Ok( if !method.ends_with(".bin") { serde_json::from_str(&res.text().await.map_err(|_| RpcError::ConnectionError)?) .map_err(|_| RpcError::InternalError("Failed to parse JSON response".to_string()))? } else { monero_epee_bin_serde::from_bytes(&res.bytes().await.map_err(|_| RpcError::ConnectionError)?) .map_err(|_| RpcError::InternalError("Failed to parse binary response".to_string()))? } ) } pub async fn get_height(&self) -> Result { #[derive(Deserialize, Debug)] struct HeightResponse { height: usize } Ok(self.rpc_call::, HeightResponse>("get_height", None).await?.height) } pub async fn get_transactions(&self, hashes: Vec<[u8; 32]>) -> Result, RpcError> { #[derive(Deserialize, Debug)] struct TransactionResponse { as_hex: String, pruned_as_hex: String } #[derive(Deserialize, Debug)] struct TransactionsResponse { txs: Vec } let txs: TransactionsResponse = self.rpc_call("get_transactions", Some(json!({ "txs_hashes": hashes.iter().map(|hash| hex::encode(&hash)).collect::>() }))).await?; if txs.txs.len() != hashes.len() { Err(RpcError::TransactionsNotFound(txs.txs.len(), hashes.len()))?; } /* Ok( txs.txs.iter().filter_map( |tx| rpc_hex(if tx.as_hex.len() != 0 { &tx.as_hex } else { &tx.pruned_as_hex }).ok() .and_then(|mut bytes| Transaction::deserialize(&mut bytes).ok()) // https://github.com/monero-project/monero/issues/8311 .filter( if tx.as_hex.len() == 0 { match res[res.len() - 1].prefix.inputs[0] { Input::Gen { .. } => true, _ => false } } ) ) ) */ Ok(vec![]) } /* pub async fn get_block(&self, height: usize) -> Result { #[derive(Deserialize, Debug)] struct BlockResponse { json: String } let block: JsonRpcResponse = self.rpc_call("json_rpc", Some(json!({ "method": "get_block", "params": { "height": height } }))).await?; Ok( deserialize( &rpc_hex(&block.result.blob)? ).expect("Monero returned a block we couldn't deserialize") ) } */ pub async fn get_block_transactions(&self, height: usize) -> Result, RpcError> { /* let block = self.get_block(height).await?; let mut res = vec![block.miner_tx]; if block.tx_hashes.len() != 0 { res.extend(self.get_transactions(block.tx_hashes.iter().map(|hash| hash.0).collect()).await?); } Ok(res) */ Ok(vec![]) } pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result, RpcError> { #[derive(Serialize, Debug)] struct Request { txid: [u8; 32] } #[allow(dead_code)] #[derive(Deserialize, Debug)] struct OIndexes { o_indexes: Vec, status: String, untrusted: bool, credits: usize, top_hash: String } let indexes: OIndexes = self.bin_call("get_o_indexes.bin", monero_epee_bin_serde::to_bytes( &Request { txid: hash }).unwrap() ).await?; Ok(indexes.o_indexes) } pub async fn get_outputs( &self, indexes: &[u64], height: usize ) -> Result>, RpcError> { #[derive(Deserialize, Debug)] pub struct Out { key: String, mask: String, txid: String } #[derive(Deserialize, Debug)] struct Outs { outs: Vec } let outs: Outs = self.rpc_call("get_outs", Some(json!({ "get_txid": true, "outputs": indexes.iter().map(|o| json!({ "amount": 0, "index": o })).collect::>() }))).await?; let txs = self.get_transactions( outs.outs.iter().map(|out| rpc_hex(&out.txid).expect("Monero returned an invalidly encoded hash") .try_into().expect("Monero returned an invalid sized hash") ).collect() ).await?; // TODO: Support time based lock times. These shouldn't be needed, and it may be painful to // get the median time for the given height, yet we do need to in order to be complete outs.outs.iter().enumerate().map( |(i, out)| Ok( if txs[i].prefix.unlock_time <= u64::try_from(height).unwrap() { Some([rpc_point(&out.key)?, rpc_point(&out.mask)?]) } else { None } ) ).collect() } pub async fn get_output_distribution(&self, height: usize) -> Result, RpcError> { #[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct Distribution { distribution: Vec } #[allow(dead_code)] #[derive(Deserialize, Debug)] struct Distributions { distributions: Vec } let mut distributions: JsonRpcResponse = self.rpc_call("json_rpc", Some(json!({ "method": "get_output_distribution", "params": { "binary": false, "amounts": [0], "cumulative": true, "to_height": height } }))).await?; Ok(distributions.result.distributions.swap_remove(0).distribution) } pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> { #[allow(dead_code)] #[derive(Deserialize, Debug)] struct SendRawResponse { status: String, double_spend: bool, fee_too_low: bool, invalid_input: bool, invalid_output: bool, low_mixin: bool, not_relayed: bool, overspend: bool, too_big: bool, too_few_outputs: bool, reason: String } let mut buf = Vec::with_capacity(2048); tx.serialize(&mut buf).unwrap(); let res: SendRawResponse = self.rpc_call("send_raw_transaction", Some(json!({ "tx_as_hex": hex::encode(&buf) }))).await?; if res.status != "OK" { Err(RpcError::InvalidTransaction)?; } Ok(()) } }