From 6d06db3259c0447bf8797864ff5182d4968bd306 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 15 Apr 2021 18:39:59 +1000 Subject: [PATCH] Use macro-based JSON-RPC client --- Cargo.lock | 30 +- monero-harness/src/lib.rs | 31 +- monero-harness/tests/monerod.rs | 3 +- monero-harness/tests/wallet.rs | 5 +- monero-rpc/Cargo.toml | 2 + monero-rpc/src/lib.rs | 5 +- monero-rpc/src/monerod.rs | 60 ++++ monero-rpc/src/rpc.rs | 63 ---- monero-rpc/src/rpc/monerod.rs | 154 --------- monero-rpc/src/rpc/wallet.rs | 569 -------------------------------- monero-rpc/src/wallet.rs | 258 +++++++++++++++ swap/src/monero/wallet.rs | 76 +++-- swap/src/monero/wallet_rpc.rs | 2 +- swap/src/protocol/alice/swap.rs | 10 +- swap/src/protocol/bob/swap.rs | 13 +- 15 files changed, 435 insertions(+), 846 deletions(-) create mode 100644 monero-rpc/src/monerod.rs delete mode 100644 monero-rpc/src/rpc.rs delete mode 100644 monero-rpc/src/rpc/monerod.rs delete mode 100644 monero-rpc/src/rpc/wallet.rs create mode 100644 monero-rpc/src/wallet.rs diff --git a/Cargo.lock b/Cargo.lock index 04d61a90..261d3600 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 48615b92..0824a44d 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -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 { - self.client().transfer(0, amount, address).await + Ok(self.client().transfer_single(0, amount, address).await?) } pub async fn address(&self) -> Result { - self.client().get_address(0).await + Ok(self.client().get_address(0).await?) } pub async fn balance(&self) -> Result { 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 { - 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?; } } diff --git a/monero-harness/tests/monerod.rs b/monero-harness/tests/monerod.rs index 257e888d..03311072 100644 --- a/monero-harness/tests/monerod.rs +++ b/monero-harness/tests/monerod.rs @@ -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); } diff --git a/monero-harness/tests/wallet.rs b/monero-harness/tests/wallet.rs index a3f4e52e..d2e97cb4 100644 --- a/monero-harness/tests/wallet.rs +++ b/monero-harness/tests/wallet.rs @@ -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"); diff --git a/monero-rpc/Cargo.toml b/monero-rpc/Cargo.toml index c70f2797..8f1b68ed 100644 --- a/monero-rpc/Cargo.toml +++ b/monero-rpc/Cargo.toml @@ -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" diff --git a/monero-rpc/src/lib.rs b/monero-rpc/src/lib.rs index 1a83b3e1..a1b2eaaa 100644 --- a/monero-rpc/src/lib.rs +++ b/monero-rpc/src/lib.rs @@ -12,6 +12,5 @@ )] #![forbid(unsafe_code)] -mod rpc; - -pub use self::rpc::*; +pub mod monerod; +pub mod wallet; diff --git a/monero-rpc/src/monerod.rs b/monero-rpc/src/monerod.rs new file mode 100644 index 00000000..9926d7ac --- /dev/null +++ b/monero-rpc/src/monerod.rs @@ -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, + 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, +} diff --git a/monero-rpc/src/rpc.rs b/monero-rpc/src/rpc.rs deleted file mode 100644 index e0e58ed7..00000000 --- a/monero-rpc/src/rpc.rs +++ /dev/null @@ -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 { - /// 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 Request { - 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 { - 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); - } -} diff --git a/monero-rpc/src/rpc/monerod.rs b/monero-rpc/src/rpc/monerod.rs deleted file mode 100644 index a98300a7..00000000 --- a/monero-rpc/src/rpc/monerod.rs +++ /dev/null @@ -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 { - 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 = 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 { - 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 = serde_json::from_str(&response)?; - - Ok(res.result.block_header) - } - - pub async fn get_block_count(&self) -> Result { - 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 = 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, - 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, -} diff --git a/monero-rpc/src/rpc/wallet.rs b/monero-rpc/src/rpc/wallet.rs deleted file mode 100644 index 675be036..00000000 --- a/monero-rpc/src/rpc/wallet.rs +++ /dev/null @@ -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 { - 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)?; - Ok(r.result) - } - - /// Gets the balance of account by index. - pub async fn get_balance(&self, index: u32) -> Result { - 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)?; - - let balance = r.result.balance; - - Ok(balance) - } - - pub async fn create_account(&self, label: &str) -> Result { - 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)?; - Ok(r.result) - } - - /// Get accounts, filtered by tag ("" for no filtering). - pub async fn get_accounts(&self, tag: &str) -> Result { - 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)?; - - 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 { - 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, - ) -> Result { - 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)?; - Ok(r.result) - } - - /// Get wallet block height, this might be behind monerod height. - pub async fn block_height(&self) -> Result { - 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)?; - 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 { - 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)?; - 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 { - 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)?; - Ok(r.result) - } - - pub async fn refresh(&self) -> Result { - 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)?; - Ok(r.result) - } - - /// Transfers the complete balance of the account to `address`. - pub async fn sweep_all(&self, address: &str) -> Result { - 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)?; - Ok(r.result) - } - - pub async fn get_version(&self) -> Result { - 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)?; - 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, - 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, - // 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, - fee_list: Vec, - multisig_txset: String, - pub tx_hash_list: Vec, - unsigned_txset: String, - weight_list: Vec, -} - -#[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 = serde_json::from_str(&response).unwrap(); - } -} diff --git a/monero-rpc/src/wallet.rs b/monero-rpc/src/wallet.rs new file mode 100644 index 00000000..67d7ce21 --- /dev/null +++ b/monero-rpc/src/wallet.rs @@ -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, + 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 { + 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, + 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, + 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 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, + fee_list: Vec, + multisig_txset: String, + pub tx_hash_list: Vec, + unsigned_txset: String, + weight_list: Vec, +} + +#[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, 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 = serde_json::from_str(&response).unwrap(); + } + + #[test] + fn can_deserialize_create_wallet() { + let response = r#"{ + "id": 0, + "jsonrpc": "2.0", + "result": { + } + }"#; + + let _: Response = serde_json::from_str(&response).unwrap(); + } +} diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index b3561281..3f9cf9d4 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -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 { 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 { - 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 { - 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 { - self.inner.lock().await.refresh().await + Ok(self.inner.lock().await.refresh().await?) } pub fn static_tx_fee_estimate(&self) -> Amount { diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index 96138098..a2aa9582 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -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; diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 57e66a9f..7e1b8e20 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -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, diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 84b8257f..0702f76e 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -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?; }