use crate::error::{Error, Result};
use crate::params::*;
use crate::response::*;
use reqwest::blocking::{Client as HttpClient, RequestBuilder, Response};
use reqwest::Method;
use serde::{de::DeserializeOwned, Serialize};
use serde_json;
use url::Url;
#[derive(Debug)]
pub struct Client {
base_url: Url,
http_client: HttpClient,
token: String,
}
fn decode_response<T>(res: Response) -> Result<T>
where
T: DeserializeOwned,
{
let data = res.text()?;
let s: T =
serde_json::from_str(&data).map_err(|orig_err| match serde_json::from_str(&data) {
Err(_) => {
println!("{}", data);
orig_err.into()
}
Ok(r) => Error::ServerError(r),
})?;
Ok(s)
}
pub enum Network {
Mainnet,
Testnet,
}
impl std::str::FromStr for Network {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"mainnet" => Ok(Network::Mainnet),
"testnet" => Ok(Network::Testnet),
_ => Err(format!("unknown recipient {}", s)),
}
}
}
impl Client {
pub fn new(network: Network, token: String, base_url: Option<String>) -> Self {
let base_url = base_url.unwrap_or_else(|| match network {
Network::Testnet => "https://test.bitgo.com/api/v2/".to_string(),
Network::Mainnet => "https://www.bitgo.com/api/v2/".to_string(),
});
Client {
base_url: Url::parse(&base_url).expect("could not parse base_url"),
http_client: HttpClient::new(),
token,
}
}
fn prep_req(&self, method: Method, url: Url) -> Result<RequestBuilder> {
let req = self
.http_client
.request(method, url)
.bearer_auth(self.token.clone())
.header("Content-Type", "application/json");
Ok(req)
}
fn get_url(&self, path: &str) -> Url {
dbg!(&self.base_url);
self.base_url.join(path).expect("could not parse url")
}
fn req<T, U>(&self, path: &str, method: Method, body: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
let res = match method {
Method::GET => self
.prep_req(method, self.get_url(path))?
.query(&body)
.send()?,
Method::POST => self
.prep_req(method, self.get_url(path))?
.json(&body)
.send()?,
_ => unimplemented!(),
};
decode_response::<T>(res)
}
fn post<T, U>(&self, path: &str, param: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
self.req(path, Method::POST, param)
}
fn get<T>(&self, path: &str) -> Result<T>
where
T: DeserializeOwned,
{
self.req(path, Method::GET, EmptyQuery::default())
}
fn getq<T, U>(&self, path: &str, params: U) -> Result<T>
where
T: DeserializeOwned,
U: Serialize,
{
self.req(path, Method::GET, params)
}
pub fn get_wallet(&self, coin: &str, wallet_id: &str) -> Result<Wallet> {
self.get(&format!("{}/wallet/{}", coin, wallet_id))
}
pub fn get_transfer_by_id(
&self,
coin: &str,
wallet_id: &str,
transfer_id: &str,
) -> Result<Transfer> {
self.get(&format!(
"{}/wallet/{}/transfer/{}",
coin, wallet_id, transfer_id
))
}
pub fn list_transfers(
&self,
coin: &str,
wallet_id: &str,
params: &ListTransfersParams,
) -> Result<TransferList> {
self.getq(&format!("{}/wallet/{}/transfer", coin, wallet_id), params)
}
pub fn create_address(
&self,
coin: &str,
wallet_id: &str,
label: &str,
) -> Result<WalletAddress> {
self.post::<WalletAddress, CreateAddressParams>(
&format!("{}/wallet/{}/address", coin, wallet_id),
CreateAddressParams::new(label.into()),
)
}
pub fn send_coins(
&self,
coin: &str,
wallet_id: &str,
wallet_passphrase: &str,
address: &str,
amount: i64,
comment: Option<&str>,
) -> Result<SendCoin> {
self.post::<SendCoin, SendCoinsParams>(
&format!("{}/wallet/{}/sendcoins", coin, wallet_id),
SendCoinsParams::new(wallet_passphrase, address, amount, comment),
)
}
}
#[cfg(test)]
mod test {
use super::*;
fn create_client() -> (Client, String) {
let token = std::env::var("BITGO_ACCESS_TOKEN").unwrap();
let wallet_id = std::env::var("BITGO_TBTC_WALLET_ID").unwrap();
let base_url = std::env::var("BITGO_BASE_URL").ok();
let client = Client::new(Network::Testnet, token, base_url);
(client, wallet_id)
}
#[test]
#[ignore]
fn test_get_wallet() {
let (client, wallet_id) = create_client();
let wallet = client.get_wallet("tbtc", &wallet_id).unwrap();
assert_eq!(wallet.id, wallet_id);
}
#[test]
#[ignore]
fn test_list_transfers() {
let (client, wallet_id) = create_client();
let params = ListTransfersParams::default();
let _transfers: TransferList = client.list_transfers("tbtc", &wallet_id, ¶ms).unwrap();
}
#[test]
fn test_get_transfer() {
let (client, wallet_id) = create_client();
let id = "5eb5f7698dcb762c0016bf16bdd306f7";
let transfer: Transfer = client.get_transfer_by_id("tbtc", &wallet_id, id).unwrap();
dbg!(&transfer);
}
#[test]
#[ignore]
fn test_list_transfers_with_params() {
let (client, wallet_id) = create_client();
let params = ListTransfersParams {
limit: Some(1),
..ListTransfersParams::default()
};
let transfers: TransferList = client.list_transfers("tbtc", &wallet_id, ¶ms).unwrap();
dbg!(&transfers);
}
#[test]
#[ignore]
fn test_send_coins() {
let (client, wallet_id) = create_client();
let passphrase = std::env::var("BITGO_TBTC_WALLET_PASSPHRASE")
.expect("BITGO_TBTC_WALLET_PASSPHRASE must be set");
let amount = 10000;
let address = "2N6Y5AYYaxDHdfbuKzSiHqBtTHTG66UanUB";
let res = client
.send_coins(
"tbtc",
&wallet_id,
&passphrase,
address,
amount,
Some("automated test"),
)
.unwrap();
dbg!(&res);
}
#[test]
#[ignore]
fn test_create_address() {
let (client, wallet_id) = create_client();
let address = client
.create_address("tbtc", &wallet_id, "test-label")
.unwrap();
dbg!(&address);
assert_eq!(address.label, "test-label");
}
}