Skip to content

Commit

Permalink
feat: add crypto service to access several chains
Browse files Browse the repository at this point in the history
  • Loading branch information
MishkaRogachev committed Sep 29, 2024
1 parent 03e6488 commit ec20db5
Show file tree
Hide file tree
Showing 15 changed files with 190 additions and 66 deletions.
34 changes: 18 additions & 16 deletions src/core/chain.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Chain {
EthereumMainnet,
Optimism,
Arbitrum,
Goerli,
Kovan,
Rinkeby,
EthereumSepolia,
OptimismMainnet,
OptimismSepolia,
ArbitrumMainnet,
ArbitrumSepolia,
}

impl Chain {
#[allow(dead_code)]
pub fn get_display_name(&self) -> &str {
match self {
Chain::EthereumMainnet => "Ethereum Mainnet",
Chain::Optimism => "Optimism",
Chain::Arbitrum => "Arbitrum",
Chain::Goerli => "Goerli Testnet",
Chain::Kovan => "Kovan Testnet",
Chain::Rinkeby => "Rinkeby Testnet",
Chain::EthereumSepolia => "Ethereum Sepolia",
Chain::OptimismMainnet => "Optimism Mainnet",
Chain::OptimismSepolia => "Optimism Sepolia",
Chain::ArbitrumMainnet => "Arbitrum Mainnet",
Chain::ArbitrumSepolia => "Arbitrum Sepolia",
}
}

pub fn is_test_network(&self) -> bool {
match self {
Chain::EthereumMainnet => false,
Chain::Optimism => false,
Chain::Arbitrum => false,
Chain::Goerli => true,
Chain::Kovan => true,
Chain::Rinkeby => true,
Chain::EthereumSepolia => true,
Chain::OptimismMainnet => false,
Chain::OptimismSepolia => true,
Chain::ArbitrumMainnet => false,
Chain::ArbitrumSepolia => true,
}
}
}
14 changes: 7 additions & 7 deletions src/core/chain_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ mod tests {
use super::super::chain::Chain;

#[test_case(Chain::EthereumMainnet, false)]
#[test_case(Chain::Optimism, false)]
#[test_case(Chain::Arbitrum, false)]
#[test_case(Chain::Goerli, true)]
#[test_case(Chain::Kovan, true)]
#[test_case(Chain::Rinkeby, true)]
#[test_case(Chain::EthereumSepolia, true)]
#[test_case(Chain::OptimismMainnet, false)]
#[test_case(Chain::OptimismSepolia, true)]
#[test_case(Chain::ArbitrumMainnet, false)]
#[test_case(Chain::ArbitrumSepolia, true)]
fn test_chain_utility(chain: Chain, is_test_net: bool) -> anyhow::Result<()> {
let infura_token = "test";
let endpoint_url = "test";

assert!(chain.get_infura_url(&infura_token).starts_with("https://"));
assert!(chain.finalize_endpoint_url(&endpoint_url).starts_with("https://"));
assert!(!chain.get_display_name().is_empty());
assert_eq!(chain.is_test_network(), is_test_net);

Expand Down
40 changes: 23 additions & 17 deletions src/core/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ use super::chain::Chain;

const CHAINLINK_ABI: &[u8] = include_bytes!("../../abi/chainlink.json");

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Balance {
pub value: f64,
pub usd_value: f64,
pub currency: String,
pub from_test_network: bool,
}
// TODO: balance timestamp

#[derive(Clone)]
pub struct Provider {
web3: Web3<Http>,
chain: Chain,
}

#[allow(dead_code)]
#[derive(Debug)]
struct PriceFeedData {
round_id: U256,
Expand All @@ -26,8 +30,8 @@ struct PriceFeedData {
}

impl Balance {
pub fn new(value: f64, usd_value: f64, currency: String) -> Self {
Self {value, usd_value, currency}
pub fn new(value: f64, usd_value: f64, currency: &str, from_test_network: bool) -> Self {
Self { value, usd_value, currency: currency.to_string(), from_test_network }
}

pub fn to_string(&self) -> String {
Expand All @@ -36,35 +40,37 @@ impl Balance {
}

impl Chain {
pub fn get_infura_url(&self, infura_token: &str) -> String {
pub fn finalize_endpoint_url(&self, endpoint_url: &str) -> String {
let chain_name = match self {
Chain::EthereumMainnet => "mainnet",
Chain::Optimism => "optimism",
Chain::Arbitrum => "arbitrum",
Chain::Goerli => "goerli",
Chain::Kovan => "kovan",
Chain::Rinkeby => "rinkeby",
Chain::EthereumSepolia => "sepolia",
Chain::OptimismMainnet => "optimism-mainnet",
Chain::OptimismSepolia => "optimism-sepolia",
Chain::ArbitrumMainnet => "arbitrum-mainnet",
Chain::ArbitrumSepolia => "arbitrum-sepolia",
};
format!("https://{}.infura.io/v3/{}", chain_name, infura_token)
format!("https://{}.{}", chain_name, endpoint_url)
}

// NOTE from https://docs.chain.link/data-feeds/price-feeds/addresses
pub fn get_chainlink_contract_address(&self) -> Address {
match self {
Chain::EthereumMainnet => "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419",
Chain::Optimism => "0x13e3Ee699D1909E989722E753853AE30b17e08c5",
Chain::Arbitrum => "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612",
Chain::Goerli => "0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e",
Chain::Kovan => "0x9326BFA02ADD2366b30bacB125260Af641031331",
Chain::Rinkeby => "0x8A753747A1Fa494EC906cE90E9f37563A8AF630e",
Chain::EthereumSepolia => "0x694AA1769357215DE4FAC081bf1f309aDC325306",
Chain::OptimismMainnet => "0x13e3Ee699D1909E989722E753853AE30b17e08c5",
Chain::OptimismSepolia => "0x61Ec26aA57019C486B10502285c5A3D4A4750AD7",
Chain::ArbitrumMainnet => "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612",
Chain::ArbitrumSepolia => "0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165",

}
.parse()
.unwrap()
}
}

impl Provider {
pub fn new(infura_token: &str, chain: Chain) -> anyhow::Result<Self> {
let transport = Http::new(&chain.get_infura_url(infura_token))?;
pub fn new(endpoint_url: &str, chain: Chain) -> anyhow::Result<Self> {
let transport = Http::new(&chain.finalize_endpoint_url(endpoint_url))?;
let web3 = Web3::new(transport);
Ok(Self {web3, chain})
}
Expand All @@ -74,7 +80,7 @@ impl Provider {
let eth = wei_to_eth(wei);
let usd_value = self.get_eth_usd_rate().await? * eth;

Ok(Balance::new(eth, usd_value, "ETH".to_string()))
Ok(Balance::new(eth, usd_value, "ETH", self.chain.is_test_network()))
}

async fn get_eth_usd_rate(&self) -> anyhow::Result<f64> {
Expand Down
8 changes: 6 additions & 2 deletions src/core/provider_test.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
#[cfg(test)]
mod tests {
use test_case::test_case;
use super::super::chain::Chain;
use super::super::provider::Provider;

#[tokio::test]
async fn test_access_web3_provider() -> anyhow::Result<()> {
#[test_case(Chain::EthereumMainnet)]
#[test_case(Chain::EthereumSepolia)]
async fn test_access_web3_provider(chain: Chain) -> anyhow::Result<()> {
let infura_token = match std::env::var("INFURA_TOKEN") {
Ok(token) => token,
Err(_) => {
eprintln!("Skipping test: INFURA_TOKEN environment variable not set");
return Ok(()); // Skip the test if the token is not set
}
};
let endpoint_url = format!("infura.io/v3/{}", infura_token);

let account = web3::types::Address::from_low_u64_be(0);
let provider = Provider::new(&infura_token, Chain::EthereumMainnet)?;
let provider = Provider::new(&endpoint_url, chain)?;

let balance = provider.get_eth_balance(account).await?;
assert_eq!(balance.currency, "ETH");
Expand Down
1 change: 0 additions & 1 deletion src/core/seed_phrase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WordCount {
Words12 = 12,
Words18 = 18,
Words24 = 24,
}

Expand Down
1 change: 1 addition & 0 deletions src/persistence/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn hash_password(password: &str) -> [u8; 32] {
key
}

#[allow(dead_code)]
pub fn generate_random_hash() -> [u8; 32] {
let mut key = [0u8; 32];
rand::thread_rng().fill(&mut key);
Expand Down
1 change: 1 addition & 0 deletions src/persistence/manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub fn list_databases(path: &PathBuf) -> anyhow::Result<Vec<Address>> {
Ok(addresses)
}

#[allow(dead_code)]
pub fn remove_all_databases(path: &Path) -> anyhow::Result<()> {
let accounts_dir = path.join(ACCOUNTS_DIR);

Expand Down
62 changes: 62 additions & 0 deletions src/service/crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::collections::HashMap;

use crate::core::{chain::Chain, provider::{Provider, Balance}};

#[derive(Clone)]
pub struct Crypto {
endpoint_url: String,
providers: HashMap<Chain, Provider>,
}

impl Crypto {
pub fn new(endpoint_url: &str) -> Self {
Self {
endpoint_url: endpoint_url.to_string(),
providers: HashMap::new()
}
}

pub fn add_chain(&mut self, chain: Chain) -> anyhow::Result<bool> {
if self.providers.contains_key(&chain) {
return Ok(false);
}
let provider = Provider::new(&self.endpoint_url, chain.clone())?;
self.providers.insert(chain, provider);
Ok(true)
}

#[allow(dead_code)]
pub fn set_chains(&mut self, chains: Vec<Chain>) -> anyhow::Result<()> {
let old_chains = self.providers.keys().cloned().collect::<Vec<_>>();
for chain in &old_chains {
if !chains.contains(&chain) {
self.providers.remove(&chain);
}
}

for chain in &chains {
if !old_chains.contains(chain) {
self.add_chain(chain.clone())?;
}
}

Ok(())
}

#[allow(dead_code)]
pub fn get_active_chains(&self) -> Vec<Chain> {
self.providers.keys().cloned().collect()
}

pub async fn get_eth_balance(&self, account: web3::types::Address) -> anyhow::Result<Balance> {
let mut summary= Balance::new(0.0, 0.0, "ETH", false);
for provider in self.providers.values() {
if let Ok(balance) = provider.get_eth_balance(account).await {
summary.value += balance.value;
summary.usd_value += balance.usd_value;
summary.from_test_network = summary.from_test_network || balance.from_test_network;
}
}
Ok(summary)
}
}
23 changes: 23 additions & 0 deletions src/service/crypto_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#[cfg(test)]
mod tests {
use crate::core::chain::Chain;
use super::super::crypto::Crypto;

#[test]
fn test_crypto_chains() -> anyhow::Result<()> {
let mut crypto = Crypto::new("http://localhost:8545");
assert_eq!(crypto.get_active_chains().len(), 0);

crypto.add_chain(Chain::EthereumMainnet)?;
assert_eq!(crypto.get_active_chains().len(), 1);

crypto.set_chains(vec![Chain::EthereumMainnet, Chain::ArbitrumMainnet])?;
assert_eq!(crypto.get_active_chains().len(), 2);

assert!(crypto.get_active_chains().contains(&Chain::EthereumMainnet));
assert!(crypto.get_active_chains().contains(&Chain::ArbitrumMainnet));
assert!(!crypto.get_active_chains().contains(&Chain::OptimismSepolia));

Ok(())
}
}
2 changes: 2 additions & 0 deletions src/service/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod session;
mod session_test;
pub mod crypto;
mod crypto_test;
13 changes: 7 additions & 6 deletions src/service/session.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::sync::Arc;

use crate::{core::{key_pair::KeyPair, seed_phrase::SeedPhrase}, persistence};
use crate::core::{key_pair::KeyPair, seed_phrase::SeedPhrase};
use crate::persistence::{db::Db, manage};

const ROOT_KEYPAIR: &[u8] = b"root_keypair";
const ROOT_SEED_PHRASE: &[u8] = b"root_seed_phrase";
Expand All @@ -12,7 +13,7 @@ const ERR_WRONG_PASSWORD_PROVIDED: &str = "Wrong password provided";
#[derive(Clone)]
pub struct Session {
pub account: web3::types::Address,
db: Arc<persistence::db::Db>
db: Arc<Db>
}

impl Session {
Expand All @@ -22,7 +23,7 @@ impl Session {
keypair.validate()?;

let account = keypair.get_address();
let db = persistence::manage::open_database(&db_path()?, account, password)?;
let db = manage::open_database(&db_path()?, account, password)?;

let words = seed_phrase.get_words();
let serialized_seed_phrase = serde_json::to_vec(&words)?;
Expand All @@ -38,7 +39,7 @@ impl Session {
}

pub fn login(account: web3::types::Address, password: &str) -> anyhow::Result<Self> {
let db = persistence::manage::open_database(&db_path()?, account, password)?;
let db = manage::open_database(&db_path()?, account, password)?;
let session = Session {
account,
db: Arc::new(db),
Expand All @@ -52,11 +53,11 @@ impl Session {
}

pub fn list_accounts() -> anyhow::Result<Vec<web3::types::Address>> {
persistence::manage::list_databases(&db_path()?)
manage::list_databases(&db_path()?)
}

pub fn remove_account(account: web3::types::Address) -> anyhow::Result<()> {
persistence::manage::remove_database(&db_path()?, account)
manage::remove_database(&db_path()?, account)
}

pub fn get_seed_phrase(&self) -> anyhow::Result<SeedPhrase> {
Expand Down
Loading

0 comments on commit ec20db5

Please sign in to comment.