Use macro-based JSON-RPC client

This commit is contained in:
Thomas Eizinger 2021-04-15 18:39:59 +10:00
parent b46dbd738d
commit 6d06db3259
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96
15 changed files with 435 additions and 846 deletions

30
Cargo.lock generated
View File

@ -356,7 +356,7 @@ dependencies = [
"bitcoincore-rpc-json",
"futures",
"hex 0.4.3",
"jsonrpc_client",
"jsonrpc_client 0.5.1",
"reqwest",
"serde",
"serde_json",
@ -1629,7 +1629,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc8515639023bf4260cf89475355fa77301685418f655c680528c380759e7782"
dependencies = [
"async-trait",
"jsonrpc_client_macro",
"jsonrpc_client_macro 0.2.0",
"reqwest",
"serde",
"serde_json",
"url 2.2.1",
]
[[package]]
name = "jsonrpc_client"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a85cf2c5ce158eabf30b2ac4f535463d7b09ce7905502e11238b7d6048ef7d02"
dependencies = [
"async-trait",
"jsonrpc_client_macro 0.3.0",
"reqwest",
"serde",
"serde_json",
@ -1646,6 +1660,16 @@ dependencies = [
"syn",
]
[[package]]
name = "jsonrpc_client_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97c11e429f0eaa41fe659013680b459d2368d8f0a3e69dccfb7a35800b0dc27b"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "keccak-hash"
version = "0.7.0"
@ -2157,6 +2181,8 @@ name = "monero-rpc"
version = "0.1.0"
dependencies = [
"anyhow",
"jsonrpc_client 0.6.0",
"monero",
"reqwest",
"serde",
"serde_json",

View File

@ -28,7 +28,8 @@ use crate::image::{
use anyhow::{anyhow, bail, Result};
use monero_rpc::{
monerod,
wallet::{self, GetAddress, Refreshed, Transfer},
monerod::MonerodRpc as _,
wallet::{self, GetAddress, MoneroWalletRpc as _, Refreshed, Transfer},
};
use std::time::Duration;
use testcontainers::{clients::Cli, core::Port, Container, Docker, RunArgs};
@ -104,7 +105,10 @@ impl<'c> Monero {
// generate the first 70 as bulk
let monerod = &self.monerod;
let res = monerod.client().generate_blocks(70, &miner_address).await?;
let res = monerod
.client()
.generateblocks(70, miner_address.clone())
.await?;
tracing::info!("Generated {:?} blocks", res.blocks.len());
miner_wallet.refresh().await?;
@ -123,7 +127,10 @@ impl<'c> Monero {
if amount > 0 {
miner_wallet.transfer(&address, amount).await?;
tracing::info!("Funded {} wallet with {}", wallet.name, amount);
monerod.client().generate_blocks(10, &miner_address).await?;
monerod
.client()
.generateblocks(10, miner_address.clone())
.await?;
wallet.refresh().await?;
}
}
@ -139,7 +146,7 @@ impl<'c> Monero {
monerod.start_miner(&miner_address).await?;
tracing::info!("Waiting for miner wallet to catch up...");
let block_height = monerod.client().get_block_count().await?;
let block_height = monerod.client().get_block_count().await?.count;
miner_wallet
.wait_for_wallet_height(block_height)
.await
@ -256,7 +263,7 @@ impl<'c> MoneroWalletRpc {
// create new wallet
wallet::Client::localhost(wallet_rpc_port)
.create_wallet(name)
.create_wallet(name.to_owned(), "English".to_owned())
.await
.unwrap();
@ -277,7 +284,7 @@ impl<'c> MoneroWalletRpc {
// It takes a little while for the wallet to sync with monerod.
pub async fn wait_for_wallet_height(&self, height: u32) -> Result<()> {
let mut retry: u8 = 0;
while self.client().block_height().await?.height < height {
while self.client().get_height().await?.height < height {
if retry >= 30 {
// ~30 seconds
bail!("Wallet could not catch up with monerod after 30 retries.")
@ -290,26 +297,28 @@ impl<'c> MoneroWalletRpc {
/// Sends amount to address
pub async fn transfer(&self, address: &str, amount: u64) -> Result<Transfer> {
self.client().transfer(0, amount, address).await
Ok(self.client().transfer_single(0, amount, address).await?)
}
pub async fn address(&self) -> Result<GetAddress> {
self.client().get_address(0).await
Ok(self.client().get_address(0).await?)
}
pub async fn balance(&self) -> Result<u64> {
self.client().refresh().await?;
self.client().get_balance(0).await
let balance = self.client().get_balance(0).await?.balance;
Ok(balance)
}
pub async fn refresh(&self) -> Result<Refreshed> {
self.client().refresh().await
Ok(self.client().refresh().await?)
}
}
/// Mine a block ever BLOCK_TIME_SECS seconds.
async fn mine(monerod: monerod::Client, reward_address: String) -> Result<()> {
loop {
time::sleep(Duration::from_secs(BLOCK_TIME_SECS)).await;
monerod.generate_blocks(1, &reward_address).await?;
monerod.generateblocks(1, reward_address.clone()).await?;
}
}

View File

@ -1,4 +1,5 @@
use monero_harness::Monero;
use monero_rpc::monerod::MonerodRpc as _;
use spectral::prelude::*;
use std::time::Duration;
use testcontainers::clients::Cli;
@ -25,7 +26,7 @@ async fn init_miner_and_mine_to_miner_address() {
time::sleep(Duration::from_millis(1010)).await;
// after a bit more than 1 sec another block should have been mined
let block_height = monerod.client().get_block_count().await.unwrap();
let block_height = monerod.client().get_block_count().await.unwrap().count;
assert_that(&block_height).is_greater_than(70);
}

View File

@ -1,4 +1,5 @@
use monero_harness::{Monero, MoneroWalletRpc};
use monero_rpc::wallet::MoneroWalletRpc as _;
use spectral::prelude::*;
use std::time::Duration;
use testcontainers::clients::Cli;
@ -45,10 +46,10 @@ async fn fund_transfer_and_check_tx_key() {
// check if tx was actually seen
let tx_id = transfer.tx_hash;
let tx_key = transfer.tx_key;
let tx_key = transfer.tx_key.unwrap().to_string();
let res = bob_wallet
.client()
.check_tx_key(&tx_id, &tx_key, &bob_address)
.check_tx_key(tx_id, tx_key, bob_address)
.await
.expect("failed to check tx by key");

View File

@ -10,3 +10,5 @@ reqwest = { version = "0.11", default-features = false, features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
jsonrpc_client = { version = "0.6", features = ["reqwest"] }
monero = "0.11"

View File

@ -12,6 +12,5 @@
)]
#![forbid(unsafe_code)]
mod rpc;
pub use self::rpc::*;
pub mod monerod;
pub mod wallet;

60
monero-rpc/src/monerod.rs Normal file
View File

@ -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,
}

View File

@ -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, &params);
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);
}
}

View File

@ -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,
}

