Skip to content

Commit

Permalink
feat(providers): adding Dune provider for EVM balances resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
geekbrother committed Jan 6, 2025
1 parent bb9670d commit cdf95fa
Show file tree
Hide file tree
Showing 19 changed files with 274 additions and 20 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export RPC_PROXY_PROVIDER_LAVA_API_KEY=""
export RPC_PROXY_PROVIDER_TENDERLY_API_KEY=""
export RPC_PROXY_PROVIDER_TENDERLY_ACCOUNT_ID=""
export RPC_PROXY_PROVIDER_TENDERLY_PROJECT_ID=""
export RPC_PROXY_PROVIDER_DUNE_API_KEY=""

# PostgreSQL URI connection string
export RPC_PROXY_POSTGRES_URI="postgres://postgres@localhost/postgres"
Expand Down
1 change: 1 addition & 0 deletions .env.terraform.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export TF_VAR_lava_api_key=""
export TF_VAR_tenderly_api_key=""
export TF_VAR_tenderly_account_id=""
export TF_VAR_tenderly_project_id=""
export TF_VAR_dune_api_key=""
export TF_VAR_grafana_endpoint=$(aws grafana list-workspaces | jq -r '.workspaces[] | select( .tags.Env == "prod") | select( .tags.Name == "grafana-9") | .endpoint')
export TF_VAR_registry_api_auth_token=""
export TF_VAR_debug_secret=""
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/event_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ jobs:
RPC_PROXY_PROVIDER_TENDERLY_ACCOUNT_ID: ""
RPC_PROXY_PROVIDER_TENDERLY_PROJECT_ID: ""
RPC_PROXY_PROVIDER_ZERION_API_KEY: ""
RPC_PROXY_PROVIDER_DUNE_API_KEY: ""
- run: docker logs mock-bundler-anvil-1
if: failure()
- run: docker logs mock-bundler-alto-1
Expand Down
40 changes: 40 additions & 0 deletions src/env/dune.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use {
super::BalanceProviderConfig,
crate::{
providers::{Priority, Weight},
utils::crypto::CaipNamespaces,
},
std::collections::HashMap,
};

#[derive(Debug)]
pub struct DuneConfig {
pub api_key: String,
pub supported_namespaces: HashMap<CaipNamespaces, Weight>,
}

impl DuneConfig {
pub fn new(api_key: String) -> Self {
Self {
api_key,
supported_namespaces: default_supported_namespaces(),
}
}
}

impl BalanceProviderConfig for DuneConfig {
fn supported_namespaces(self) -> HashMap<CaipNamespaces, Weight> {
self.supported_namespaces
}

fn provider_kind(&self) -> crate::providers::ProviderKind {
crate::providers::ProviderKind::Dune
}
}

