Use macro-based JSON-RPC client
parent
b46dbd738d
commit
6d06db3259
@ -0,0 +1,60 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[jsonrpc_client::api(version = "2.0")]
|
||||
pub trait MonerodRpc {
|
||||
async fn generateblocks(&self, amount_of_blocks: u32, wallet_address: String)
|
||||
-> GenerateBlocks;
|
||||
async fn get_block_header_by_height(&self, height: u32) -> BlockHeader;
|
||||
async fn get_block_count(&self) -> BlockCount;
|
||||
}
|
||||
|
||||
#[jsonrpc_client::implement(MonerodRpc)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
inner: reqwest::Client,
|
||||
base_url: reqwest::Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// New local host monerod RPC client.
|
||||
pub fn localhost(port: u16) -> Self {
|
||||
Self {
|
||||
inner: reqwest::Client::new(),
|
||||
base_url: format!("http://127.0.0.1:{}/json_rpc", port)
|
||||
.parse()
|
||||
.expect("url is well formed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GenerateBlocks {
|
||||
pub blocks: Vec<String>,
|
||||
pub height: u32,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct BlockCount {
|
||||
pub count: u32,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
// We should be able to use monero-rs for this but it does not include all
|
||||
// the fields.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct BlockHeader {
|
||||
pub block_size: u32,
|
||||
pub depth: u32,
|
||||
pub difficulty: u32,
|
||||
pub hash: String,
|
||||
pub height: u32,
|
||||
pub major_version: u32,
|
||||
pub minor_version: u32,
|
||||
pub nonce: u32,
|
||||
pub num_txes: u32,
|
||||
pub orphan_status: bool,
|
||||
pub prev_hash: String,
|
||||
pub reward: u64,
|
||||
pub timestamp: u32,
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
//! JSON RPC clients for `monerd` and `monero-wallet-rpc`.
|
||||
pub mod monerod;
|
||||
pub mod wallet;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Request<T> {
|
||||
/// JSON RPC version, we hard cod this to 2.0.
|
||||
jsonrpc: String,
|
||||
/// Client controlled identifier, we hard code this to 1.
|
||||
id: String,
|
||||
/// The method to call.
|
||||
method: String,
|
||||
/// The method parameters.
|
||||
params: T,
|
||||
}
|
||||
|
||||
/// JSON RPC request.
|
||||
impl<T> Request<T> {
|
||||
pub fn new(method: &str, params: T) -> Self {
|
||||
Self {
|
||||
jsonrpc: "2.0".to_owned(),
|
||||
id: "1".to_owned(),
|
||||
method: method.to_owned(),
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON RPC response.
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct Response<T> {
|
||||
pub id: String,
|
||||
pub jsonrpc: String,
|
||||
pub result: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct Params {
|
||||
val: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_serialize_request_with_params() {
|
||||
// Dummy method and parameters.
|
||||
let params = Params { val: 0 };
|
||||
let method = "get_block";
|
||||
|
||||
let r = Request::new(method, ¶ms);
|
||||
let got = serde_json::to_string(&r).expect("failed to serialize request");
|
||||
|
||||
let want =
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"get_block\",\"params\":{\"val\":0}}"
|
||||
.to_string();
|
||||
|
||||
assert_eq!(got, want);
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
use crate::rpc::{Request, Response};
|
||||
use anyhow::Result;
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
/// RPC client for monerod and monero-wallet-rpc.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
pub inner: reqwest::Client,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// New local host monerod RPC client.
|
||||
pub fn localhost(port: u16) -> Self {
|
||||
let url = format!("http://127.0.0.1:{}/json_rpc", port);
|
||||
let url = Url::parse(&url).expect("url is well formed");
|
||||
|
||||
Self {
|
||||
inner: reqwest::Client::new(),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_blocks(
|
||||
&self,
|
||||
amount_of_blocks: u32,
|
||||
wallet_address: &str,
|
||||
) -> Result<GenerateBlocks> {
|
||||
let params = GenerateBlocksParams {
|
||||
amount_of_blocks,
|
||||
wallet_address: wallet_address.to_owned(),
|
||||
};
|
||||
let url = self.url.clone();
|
||||
// // Step 1: Get the auth header
|
||||
// let res = self.inner.get(url.clone()).send().await?;
|
||||
// let headers = res.headers();
|
||||
// let wwwauth = headers["www-authenticate"].to_str()?;
|
||||
//
|
||||
// // Step 2: Given the auth header, sign the digest for the real req.
|
||||
// let tmp_url = url.clone();
|
||||
// let context = AuthContext::new("username", "password", tmp_url.path());
|
||||
// let mut prompt = digest_auth::parse(wwwauth)?;
|
||||
// let answer = prompt.respond(&context)?.to_header_string();
|
||||
|
||||
let request = Request::new("generateblocks", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("generate blocks response: {}", response);
|
||||
|
||||
let res: Response<GenerateBlocks> = serde_json::from_str(&response)?;
|
||||
|
||||
Ok(res.result)
|
||||
}
|
||||
|
||||
// $ curl http://127.0.0.1:18081/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"get_block_header_by_height","params":{"height":1}}' -H 'Content-Type: application/json'
|
||||
pub async fn get_block_header_by_height(&self, height: u32) -> Result<BlockHeader> {
|
||||
let params = GetBlockHeaderByHeightParams { height };
|
||||
let request = Request::new("get_block_header_by_height", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("get block header by height response: {}", response);
|
||||
|
||||
let res: Response<GetBlockHeaderByHeight> = serde_json::from_str(&response)?;
|
||||
|
||||
Ok(res.result.block_header)
|
||||
}
|
||||
|
||||
pub async fn get_block_count(&self) -> Result<u32> {
|
||||
let request = Request::new("get_block_count", "");
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("get block count response: {}", response);
|
||||
|
||||
let res: Response<BlockCount> = serde_json::from_str(&response)?;
|
||||
|
||||
Ok(res.result.count)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct GenerateBlocksParams {
|
||||
amount_of_blocks: u32,
|
||||
wallet_address: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GenerateBlocks {
|
||||
pub blocks: Vec<String>,
|
||||
pub height: u32,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct GetBlockHeaderByHeightParams {
|
||||
height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct GetBlockHeaderByHeight {
|
||||
block_header: BlockHeader,
|
||||
status: String,
|
||||
untrusted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct BlockCount {
|
||||
count: u32,
|
||||
status: String,
|
||||
}
|
||||
|
||||
// We should be able to use monero-rs for this but it does not include all
|
||||
// the fields.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct BlockHeader {
|
||||
pub block_size: u32,
|
||||
pub depth: u32,
|
||||
pub difficulty: u32,
|
||||
pub hash: String,
|
||||
pub height: u32,
|
||||
pub major_version: u32,
|
||||
pub minor_version: u32,
|
||||
pub nonce: u32,
|
||||
pub num_txes: u32,
|
||||
pub orphan_status: bool,
|
||||
pub prev_hash: String,
|
||||
pub reward: u64,
|
||||
pub timestamp: u32,
|
||||
}
|
@ -1,569 +0,0 @@
|
||||
use crate::rpc::{Request, Response};
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
/// JSON RPC client for monero-wallet-rpc.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
pub inner: reqwest::Client,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Constructs a monero-wallet-rpc client with localhost endpoint.
|
||||
pub fn localhost(port: u16) -> Self {
|
||||
let url = format!("http://127.0.0.1:{}/json_rpc", port);
|
||||
let url = Url::parse(&url).expect("url is well formed");
|
||||
|
||||
Client::new(url)
|
||||
}
|
||||
|
||||
/// Constructs a monero-wallet-rpc client with `url` endpoint.
|
||||
pub fn new(url: Url) -> Self {
|
||||
Self {
|
||||
inner: reqwest::Client::new(),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get addresses for account by index.
|
||||
pub async fn get_address(&self, account_index: u32) -> Result<GetAddress> {
|
||||
let params = GetAddressParams { account_index };
|
||||
let request = Request::new("get_address", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("get address RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<GetAddress>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
/// Gets the balance of account by index.
|
||||
pub async fn get_balance(&self, index: u32) -> Result<u64> {
|
||||
let params = GetBalanceParams {
|
||||
account_index: index,
|
||||
};
|
||||
let request = Request::new("get_balance", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!(
|
||||
"get balance of account index {} RPC response: {}",
|
||||
index, response
|
||||
);
|
||||
|
||||
let r = serde_json::from_str::<Response<GetBalance>>(&response)?;
|
||||
|
||||
let balance = r.result.balance;
|
||||
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
pub async fn create_account(&self, label: &str) -> Result<CreateAccount> {
|
||||
let params = LabelParams {
|
||||
label: label.to_owned(),
|
||||
};
|
||||
let request = Request::new("create_account", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("create account RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<CreateAccount>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
/// Get accounts, filtered by tag ("" for no filtering).
|
||||
pub async fn get_accounts(&self, tag: &str) -> Result<GetAccounts> {
|
||||
let params = TagParams {
|
||||
tag: tag.to_owned(),
|
||||
};
|
||||
let request = Request::new("get_accounts", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("get accounts RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<GetAccounts>>(&response)?;
|
||||
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
/// Opens a wallet using `filename`.
|
||||
pub async fn open_wallet(&self, filename: &str) -> Result<()> {
|
||||
let params = OpenWalletParams {
|
||||
filename: filename.to_owned(),
|
||||
};
|
||||
let request = Request::new("open_wallet", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("open wallet RPC response: {}", response);
|
||||
|
||||
// TODO: Proper error handling once switching to https://github.com/thomaseizinger/rust-jsonrpc-client/
|
||||
// Currently blocked by https://github.com/thomaseizinger/rust-jsonrpc-client/issues/20
|
||||
if response.contains("error") {
|
||||
bail!("Failed to open wallet")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close the currently opened wallet, after trying to save it.
|
||||
pub async fn close_wallet(&self) -> Result<()> {
|
||||
let request = Request::new("close_wallet", "");
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("close wallet RPC response: {}", response);
|
||||
|
||||
if response.contains("error") {
|
||||
bail!("Failed to close wallet")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a wallet using `filename`.
|
||||
pub async fn create_wallet(&self, filename: &str) -> Result<()> {
|
||||
let params = CreateWalletParams {
|
||||
filename: filename.to_owned(),
|
||||
language: "English".to_owned(),
|
||||
};
|
||||
let request = Request::new("create_wallet", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("create wallet RPC response: {}", response);
|
||||
|
||||
if response.contains("error") {
|
||||
bail!("Failed to create wallet")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfers `amount` moneroj from `account_index` to `address`.
|
||||
pub async fn transfer(
|
||||
&self,
|
||||
account_index: u32,
|
||||
amount: u64,
|
||||
address: &str,
|
||||
) -> Result<Transfer> {
|
||||
let dest = vec![Destination {
|
||||
amount,
|
||||
address: address.to_owned(),
|
||||
}];
|
||||
self.multi_transfer(account_index, dest).await
|
||||
}
|
||||
|
||||
/// Transfers moneroj from `account_index` to `destinations`.
|
||||
pub async fn multi_transfer(
|
||||
&self,
|
||||
account_index: u32,
|
||||
destinations: Vec<Destination>,
|
||||
) -> Result<Transfer> {
|
||||
let params = TransferParams {
|
||||
account_index,
|
||||
destinations,
|
||||
get_tx_key: true,
|
||||
};
|
||||
let request = Request::new("transfer", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("transfer RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<Transfer>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
/// Get wallet block height, this might be behind monerod height.
|
||||
pub async fn block_height(&self) -> Result<BlockHeight> {
|
||||
let request = Request::new("get_height", "");
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("wallet height RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<BlockHeight>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
/// Check a transaction in the blockchain with its secret key.
|
||||
pub async fn check_tx_key(
|
||||
&self,
|
||||
tx_id: &str,
|
||||
tx_key: &str,
|
||||
address: &str,
|
||||
) -> Result<CheckTxKey> {
|
||||
let params = CheckTxKeyParams {
|
||||
tx_id: tx_id.to_owned(),
|
||||
tx_key: tx_key.to_owned(),
|
||||
address: address.to_owned(),
|
||||
};
|
||||
let request = Request::new("check_tx_key", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("check_tx_key RPC response: {}", response);
|
||||
|
||||
let check_tx_key = serde_json::from_str::<Response<CheckTxKey>>(&response)?;
|
||||
let mut check_tx_key = check_tx_key.result;
|
||||
|
||||
// Due to a bug in monerod that causes check_tx_key confirmations
|
||||
// to overflow we safeguard the confirmations to avoid unwanted
|
||||
// side effects.
|
||||
if check_tx_key.confirmations > u64::MAX - 1000 {
|
||||
check_tx_key.confirmations = 0u64;
|
||||
}
|
||||
|
||||
Ok(check_tx_key)
|
||||
}
|
||||
|
||||
pub async fn generate_from_keys(
|
||||
&self,
|
||||
filename: &str,
|
||||
address: &str,
|
||||
spend_key: &str,
|
||||
view_key: &str,
|
||||
restore_height: u32,
|
||||
) -> Result<GenerateFromKeys> {
|
||||
let params = GenerateFromKeysParams {
|
||||
restore_height,
|
||||
filename: filename.into(),
|
||||
address: address.into(),
|
||||
spendkey: spend_key.into(),
|
||||
viewkey: view_key.into(),
|
||||
password: "".into(),
|
||||
autosave_current: true,
|
||||
};
|
||||
let request = Request::new("generate_from_keys", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("generate_from_keys RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<GenerateFromKeys>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
pub async fn refresh(&self) -> Result<Refreshed> {
|
||||
let request = Request::new("refresh", "");
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("refresh RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<Refreshed>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
/// Transfers the complete balance of the account to `address`.
|
||||
pub async fn sweep_all(&self, address: &str) -> Result<SweepAll> {
|
||||
let params = SweepAllParams {
|
||||
address: address.into(),
|
||||
};
|
||||
let request = Request::new("sweep_all", params);
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("sweep_all RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<SweepAll>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
|
||||
pub async fn get_version(&self) -> Result<Version> {
|
||||
let request = Request::new("get_version", "");
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
.post(self.url.clone())
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("get_version RPC response: {}", response);
|
||||
|
||||
let r = serde_json::from_str::<Response<Version>>(&response)?;
|
||||
Ok(r.result)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct GetAddressParams {
|
||||
account_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GetAddress {
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct GetBalanceParams {
|
||||
account_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
struct GetBalance {
|
||||
balance: u64,
|
||||
blocks_to_unlock: u32,
|
||||
multisig_import_needed: bool,
|
||||
time_to_unlock: u32,
|
||||
unlocked_balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct LabelParams {
|
||||
label: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct CreateAccount {
|
||||
pub account_index: u32,
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct TagParams {
|
||||
tag: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GetAccounts {
|
||||
pub subaddress_accounts: Vec<SubAddressAccount>,
|
||||
pub total_balance: u64,
|
||||
pub total_unlocked_balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct SubAddressAccount {
|
||||
pub account_index: u32,
|
||||
pub balance: u32,
|
||||
pub base_address: String,
|
||||
pub label: String,
|
||||
pub tag: String,
|
||||
pub unlocked_balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct OpenWalletParams {
|
||||
filename: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct CreateWalletParams {
|
||||
filename: String,
|
||||
language: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct TransferParams {
|
||||
// Transfer from this account.
|
||||
account_index: u32,
|
||||
// Destinations to receive XMR:
|
||||
destinations: Vec<Destination>,
|
||||
// Return the transaction key after sending.
|
||||
get_tx_key: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Destination {
|
||||
amount: u64,
|
||||
address: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Transfer {
|
||||
pub amount: u64,
|
||||
pub fee: u64,
|
||||
pub multisig_txset: String,
|
||||
pub tx_blob: String,
|
||||
pub tx_hash: String,
|
||||
pub tx_key: String,
|
||||
pub tx_metadata: String,
|
||||
pub unsigned_txset: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct BlockHeight {
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct CheckTxKeyParams {
|
||||
#[serde(rename = "txid")]
|
||||
tx_id: String,
|
||||
tx_key: String,
|
||||
address: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
pub struct CheckTxKey {
|
||||
pub confirmations: u64,
|
||||
pub received: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct GenerateFromKeysParams {
|
||||
pub restore_height: u32,
|
||||
pub filename: String,
|
||||
pub address: String,
|
||||
pub spendkey: String,
|
||||
pub viewkey: String,
|
||||
pub password: String,
|
||||
pub autosave_current: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GenerateFromKeys {
|
||||
pub address: String,
|
||||
pub info: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
pub struct Refreshed {
|
||||
pub blocks_fetched: u32,
|
||||
pub received_money: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SweepAllParams {
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SweepAll {
|
||||
amount_list: Vec<u64>,
|
||||
fee_list: Vec<u64>,
|
||||
multisig_txset: String,
|
||||
pub tx_hash_list: Vec<String>,
|
||||
unsigned_txset: String,
|
||||
weight_list: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||
pub struct Version {
|
||||
version: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_sweep_all_response() {
|
||||
let response = r#"{
|
||||
"id": "0",
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"amount_list": [29921410000],
|
||||
"fee_list": [78590000],
|
||||
"multisig_txset": "",
|
||||
"tx_hash_list": ["c1d8cfa87d445c1915a59d67be3e93ba8a29018640cf69b465f07b1840a8f8c8"],
|
||||
"unsigned_txset": "",
|
||||
"weight_list": [1448]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let _: Response<SweepAll> = serde_json::from_str(&response).unwrap();
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
use anyhow::Result;
|
||||
use serde::{de::Error, Deserialize, Deserializer, Serialize};
|
||||
|
||||
#[jsonrpc_client::api(version = "2.0")]
|
||||
pub trait MoneroWalletRpc {
|
||||
async fn get_address(&self, account_index: u32) -> GetAddress;
|
||||
async fn get_balance(&self, account_index: u32) -> GetBalance;
|
||||
async fn create_account(&self, label: String) -> CreateAccount;
|
||||
async fn get_accounts(&self, tag: String) -> GetAccounts;
|
||||
async fn open_wallet(&self, filename: String) -> WalletOpened;
|
||||
async fn close_wallet(&self) -> WalletClosed;
|
||||
async fn create_wallet(&self, filename: String, language: String) -> WalletCreated;
|
||||
async fn transfer(
|
||||
&self,
|
||||
account_index: u32,
|
||||
destinations: Vec<Destination>,
|
||||
get_tx_key: bool,
|
||||
) -> Transfer;
|
||||
async fn get_height(&self) -> BlockHeight;
|
||||
async fn check_tx_key(&self, txid: String, tx_key: String, address: String) -> CheckTxKey;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn generate_from_keys(
|
||||
&self,
|
||||
filename: String,
|
||||
address: String,
|
||||
spendkey: String,
|
||||
viewkey: String,
|
||||
restore_height: u32,
|
||||
password: String,
|
||||
autosave_current: bool,
|
||||
) -> GenerateFromKeys;
|
||||
async fn refresh(&self) -> Refreshed;
|
||||
async fn sweep_all(&self, address: String) -> SweepAll;
|
||||
async fn get_version(&self) -> Version;
|
||||
}
|
||||
|
||||
#[jsonrpc_client::implement(MoneroWalletRpc)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
inner: reqwest::Client,
|
||||
base_url: reqwest::Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Constructs a monero-wallet-rpc client with localhost endpoint.
|
||||
pub fn localhost(port: u16) -> Self {
|
||||
Client::new(
|
||||
format!("http://127.0.0.1:{}/json_rpc", port)
|
||||
.parse()
|
||||
.expect("url is well formed"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Constructs a monero-wallet-rpc client with `url` endpoint.
|
||||
pub fn new(url: reqwest::Url) -> Self {
|
||||
Self {
|
||||
inner: reqwest::Client::new(),
|
||||
base_url: url,
|
||||
}
|
||||
}
|
||||
|
||||
/// Transfers `amount` monero from `account_index` to `address`.
|
||||
pub async fn transfer_single(
|
||||
&self,
|
||||
account_index: u32,
|
||||
amount: u64,
|
||||
address: &str,
|
||||
) -> Result<Transfer> {
|
||||
let dest = vec![Destination {
|
||||
amount,
|
||||
address: address.to_owned(),
|
||||
}];
|
||||
|
||||
Ok(self.transfer(account_index, dest, true).await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GetAddress {
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy)]
|
||||
pub struct GetBalance {
|
||||
pub balance: u64,
|
||||
pub blocks_to_unlock: u32,
|
||||
pub multisig_import_needed: bool,
|
||||
pub time_to_unlock: u32,
|
||||
pub unlocked_balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct CreateAccount {
|
||||
pub account_index: u32,
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GetAccounts {
|
||||
pub subaddress_accounts: Vec<SubAddressAccount>,
|
||||
pub total_balance: u64,
|
||||
pub total_unlocked_balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct SubAddressAccount {
|
||||
pub account_index: u32,
|
||||
pub balance: u32,
|
||||
pub base_address: String,
|
||||
pub label: String,
|
||||
pub tag: String,
|
||||
pub unlocked_balance: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Destination {
|
||||
pub amount: u64,
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Transfer {
|
||||
pub amount: u64,
|
||||
pub fee: u64,
|
||||
pub multisig_txset: String,
|
||||
pub tx_blob: String,
|
||||
pub tx_hash: String,
|
||||
#[serde(deserialize_with = "opt_key_from_blank")]
|
||||
pub tx_key: Option<monero::PrivateKey>,
|
||||
pub tx_metadata: String,
|
||||
pub unsigned_txset: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct BlockHeight {
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
#[serde(from = "CheckTxKeyResponse")]
|
||||
pub struct CheckTxKey {
|
||||
pub confirmations: u64,
|
||||
pub received: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
struct CheckTxKeyResponse {
|
||||
pub confirmations: u64,
|
||||
pub received: u64,
|
||||
}
|
||||
|
||||
impl From<CheckTxKeyResponse> for CheckTxKey {
|
||||
fn from(response: CheckTxKeyResponse) -> Self {
|
||||
// Due to a bug in monerod that causes check_tx_key confirmations
|
||||
// to overflow we safeguard the confirmations to avoid unwanted
|
||||
// side effects.
|
||||
let confirmations = if response.confirmations > u64::MAX - 1000 {
|
||||
0
|
||||
} else {
|
||||
response.confirmations
|
||||
};
|
||||
|
||||
CheckTxKey {
|
||||
confirmations,
|
||||
received: response.received,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GenerateFromKeys {
|
||||
pub address: String,
|
||||
pub info: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
pub struct Refreshed {
|
||||
pub blocks_fetched: u32,
|
||||
pub received_money: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct SweepAll {
|
||||
amount_list: Vec<u64>,
|
||||
fee_list: Vec<u64>,
|
||||
multisig_txset: String,
|
||||
pub tx_hash_list: Vec<String>,
|
||||
unsigned_txset: String,
|
||||
weight_list: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||
pub struct Version {
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
pub type WalletCreated = Empty;
|
||||
pub type WalletClosed = Empty;
|
||||
pub type WalletOpened = Empty;
|
||||
|
||||
/// Zero-sized struct to allow serde to deserialize an empty JSON object.
|
||||
///
|
||||
/// With `serde`, an empty JSON object (`{ }`) does not deserialize into Rust's
|
||||
/// `()`. With the adoption of `jsonrpc_client`, we need to be explicit about
|
||||
/// what the response of every RPC call is. Unfortunately, monerod likes to
|
||||
/// return empty objects instead of `null`s in certain cases. We use this struct
|
||||
/// to all the "deserialization" to happily continue.
|
||||
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||
pub struct Empty {}
|
||||
|
||||
fn opt_key_from_blank<'de, D>(deserializer: D) -> Result<Option<monero::PrivateKey>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let string = String::deserialize(deserializer)?;
|
||||
|
||||
if string.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(string.parse().map_err(D::Error::custom)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jsonrpc_client::Response;
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_sweep_all_response() {
|
||||
let response = r#"{
|
||||
"id": "0",
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"amount_list": [29921410000],
|
||||
"fee_list": [78590000],
|
||||
"multisig_txset": "",
|
||||
"tx_hash_list": ["c1d8cfa87d445c1915a59d67be3e93ba8a29018640cf69b465f07b1840a8f8c8"],
|
||||
"unsigned_txset": "",
|
||||
"weight_list": [1448]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let _: Response<SweepAll> = serde_json::from_str(&response).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_create_wallet() {
|
||||
let response = r#"{
|
||||
"id": 0,
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
}
|
||||
}"#;
|
||||
|
||||
let _: Response<WalletCreated> = serde_json::from_str(&response).unwrap();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue