use crate::{error::Error, network::Network, serde_override::SerdeResponseParse};
use alloy::primitives::Address;
use reqwest::Client;
use serde_json::{Value, json};
pub mod types;
use types::{AlchemyData, AlchemyTokens, TokenBalancesByWalletEntry, TokensByWalletEntry};
pub struct Alchemy {
api_key: String,
}
impl Alchemy {
pub fn new(api_key: String) -> Self {
Alchemy { api_key }
}
pub fn network(network: &Network) -> &'static str {
match network {
Network::EthereumMainnet => "eth-mainnet",
Network::EthereumSepolia => "eth-sepolia",
Network::EthereumHolesky => "eth-holesky",
Network::EthereumHoodi => "eth-hoodi",
Network::OptimismMainnet => "opt-mainnet",
Network::OptimismSepolia => "opt-sepolia",
Network::ArbitrumMainnet => "arb-mainnet",
Network::ArbitrumSepolia => "arb-sepolia",
Network::BaseMainnet => "base-mainnet",
Network::BaseSepolia => "base-sepolia",
_ => unimplemented!(),
}
}
pub async fn get_price(&self, symbol: &str) -> crate::Result<(f64, String)> {
let client = reqwest::Client::new();
let res = client
.get(format!(
"https://api.g.alchemy.com/prices/v1/{}/tokens/by-symbol?symbols={symbol}",
self.api_key
))
.header("accept", "application/json")
.send()
.await?
.json::<Value>()
.await?;
let res = res.as_object().ok_or("response not an object")?;
let data = res.get("data").ok_or("data not found in response")?;
let data = data
.as_array()
.ok_or("data not an object")?
.first()
.ok_or("data array is empty")?;
let data_symbol = data
.get("symbol")
.ok_or("symbol not found in response")?
.as_str()
.ok_or("symbol not a string")?;
if data_symbol != symbol {
return Err("symbol in response does not match requested symbol".into());
}
let prices = data
.get("prices")
.ok_or("prices not found in response")?
.as_array()
.ok_or("prices not array")?
.first()
.ok_or("prices array is empty")?
.as_object()
.ok_or("prices[0] is not object")?;
let currency = prices
.get("currency")
.ok_or("currency not found in prices[0]")?
.as_str()
.ok_or("currency not a string")?;
if currency != "usd" {
return Err("currency is not USD".into());
}
let value = prices
.get("value")
.ok_or("value not found in prices[0]")?
.as_str()
.ok_or("currency not a string")?;
let last_updated_at = prices
.get("lastUpdatedAt")
.ok_or("lastUpdatedAt not found in prices[0]")?
.as_str()
.ok_or("currency not a string")?;
Ok((value.parse()?, last_updated_at.to_string()))
}
pub async fn get_tokens_by_wallet(
&self,
address: Address,
networks: Vec<Network>,
) -> Result<Vec<TokensByWalletEntry>, Error> {
let mut result = Vec::new();
for networks in networks.chunks(5) {
let body = json!({
"addresses": [
{
"address": address,
"networks": networks.iter().map(Alchemy::network).collect::<Vec<&str>>()
}
],
"withMetadata": true,
"withPrices": true
});
let client = Client::new();
let response = client
.post(format!(
"https://api.g.alchemy.com/data/v1/{}/assets/tokens/by-address",
self.api_key
))
.header("accept", "application/json")
.header("content-type", "application/json")
.json(&body)
.send()
.await?;
let parsed = response
.serde_parse_custom::<AlchemyData<AlchemyTokens<TokensByWalletEntry>>>()
.await?;
result.extend(parsed.data.tokens);
}
Ok(result)
}
pub async fn get_token_balances_by_wallet(
&self,
address: Address,
networks: Vec<Network>,
) -> Result<Vec<TokenBalancesByWalletEntry>, Error> {
let body = json!({
"addresses": [
{
"address": address,
"networks": networks.iter().map(Alchemy::network).collect::<Vec<&str>>()
}
]
});
let client = Client::new();
let response = client
.post(format!(
"https://api.g.alchemy.com/data/v1/{}/assets/tokens/balances/by-address",
self.api_key
))
.header("accept", "application/json")
.header("content-type", "application/json")
.json(&body)
.send()
.await?;
let parsed = response
.serde_parse_custom::<AlchemyData<AlchemyTokens<TokenBalancesByWalletEntry>>>()
.await?;
Ok(parsed.data.tokens)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_price() {
let api_key = std::env::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY key not set in env");
let alchemy = Alchemy::new(api_key);
let (price, last_updated_at) = alchemy.get_price("ETH").await.unwrap();
assert!(price > 0.0);
assert!(!last_updated_at.is_empty());
}
#[tokio::test]
async fn test_get_tokens_by_wallet() {
let api_key = std::env::var("ALCHEMY_API_KEY").expect("ALCHEMY_API_KEY key not set in env");
let alchemy = Alchemy::new(api_key);
let tokens = alchemy
.get_tokens_by_wallet(
Address::ZERO,
vec![
Network::EthereumMainnet,
Network::ArbitrumMainnet,
Network::OptimismMainnet,
],
)
.await
.unwrap();
assert!(!tokens.is_empty());
}
}