Skip to content

Instantly share code, notes, and snippets.

@larry0x
Created June 19, 2023 20:53
Show Gist options
  • Save larry0x/c91cdbec507d30cf432d4a33b8036de6 to your computer and use it in GitHub Desktop.
Save larry0x/c91cdbec507d30cf432d4a33b8036de6 to your computer and use it in GitHub Desktop.
//! ```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