Skip to content

Commit

Permalink
Client: tokens rpc call (#2)
Browse files Browse the repository at this point in the history
* feat: tokens route functionality

* feat: expose routes thru client

* feat: smol test

* fix: remove arbitrary parsing test

* chore: TokenIfResponse -> TokenResponse

* chore: change AsRef<HashMap<>> -> IntoIterator

* fix: remove iterator cast on json_get! macro

* chore: remove unnecessary rename
  • Loading branch information
Evalir committed Mar 27, 2023
1 parent 0714822 commit 03c1a13
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 2 deletions.
33 changes: 33 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
info::{SafeInfoRequest, SafeInfoResponse},
msig_history::{MsigHistoryFilters, MsigHistoryResponse, MsigTxRequest, MsigTxResponse},
propose::{MetaTransactionData, ProposeRequest, SafeTransactionData},
tokens::{TokenInfoFilters, TokenInfoRequest, TokenInfoResponse},
},
};

Expand Down Expand Up @@ -167,6 +168,38 @@ impl SafeClient {
.map(Option::unwrap)
}

/// Get information about tokens available on the API
#[tracing::instrument(skip(self))]
pub async fn tokens(&self) -> ClientResult<TokenInfoResponse> {
json_get!(
&self.client,
TokenInfoRequest::url(self.url()),
TokenInfoResponse,
)
.map(Option::unwrap)
}

/// Get fitered information about tokens available on the API
#[tracing::instrument(skip(self, filters))]
pub async fn filtered_tokens(
&self,
filters: impl IntoIterator<Item = (&'static str, String)>,
) -> ClientResult<TokenInfoResponse> {
json_get!(
&self.client,
TokenInfoRequest::url(self.url()),
TokenInfoResponse,
filters,
)
.map(Option::unwrap)
}

/// Create a filter builder for tokens
#[tracing::instrument(skip(self))]
pub fn tokens_builder(&self) -> TokenInfoFilters<'_> {
TokenInfoFilters::new(self)
}

/// Get the history of Msig transactions from the API
#[tracing::instrument(skip(self))]
pub async fn msig_history(&self, safe_address: Address) -> ClientResult<MsigHistoryResponse> {
Expand Down
3 changes: 1 addition & 2 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ macro_rules! json_get {
};
($client:expr, $url:expr, $expected:ty, $query:expr) => {{
let mut url = $url.clone();
let pairs = $query.iter();
url.query_pairs_mut().extend_pairs(pairs);
url.query_pairs_mut().extend_pairs($query);
tracing::debug!(url = url.as_str(), "Dispatching api request");
let resp = $client.get($url).send().await?;
let status = resp.status();
Expand Down
3 changes: 3 additions & 0 deletions src/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ pub mod common;
/// General Safe Info
pub mod info;

/// Token Info
pub mod tokens;

/// History of Safe msig transactions
pub mod msig_history;

Expand Down
157 changes: 157 additions & 0 deletions src/rpc/tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use std::collections::HashMap;

use ethers::types::Address;
use reqwest::Url;
use serde::Serialize;

use crate::{client::ClientResult, SafeClient};

use super::common::Paginated;

/// token info request
#[derive(Debug, Clone, serde::Serialize)]
pub struct TokenInfoRequest;

impl TokenInfoRequest {
/// Return the URL to which to dispatch this request
pub fn url(root: &Url) -> Url {
let mut url = root.clone();
url.set_path("api/v1/tokens/");
url
}
}

/// Token info Request with filters
#[derive(Clone, Serialize)]
pub struct TokenInfoFilters<'a> {
#[serde(flatten)]
pub(crate) filters: HashMap<&'static str, String>,
#[serde(skip)]
pub(crate) client: &'a SafeClient,
}

impl<'a> TokenInfoFilters<'a> {
const DECIMAL_KEYS: &'static [&'static str] = &["decimals__lt", "decimals__gt", "decimals"];

/// Dispatch the request to the API, querying tokens from the API
pub async fn query(self) -> ClientResult<TokenInfoResponse> {
self.client.filtered_tokens(self.filters).await
}

/// Insert a KV pair into the internal mapping for later URL encoding
///
/// Somewhat more expensive and brittle than required, as it uses
/// serde_json. Using display would cause hashes and addresses to be
/// abbreviated `0xabcd....1234`
fn insert<S: Serialize>(&mut self, k: &'static str, v: S) {
self.filters.insert(k, serde_json::to_string(&v).unwrap());
}

/// Return the URL to which to dispatch this request
pub fn url(root: &Url) -> Url {
let mut url = root.clone();
url.set_path("api/v1/tokens/");
url
}

/// Instantiate from a client
pub(crate) fn new(client: &'a SafeClient) -> Self {
Self {
filters: Default::default(),
client,
}
}

fn clear_decimals(&mut self) {
for k in Self::DECIMAL_KEYS {
self.filters.remove(k);
}
}

/// Filters tokens by name
pub fn name(mut self, name: String) -> Self {
self.filters.insert("name", name);
self
}

/// Filter tokens by address
pub fn address(mut self, address: Address) -> Self {
self.insert("address", address);
self
}

/// Filter tokens by symbol
pub fn symbol(mut self, symbol: String) -> Self {
self.filters.insert("symbol", symbol);
self
}

/// Filter tokens with `decimals >= min_decimals`
/// Clears any exact decimals filter
pub fn min_decimals(mut self, decimals: u64) -> Self {
self.filters.remove("decimals");
self.insert("decimals__gt", decimals.saturating_sub(1));
self
}

/// Filter tokens with `decimals <= max_decimals`
/// Clears any exact decimals filter
pub fn max_decimals(mut self, decimals: u64) -> Self {
self.filters.remove("decimals");
self.insert("decimals__lt", decimals.saturating_add(1));
self
}

/// Filter tokens by exact decimals
/// Clears any min or max decimals filters
pub fn decimals(mut self, decimals: u64) -> Self {
self.clear_decimals();
self.insert("decimals", decimals);
self
}

/// Specify page limit. If more results than limit are returned, results in
/// a paginated response
pub fn limit(mut self, limit: u64) -> Self {
self.insert("limit", limit);
self
}

/// Specify offset in results. Used in pagination, not recommended to be
/// specified manually
pub fn offset(mut self, offset: u64) -> Self {
self.insert("offset", offset);
self
}

/// Converts to a URL with query string
pub fn to_url(self) -> Url {
let mut url = self.client.url().clone();
url = Self::url(&url);
url.query_pairs_mut().extend_pairs(self.filters.iter());
url
}
}

/// Token info response
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenResponse {
/// The token type (ERC20, ERC721, etc)
#[serde(rename(deserialize = "type"))]
// #[serde(skip)]
pub token_type: String,
/// The address of the token
pub address: String,
/// The name of the token
pub name: String,
/// The symbol of the token
pub symbol: String,
/// The number of decimals of the token
pub decimals: Option<u32>,
/// The Logo URI of the token, if it exists
pub logo_uri: String,
}

/// Response for Token Info requests
pub type TokenInfoResponse = Paginated<TokenResponse>;
6 changes: 6 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ async fn it_gets_history() {
dbg!(CLIENT.msig_history(*SAFE).await.unwrap());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn it_gets_tokens() {
dbg!(CLIENT.tokens().await.unwrap());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn it_proposes() {
Expand Down

0 comments on commit 03c1a13

Please sign in to comment.