use anyhow::{Context, Result}; use serde::de::Error; use serde::{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) -> Result { Client::new( format!("http://127.0.0.1:{}/json_rpc", port) .parse() .context("url is well formed")?, ) } /// Constructs a monero-wallet-rpc client with `url` endpoint. pub fn new(url: reqwest::Url) -> Result { Ok(Self { inner: reqwest::ClientBuilder::new() .connection_verbose(true) .build()?, 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 { pub tx_hash_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(); } }