fn default_supported_namespaces() -> HashMap<CaipNamespaces, Weight> {
HashMap::from([(
CaipNamespaces::Eip155,
Weight::new(Priority::Normal).unwrap(),
)])
}
9 changes: 6 additions & 3 deletions src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ use {
std::{collections::HashMap, fmt::Display},
};
pub use {
arbitrum::*, aurora::*, base::*, berachain::*, binance::*, getblock::*, infura::*, lava::*,
mantle::*, morph::*, near::*, pokt::*, publicnode::*, quicknode::*, server::*, solscan::*,
unichain::*, zerion::*, zksync::*, zora::*,
arbitrum::*, aurora::*, base::*, berachain::*, binance::*, dune::*, getblock::*, infura::*,
lava::*, mantle::*, morph::*, near::*, pokt::*, publicnode::*, quicknode::*, server::*,
solscan::*, unichain::*, zerion::*, zksync::*, zora::*,
};
mod arbitrum;
mod aurora;
mod base;
mod berachain;
mod binance;
mod dune;
mod getblock;
mod infura;
mod lava;
Expand Down Expand Up @@ -192,6 +193,7 @@ mod test {
"RPC_PROXY_PROVIDER_TENDERLY_PROJECT_ID",
"TENDERLY_PROJECT_ID",
),
("RPC_PROXY_PROVIDER_DUNE_API_KEY", "DUNE_API_KEY"),
(
"RPC_PROXY_PROVIDER_PROMETHEUS_QUERY_URL",
"PROMETHEUS_QUERY_URL",
Expand Down Expand Up @@ -297,6 +299,7 @@ mod test {
tenderly_api_key: "TENDERLY_KEY".to_string(),
tenderly_account_id: "TENDERLY_ACCOUNT_ID".to_string(),
tenderly_project_id: "TENDERLY_PROJECT_ID".to_string(),
dune_api_key: "DUNE_API_KEY".to_string(),
override_bundler_urls: None,
},
rate_limiting: RateLimitingConfig {
Expand Down
8 changes: 4 additions & 4 deletions src/handlers/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,13 @@ async fn handler_internal(
{
balance.quantity.numeric = crypto::format_token_amount(
rpc_balance,
balance.quantity.decimals.parse::<u32>().unwrap_or(0),
balance.quantity.decimals.parse::<u8>().unwrap_or(0),
);
// Recalculating the value with the latest balance
balance.value = Some(crypto::convert_token_amount_to_value(
rpc_balance,
balance.price,
balance.quantity.decimals.parse::<u32>().unwrap_or(0),
balance.quantity.decimals.parse::<u8>().unwrap_or(0),
));
continue;
}
Expand All @@ -231,13 +231,13 @@ async fn handler_internal(
{
balance.quantity.numeric = crypto::format_token_amount(
rpc_balance,
balance.quantity.decimals.parse::<u32>().unwrap_or(0),
balance.quantity.decimals.parse::<u8>().unwrap_or(0),
);
// Recalculate the value with the latest balance
balance.value = Some(crypto::convert_token_amount_to_value(
rpc_balance,
balance.price,
balance.quantity.decimals.parse::<u32>().unwrap_or(0),
balance.quantity.decimals.parse::<u8>().unwrap_or(0),
));
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/convert/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub struct TokenItem {
pub name: String,
pub symbol: String,
pub address: String,
pub decimals: u32,
pub decimals: u8,
pub logo_uri: Option<String>,
pub eip2612: Option<bool>,
}
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/fungible_price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub struct FungiblePriceItem {
pub symbol: String,
pub icon_url: String,
pub price: f64,
pub decimals: u32,
pub decimals: u8,
}

pub async fn handler(
Expand Down
18 changes: 11 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ use {
Router,
},
env::{
ArbitrumConfig, AuroraConfig, BaseConfig, BerachainConfig, BinanceConfig, GetBlockConfig,
InfuraConfig, LavaConfig, MantleConfig, MorphConfig, NearConfig, PoktConfig,
PublicnodeConfig, QuicknodeConfig, SolScanConfig, UnichainConfig, ZKSyncConfig,
ArbitrumConfig, AuroraConfig, BaseConfig, BerachainConfig, BinanceConfig, DuneConfig,
GetBlockConfig, InfuraConfig, LavaConfig, MantleConfig, MorphConfig, NearConfig,
PoktConfig, PublicnodeConfig, QuicknodeConfig, SolScanConfig, UnichainConfig, ZKSyncConfig,
ZerionConfig, ZoraConfig,
},
error::RpcResult,
http::Request,
hyper::{header::HeaderName, http, server::conn::AddrIncoming, Body, Server},
providers::{
ArbitrumProvider, AuroraProvider, BaseProvider, BerachainProvider, BinanceProvider,
GetBlockProvider, InfuraProvider, InfuraWsProvider, LavaProvider, MantleProvider,
MorphProvider, NearProvider, PoktProvider, ProviderRepository, PublicnodeProvider,
QuicknodeProvider, SolScanProvider, UnichainProvider, ZKSyncProvider, ZerionProvider,
ZoraProvider, ZoraWsProvider,
DuneProvider, GetBlockProvider, InfuraProvider, InfuraWsProvider, LavaProvider,
MantleProvider, MorphProvider, NearProvider, PoktProvider, ProviderRepository,
PublicnodeProvider, QuicknodeProvider, SolScanProvider, UnichainProvider, ZKSyncProvider,
ZerionProvider, ZoraProvider, ZoraWsProvider,
},
sqlx::postgres::PgPoolOptions,
std::{
Expand Down Expand Up @@ -527,6 +527,10 @@ fn init_providers(config: &ProvidersConfig) -> ProviderRepository {
ZerionConfig::new(config.zerion_api_key.clone()),
None,
);
providers.add_balance_provider::<DuneProvider, DuneConfig>(
DuneConfig::new(config.dune_api_key.clone()),
None,
);
providers.add_balance_provider::<SolScanProvider, SolScanConfig>(
SolScanConfig::new(config.solscan_api_v2_token.clone()),
redis_pool.clone(),
Expand Down
181 changes: 181 additions & 0 deletions src/providers/dune.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use {
super::{BalanceProvider, BalanceProviderFactory},
crate::{
env::DuneConfig,
error::{RpcError, RpcResult},
handlers::balance::{BalanceQueryParams, BalanceResponseBody},
providers::{
balance::{BalanceItem, BalanceQuantity},
ProviderKind,
},
utils::crypto,
Metrics,
},
async_trait::async_trait,
deadpool_redis::Pool,
ethers::types::U256,
phf::phf_map,
serde::{Deserialize, Serialize},
std::{sync::Arc, time::SystemTime},
tracing::log::error,
url::Url,
};

/// Native token icons, since Dune doesn't provide them yet
/// TODO: Hardcoding icon urls temporarily until Dune provides them
pub static NATIVE_TOKEN_ICONS: phf::Map<&'static str, &'static str> = phf_map! {
// Ethereum
"ETH" => "https://assets.coingecko.com/coins/images/279/small/ethereum.png",
// Polygon
"POL" => "https://assets.coingecko.com/coins/images/32440/standard/polygon.png",
// xDAI
"XDAI" => "https://assets.coingecko.com/coins/images/662/standard/logo_square_simple_300px.png",
// BNB
"BNB" => "https://assets.coingecko.com/coins/images/825/small/binance-coin-logo.png",
};

#[derive(Debug, Serialize, Deserialize)]
struct DuneResponseBody {
balances: Vec<Balance>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Balance {
chain: String,
chain_id: u64,
address: String,
amount: String,
decimals: Option<u8>,
symbol: Option<String>,
price_usd: Option<f64>,
value_usd: Option<f64>,
token_metadata: Option<Metadata>,
}

#[derive(Debug, Serialize, Deserialize, Default)]
struct Metadata {
logo: String,
}

#[derive(Debug)]
pub struct DuneProvider {
pub provider_kind: ProviderKind,
pub api_key: String,
pub http_client: reqwest::Client,
}

impl DuneProvider {
async fn send_request(&self, url: Url) -> Result<reqwest::Response, reqwest::Error> {
self.http_client
.get(url)
.header("X-Dune-Api-Key", self.api_key.clone())
.send()
.await
}
}

#[async_trait]
impl BalanceProvider for DuneProvider {
#[tracing::instrument(skip(self, params), fields(provider = "Dune"), level = "debug")]
async fn get_balance(
&self,
address: String,
params: BalanceQueryParams,
metrics: Arc<Metrics>,
) -> RpcResult<BalanceResponseBody> {
let base = format!("https://api.dune.com/api/echo/v1/balances/evm/{}", &address);
let mut url = Url::parse(&base).map_err(|_| RpcError::BalanceParseURLError)?;
url.query_pairs_mut()
.append_pair("exclude_spam_tokens", "true");
url.query_pairs_mut().append_pair("metadata", "logo");
if let Some(caip2_chain_id) = params.chain_id {
let (_, chain_id) = crypto::disassemble_caip2(&caip2_chain_id)
.map_err(|_| RpcError::InvalidParameter(caip2_chain_id))?;
url.query_pairs_mut().append_pair("chain_ids", &chain_id);
}

let latency_start = SystemTime::now();
let response = self.send_request(url.clone()).await?;
metrics.add_latency_and_status_code_for_provider(
self.provider_kind,
response.status().into(),
latency_start,
None,
Some("balances".to_string()),
);

if !response.status().is_success() {
error!(
"Error on Dune balance response. Status is not OK: {:?}",
response.status(),
);
return Err(RpcError::BalanceProviderError);
}
let body = response.json::<DuneResponseBody>().await?;

let balances_vec = body
.balances
.into_iter()
.filter_map(|mut f| {
// Skip the asset if there are no symbol, decimals, since this
// is likely a spam token
let symbol = f.symbol.take()?;
let price_usd = f.price_usd.take()?;
let decimals = f.decimals.take()?;
let caip2_chain_id = format!("eip155:{}", f.chain_id.clone());
Some(BalanceItem {
name: {
if f.address == "native" {
f.chain
} else {
symbol.clone()
}
},
symbol: symbol.clone(),
chain_id: Some(caip2_chain_id),
address: {
// Return None if the address is native for the native token
if f.address == "native" {
None
} else {
Some(format!("eip155:{}:{}", f.chain_id, f.address.clone()))
}
},
value: f.value_usd,
price: price_usd,
quantity: BalanceQuantity {
decimals: decimals.to_string(),
numeric: crypto::format_token_amount(
U256::from_dec_str(&f.amount).unwrap_or_default(),
decimals,
),
},
icon_url: {
if f.address == "native" {
NATIVE_TOKEN_ICONS.get(&symbol).unwrap_or(&"").to_string()
} else {
f.token_metadata?.logo
}
},
})
})
.collect::<Vec<_>>();

let response = BalanceResponseBody {
balances: balances_vec,
};

Ok(response)
}
}

impl BalanceProviderFactory<DuneConfig> for DuneProvider {
fn new(provider_config: &DuneConfig, _cache: Option<Arc<Pool>>) -> Self {
let http_client = reqwest::Client::new();
Self {
provider_kind: ProviderKind::Dune,
api_key: provider_config.api_key.clone(),
http_client,
}
}
}
Loading

0 comments on commit cdf95fa

Please sign in to comment.