Skip to main content

gm_lib/
alchemy.rs

1use alloy::primitives::{Address, U256};
2use reqwest::Client;
3use serde::{Deserialize, Serialize};
4use serde_json::{json, Value};
5
6use crate::{disk::Config, error::Error, utils::SerdeResponseParse}; // for building the JSON body
7
8#[derive(Serialize, Deserialize, Debug)]
9pub struct TokensByWalletEntry {
10    pub address: Address,
11    pub network: String,
12    #[serde(rename = "tokenAddress")]
13    pub token_address: Option<Address>,
14    #[serde(rename = "tokenBalance")]
15    pub token_balance: U256,
16    #[serde(rename = "tokenMetadata")]
17    pub token_metadata: TokenMetadata,
18    #[serde(rename = "tokenPrices")]
19    pub token_prices: Vec<TokenPricesEntry>,
20}
21
22#[derive(Serialize, Deserialize, Debug)]
23pub struct TokenMetadata {
24    pub symbol: Option<String>,
25    #[serde(default)]
26    pub decimals: Option<u8>,
27    pub name: Option<String>,
28    pub logo: Option<String>,
29}
30
31#[derive(Serialize, Deserialize, Debug)]
32pub struct TokenPricesEntry {
33    pub currency: String,
34    pub value: String,
35    #[serde(rename = "lastUpdatedAt")]
36    pub last_updated_at: String,
37}
38
39#[derive(Serialize, Deserialize, Debug)]
40pub struct TokenBalancesByWalletEntry {
41    address: Address,
42    network: String,
43    #[serde(rename = "tokenAddress")]
44    token_address: Address,
45    #[serde(rename = "tokenBalance")]
46    token_balance: U256,
47}
48
49#[derive(Serialize, Deserialize, Debug)]
50pub struct AlchemyData<T> {
51    pub data: T,
52}
53
54#[derive(Serialize, Deserialize, Debug)]
55pub struct TokensByWallet {
56    pub tokens: Vec<TokensByWalletEntry>,
57}
58
59pub struct Alchemy;
60
61impl Alchemy {
62    pub async fn get_price(symbol: &str) -> crate::Result<(f64, String)> {
63        let api_key = Config::alchemy_api_key()?;
64
65        let client = reqwest::Client::new();
66
67        let res = client
68            .get(format!(
69                "https://api.g.alchemy.com/prices/v1/{api_key}/tokens/by-symbol?symbols={symbol}"
70            ))
71            .header("accept", "application/json")
72            .send()
73            .await?
74            .json::<Value>()
75            .await?;
76        let res = res.as_object().ok_or("response not an object")?;
77
78        let data = res.get("data").ok_or("data not found in response")?;
79
80        let data = data
81            .as_array()
82            .ok_or("data not an object")?
83            .first()
84            .ok_or("data array is empty")?;
85
86        let data_symbol = data
87            .get("symbol")
88            .ok_or("symbol not found in response")?
89            .as_str()
90            .ok_or("symbol not a string")?;
91
92        if data_symbol != symbol {
93            return Err("symbol in response does not match requested symbol".into());
94        }
95
96        let prices = data
97            .get("prices")
98            .ok_or("prices not found in response")?
99            .as_array()
100            .ok_or("prices not array")?
101            .first()
102            .ok_or("prices array is empty")?
103            .as_object()
104            .ok_or("prices[0] is not object")?;
105
106        let currency = prices
107            .get("currency")
108            .ok_or("currency not found in prices[0]")?
109            .as_str()
110            .ok_or("currency not a string")?;
111
112        if currency != "usd" {
113            return Err("currency is not USD".into());
114        }
115
116        let value = prices
117            .get("value")
118            .ok_or("value not found in prices[0]")?
119            .as_str()
120            .ok_or("currency not a string")?;
121
122        let last_updated_at = prices
123            .get("lastUpdatedAt")
124            .ok_or("lastUpdatedAt not found in prices[0]")?
125            .as_str()
126            .ok_or("currency not a string")?;
127
128        Ok((value.parse()?, last_updated_at.to_string()))
129    }
130
131    // docs: https://docs.alchemy.com/reference/get-tokens-by-address
132    pub async fn get_tokens_by_wallet(
133        address: Address,
134        networks: Vec<String>,
135    ) -> Result<Vec<TokensByWalletEntry>, Error> {
136        let api_key = Config::alchemy_api_key()?;
137
138        let mut result = Vec::new();
139        for networks in networks.chunks(5) {
140            // Build the request body using serde_json::json! macro:
141            let body = json!({
142                "addresses": [
143                    {
144                        "address": address,
145                        "networks": networks
146                    }
147                ],
148                "withMetadata": true,
149                "withPrices": true
150            });
151
152            // Initialize the reqwest Client
153            let client = Client::new();
154
155            // Make the POST request
156            let response = client
157                .post(format!(
158                    "https://api.g.alchemy.com/data/v1/{api_key}/assets/tokens/by-address"
159                ))
160                .header("accept", "application/json")
161                .header("content-type", "application/json")
162                .json(&body) // send JSON body
163                .send() // execute the request
164                .await?;
165
166            // let text = response.text().await?;
167
168            // Err(Error::InternalError(format!("Response: {:?}", text)))?;
169
170            let parsed = response
171                .serde_parse_custom::<AlchemyData<TokensByWallet>>()
172                .await?;
173
174            result.extend(parsed.data.tokens);
175        }
176
177        Ok(result)
178    }
179
180    pub async fn get_token_balances_by_wallet(
181        address: Address,
182    ) -> Result<Vec<TokenBalancesByWalletEntry>, Error> {
183        // Build the request body using serde_json::json! macro:
184        let body = json!({
185            "addresses": [
186                {
187                    "address": address,
188                    "networks": ["eth-mainnet", "base-mainnet", "matic-mainnet"]
189                }
190            ]
191        });
192
193        // Initialize the reqwest Client
194        let client = Client::new();
195
196        let api_key = Config::alchemy_api_key()?;
197
198        // Make the POST request
199        let response = client
200            .post(format!(
201                "https://api.g.alchemy.com/data/v1/{api_key}/assets/tokens/balances/by-address"
202            ))
203            .header("accept", "application/json")
204            .header("content-type", "application/json")
205            .json(&body) // send JSON body
206            .send() // execute the request
207            .await? // await the response
208            .json::<Value>() // Parse the JSON into serde_json::Value
209            .await?;
210
211        let response = response
212            .get("data")
213            .expect("'data' not present in response")
214            .get("tokens")
215            .expect("'tokens' not present in response");
216
217        let parsed: Vec<TokenBalancesByWalletEntry> = serde_json::from_value(response.clone())
218            .map_err(|e| {
219                crate::Error::SerdeJsonWithValue(Box::new(e), Box::new(response.clone()))
220            })?;
221        Ok(parsed)
222    }
223}