View File

@ -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();
}
}

258
monero-rpc/src/wallet.rs Normal file
View File

@ -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();
}
}

View File

@ -5,7 +5,7 @@ use crate::monero::{
use ::monero::{Address, Network, PrivateKey, PublicKey};
use anyhow::{Context, Result};
use monero_rpc::wallet;
use monero_rpc::wallet::{BlockHeight, CheckTxKey, Refreshed};
use monero_rpc::wallet::{BlockHeight, CheckTxKey, MoneroWalletRpc as _, Refreshed};
use std::future::Future;
use std::str::FromStr;
use std::time::Duration;
@ -27,9 +27,9 @@ impl Wallet {
pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
let client = wallet::Client::new(url);
let open_wallet_response = client.open_wallet(name.as_str()).await;
let open_wallet_response = client.open_wallet(name.clone()).await;
if open_wallet_response.is_err() {
client.create_wallet(name.as_str()).await.context(
client.create_wallet(name.clone(), "English".to_owned()).await.context(
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
)?;
@ -59,12 +59,12 @@ impl Wallet {
self.inner
.lock()
.await
.open_wallet(self.name.as_str())
.open_wallet(self.name.clone())
.await?;
Ok(())
}
pub async fn open(&self, filename: &str) -> Result<()> {
pub async fn open(&self, filename: String) -> Result<()> {
self.inner.lock().await.open_wallet(filename).await?;
Ok(())
}
@ -73,7 +73,7 @@ impl Wallet {
/// keys. The generated wallet will remain loaded.
pub async fn create_from_and_load(
&self,
file_name: &str,
file_name: String,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
restore_height: BlockHeight,
@ -87,17 +87,23 @@ impl Wallet {
// Properly close the wallet before generating the other wallet to ensure that
// it saves its state correctly
let _ = wallet.close_wallet().await?;
let _ = wallet
.close_wallet()
.await
.context("Failed to close wallet")?;
let _ = wallet
.generate_from_keys(
file_name,
&address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
address.to_string(),
private_spend_key.to_string(),
PrivateKey::from(private_view_key).to_string(),
restore_height.height,
String::from(""),
true,
)
.await?;
.await
.context("Failed to generate new wallet from keys")?;
Ok(())
}
@ -108,7 +114,7 @@ impl Wallet {
/// stored name.
pub async fn create_from(
&self,
file_name: &str,
file_name: String,
private_spend_key: PrivateKey,
private_view_key: PrivateViewKey,
restore_height: BlockHeight,
@ -128,19 +134,18 @@ impl Wallet {
let _ = wallet
.generate_from_keys(
file_name,
&temp_wallet_address.to_string(),
&private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(),
temp_wallet_address.to_string(),
private_spend_key.to_string(),
PrivateKey::from(private_view_key).to_string(),
restore_height.height,
String::from(""),
true,
)
.await?;
// Try to send all the funds from the generated wallet to the default wallet
match wallet.refresh().await {
Ok(_) => match wallet
.sweep_all(self.main_address.to_string().as_str())
.await
{
Ok(_) => match wallet.sweep_all(self.main_address.to_string()).await {
Ok(sweep_all) => {
for tx in sweep_all.tx_hash_list {
tracing::info!(%tx, "Monero transferred back to default wallet {}", self.main_address);
@ -159,7 +164,7 @@ impl Wallet {
}
}
let _ = wallet.open_wallet(self.name.as_str()).await?;
let _ = wallet.open_wallet(self.name.clone()).await?;
Ok(())
}
@ -178,7 +183,7 @@ impl Wallet {
.inner
.lock()
.await
.transfer(0, amount.as_piconero(), &destination_address.to_string())
.transfer_single(0, amount.as_piconero(), &destination_address.to_string())
.await?;
tracing::debug!(
@ -190,7 +195,8 @@ impl Wallet {
Ok(TransferProof::new(
TxHash(res.tx_hash),
PrivateKey::from_str(&res.tx_key)?,
res.tx_key
.context("Missing tx_key in `transfer` response")?,
))
}
@ -210,16 +216,20 @@ impl Wallet {
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
let check_interval = tokio::time::interval(self.sync_interval);
let key = &transfer_proof.tx_key().to_string();
let key = transfer_proof.tx_key().to_string();
wait_for_confirmations(
txid.0,
|txid| async move {
self.inner
.lock()
.await
.check_tx_key(&txid, &key, &address.to_string())
.await
move |txid| {
let key = key.clone();
async move {
Ok(self
.inner
.lock()
.await
.check_tx_key(txid, key, address.to_string())
.await?)
}
},
check_interval,
expected,
@ -235,7 +245,7 @@ impl Wallet {
.inner
.lock()
.await
.sweep_all(address.to_string().as_str())
.sweep_all(address.to_string())
.await?;
let tx_hashes = sweep_all.tx_hash_list.into_iter().map(TxHash).collect();
@ -244,13 +254,13 @@ impl Wallet {
/// Get the balance of the primary account.
pub async fn get_balance(&self) -> Result<Amount> {
let amount = self.inner.lock().await.get_balance(0).await?;
let amount = self.inner.lock().await.get_balance(0).await?.balance;
Ok(Amount::from_piconero(amount))
}
pub async fn block_height(&self) -> Result<BlockHeight> {
self.inner.lock().await.block_height().await
Ok(self.inner.lock().await.get_height().await?)
}
pub fn get_main_address(&self) -> Address {
@ -258,7 +268,7 @@ impl Wallet {
}
pub async fn refresh(&self) -> Result<Refreshed> {
self.inner.lock().await.refresh().await
Ok(self.inner.lock().await.refresh().await?)
}
pub fn static_tx_fee_estimate(&self) -> Amount {

View File

@ -2,7 +2,7 @@ use ::monero::Network;
use anyhow::{Context, Result};
use big_bytes::BigByte;
use futures::{StreamExt, TryStreamExt};
use monero_rpc::wallet::Client;
use monero_rpc::wallet::{Client, MoneroWalletRpc as _};
use reqwest::header::CONTENT_LENGTH;
use reqwest::Url;
use std::io::ErrorKind;

View File

@ -104,7 +104,13 @@ async fn next_state(
ExpiredTimelocks::None => {
monero_wallet
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
.await?;
.await
.with_context(|| {
format!(
"Failed to watch for transfer of XMR in transaction {}",
transfer_proof.tx_hash()
)
})?;
AliceState::XmrLocked {
monero_wallet_restore_blockheight,
@ -299,7 +305,7 @@ async fn next_state(
monero_wallet
.create_from(
&swap_id.to_string(),
swap_id.to_string(),
spend_key,
view_key,
monero_wallet_restore_blockheight,

View File

@ -197,21 +197,24 @@ async fn next_state(
BobState::BtcRedeemed(state) => {
let (spend_key, view_key) = state.xmr_keys();
let generated_wallet_file_name = &swap_id.to_string();
if monero_wallet
let generated_wallet_file_name = swap_id.to_string();
if let Err(e) = monero_wallet
.create_from_and_load(
generated_wallet_file_name,
generated_wallet_file_name.clone(),
spend_key,
view_key,
state.monero_wallet_restore_blockheight,
)
.await
.is_err()
{
// In case we failed to refresh/sweep, when resuming the wallet might already
// exist! This is a very unlikely scenario, but if we don't take care of it we
// might not be able to ever transfer the Monero.
tracing::warn!("Failed to generate monero wallet from keys, falling back to trying to open the the wallet if it already exists: {}", swap_id);
tracing::warn!("Failed to generate monero wallet from keys: {:#}", e);
tracing::info!(
"Falling back to trying to open the the wallet if it already exists: {}",
swap_id
);
monero_wallet.open(generated_wallet_file_name).await?;
}