2022-05-04 10:24:52 +00:00
|
|
|
use std::{fmt::Debug, str::FromStr};
|
2022-04-28 07:31:09 +00:00
|
|
|
|
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
use hex::ToHex;
|
|
|
|
|
|
|
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
|
|
|
|
|
|
|
use monero::{
|
|
|
|
Hash,
|
2022-05-04 10:24:52 +00:00
|
|
|
cryptonote::hash::Hashable,
|
2022-04-28 07:31:09 +00:00
|
|
|
blockdata::{
|
2022-05-04 10:24:52 +00:00
|
|
|
transaction::{TxIn, Transaction},
|
2022-04-28 07:31:09 +00:00
|
|
|
block::Block
|
|
|
|
},
|
|
|
|
consensus::encode::{serialize, deserialize}
|
|
|
|
};
|
|
|
|
|
|
|
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
|
use reqwest;
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
2022-04-29 00:41:43 +00:00
|
|
|
pub struct EmptyResponse {}
|
2022-04-28 07:31:09 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
2022-04-29 00:41:43 +00:00
|
|
|
pub struct JsonRpcResponse<T> {
|
2022-04-28 07:31:09 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
pub struct Rpc(String);
|
|
|
|
|
2022-04-28 07:31:09 +00:00
|
|
|
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
|
|
|
|
hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string()))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-04-29 00:41:43 +00:00
|
|
|
pub async fn rpc_call<
|
2022-04-28 07:31:09 +00:00
|
|
|
Params: Serialize + Debug,
|
|
|
|
Response: DeserializeOwned + Debug
|
|
|
|
>(&self, method: &str, params: Option<Params>) -> Result<Response, RpcError> {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-29 00:41:43 +00:00
|
|
|
pub async fn bin_call<
|
2022-04-28 07:31:09 +00:00
|
|
|
Response: DeserializeOwned + Debug
|
|
|
|
>(&self, method: &str, params: Vec<u8>) -> Result<Response, RpcError> {
|
|
|
|
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<Response, RpcError> {
|
|
|
|
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)?)
|
2022-05-04 10:24:52 +00:00
|
|
|
.map_err(|_| RpcError::InternalError("Failed to parse JSON response".to_string()))?
|
2022-04-28 07:31:09 +00:00
|
|
|
} 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<usize, RpcError> {
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct HeightResponse {
|
|
|
|
height: usize
|
|
|
|
}
|
|
|
|
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_transactions(&self, hashes: Vec<Hash>) -> Result<Vec<Transaction>, RpcError> {
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct TransactionResponse {
|
2022-05-04 10:24:52 +00:00
|
|
|
as_hex: String,
|
|
|
|
pruned_as_hex: String
|
2022-04-28 07:31:09 +00:00
|
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct TransactionsResponse {
|
|
|
|
txs: Vec<TransactionResponse>
|
|
|
|
}
|
|
|
|
|
|
|
|
let txs: TransactionsResponse = self.rpc_call("get_transactions", Some(json!({
|
|
|
|
"txs_hashes": hashes.iter().map(|hash| hash.encode_hex()).collect::<Vec<String>>()
|
|
|
|
}))).await?;
|
|
|
|
if txs.txs.len() != hashes.len() {
|
|
|
|
Err(RpcError::TransactionsNotFound(txs.txs.len(), hashes.len()))?;
|
|
|
|
}
|
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
let mut res: Vec<Transaction> = Vec::with_capacity(txs.txs.len());
|
2022-04-28 07:31:09 +00:00
|
|
|
for tx in txs.txs {
|
|
|
|
res.push(
|
|
|
|
deserialize(
|
2022-05-04 10:24:52 +00:00
|
|
|
&rpc_hex(if tx.as_hex.len() != 0 { &tx.as_hex } else { &tx.pruned_as_hex })?
|
|
|
|
).map_err(|_| RpcError::InvalidTransaction)?
|
2022-04-28 07:31:09 +00:00
|
|
|
);
|
2022-05-04 10:24:52 +00:00
|
|
|
|
|
|
|
if tx.as_hex.len() == 0 {
|
|
|
|
match res[res.len() - 1].prefix.inputs[0] {
|
|
|
|
TxIn::Gen { .. } => 0,
|
|
|
|
_ => Err(RpcError::TransactionsNotFound(hashes.len() - 1, hashes.len()))?
|
|
|
|
};
|
|
|
|
}
|
2022-04-28 07:31:09 +00:00
|
|
|
}
|
2022-05-04 10:24:52 +00:00
|
|
|
|
2022-04-28 07:31:09 +00:00
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
pub async fn get_block(&self, height: usize) -> Result<Block, RpcError> {
|
2022-04-28 07:31:09 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct BlockResponse {
|
|
|
|
blob: String
|
|
|
|
}
|
|
|
|
|
|
|
|
let block: JsonRpcResponse<BlockResponse> = self.rpc_call("json_rpc", Some(json!({
|
|
|
|
"method": "get_block",
|
|
|
|
"params": {
|
|
|
|
"height": height
|
|
|
|
}
|
|
|
|
}))).await?;
|
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
Ok(
|
|
|
|
deserialize(
|
|
|
|
&rpc_hex(&block.result.blob)?
|
|
|
|
).expect("Monero returned a block we couldn't deserialize")
|
|
|
|
)
|
|
|
|
}
|
2022-04-28 07:31:09 +00:00
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
pub async fn get_block_transactions(&self, height: usize) -> Result<Vec<Transaction>, RpcError> {
|
|
|
|
let block = self.get_block(height).await?;
|
2022-04-28 07:31:09 +00:00
|
|
|
let mut res = vec![block.miner_tx];
|
|
|
|
if block.tx_hashes.len() != 0 {
|
|
|
|
res.extend(self.get_transactions(block.tx_hashes).await?);
|
|
|
|
}
|
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_o_indexes(&self, hash: Hash) -> Result<Vec<u64>, RpcError> {
|
|
|
|
#[derive(Serialize, Debug)]
|
|
|
|
struct Request {
|
|
|
|
txid: [u8; 32]
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct OIndexes {
|
|
|
|
o_indexes: Vec<u64>,
|
|
|
|
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.0
|
|
|
|
}).expect("Couldn't serialize a request")
|
|
|
|
).await?;
|
|
|
|
|
|
|
|
Ok(indexes.o_indexes)
|
|
|
|
}
|
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
pub async fn get_outputs(
|
|
|
|
&self,
|
|
|
|
indexes: &[u64],
|
|
|
|
height: usize
|
|
|
|
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
|
2022-04-28 07:31:09 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
2022-05-04 10:24:52 +00:00
|
|
|
pub struct Out {
|
2022-04-28 07:31:09 +00:00
|
|
|
key: String,
|
2022-05-04 10:24:52 +00:00
|
|
|
mask: String,
|
|
|
|
txid: String
|
2022-04-28 07:31:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct Outs {
|
|
|
|
outs: Vec<Out>
|
|
|
|
}
|
|
|
|
|
|
|
|
let outs: Outs = self.rpc_call("get_outs", Some(json!({
|
2022-05-04 10:24:52 +00:00
|
|
|
"get_txid": true,
|
|
|
|
"outputs": indexes.iter().map(|o| json!({
|
2022-04-28 07:31:09 +00:00
|
|
|
"amount": 0,
|
2022-05-04 10:24:52 +00:00
|
|
|
"index": o
|
2022-04-28 07:31:09 +00:00
|
|
|
})).collect::<Vec<_>>()
|
|
|
|
}))).await?;
|
|
|
|
|
2022-05-04 10:24:52 +00:00
|
|
|
let txs = self.get_transactions(
|
|
|
|
outs.outs.iter().map(|out| Hash::from_str(&out.txid).expect("Monero returned an invalid 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.0 <= u64::try_from(height).unwrap() {
|
|
|
|
Some([rpc_point(&out.key)?, rpc_point(&out.mask)?])
|
|
|
|
} else { None }
|
|
|
|
)
|
|
|
|
).collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_high_output(&self, height: usize) -> Result<u64, RpcError> {
|
|
|
|
let block = self.get_block(height).await?;
|
|
|
|
Ok(
|
|
|
|
*self.get_o_indexes(
|
|
|
|
*block.tx_hashes.last().unwrap_or(&block.miner_tx.hash())
|
|
|
|
).await?.last().ok_or(RpcError::InvalidTransaction)?
|
|
|
|
)
|
2022-04-28 07:31:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 res: SendRawResponse = self.rpc_call("send_raw_transaction", Some(json!({
|
|
|
|
"tx_as_hex": hex::encode(&serialize(tx))
|
|
|
|
}))).await?;
|
|
|
|
|
|
|
|
if res.status != "OK" {
|
|
|
|
Err(RpcError::InvalidTransaction)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|