diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index abc48281..5b572ce6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,15 +15,15 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 with: toolchain: stable - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 with: # Add a key to prevent rust cache collision with rust.yml workflows key: 'release' - + - name: Build agents (release) run: cargo build --release @@ -43,14 +43,14 @@ jobs: type=sha - name: Login to Docker repository - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: gcr.io username: _json_key password: ${{ secrets.GCLOUD_SERVICE_KEY }} - name: Build and push container - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . push: true diff --git a/Cargo.lock b/Cargo.lock index 93609019..558f12f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,16 @@ dependencies = [ "regex", ] +[[package]] +name = "accessed-funds-calculator" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde 1.0.147", + "serde_json", + "tokio", +] + [[package]] name = "accumulator" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3cdd0628..934e8adf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,4 +21,5 @@ members = [ "tools/nomad-cli", "tools/balance-exporter", "tools/killswitch", + "tools/accessed-funds-calculator", ] diff --git a/tools/accessed-funds-calculator/CHANGELOG.md b/tools/accessed-funds-calculator/CHANGELOG.md new file mode 100644 index 00000000..21251ff4 --- /dev/null +++ b/tools/accessed-funds-calculator/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +### 1.0 + +- Added initial cli tool to calculate the total amount of recovered funds that have been accessed \ No newline at end of file diff --git a/tools/accessed-funds-calculator/Cargo.toml b/tools/accessed-funds-calculator/Cargo.toml new file mode 100644 index 00000000..ffd52d30 --- /dev/null +++ b/tools/accessed-funds-calculator/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "accessed-funds-calculator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/tools/accessed-funds-calculator/README.md b/tools/accessed-funds-calculator/README.md new file mode 100644 index 00000000..0d4227c4 --- /dev/null +++ b/tools/accessed-funds-calculator/README.md @@ -0,0 +1,28 @@ +# Accessed Funds Calculator + +A quick and simple (and hopefully portable-ish) calculator to answer the question "how much has been accessed from the contract so far" +## Requirements + +- Needs a valid Etherscan API key to be configured in your local environment, or to be included with the run command +``` +export ETHERSCAN_KEY=YOUR_KEY +``` +- Requires a valid etherscan api url +``` +export ETHERSCAN_API=https://api.etherscan.io/api +``` +- Requires a valid token price api +``` +export PRICING_API=https://api.coingecko.com/api/v3/simple/price +``` + + +## Usage +*Using the binary:* +``` +./accessed_funds_calculator +``` +*Using cargo:* +``` +cargo run -p accessed-funds-calculator +``` diff --git a/tools/accessed-funds-calculator/src/main.rs b/tools/accessed-funds-calculator/src/main.rs new file mode 100644 index 00000000..fa4c27b5 --- /dev/null +++ b/tools/accessed-funds-calculator/src/main.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; +use std::env; + +mod tokens; + +use crate::tokens::{Token, TokenName}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let address = "0xa4B86BcbB18639D8e708d6163a0c734aFcDB770c"; + let usdc = Token::get_instance_of(TokenName::Usdc); + let usdt = Token::get_instance_of(TokenName::Usdt); + let cqt = Token::get_instance_of(TokenName::Cqt); + let wbtc = Token::get_instance_of(TokenName::Wbtc); + let frax = Token::get_instance_of(TokenName::Frax); + let iag = Token::get_instance_of(TokenName::Iag); + let weth = Token::get_instance_of(TokenName::Weth); + let dai = Token::get_instance_of(TokenName::Dai); + let c3 = Token::get_instance_of(TokenName::C3); + let fxs = Token::get_instance_of(TokenName::Fxs); + let cards = Token::get_instance_of(TokenName::Cards); + let hbot = Token::get_instance_of(TokenName::Hbot); + let sdl = Token::get_instance_of(TokenName::Sdl); + let gero = Token::get_instance_of(TokenName::Gero); + + let tokens: Vec = vec![ + usdc, cqt, usdt, wbtc, frax, iag, weth, dai, c3, fxs, cards, hbot, sdl, gero, + ]; + + let mut total_accessed_value: f64 = 0.0; + for token in tokens { + let balance: f64 = get_token_balance(&token, address).await?; + let accessed: f64 = token.recovered_total - balance; + let token_price: f64 = get_token_price(&token).await?; + total_accessed_value += accessed * token_price; + println!("{}:{} accessed, price: {}", token.id, accessed, token_price); + } + + println!(); + println!("#################################################"); + println!("#################################################"); + println!("### total accessed value: ${} ###", total_accessed_value); + println!("#################################################"); + println!("#################################################"); + + Ok(()) +} + +async fn get_token_balance( + token: &Token, + address: &str, +) -> Result> { + let etherscan_url = env::var("ETHERSCAN_API")?; + let module = "account"; + let action = "tokenbalance"; + let api_key = env::var("ETHERSCAN_KEY")?; + let request_url = format!( + "{}?module={}&action={}&contractaddress={}&address={}&apiKey={}", + ðerscan_url, &module, &action, &token.contract_address, &address, &api_key + ); + let resp = reqwest::get(request_url) + .await? + .json::>() + .await?; + let balance: f64 = resp["result"].parse().unwrap(); + let balance = balance / ((10.0_f64).powf(token.decimals)); + + Ok(balance) +} + +async fn get_token_price(token: &Token) -> Result> { + let coingecko_url = env::var("PRICING_API")?; + let vs_currency = "usd"; + + let request_url = format!( + "{}?ids={}&vs_currencies={}", + coingecko_url, &token.id, vs_currency + ); + let resp = reqwest::get(request_url) + .await? + .json::>>() + .await?; + let price = resp[&token.id].clone(); + let price = price["usd"]; + + Ok(price) +} diff --git a/tools/accessed-funds-calculator/src/tokens.rs b/tools/accessed-funds-calculator/src/tokens.rs new file mode 100644 index 00000000..0b26875e --- /dev/null +++ b/tools/accessed-funds-calculator/src/tokens.rs @@ -0,0 +1,333 @@ +#[derive(Debug)] +pub enum TokenName { + Usdc, + Cqt, + Usdt, + Frax, + Wbtc, + Iag, + Weth, + Dai, + C3, + Fxs, + Cards, + Hbot, + Sdl, + Gero, +} + +#[derive(Debug)] +pub struct Token { + pub name: TokenName, + pub id: String, + pub decimals: f64, + pub contract_address: String, + pub recovered_total: f64, +} + +impl Token { + pub fn get_instance_of(token_name: TokenName) -> Token { + match token_name { + TokenName::Usdc => Token { + name: TokenName::Usdc, + id: "usd-coin".to_string(), + decimals: 6.0, + contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), + recovered_total: 12_890_538.932_401, + }, + TokenName::Cqt => Token { + name: TokenName::Cqt, + id: "covalent".to_string(), + decimals: 18.0, + contract_address: "0xD417144312DbF50465b1C641d016962017Ef6240".to_string(), + recovered_total: 34_082_775.751_599_7, + }, + TokenName::Usdt => Token { + name: TokenName::Usdt, + id: "tether".to_string(), + decimals: 6.0, + contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(), + recovered_total: 4_673_863.595_197, + }, + TokenName::Frax => Token { + name: TokenName::Frax, + id: "frax".to_string(), + decimals: 18.0, + contract_address: "0x853d955aCEf822Db058eb8505911ED77F175b99e".to_string(), + recovered_total: 2_644_469.918_609_09, + }, + TokenName::Wbtc => Token { + name: TokenName::Wbtc, + id: "wrapped-bitcoin".to_string(), + decimals: 8.0, + contract_address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599".to_string(), + recovered_total: 280.731_173_99, + }, + TokenName::Iag => Token { + name: TokenName::Iag, + id: "iagon".to_string(), + decimals: 18.0, + contract_address: "0x40EB746DEE876aC1E78697b7Ca85142D178A1Fc8".to_string(), + recovered_total: 349_507_392.187_402, + }, + TokenName::Weth => Token { + name: TokenName::Weth, + id: "weth".to_string(), + decimals: 18.0, + contract_address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), + recovered_total: 1_049.635_629_8, + }, + TokenName::Dai => Token { + name: TokenName::Dai, + id: "dai".to_string(), + decimals: 18.0, + contract_address: "0x6B175474E89094C44Da98b954EedeAC495271d0F".to_string(), + recovered_total: 866_070.756_876_35, + }, + TokenName::C3 => Token { + name: TokenName::C3, + id: "charli3".to_string(), + decimals: 18.0, + contract_address: "0xf1a91C7d44768070F711c68f33A7CA25c8D30268".to_string(), + recovered_total: 1_684_711.122_391_36, + }, + TokenName::Fxs => Token { + name: TokenName::Fxs, + id: "frax-share".to_string(), + decimals: 18.0, + contract_address: "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0".to_string(), + recovered_total: 46_895.688_044_5, + }, + TokenName::Cards => Token { + name: TokenName::Cards, + id: "cardstarter".to_string(), + decimals: 18.0, + contract_address: "0x3d6F0DEa3AC3C607B3998e6Ce14b6350721752d9".to_string(), + recovered_total: 165_005.819_480_28, + }, + TokenName::Hbot => Token { + name: TokenName::Hbot, + id: "hummingbot".to_string(), + decimals: 18.0, + contract_address: "0xE5097D9baeAFB89f9bcB78C9290d545dB5f9e9CB".to_string(), + recovered_total: 900_239.997_966, + }, + TokenName::Sdl => Token { + name: TokenName::Sdl, + id: "saddle-finance".to_string(), + decimals: 18.0, + contract_address: "0xf1Dc500FdE233A4055e25e5BbF516372BC4F6871".to_string(), + recovered_total: 9_790.824_057, + }, + TokenName::Gero => Token { + name: TokenName::Gero, + id: "gerowallet".to_string(), + decimals: 18.0, + contract_address: "0x3431F91b3a388115F00C5Ba9FdB899851D005Fb5".to_string(), + recovered_total: 23_245_641.666_183_1, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_usdc() { + let usdc = Token { + name: TokenName::Usdc, + id: "usd-coin".to_string(), + decimals: 6.0, + contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), + recovered_total: 12_890_538.932_401, + }; + let test_token = Token::get_instance_of(TokenName::Usdc); + assert_eq!(test_token.id, usdc.id); + assert_eq!(test_token.decimals, usdc.decimals); + assert_eq!(test_token.recovered_total, usdc.recovered_total); + } + #[test] + fn validate_cqt() { + let cqt = Token { + name: TokenName::Cqt, + id: "covalent".to_string(), + decimals: 18.0, + contract_address: "0xD417144312DbF50465b1C641d016962017Ef6240".to_string(), + recovered_total: 34_082_775.751_599_7, + }; + let test_token = Token::get_instance_of(TokenName::Cqt); + assert_eq!(test_token.id, cqt.id); + assert_eq!(test_token.decimals, cqt.decimals); + assert_eq!(test_token.recovered_total, cqt.recovered_total); + } + #[test] + fn validate_usdt() { + let usdt = Token { + name: TokenName::Usdt, + id: "tether".to_string(), + decimals: 6.0, + contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(), + recovered_total: 4_673_863.595_197, + }; + let test_token = Token::get_instance_of(TokenName::Usdt); + assert_eq!(test_token.id, usdt.id); + assert_eq!(test_token.decimals, usdt.decimals); + assert_eq!(test_token.recovered_total, usdt.recovered_total); + } + #[test] + fn validate_frax() { + let frax = Token { + name: TokenName::Frax, + id: "frax".to_string(), + decimals: 18.0, + contract_address: "0x853d955aCEf822Db058eb8505911ED77F175b99e".to_string(), + recovered_total: 2_644_469.918_609_09, + }; + let test_token = Token::get_instance_of(TokenName::Frax); + assert_eq!(test_token.id, frax.id); + assert_eq!(test_token.decimals, frax.decimals); + assert_eq!(test_token.recovered_total, frax.recovered_total); + } + #[test] + fn validate_wbtc() { + let wbtc = Token { + name: TokenName::Wbtc, + id: "wrapped-bitcoin".to_string(), + decimals: 8.0, + contract_address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599".to_string(), + recovered_total: 280.731_173_99, + }; + let test_token = Token::get_instance_of(TokenName::Wbtc); + assert_eq!(test_token.id, wbtc.id); + assert_eq!(test_token.decimals, wbtc.decimals); + assert_eq!(test_token.recovered_total, wbtc.recovered_total); + } + #[test] + fn validate_iag() { + let iag = Token { + name: TokenName::Iag, + id: "iagon".to_string(), + decimals: 18.0, + contract_address: "0x40EB746DEE876aC1E78697b7Ca85142D178A1Fc8".to_string(), + recovered_total: 349_507_392.187_402, + }; + let test_token = Token::get_instance_of(TokenName::Iag); + assert_eq!(test_token.id, iag.id); + assert_eq!(test_token.decimals, iag.decimals); + assert_eq!(test_token.recovered_total, iag.recovered_total); + } + #[test] + fn validate_weth() { + let weth = Token { + name: TokenName::Weth, + id: "weth".to_string(), + decimals: 18.0, + contract_address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".to_string(), + recovered_total: 1_049.635_629_8, + }; + let test_token = Token::get_instance_of(TokenName::Weth); + assert_eq!(test_token.id, weth.id); + assert_eq!(test_token.decimals, weth.decimals); + assert_eq!(test_token.recovered_total, weth.recovered_total); + } + #[test] + fn validate_dai() { + let dai = Token { + name: TokenName::Dai, + id: "dai".to_string(), + decimals: 18.0, + contract_address: "0x6B175474E89094C44Da98b954EedeAC495271d0F".to_string(), + recovered_total: 866_070.756_876_35, + }; + let test_token = Token::get_instance_of(TokenName::Dai); + assert_eq!(test_token.id, dai.id); + assert_eq!(test_token.decimals, dai.decimals); + assert_eq!(test_token.recovered_total, dai.recovered_total); + } + #[test] + fn validate_c3() { + let c3 = Token { + name: TokenName::C3, + id: "charli3".to_string(), + decimals: 18.0, + contract_address: "0xf1a91C7d44768070F711c68f33A7CA25c8D30268".to_string(), + recovered_total: 1_684_711.122_391_36, + }; + let test_token = Token::get_instance_of(TokenName::C3); + assert_eq!(test_token.id, c3.id); + assert_eq!(test_token.decimals, c3.decimals); + assert_eq!(test_token.recovered_total, c3.recovered_total); + } + #[test] + fn validate_fxs() { + let fxs = Token { + name: TokenName::Fxs, + id: "frax-share".to_string(), + decimals: 18.0, + contract_address: "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0".to_string(), + recovered_total: 46_895.688_044_5, + }; + let test_token = Token::get_instance_of(TokenName::Fxs); + assert_eq!(test_token.id, fxs.id); + assert_eq!(test_token.decimals, fxs.decimals); + assert_eq!(test_token.recovered_total, fxs.recovered_total); + } + #[test] + fn validate_cards() { + let cards = Token { + name: TokenName::Cards, + id: "cardstarter".to_string(), + decimals: 18.0, + contract_address: "0x3d6F0DEa3AC3C607B3998e6Ce14b6350721752d9".to_string(), + recovered_total: 165_005.819_480_28, + }; + let test_token = Token::get_instance_of(TokenName::Cards); + assert_eq!(test_token.id, cards.id); + assert_eq!(test_token.decimals, cards.decimals); + assert_eq!(test_token.recovered_total, cards.recovered_total); + } + #[test] + fn validate_hbot() { + let hbot = Token { + name: TokenName::Hbot, + id: "hummingbot".to_string(), + decimals: 18.0, + contract_address: "0xE5097D9baeAFB89f9bcB78C9290d545dB5f9e9CB".to_string(), + recovered_total: 900_239.997_966, + }; + let test_token = Token::get_instance_of(TokenName::Hbot); + assert_eq!(test_token.id, hbot.id); + assert_eq!(test_token.decimals, hbot.decimals); + assert_eq!(test_token.recovered_total, hbot.recovered_total); + } + #[test] + fn validate_sdl() { + let sdl = Token { + name: TokenName::Sdl, + id: "saddle-finance".to_string(), + decimals: 18.0, + contract_address: "0xf1Dc500FdE233A4055e25e5BbF516372BC4F6871".to_string(), + recovered_total: 9_790.824_057, + }; + let test_token = Token::get_instance_of(TokenName::Sdl); + assert_eq!(test_token.id, sdl.id); + assert_eq!(test_token.decimals, sdl.decimals); + assert_eq!(test_token.recovered_total, sdl.recovered_total); + } + #[test] + fn validate_gero() { + let gero = Token { + name: TokenName::Gero, + id: "gerowallet".to_string(), + decimals: 18.0, + contract_address: "0x3431F91b3a388115F00C5Ba9FdB899851D005Fb5".to_string(), + recovered_total: 23_245_641.666_183_1, + }; + let test_token = Token::get_instance_of(TokenName::Gero); + assert_eq!(test_token.id, gero.id); + assert_eq!(test_token.decimals, gero.decimals); + assert_eq!(test_token.recovered_total, gero.recovered_total); + } +}