pub mod btc;
pub mod error;
pub mod ext;
use base64;
use btc::BtcAddress;
use error::Result;
use hyper::client::HttpConnector;
use hyper::header::AUTHORIZATION;
use hyper::{Body, Client, Method, Request, Response, Uri};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::path::PathBuf;
use std::str;
use log::info;
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
enum ElectrumMethod {
Broadcast,
PayToMany,
#[serde(rename = "getinfo")]
GetInfo,
GetBalance,
GetAddressHistory,
GetAddressBalance,
#[serde(rename = "list_wallets")]
ListWallets,
#[serde(rename = "close_wallet")]
CloseWallet,
#[serde(rename = "load_wallet")]
LoadWallet,
#[serde(rename = "create")]
CreateWallet,
#[serde(rename = "restore")]
RestoreWallet,
#[serde(rename = "listaddresses")]
ListAddresses,
Notify,
Help,
Empty,
SignTransaction,
}
#[derive(Hash, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
enum Param {
Text,
#[serde(rename = "tx")]
Transaction,
#[serde(rename = "address")]
BtcAddress,
#[serde(rename = "wallet_path")]
WalletPath,
#[serde(rename = "URL")]
Url,
Password,
Fee,
Outputs,
}
struct JsonRpcBodyBuilder {
json_rpc: f32,
id: u64,
method: ElectrumMethod,
params: HashMap<Param, Value>,
}
impl JsonRpcBodyBuilder {
pub fn new() -> Self {
Self {
json_rpc: 2.0,
id: 0,
method: ElectrumMethod::Empty,
params: HashMap::new(),
}
}
pub fn id(mut self, id: u64) -> Self {
self.id = id;
self
}
pub fn method(mut self, method: ElectrumMethod) -> Self {
self.method = method;
self
}
pub fn add_param(mut self, param: Param, value: Value) -> Self {
self.params.insert(param, value);
self
}
pub fn build(self) -> JsonRpcBody {
JsonRpcBody {
json_rpc: self.json_rpc,
id: self.id,
method: self.method,
params: self.params,
}
}
}
#[derive(Serialize)]
struct JsonRpcBody {
json_rpc: f32,
id: u64,
method: ElectrumMethod,
params: HashMap<Param, Value>,
}
impl JsonRpcBody {
pub fn new() -> JsonRpcBodyBuilder {
JsonRpcBodyBuilder::new()
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Invoice<'a> {
amount: Decimal,
#[serde(flatten, borrow)]
address: BtcAddress<'a>,
}
impl<'a> Invoice<'a> {
pub fn get_amount(&self) -> Decimal {
self.amount
}
pub fn get_address(&self) -> &BtcAddress<'a> {
&self.address
}
}
pub struct Electrum {
auth: String,
address: Uri,
client: Client<HttpConnector>,
}
impl Electrum {
pub fn new(login: String, password: String, address: String) -> Result<Self> {
let client = Client::new();
let address = address.parse::<Uri>()?;
let credentials = base64::encode(format!("{}:{}", login, password));
let auth = format!("Basic {}", credentials);
Ok(Self {
auth,
address,
client,
})
}
async fn call_method(&self, body: &JsonRpcBody) -> Result<Response<Body>> {
let payload = serde_json::to_string(body)?;
info!("Payload is: {}", payload);
let req = Request::builder()
.method(Method::POST)
.header("accept", "application/json")
.header(AUTHORIZATION, &self.auth)
.uri(&self.address)
.body(Body::from(payload))?;
let resp = self.client.request(req).await?;
Ok(resp)
}
pub async fn get_help(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.id(0)
.method(ElectrumMethod::Help)
.build()
.borrow(),
)
.await
}
pub async fn get_info(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::GetInfo)
.build()
.borrow(),
)
.await
}
pub async fn get_balance(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::GetBalance)
.build()
.borrow(),
)
.await
}
pub async fn get_address_history<'a>(
&self,
address: &BtcAddress<'a>,
) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::GetAddressHistory)
.add_param(Param::BtcAddress, Value::from(String::from(address)))
.build()
.borrow(),
)
.await
}
pub async fn get_address_balance<'a>(
&self,
address: &BtcAddress<'a>,
) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::GetAddressBalance)
.add_param(Param::BtcAddress, Value::from(String::from(address)))
.build()
.borrow(),
)
.await
}
pub async fn list_wallets(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::ListWallets)
.build()
.borrow(),
)
.await
}
pub async fn load_wallet(
&self,
wallet_path: Option<PathBuf>,
password: Option<&str>,
) -> Result<Response<Body>> {
let mut builder = JsonRpcBody::new().method(ElectrumMethod::LoadWallet);
if let Some(path) = &wallet_path {
let path = path.to_str().unwrap();
builder = builder.add_param(Param::WalletPath, Value::from(path))
};
if let Some(password) = password {
builder = builder.add_param(Param::Password, Value::from(password))
};
self.call_method(&builder.build()).await
}
pub async fn create_wallet(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::CreateWallet)
.build()
.borrow(),
)
.await
}
pub async fn list_addresses(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::ListAddresses)
.build()
.borrow(),
)
.await
}
pub async fn notify<'a>(
&self,
address: &BtcAddress<'a>,
url: Option<Uri>,
) -> Result<Response<Body>> {
let url = url.unwrap_or(Uri::from_static("")).to_string();
let builder = JsonRpcBody::new()
.method(ElectrumMethod::Notify)
.add_param(Param::BtcAddress, Value::from(String::from(address)))
.add_param(Param::Url, Value::from(url));
self.call_method(&builder.build()).await
}
pub async fn restore_wallet(&self, text: &str) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::RestoreWallet)
.add_param(Param::Text, Value::from(text))
.build()
.borrow(),
).await
}
pub async fn sign_transaction(&self, tx: &str) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::SignTransaction)
.add_param(Param::Transaction, Value::from(tx))
.build()
.borrow()
).await
}
pub async fn broadcast(&self, tx: &str) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::Broadcast)
.add_param(Param::Transaction, Value::from(tx))
.build()
.borrow()
).await
}
pub async fn pay_to_many(
&self,
fee: Decimal,
outputs: Vec<(String, Decimal)>,
) -> Result<Response<Body>> {
let outputs = json!(outputs);
let fee = fee.to_string();
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::PayToMany)
.add_param(Param::Fee, Value::from(fee))
.add_param(Param::Outputs, outputs)
.build()
.borrow(),
).await
}
pub async fn close_wallet(&self) -> Result<Response<Body>> {
self.call_method(
JsonRpcBody::new()
.method(ElectrumMethod::CloseWallet)
.build()
.borrow(),
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::{ElectrumRpcError, InvalidUri};
use crate::ext::tests::*;
#[test]
fn new_electrum_instance0() {
let electrum = get_electrum_rpc();
let port = electrum.address.port();
assert_eq!(port.unwrap().as_u16(), 7000);
let host = electrum.address.host();
assert_eq!(host, Some("127.0.0.1"));
let encoded_creds = electrum.auth.split(' ').collect::<Vec<&str>>()[1];
let decoded_creds = base64::decode(encoded_creds).unwrap();
assert_eq!("test:test", std::str::from_utf8(&decoded_creds).unwrap());
}
#[test]
#[should_panic]
fn new_electrum_instance_empty_address() {
Electrum::new(LOGIN.clone(), PASSWORD.clone(), "".to_string()).unwrap();
}
#[test]
fn error_casting_address_error() {
let electrum = Electrum::new(LOGIN.clone(), PASSWORD.clone(), "".to_string());
assert!(matches!(electrum, Err(ElectrumRpcError::AddressError(InvalidUri {..}))))
}
#[test]
fn rpc_body_builder() {
let body = JsonRpcBody::new()
.id(1111)
.method(ElectrumMethod::GetInfo)
.build();
let actual = serde_json::to_string(&body).unwrap();
let expected = r#"{"json_rpc":2.0,"id":1111,"method":"getinfo","params":{}}"#;
assert_eq!(expected, actual);
}
}