Created
June 19, 2023 20:53
-
-
Save larry0x/c91cdbec507d30cf432d4a33b8036de6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! ```cargo | |
//! [dependencies] | |
//! chrono = "0.4" | |
//! csv = "1" | |
//! ethers = "2" | |
//! reqwest = { version = "0.11", features = ["json"] } | |
//! rustc-hex = "2" | |
//! serde = "1" | |
//! thiserror = "1" | |
//! tokio = { version = "1", features = ["full"] } | |
//! url = "2" | |
//! ``` | |
use std::{fs, io, sync::Arc}; | |
use chrono::{prelude::Utc, Duration, NaiveDate}; | |
use csv::Writer; | |
use ethers::{ | |
contract::abigen, | |
core::types::Address, | |
providers::{Http, Provider}, | |
}; | |
use serde::{Deserialize, Serialize}; | |
const ETHEREUM_RPC: &str = "https://mainnet.infura.io/v3/YOUR_API_KEY_HERE"; | |
const USER: &str = "YOUR_ADDRESS_HERE"; | |
const TOKEN: &str = "TOKEN_ADDRESS_HERE"; | |
const DECIMALS: u32 = 18; | |
const START_DATE: (i32, u32, u32) = (2023, 1, 1); // yyyy-mm-dd | |
const OUTPUT_FILE: &str = "./balances.csv"; | |
abigen!( | |
IERC20, | |
r#"[ | |
function balanceOf(address _owner) public view returns (uint256 balance) | |
]"#, | |
); | |
#[derive(Deserialize)] | |
struct BlockInfo { | |
pub height: u64, | |
pub timestamp: u64, | |
} | |
#[derive(Serialize)] | |
struct Row { | |
pub height: u64, | |
pub timestamp: u64, | |
pub balance: f64, | |
} | |
/// Find the Ethereum block number whose time is closest to specified time, | |
/// using DeFiLlama API. | |
async fn block_by_timestamp(timestamp: i64) -> Result<BlockInfo> { | |
reqwest::get(format!("https://coins.llama.fi/block/ethereum/{timestamp}")) | |
.await? | |
.json() | |
.await | |
.map_err(Into::into) | |
} | |
/// Query the account's ERC20 balance at the given height. | |
async fn balance_by_height( | |
token: &IERC20<Provider<Http>>, | |
user: Address, | |
height: u64, | |
) -> Result<f64> { | |
let balance_raw = token | |
.balance_of(user) | |
.block(height) | |
.call() | |
.await? | |
.as_u128(); | |
Ok(shift_decimals(balance_raw, DECIMALS)) | |
} | |
fn shift_decimals(amount_raw: u128, decimals: u32) -> f64 { | |
amount_raw as f64 / 10usize.pow(decimals) as f64 | |
} | |
#[tokio::main] | |
async fn main() -> Result<()> { | |
let client = Provider::try_from(ETHEREUM_RPC)?; | |
let client = Arc::new(client); | |
let token_addr = TOKEN.parse::<Address>()?; | |
let token = IERC20::new(token_addr, client); | |
let user_addr = USER.parse::<Address>()?; | |
let today = Utc::now() | |
.date_naive() | |
.and_hms_opt(12, 0, 0) | |
.ok_or(Error::InvalidTime)?; | |
let start_date = NaiveDate::from_ymd_opt(START_DATE.0, START_DATE.1, START_DATE.2) | |
.ok_or(Error::InvalidTime)? | |
.and_hms_opt(12, 0, 0) | |
.ok_or(Error::InvalidTime)?; | |
let mut day = start_date; | |
let mut wtr = Writer::from_writer(vec![]); | |
while day <= today { | |
let block = block_by_timestamp(day.timestamp()).await?; | |
let balance = balance_by_height(&token, user_addr, block.height).await?; | |
wtr.serialize(Row { | |
height: block.height, | |
timestamp: block.timestamp, | |
balance, | |
})?; | |
println!("block.timestamp: {}", block.timestamp); | |
println!("block.height: {}", block.height); | |
println!("balance: {}", balance); | |
day += Duration::days(1); | |
} | |
let data = String::from_utf8(wtr.into_inner()?)?; | |
fs::write(OUTPUT_FILE, data).map_err(Into::into) | |
} | |
#[derive(Debug, thiserror::Error)] | |
enum Error { | |
#[error(transparent)] | |
Contract(#[from] ethers::contract::ContractError<Provider<Http>>), | |
#[error(transparent)] | |
Csv(#[from] csv::Error), | |
#[error(transparent)] | |
CsvIntoInner(#[from] csv::IntoInnerError<Writer<Vec<u8>>>), | |
#[error(transparent)] | |
FromHex(#[from] rustc_hex::FromHexError), | |
#[error(transparent)] | |
FromUtf8(#[from] std::string::FromUtf8Error), | |
#[error(transparent)] | |
Io(#[from] io::Error), | |
#[error(transparent)] | |
Reqwest(#[from] reqwest::Error), | |
#[error(transparent)] | |
UrlParse(#[from] url::ParseError), | |
#[error("invalid time")] | |
InvalidTime, | |
} | |
type Result<T> = core::result::Result<T, Error>; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment