Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 248 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ cardano-serialization-lib = "10.2.0"
dotenv = "0.15.0"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
tokio = { version = "1.23.0", features = ["full"] }
tokio = { version = "1.23.0", features = ["full"] }
tokio-native-tls = "0.3.0"
2 changes: 1 addition & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ We need to create a simple Rust program to list the books available for a given
1. Create a free account at https://www.tangocrypto.com/.
1. Copy `.env.dist` to `.env`.
1. Create a testnet app and copy your `APP_ID` and `API_KEY` into `.env`.
1. Implement the `CardanoApi` trait for `TangoClient` in [src/cardano/tango.rs](src/cardano/tango/client.rs).
1. Implement the `CardanoApi` trait for `TangoClient` in [src/cardano/tango/client.rs](src/cardano/tango/client.rs).
1. Implement the `Bookshelf` functionality in [src/model/bookshelf.rs](src/model/bookshelf.rs).

**IMPORTANT**: Please do not modify any code in [main.rs](src/main.rs). You can add dependencies to [Cargo.toml](Cargo.toml), add structs and functions in the [cardano](src/cardano) module, [tango](src/cardano/tango) module, and can add (but not remove) functions to the [CardanoApi](src/cardano/api.rs) trait if you wish.
Expand Down
1 change: 1 addition & 0 deletions src/cardano/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod api;
pub mod model;
pub mod tango;
pub mod request_helper;
1 change: 1 addition & 0 deletions src/cardano/model.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[derive(Debug)]
pub struct Asset {
pub policy_id: String,
pub asset_name: String,
Expand Down
120 changes: 120 additions & 0 deletions src/cardano/request_helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use tokio_native_tls::TlsConnector as TlsConnector_Async;
use tokio_native_tls::native_tls::TlsConnector;
use tokio_native_tls::TlsStream;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
use tokio::net::TcpStream;
use std::io::Write;
use std::io::Read;
use tokio::time::{sleep, Duration, Instant};
use std::net::*;
use std::vec::Vec;
use anyhow::Context;

pub struct HttpGetRequest<'a>
{
host: &'a str,
path: &'a str,
headers: Vec<(&'a str, &'a str)>
}

impl HttpGetRequest<'_>
{
pub fn new<'a>(host: &'a str, path: &'a str, headers: Vec<(&'a str, &'a str)>) -> HttpGetRequest<'a>
{
HttpGetRequest {
host,
path,
headers
}
}
}

pub async fn http_s_get(req: HttpGetRequest<'_>) -> anyhow::Result<String>
{
println!("making request: GET {} ", req.path);
//1. handshake
let stream = TcpStream::connect((req.host, 443)).await.context("tcp handshake failed")?;
let connector = TlsConnector::new().unwrap();
let mut stream = TlsConnector_Async::from(connector).connect(req.host, stream).await.context("tls handshake failed")?;

//2. headers
let mut request = String::from("");
request.push_str(format!("GET {} HTTP/1.1\r\n", req.path).as_str());
request.push_str(format!("Host: {}\r\n", req.host).as_str());
request.push_str(format!("Accept: {}\r\n", "application/json").as_str());
request.push_str("Connection: close\r\n");
for header in req.headers
{
request.push_str(format!("{}: {}\r\n", header.0, header.1).as_str());
}
request.push_str("\r\n");

//3. make request
stream.write_all(request.as_bytes()).await.context("Writing to tcp socket failed")?;

//4. read response
let mut response = String::from("");
stream.read_to_string(&mut response).await.context("Reading from tcp socket failed")?;

Ok(response)
}

pub fn parse_httpStatusCode(response: &String) -> i32
{
let line = response.lines().next();

match line
{
Some(line) =>
{
let from = "HTTP/1.1 ".len();
let until = "HTTP/1.1 xyz".len();
if line.len() >= until
{
let status = &line[from..until];
match status.parse::<i32>()
{
Ok(n) => n,
Err(_) =>
{
eprintln!("Error parsing status {}.", status);
500
},
}
}
else
{
500
}
},
None => 500
}
}

pub fn parse_extractJsonStringFromBody(res: &String) -> anyhow::Result<String>
{
let mut lines = res.lines();
let mut s: &str = "";

loop
{
s = match lines.next()
{
Some(value) => value,
None => break
}
}

let first: &str = &s[0..1];
let last: &str = &s[s.len() - 1..];

//TODO: this suffices for now, but do better
if first == "{" && last == "}"
{
Ok(s.to_string())
}
else
{
Err(anyhow::anyhow!("Error getting json string from response"))
}
}
203 changes: 163 additions & 40 deletions src/cardano/tango/client.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,120 @@
use tokio_native_tls::TlsConnector as TlsConnector_Async;
use tokio_native_tls::native_tls::TlsConnector;
use tokio_native_tls::TlsStream;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
use tokio::net::TcpStream;
use std::io::Write;
use std::io::Read;
use std::io::ErrorKind;
use std::net::*;
use std::vec::Vec;
use serde_json::Value as SerdeValue;
use anyhow::Context;

use std::{collections::HashSet, future::Future};

use serde::de::DeserializeOwned;

use crate::cardano::{api::CardanoApi, model::Asset};
use crate::cardano::{api::CardanoApi, model::Asset, request_helper::*};

use super::model::ApiListRes;
use super::model::*;

// feel free to add/remove properties as needed.
pub struct TangoClient {
base_url: String,
app_id: String,
api_key: String,
pub struct TangoClient
{
host: String,
base_url: String,
app_id: String,
api_key: String
}

impl TangoClient {
pub fn new(base_url: String, app_id: String, api_key: String) -> anyhow::Result<Self> {
Ok(TangoClient {
base_url,
app_id,
api_key,
})
impl TangoClient
{
pub fn new(base_url: String, app_id: String, api_key: String) -> anyhow::Result<Self>
{
let host: &str = base_url.as_str();

let host: &str = if host.starts_with("https://")
{
&host["https://".len()..]
}
else
{
&host
};
let host: &str = if host.ends_with("/")
{
&host[..host.len() - 1]
}
else
{
&host
};

let host = host.to_owned();

Ok(TangoClient {
host,
base_url,
app_id,
api_key
})
}

async fn get_addresses(&self, stake_address: &str, cursor: Option<String>) -> anyhow::Result<ApiListRes<Address>>
{
let mut path = format!("/{}/v1/wallets/{}/addresses", self.app_id, stake_address);
self.make_get_request::<Address>(path, cursor).await
}

async fn get_address_assets(&self, address: &str, cursor: Option<String>) -> anyhow::Result<ApiListRes<AddressAsset>>
{
let mut path = format!("/{}/v1/addresses/{}/assets", self.app_id, address);
self.make_get_request::<AddressAsset>(path, cursor).await
}

async fn get_asset_addresses(&self, asset_id: &str, cursor: Option<String>) -> anyhow::Result<ApiListRes<AssetAddress>>
{
let mut path = format!("/{}/v1/assets/{}/addresses", self.app_id, asset_id);
self.make_get_request::<AssetAddress>(path, cursor).await
}

async fn make_get_request<T>(&self, mut path: String, cursor: Option<String>) -> anyhow::Result<ApiListRes<T>>
where
T: DeserializeOwned
{
if let Some(x) = cursor
{
path.push_str("?cursor=");
path.push_str(x.as_str());
}

let req = HttpGetRequest::new(
self.host.as_str(),
path.as_str(),
vec![
("Accept", "application/json"),
("x-api-key", self.api_key.as_str())
]
);

let mut res = http_s_get(req).await?;

let statusCode = parse_httpStatusCode(&res);

if statusCode == 200
{
let body = parse_extractJsonStringFromBody(&res)?;
let result: ApiListRes<T> = parse_apiBody(&body)?;
Ok(result)
}
else
{
eprintln!("{}", res);
Err(anyhow::anyhow!("Recieved {} status code", statusCode))
}
}
}

/**
Expand All @@ -41,40 +135,69 @@ where
T: DeserializeOwned,
T: Clone,
{
let mut data = Vec::new();
let mut cursor = None;
loop {
let res = f(cursor.clone()).await?;

data.append(&mut res.data.clone());
cursor = res.cursor;

match cursor {
None => break,
_ => {}
}
let mut data = Vec::new();
let mut cursor = None;
loop
{
let res = f(cursor.clone()).await?;

data.append(&mut res.data.clone());
cursor = res.cursor;

match cursor
{
None => break,
_ => {}
}
}

Ok(data)
Ok(data)
}

#[async_trait::async_trait]
impl CardanoApi for TangoClient {
// recommended api:
// https://www.tangocrypto.com/api-reference/#/operations/list-stake_address-addresses
async fn get_all_addresses(&self, stake_address: &str) -> anyhow::Result<Vec<String>> {
todo!()
}
impl CardanoApi for TangoClient
{
// recommended api:
// https://www.tangocrypto.com/api-reference/#/operations/list-stake_address-addresses
async fn get_all_addresses(&self, stake_address: &str) -> anyhow::Result<Vec<String>> {
let result = get_all(|cursor| self.get_addresses(stake_address, cursor)).await?;
Ok(result.into_iter().map(|x| x.address).collect())
}

// recommended api:
// https://www.tangocrypto.com/api-reference/#/operations/list-address-assets
async fn get_address_assets(&self, address: &str) -> anyhow::Result<Vec<Asset>> {
todo!()
}
// recommended api:
// https://www.tangocrypto.com/api-reference/#/operations/list-address-assets
async fn get_address_assets(&self, address: &str) -> anyhow::Result<Vec<Asset>> {
let result = get_all(|cursor| self.get_address_assets(address, cursor)).await?;
Ok(result.into_iter().map(|x| Asset {
policy_id: x.policy_id,
asset_name: x.asset_name,
quantity: x.quantity
}).collect())
}

// recommended api:
// https://www.tangocrypto.com/api-reference/#/operations/list-asset-addresses
async fn get_asset_addresses(&self, asset_id: &str) -> anyhow::Result<HashSet<String>> {
let getAllResult = get_all(|cursor| self.get_asset_addresses(asset_id, cursor)).await?;

// recommended api:
// https://www.tangocrypto.com/api-reference/#/operations/list-asset-addresses
async fn get_asset_addresses(&self, asset_id: &str) -> anyhow::Result<HashSet<String>> {
todo!()
let mut result = HashSet::new();
for asset in getAllResult.into_iter()
{
result.insert(asset.address);
}

Ok(result)
}
}

fn parse_apiBody<T>(body: &String) -> anyhow::Result<ApiListRes<T>>
where
T: DeserializeOwned
{
let parsed: serde_json::Result<ApiListRes<T>> = serde_json::from_str(body);
match parsed
{
Ok(result) => Ok(result),
Err(_) => Err(anyhow::anyhow!("Error parsing json into custom type"))
}
}
Loading