diff --git a/hermes/Cargo.lock b/hermes/Cargo.lock index 9595b863..d5512973 100644 --- a/hermes/Cargo.lock +++ b/hermes/Cargo.lock @@ -136,6 +136,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.72" @@ -877,12 +925,58 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.10.0", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.66", + "quote 1.0.31", + "syn 2.0.26", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -1764,7 +1858,7 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermes" -version = "0.1.20" +version = "0.1.21" dependencies = [ "anyhow", "async-trait", @@ -1774,6 +1868,7 @@ dependencies = [ "borsh 0.10.3", "byteorder", "chrono", + "clap 4.4.4", "dashmap", "derive_more", "env_logger 0.10.0", @@ -1799,7 +1894,6 @@ dependencies = [ "solana-account-decoder", "solana-client", "solana-sdk", - "structopt", "strum", "tokio", "tower-http", @@ -5001,7 +5095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76b0c923e89c64ed398964ec2a3ddecacb5a712664d19812ebbf65eefd599145" dependencies = [ "chrono", - "clap", + "clap 2.34.0", "rpassword", "solana-perf", "solana-remote-wallet", @@ -5040,7 +5134,7 @@ dependencies = [ "bincode", "bs58", "bytes", - "clap", + "clap 2.34.0", "crossbeam-channel", "enum_dispatch", "futures", @@ -5105,7 +5199,7 @@ checksum = "c9aae7a9b16c3e81b6532c3a5d886c58ee0cb518ee6b417f28ea0cfee972ac60" dependencies = [ "bincode", "byteorder", - "clap", + "clap 2.34.0", "crossbeam-channel", "log", "serde", @@ -5197,7 +5291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3093c7701cc7d3603a32aec204e5a2e4935f71b37eabd7302d5b67bdb4ea0a02" dependencies = [ "bincode", - "clap", + "clap 2.34.0", "crossbeam-channel", "log", "nix 0.23.2", @@ -5615,28 +5709,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "structopt" -version = "0.3.26" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2 1.0.66", - "quote 1.0.31", - "syn 1.0.109", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" @@ -6345,6 +6421,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "utoipa" version = "3.4.3" diff --git a/hermes/Cargo.toml b/hermes/Cargo.toml index ee4cb341..de3c0452 100644 --- a/hermes/Cargo.toml +++ b/hermes/Cargo.toml @@ -1,7 +1,8 @@ [package] -name = "hermes" -version = "0.1.20" -edition = "2021" +name = "hermes" +version = "0.1.21" +description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle." +edition = "2021" [dependencies] async-trait = { version = "0.1.73" } @@ -33,7 +34,7 @@ serde_json = { version = "1.0.93" } serde_qs = { version = "0.12.0", features = ["axum"] } serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" } sha3 = { version = "0.10.4" } -structopt = { version = "0.3.26" } +clap = { version = "4.4.4", features = ["derive", "env", "cargo"] } strum = { version = "0.24.1", features = ["derive"] } tokio = { version = "1.26.0", features = ["full"] } tower-http = { version = "0.4.0", features = ["cors"] } diff --git a/hermes/src/config.rs b/hermes/src/config.rs index 5af55314..9716594a 100644 --- a/hermes/src/config.rs +++ b/hermes/src/config.rs @@ -1,98 +1,58 @@ -use { - libp2p::Multiaddr, - reqwest::Url, - solana_sdk::pubkey::Pubkey, - std::net::SocketAddr, - structopt::StructOpt, +use clap::{ + crate_authors, + crate_description, + crate_name, + crate_version, + Args, + Parser, }; -const DEFAULT_NETWORK_ID: &str = "/wormhole/mainnet/2"; -const DEFAULT_WORMHOLE_BOOTSTRAP_ADDRS: &str = "/dns4/wormhole-mainnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWQp644DK27fd3d4Km3jr7gHiuJJ5ZGmy8hH4py7fP4FP7,/dns4/wormhole-v2-mainnet-bootstrap.xlabs.xyz/udp/8999/quic/p2p/12D3KooWNQ9tVrcb64tw6bNs2CaNrUGPM7yRrKvBBheQ5yCyPHKC"; -const DEFAULT_WORMHOLE_LISTEN_ADDRS: &str = "/ip4/0.0.0.0/udp/30910/quic,/ip6/::/udp/30910/quic"; -const DEFAULT_API_ADDR: &str = "127.0.0.1:33999"; +mod benchmarks; +mod pythnet; +mod rpc; +mod wormhole; -/// `Options` is a structup definition to provide clean command-line args for Hermes. -#[derive(StructOpt, Debug)] -#[structopt(name = "hermes", about = "Hermes")] +// `Options` is a structup definition to provide clean command-line args for Hermes. +#[derive(Parser, Debug)] +#[command(name = crate_name!())] +#[command(author = crate_authors!())] +#[command(about = crate_description!())] +#[command(version = crate_version!())] +#[allow(clippy::large_enum_variant)] pub enum Options { - /// Run the hermes service. + /// Run the Hermes Price Service. Run(RunOptions), + + /// Show Overridden Environment Variables. + ShowEnv(ShowEnvOptions), } -#[derive(Clone, Debug, StructOpt)] +#[derive(Args, Clone, Debug)] pub struct RunOptions { /// Wormhole Options. - #[structopt(flatten)] - pub wormhole: WormholeOptions, + #[command(flatten)] + pub wormhole: wormhole::Options, /// PythNet Options - #[structopt(flatten)] - pub pythnet: PythNetOptions, + #[command(flatten)] + pub pythnet: pythnet::Options, /// RPC Options - #[structopt(flatten)] - pub rpc: RpcOptions, + #[command(flatten)] + pub rpc: rpc::Options, /// Benchmarks Options - #[structopt(flatten)] - pub benchmarks: BenchmarksOptions, + #[command(flatten)] + pub benchmarks: benchmarks::Options, } -#[derive(Clone, Debug, StructOpt)] -pub struct WormholeOptions { - /// Multiaddresses for Wormhole bootstrap peers (separated by comma). - #[structopt(long)] - #[structopt(use_delimiter = true)] - #[structopt(default_value = DEFAULT_WORMHOLE_BOOTSTRAP_ADDRS)] - #[structopt(env = "WORMHOLE_BOOTSTRAP_ADDRS")] - pub bootstrap_addrs: Vec, - - /// Address of the Wormhole contract on the target PythNet cluster. - #[structopt(long)] - #[structopt(default_value = "H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU")] - #[structopt(env = "WORMHOLE_CONTRACT_ADDR")] - pub contract_addr: Pubkey, - - /// Multiaddresses to bind Wormhole P2P to (separated by comma) - #[structopt(long)] - #[structopt(use_delimiter = true)] - #[structopt(default_value = DEFAULT_WORMHOLE_LISTEN_ADDRS)] - #[structopt(env = "WORMHOLE_LISTEN_ADDRS")] - pub listen_addrs: Vec, - - /// Network ID for Wormhole - #[structopt(long)] - #[structopt(default_value = DEFAULT_NETWORK_ID)] - #[structopt(env = "WORMHOLE_NETWORK_ID")] - pub network_id: String, -} - -#[derive(Clone, Debug, StructOpt)] -pub struct PythNetOptions { - /// Address of a PythNet compatible websocket RPC endpoint. - #[structopt(long)] - #[structopt(env = "PYTHNET_WS_ENDPOINT")] - pub ws_endpoint: String, - - /// Addres of a PythNet compatible HTP RPC endpoint. - #[structopt(long)] - #[structopt(env = "PYTHNET_HTTP_ENDPOINT")] - pub http_endpoint: String, -} - -#[derive(Clone, Debug, StructOpt)] -pub struct RpcOptions { - /// Address to bind the API server to. - #[structopt(long)] - #[structopt(default_value = DEFAULT_API_ADDR)] - #[structopt(env = "API_ADDR")] - pub addr: SocketAddr, -} - -#[derive(Clone, Debug, StructOpt)] -pub struct BenchmarksOptions { - /// Benchmarks endpoint to retrieve historical update data from. - #[structopt(long)] - #[structopt(env = "BENCHMARKS_ENDPOINT")] - pub endpoint: Option, +#[derive(Args, Clone, Debug)] +pub struct ShowEnvOptions { + /// Show Hermes environment variables. + /// + /// By default this command will attempt to read the variable from the environment and fall + /// back to the argument default if not present. Set this flag if you want only the defaults + /// and to ignore the current environment. + #[arg(long = "defaults")] + pub defaults: bool, } diff --git a/hermes/src/config/benchmarks.rs b/hermes/src/config/benchmarks.rs new file mode 100644 index 00000000..75f7115c --- /dev/null +++ b/hermes/src/config/benchmarks.rs @@ -0,0 +1,14 @@ +use { + clap::Args, + reqwest::Url, +}; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Benchmark Options")] +#[group(id = "Benchmarks")] +pub struct Options { + /// Benchmarks endpoint to retrieve historical update data from. + #[arg(long = "benchmarks-endpoint")] + #[arg(env = "BENCHMARKS_ENDPOINT")] + pub endpoint: Option, +} diff --git a/hermes/src/config/pythnet.rs b/hermes/src/config/pythnet.rs new file mode 100644 index 00000000..53d0f6c1 --- /dev/null +++ b/hermes/src/config/pythnet.rs @@ -0,0 +1,16 @@ +use clap::Args; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Pythnet Options")] +#[group(id = "Pythnet")] +pub struct Options { + /// Address of a PythNet compatible websocket RPC endpoint. + #[arg(long = "pythnet-ws-addr")] + #[arg(env = "PYTHNET_WS_ENDPOINT")] + pub ws_endpoint: String, + + /// Addres of a PythNet compatible HTP RPC endpoint. + #[arg(long = "pythnet-http-addr")] + #[arg(env = "PYTHNET_HTTP_ENDPOINT")] + pub http_endpoint: String, +} diff --git a/hermes/src/config/rpc.rs b/hermes/src/config/rpc.rs new file mode 100644 index 00000000..6700d399 --- /dev/null +++ b/hermes/src/config/rpc.rs @@ -0,0 +1,17 @@ +use { + clap::Args, + std::net::SocketAddr, +}; + +const DEFAULT_RPC_ADDR: &str = "127.0.0.1:33999"; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "RPC Options")] +#[group(id = "RPC")] +pub struct Options { + /// Address and port the RPC server will bind to. + #[arg(long = "rpc-listen-addr")] + #[arg(default_value = DEFAULT_RPC_ADDR)] + #[arg(env = "RPC_ADDR")] + pub addr: SocketAddr, +} diff --git a/hermes/src/config/wormhole.rs b/hermes/src/config/wormhole.rs new file mode 100644 index 00000000..2b62c5c5 --- /dev/null +++ b/hermes/src/config/wormhole.rs @@ -0,0 +1,48 @@ +use { + clap::Args, + libp2p::Multiaddr, + solana_sdk::pubkey::Pubkey, +}; + +const DEFAULT_LISTEN_ADDRS: &str = "/ip4/0.0.0.0/udp/30910/quic,/ip6/::/udp/30910/quic"; +const DEFAULT_CONTRACT_ADDR: &str = "H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU"; +const DEFAULT_NETWORK_ID: &str = "/wormhole/mainnet/2"; +const DEFAULT_BOOTSTRAP_ADDRS: &str = concat![ + "/dns4/wormhole-mainnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWQp644DK27fd3d4Km3jr7gHiuJJ5ZGmy8hH4py7fP4FP7,", + "/dns4/wormhole-v2-mainnet-bootstrap.xlabs.xyz/udp/8999/quic/p2p/12D3KooWNQ9tVrcb64tw6bNs2CaNrUGPM7yRrKvBBheQ5yCyPHKC", +]; + +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Wormhole Options")] +#[group(id = "Wormhole")] +pub struct Options { + /// Multiaddresses for Wormhole bootstrap peers (separated by comma). + /// + /// Bootstraps can be found from the official Wormhole repository, note that these addresses + /// are only used to bootstrap peer discovery and are not necessarily used for data transfer. + /// Adding more peers will speed up P2P peer discovery. + #[arg(long = "wormhole-bootstrap-addrs")] + #[arg(value_delimiter = ',')] + #[arg(default_value = DEFAULT_BOOTSTRAP_ADDRS)] + #[arg(env = "WORMHOLE_BOOTSTRAP_ADDRS")] + pub bootstrap_addrs: Vec, + + /// Address of the Wormhole contract on the target PythNet cluster. + #[arg(long = "wormhole-contract-addr")] + #[arg(default_value = DEFAULT_CONTRACT_ADDR)] + #[arg(env = "WORMHOLE_CONTRACT_ADDR")] + pub contract_addr: Pubkey, + + /// Multiaddresses to bind for Wormhole P2P (separated by comma) + #[arg(long = "wormhole-listen-addrs")] + #[arg(value_delimiter = ',')] + #[arg(default_value = DEFAULT_LISTEN_ADDRS)] + #[arg(env = "WORMHOLE_LISTEN_ADDRS")] + pub listen_addrs: Vec, + + /// Network ID for Wormhole + #[arg(long = "wormhole-network-id")] + #[arg(default_value = DEFAULT_NETWORK_ID)] + #[arg(env = "WORMHOLE_NETWORK_ID")] + pub network_id: String, +} diff --git a/hermes/src/main.rs b/hermes/src/main.rs index a8e94928..0006ba8d 100644 --- a/hermes/src/main.rs +++ b/hermes/src/main.rs @@ -4,12 +4,15 @@ use { crate::state::State, anyhow::Result, + clap::{ + CommandFactory, + Parser, + }, futures::future::join_all, std::{ io::IsTerminal, sync::atomic::AtomicBool, }, - structopt::StructOpt, tokio::spawn, }; @@ -37,7 +40,7 @@ async fn init() -> Result<()> { // Parse the command line arguments with StructOpt, will exit automatically on `--help` or // with invalid arguments. - match config::Options::from_args() { + match config::Options::parse() { config::Options::Run(opts) => { tracing::info!("Starting hermes service..."); @@ -68,6 +71,33 @@ async fn init() -> Result<()> { task??; } } + + config::Options::ShowEnv(opts) => { + // For each subcommand, scan for arguments that allow overriding with an ENV variable + // and print that variable. + for subcommand in config::Options::command().get_subcommands() { + for arg in subcommand.get_arguments() { + if let Some(env) = arg.get_env().and_then(|env| env.to_str()) { + // Find the defaults for this argument, if present. + let defaults = arg + .get_default_values() + .iter() + .map(|v| v.to_str().unwrap()) + .collect::>() + .join(","); + + println!( + "{}={}", + env, + match opts.defaults { + true => defaults, + false => std::env::var(env).unwrap_or(defaults), + } + ); + } + } + } + } } Ok(())