From b7b22df1a1d5490a8fa21a1ec6254d48a3334dd0 Mon Sep 17 00:00:00 2001 From: hopeyen <60078528+hopeyen@users.noreply.github.com> Date: Mon, 20 Mar 2023 17:54:33 +0100 Subject: [PATCH] feat: mnemonics allowed, refactor network config (#140) --- examples/ping-pong/src/main.rs | 10 +- src/config.rs | 166 +++++--------------------- src/graphcast_agent/message_typing.rs | 2 +- src/graphcast_agent/mod.rs | 8 +- src/graphql/client_graph_node.rs | 2 +- src/lib.rs | 14 ++- src/networks.rs | 128 ++++++++++++++++++++ 7 files changed, 181 insertions(+), 149 deletions(-) create mode 100644 src/networks.rs diff --git a/examples/ping-pong/src/main.rs b/examples/ping-pong/src/main.rs index c0f26d6..74c1278 100644 --- a/examples/ping-pong/src/main.rs +++ b/examples/ping-pong/src/main.rs @@ -1,11 +1,12 @@ use dotenv::dotenv; use graphcast_sdk::{ - config::{Config, NetworkName}, + config::Config, graphcast_agent::{ message_typing::GraphcastMessage, waku_handling::WakuHandlingError, GraphcastAgent, }, graphql::client_graph_node::{get_indexing_statuses, update_network_chainheads}, + networks::NetworkName, BlockPointer, }; use once_cell::sync::OnceCell; @@ -45,7 +46,7 @@ async fn main() -> ! { // subtopics are optionally provided and used as the content topic identifier of the message subject, // if not provided then they are usually generated based on indexer allocations let mut subtopics = vec!["ping-pong-content-topic".to_string()]; - for topic in config.topics { + for topic in config.topics.clone() { if !subtopics.contains(&topic) { subtopics.push(topic); } @@ -53,8 +54,9 @@ async fn main() -> ! { debug!("Initializing the Graphcast Agent"); let graphcast_agent = GraphcastAgent::new( - // private_key resolves into ethereum wallet and indexer identity. - config.private_key, + // wallet key can be either private key or mnemonic, resolves into ethereum wallet and indexer identity. + // Unwrap is okay here thanks to configuration validate_set_up + config.wallet_input().unwrap().to_string(), // radio_name is used as part of the content topic for the radio application radio_name, &config.registry_subgraph, diff --git a/src/config.rs b/src/config.rs index 0e0ca2e..01c49d3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,6 @@ use clap::Parser; -use ethers::signers::{LocalWallet, WalletError}; -use once_cell::sync::Lazy; +use ethers::signers::WalletError; use serde::{Deserialize, Serialize}; -use std::fmt; use std::fs::read_to_string; use std::str::FromStr; use tracing::{debug, info}; @@ -11,7 +9,7 @@ use waku::Multiaddr; use crate::graphql::client_graph_node::get_indexing_statuses; use crate::graphql::client_network::query_network_subgraph; use crate::graphql::client_registry::query_registry_indexer; -use crate::{graphcast_id_address, init_tracing}; +use crate::{build_wallet, graphcast_id_address, init_tracing}; #[derive(Clone, Debug, Parser, Serialize, Deserialize)] #[clap( @@ -39,9 +37,18 @@ pub struct Config { value_parser = Config::parse_key, env = "PRIVATE_KEY", hide_env_values = true, - help = "Private key to the Graphcase ID wallet", + help = "Private key to the Graphcast ID wallet (Precendence over mnemonics)", )] - pub private_key: String, + pub private_key: Option, + #[clap( + long, + value_name = "KEY", + value_parser = Config::parse_key, + env = "MNEMONIC", + hide_env_values = true, + help = "Mnemonic to the Graphcast ID wallet (first address of the wallet is used; Only one of private key or mnemonic is needed)", + )] + pub mnemonic: Option, #[clap( long, value_name = "SUBGRAPH", @@ -198,7 +205,7 @@ impl Config { /// Validate that private key as an Eth wallet fn parse_key(value: &str) -> Result { // The wallet can be stored instead of the original private key - let wallet = value.parse::()?; + let wallet = build_wallet(value)?; let addr = graphcast_id_address(&wallet); info!("Resolved Graphcast id: {}", addr); Ok(String::from(value)) @@ -224,12 +231,23 @@ impl Config { Multiaddr::from_str(address).map_err(|e| ConfigError::ValidateInput(e.to_string())) } + /// Private key takes precedence over mnemonic + pub fn wallet_input(&self) -> Result<&String, ConfigError> { + match (&self.private_key, &self.mnemonic) { + (Some(p), _) => Ok(p), + (_, Some(m)) => Ok(m), + _ => Err(ConfigError::ValidateInput( + "Must provide either private key or mnemonic".to_string(), + )), + } + } /// Asynchronous validation to the configuration set ups pub async fn validate_set_up(&self) -> Result<&Self, ConfigError> { - let wallet = self - .private_key - .parse::() - .map_err(|e| ConfigError::ValidateInput(format!("Private key links to wallet: {e}")))?; + let wallet = build_wallet(self.wallet_input()?).map_err(|e| { + ConfigError::ValidateInput(format!( + "Invalid key to wallet, use private key or mnemonic: {e}" + )) + })?; let graphcast_id = graphcast_id_address(&wallet); // TODO: Implies invalidity for both graphcast id and registry, maybe map_err more specifically let indexer = query_registry_indexer(self.registry_subgraph.to_string(), graphcast_id) @@ -258,132 +276,6 @@ impl Config { } } -/// Struct for Network and block interval for updates -#[derive(Debug, Clone)] -pub struct Network { - pub name: NetworkName, - pub interval: u64, -} - -/// List of supported networks -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum NetworkName { - Goerli, - Mainnet, - Gnosis, - Hardhat, - ArbitrumOne, - ArbitrumGoerli, - Avalanche, - Polygon, - Celo, - Optimism, - Fantom, - Unknown, -} - -impl NetworkName { - pub fn from_string(name: &str) -> Self { - match name { - "goerli" => NetworkName::Goerli, - "mainnet" => NetworkName::Mainnet, - "gnosis" => NetworkName::Gnosis, - "hardhat" => NetworkName::Hardhat, - "arbitrum-one" => NetworkName::ArbitrumOne, - "arbitrum-goerli" => NetworkName::ArbitrumGoerli, - "avalanche" => NetworkName::Avalanche, - "polygon" => NetworkName::Polygon, - "celo" => NetworkName::Celo, - "optimism" => NetworkName::Optimism, - "fantom" => NetworkName::Fantom, - _ => NetworkName::Unknown, - } - } -} - -impl fmt::Display for NetworkName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match self { - NetworkName::Goerli => "goerli", - NetworkName::Mainnet => "mainnet", - NetworkName::Gnosis => "gnosis", - NetworkName::Hardhat => "hardhat", - NetworkName::ArbitrumOne => "arbitrum-one", - NetworkName::ArbitrumGoerli => "arbitrum-goerli", - NetworkName::Avalanche => "avalanche", - NetworkName::Polygon => "polygon", - NetworkName::Celo => "celo", - NetworkName::Optimism => "optimism", - NetworkName::Fantom => "fantom", - NetworkName::Unknown => "unknown", - }; - - write!(f, "{name}") - } -} - -/// Maintained static list of supported Networks, the intervals target ~5minutes -/// depending on the blockchain average block processing time -pub static NETWORKS: Lazy> = Lazy::new(|| { - vec![ - // Goerli (Ethereum Testnet): ~15 seconds - Network { - name: NetworkName::from_string("goerli"), - interval: 20, - }, - // Mainnet (Ethereum): ~10-12 seconds - Network { - name: NetworkName::from_string("mainnet"), - interval: 30, - }, - // Gnosis: ~5 seconds - Network { - name: NetworkName::from_string("gnosis"), - interval: 60, - }, - // Local test network - Network { - name: NetworkName::from_string("hardhat"), - interval: 10, - }, - // ArbitrumOne: ~0.5-1 second - Network { - name: NetworkName::from_string("arbitrum-one"), - interval: 50, - }, - // ArbitrumGoerli (Arbitrum Testnet): ~6 seconds - Network { - name: NetworkName::from_string("arbitrum-goerli"), - interval: 60, - }, - // Avalanche: ~3-5 seconds - Network { - name: NetworkName::from_string("avalanche"), - interval: 60, - }, - // Polygon: ~2 seconds - Network { - name: NetworkName::from_string("polygon"), - interval: 150, - }, - // Celo: ~5-10 seconds - Network { - name: NetworkName::from_string("celo"), - interval: 30, - }, - // Optimism: ~10-15 seconds - Network { - name: NetworkName::from_string("optimism"), - interval: 20, - }, - // Fantom: ~2-3 seconds - Network { - name: NetworkName::from_string("optimism"), - interval: 100, - }, - ] -}); - #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("Validate the input: {0}")] diff --git a/src/graphcast_agent/message_typing.rs b/src/graphcast_agent/message_typing.rs index 67336f0..2c2d0ee 100644 --- a/src/graphcast_agent/message_typing.rs +++ b/src/graphcast_agent/message_typing.rs @@ -13,12 +13,12 @@ use tracing::debug; use waku::{Running, WakuContentTopic, WakuMessage, WakuNodeHandle, WakuPeerData, WakuPubSubTopic}; use crate::{ - config::NetworkName, graphql::{ client_graph_node::query_graph_node_network_block_hash, client_network::query_network_subgraph, client_registry::query_registry_indexer, QueryError, }, + networks::NetworkName, NoncesMap, }; diff --git a/src/graphcast_agent/mod.rs b/src/graphcast_agent/mod.rs index 4455836..2b9a3fa 100644 --- a/src/graphcast_agent/mod.rs +++ b/src/graphcast_agent/mod.rs @@ -30,12 +30,12 @@ use self::waku_handling::{ setup_node_handle, WakuHandlingError, }; -use crate::config::NetworkName; use crate::graphcast_agent::waku_handling::unsubscribe_peer; use crate::graphql::client_graph_node::query_graph_node_network_block_hash; use crate::graphql::client_registry::query_registry_indexer; use crate::graphql::QueryError; -use crate::{graphcast_id_address, NoncesMap}; +use crate::networks::NetworkName; +use crate::{build_wallet, graphcast_id_address, NoncesMap}; pub mod message_typing; pub mod waku_handling; @@ -105,7 +105,7 @@ impl GraphcastAgent { /// ``` #[allow(clippy::too_many_arguments)] pub async fn new( - private_key: String, + wallet_key: String, radio_name: &'static str, registry_subgraph: &str, network_subgraph: &str, @@ -118,7 +118,7 @@ impl GraphcastAgent { waku_port: Option, waku_addr: Option, ) -> Result { - let wallet = private_key.parse::()?; + let wallet = build_wallet(&wallet_key)?; let pubsub_topic: WakuPubSubTopic = pubsub_topic(graphcast_namespace); //Should we allow the setting of waku node host and port? diff --git a/src/graphql/client_graph_node.rs b/src/graphql/client_graph_node.rs index e2ba8c1..c033f7f 100644 --- a/src/graphql/client_graph_node.rs +++ b/src/graphql/client_graph_node.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use crate::graphql::QueryError; use crate::NetworkPointer; -use crate::{config::NetworkName, BlockPointer}; +use crate::{networks::NetworkName, BlockPointer}; use graphql_client::{GraphQLQuery, Response}; use serde_derive::{Deserialize, Serialize}; use tracing::{debug, trace, warn}; diff --git a/src/lib.rs b/src/lib.rs index 4ac55b5..c819ee9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,10 +16,12 @@ //! For more explanation, see the crate documentation. //! -use config::{NetworkName, NETWORKS}; -use ethers::signers::{Signer, Wallet}; +use ethers::signers::{ + coins_bip39::English, LocalWallet, MnemonicBuilder, Signer, Wallet, WalletError, +}; use ethers_core::k256::ecdsa::SigningKey; use graphcast_agent::message_typing::GraphcastMessage; +use networks::{NetworkName, NETWORKS}; use once_cell::sync::OnceCell; use prost::Message; @@ -42,6 +44,7 @@ pub mod bots; pub mod config; pub mod graphcast_agent; pub mod graphql; +pub mod networks; type NoncesMap = HashMap>; @@ -78,6 +81,13 @@ pub fn config_env_var(name: &str) -> Result { env::var(name).map_err(|e| format!("{name}: {e}")) } +/// Build Wallet from Private key or Mnemonic +pub fn build_wallet(value: &str) -> Result, WalletError> { + value + .parse::() + .or(MnemonicBuilder::::default().phrase(value).build()) +} + /// Get the graphcastID address from the wallet pub fn graphcast_id_address(wallet: &Wallet) -> String { debug!("{}", format!("Wallet address: {:?}", wallet.address())); diff --git a/src/networks.rs b/src/networks.rs new file mode 100644 index 0000000..34721fb --- /dev/null +++ b/src/networks.rs @@ -0,0 +1,128 @@ +use once_cell::sync::Lazy; +use std::fmt; + +/// Struct for Network and block interval for updates +#[derive(Debug, Clone)] +pub struct Network { + pub name: NetworkName, + pub interval: u64, +} + +/// List of supported networks +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum NetworkName { + Goerli, + Mainnet, + Gnosis, + Hardhat, + ArbitrumOne, + ArbitrumGoerli, + Avalanche, + Polygon, + Celo, + Optimism, + Fantom, + Unknown, +} + +impl NetworkName { + pub fn from_string(name: &str) -> Self { + match name { + "goerli" => NetworkName::Goerli, + "mainnet" => NetworkName::Mainnet, + "gnosis" => NetworkName::Gnosis, + "hardhat" => NetworkName::Hardhat, + "arbitrum-one" => NetworkName::ArbitrumOne, + "arbitrum-goerli" => NetworkName::ArbitrumGoerli, + "avalanche" => NetworkName::Avalanche, + "polygon" => NetworkName::Polygon, + "celo" => NetworkName::Celo, + "optimism" => NetworkName::Optimism, + "fantom" => NetworkName::Fantom, + _ => NetworkName::Unknown, + } + } +} + +impl fmt::Display for NetworkName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + NetworkName::Goerli => "goerli", + NetworkName::Mainnet => "mainnet", + NetworkName::Gnosis => "gnosis", + NetworkName::Hardhat => "hardhat", + NetworkName::ArbitrumOne => "arbitrum-one", + NetworkName::ArbitrumGoerli => "arbitrum-goerli", + NetworkName::Avalanche => "avalanche", + NetworkName::Polygon => "polygon", + NetworkName::Celo => "celo", + NetworkName::Optimism => "optimism", + NetworkName::Fantom => "fantom", + NetworkName::Unknown => "unknown", + }; + + write!(f, "{name}") + } +} + +/// Maintained static list of supported Networks, the intervals target ~5minutes +/// depending on the blockchain average block processing time +pub static NETWORKS: Lazy> = Lazy::new(|| { + vec![ + // Goerli (Ethereum Testnet): ~15 seconds + Network { + name: NetworkName::from_string("goerli"), + interval: 20, + }, + // Mainnet (Ethereum): ~10-12 seconds + Network { + name: NetworkName::from_string("mainnet"), + interval: 30, + }, + // Gnosis: ~5 seconds + Network { + name: NetworkName::from_string("gnosis"), + interval: 60, + }, + // Local test network + Network { + name: NetworkName::from_string("hardhat"), + interval: 10, + }, + // ArbitrumOne: ~0.25-1 second + Network { + name: NetworkName::from_string("arbitrum-one"), + interval: 600, + }, + // ArbitrumGoerli (Arbitrum Testnet): ~.6 seconds + Network { + name: NetworkName::from_string("arbitrum-goerli"), + interval: 500, + }, + // Avalanche: ~3-5 seconds + Network { + name: NetworkName::from_string("avalanche"), + interval: 60, + }, + // Polygon: ~2 seconds + Network { + name: NetworkName::from_string("polygon"), + interval: 150, + }, + // Celo: ~5-10 seconds + Network { + name: NetworkName::from_string("celo"), + interval: 30, + }, + // Optimism: ~10-15 seconds + Network { + name: NetworkName::from_string("optimism"), + interval: 20, + }, + // Fantom: ~2-3 seconds + Network { + name: NetworkName::from_string("optimism"), + interval: 100, + }, + ] +});