bitgo 0.0.3

BitGo API client
Documentation
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,
    // private_key: String,
    http_client: HttpClient,
    token: String,
}

fn decode_response<T>(res: Response) -> Result<T>
where
    T: DeserializeOwned,
{
    let data = res.text()?;
    // dbg!(&data);

    let s: T =
        serde_json::from_str(&data).map_err(|orig_err| match serde_json::from_str(&data) {
            Err(_) => {
                println!("{}", data);
                // dbg!(&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())
    }

    // like get but with query params
    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()),
        )
    }

    // Note: uses bitgo-express
    // TODO: implement it without requiring bitgo-express
    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, &params).unwrap();
        // dbg!(&transfers);
        // assert_eq!(transfers, vec![]);
    }

    #[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);
        // assert_eq!(transfers, vec![]);
    }

    #[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, &params).unwrap();
        dbg!(&transfers);
        // assert_eq!(transfers, vec![]);
    }

    #[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);
        // assert_eq!(address.label, "test-label");
    }

    #[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");
    }
}