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}; #[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 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 let body = json!({
142 "addresses": [
143 {
144 "address": address,
145 "networks": networks
146 }
147 ],
148 "withMetadata": true,
149 "withPrices": true
150 });
151
152 let client = Client::new();
154
155 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() .await?;
165
166 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 let body = json!({
185 "addresses": [
186 {
187 "address": address,
188 "networks": ["eth-mainnet", "base-mainnet", "matic-mainnet"]
189 }
190 ]
191 });
192
193 let client = Client::new();
195
196 let api_key = Config::alchemy_api_key()?;
197
198 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() .await? .json::<Value>() .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}