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
652 changes: 644 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ edition = "2021"
[dependencies]
anyhow = "1.0.68"
async-trait = "0.1.60"
reqwest = { version = "0.11.13", features = ["json"] }
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"] }
hex = "0.4.3"
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
13 changes: 13 additions & 0 deletions issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Issue Tracker
### (since I can't use native GH issues on a fork 🙂)

#### Will still use GH prefix for commits (and branches where necessary)

0. ~~Create issue tracker~~
1. ~~Install reqwest~~
2. ~~Create endpoints helper~~
3. ~~Create lib for constants~~
4. ~~Implement first Tango client fn~~
5. ~~Implement Tango Client~~
6. ~~Implement Bookshelf~~
7. ~~Add address helper to see if address_belongs_to_stake_address~~
9 changes: 9 additions & 0 deletions src/cardano/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ pub fn get_address_stake_key(
None => Ok(None),
}
}

/**
* Returns if an address corresponds to a stake address
*/
pub fn address_belongs_to_stake_address(address: &str, stake_address: &str) -> bool {
let address_option = get_address_stake_key(address).unwrap();

address_option.is_some() && (address_option.unwrap() == stake_address)
}
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 address;
pub mod api;
pub mod model;
pub mod tango;
90 changes: 87 additions & 3 deletions src/cardano/tango/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ use std::{collections::HashSet, future::Future};

use serde::de::DeserializeOwned;

use crate::cardano::tango::endpoints::{
add_cursor_to_url, generate_get_all_addresses_for_asset_endpoint,
generate_get_all_addresses_for_stake_address_endpoint, generate_get_all_assets_endpoint,
};
use crate::cardano::tango::lib::TANGO_API_KEY_HEADER;
use crate::cardano::tango::model::{Address, AddressAsset, AssetAddress};
use crate::cardano::{api::CardanoApi, model::Asset};

use super::model::ApiListRes;
Expand Down Expand Up @@ -58,23 +64,101 @@ where
Ok(data)
}

/**
* Helper method that abstracts calls to Tango Crypto for an arbitrary url,
* which can optionally have a cursor appended
*/
async fn get_collection_from_tango<T>(
url: &str,
api_key: &str,
cursor: Option<String>,
) -> anyhow::Result<ApiListRes<T>>
where
T: DeserializeOwned,
{
let full_url = if cursor.is_some() {
add_cursor_to_url(url, &cursor.unwrap())
} else {
url.to_string()
};

let res = reqwest::Client::new()
.get(full_url)
.header(TANGO_API_KEY_HEADER, api_key)
.send()
.await?
.json()
.await?;

Ok(res)
}

#[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!()
let url = generate_get_all_addresses_for_stake_address_endpoint(
&self.base_url,
&self.app_id,
stake_address,
);
let api_key = &self.api_key;

let response: Vec<Address> =
get_all(|cursor| get_collection_from_tango(&url, api_key, cursor)).await?;

let addresses: Vec<String> = response
.into_iter()
.map(|Address { address }| address)
.collect();

Ok(addresses)
}

// 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!()
let url = generate_get_all_assets_endpoint(&self.base_url, &self.app_id, address);
let api_key = &self.api_key;

let response: Vec<AddressAsset> =
get_all(|cursor| get_collection_from_tango(&url, api_key, cursor)).await?;

let assets: Vec<Asset> = response
.into_iter()
.map(
|AddressAsset {
policy_id,
asset_name,
quantity,
..
}| Asset {
policy_id,
asset_name,
quantity,
},
)
.collect();

Ok(assets)
}

// 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 url =
generate_get_all_addresses_for_asset_endpoint(&self.base_url, &self.app_id, asset_id);
let api_key = &self.api_key;

let response: Vec<AssetAddress> =
get_all(|cursor| get_collection_from_tango(&url, api_key, cursor)).await?;

let addresses: HashSet<String> = response
.into_iter()
.map(|AssetAddress { address, .. }| address)
.collect();

Ok(addresses)
}
}
23 changes: 23 additions & 0 deletions src/cardano/tango/endpoints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pub fn add_cursor_to_url(url: &str, cursor: &str) -> String {
format!("{url}?cursor={cursor}")
}

pub fn generate_get_all_addresses_for_stake_address_endpoint(
base_url: &str,
app_id: &str,
stake_address: &str,
) -> String {
format!("{base_url}/{app_id}/v1/wallets/{stake_address}/addresses")
}

pub fn generate_get_all_assets_endpoint(base_url: &str, app_id: &str, address: &str) -> String {
format!("{base_url}/{app_id}/v1/addresses/{address}/assets")
}

pub fn generate_get_all_addresses_for_asset_endpoint(
base_url: &str,
app_id: &str,
asset_id: &str,
) -> String {
format!("{base_url}/{app_id}/v1/assets/{asset_id}/addresses")
}
1 change: 1 addition & 0 deletions src/cardano/tango/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const TANGO_API_KEY_HEADER: &str = "x-api-key";
2 changes: 2 additions & 0 deletions src/cardano/tango/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod client;
pub mod endpoints;
pub mod lib;
pub mod model;
53 changes: 51 additions & 2 deletions src/model/bookshelf.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::cardano::address::address_belongs_to_stake_address;
use std::{collections::HashSet, sync::Arc};

use crate::cardano::api::CardanoApi;
Expand All @@ -21,7 +22,36 @@ impl Bookshelf {
&self,
policy_ids: HashSet<String>,
) -> anyhow::Result<Vec<BookListItem>> {
todo!()
let mut books: Vec<BookListItem> = vec![];

let addresses = self.api.get_all_addresses(&self.stake_address).await?;

for address in addresses {
let assets = self.api.get_address_assets(&address).await?;

for asset in &assets {
let asset_policy_id = asset.policy_id.clone();
let asset_policy_is_for_valid_book = policy_ids
.iter()
.any(|policy_id| &asset_policy_id == policy_id);

if asset_policy_is_for_valid_book {
let asset_name_hex = hex::encode(asset.asset_name.clone());
let token_name = asset.asset_name.clone();

let book_id: BookId = BookId::new(asset_policy_id, asset_name_hex);

let book: BookListItem = BookListItem {
id: book_id,
token_name,
};

books.push(book);
}
}
}

Ok(books)
}

/**
Expand All @@ -30,6 +60,25 @@ impl Bookshelf {
pub async fn has_book(&self, id: &BookId) -> bool {
// bonus points if you can implement this more efficiently than just
// calling get_books and seeing if the BookId exists in that set.
todo!()
let mut has_book = false;

let asset_id = id.as_asset_id();
let book_addresses: Vec<String> = self
.api
.get_asset_addresses(&asset_id)
.await
.unwrap_or(HashSet::new())
.into_iter()
.collect();

let is_an_nft = book_addresses.len() == 1;

if is_an_nft {
let book_address = book_addresses[0].clone();

has_book = address_belongs_to_stake_address(&book_address, &self.stake_address);
}

has_book
}
}