You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
xmr-btc-swap/monero-rpc/src/rpc/wallet.rs

570 lines
14 KiB
Rust

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