diff --git a/crates/interledger-settlement-engines/Cargo.toml b/crates/interledger-settlement-engines/Cargo.toml index 4cbf86bbc..4e63d5396 100644 --- a/crates/interledger-settlement-engines/Cargo.toml +++ b/crates/interledger-settlement-engines/Cargo.toml @@ -37,6 +37,7 @@ sha3 = "0.8.2" num-bigint = "0.2.2" num-traits = "0.2.8" lazy_static = "1.3.0" +config = "0.9.3" [dev-dependencies] lazy_static = "1.3" diff --git a/crates/interledger-settlement-engines/src/main.rs b/crates/interledger-settlement-engines/src/main.rs index 061ed8258..9c3e920d6 100644 --- a/crates/interledger-settlement-engines/src/main.rs +++ b/crates/interledger-settlement-engines/src/main.rs @@ -1,106 +1,266 @@ -use clap::{value_t, App, Arg, SubCommand}; +use clap::{crate_version, App, AppSettings, Arg, ArgMatches, SubCommand}; +use config::{Config, ConfigError, Source, Value}; +use serde::Deserialize; +use std::ffi::{OsStr, OsString}; use std::str::FromStr; use tokio; use url::Url; use interledger_settlement_engines::engines::ethereum_ledger::{run_ethereum_engine, EthAddress}; -#[allow(clippy::cognitive_complexity)] pub fn main() { env_logger::init(); let mut app = App::new("interledger-settlement-engines") .about("Interledger Settlement Engines CLI") + .version(crate_version!()) + .setting(AppSettings::SubcommandsNegateReqs) + .after_help("") .subcommands(vec![ SubCommand::with_name("ethereum-ledger") .about("Ethereum settlement engine which performs ledger (layer 1) transactions") - .args(&[ - Arg::with_name("port") - .long("port") - .help("Port to listen for settlement requests on") - .default_value("3000"), - Arg::with_name("key") - .long("key") - .help("private key for settlement account") - .takes_value(true) - .required(true), - Arg::with_name("ethereum_endpoint") - .long("ethereum_endpoint") - .help("Ethereum node endpoint") - .default_value("http://127.0.0.1:8545"), - Arg::with_name("token_address") - .long("token_address") - .help("The address of the ERC20 token to be used for settlement (defaults to sending ETH if no token address is provided)") - .default_value(""), - Arg::with_name("connector_url") - .long("connector_url") - .help("Connector Settlement API endpoint") - .default_value("http://127.0.0.1:7771"), - Arg::with_name("redis_uri") - .long("redis_uri") - .help("Redis database to add the account to") - .default_value("redis://127.0.0.1:6379"), - Arg::with_name("chain_id") - .long("chain_id") - .help("The chain id so that the signer calculates the v value of the sig appropriately") - .default_value("1"), - Arg::with_name("confirmations") - .long("confirmations") - .help("The number of confirmations the engine will wait for a transaction's inclusion before it notifies the node of its success") - .default_value("6"), - Arg::with_name("asset_scale") - .long("asset_scale") - .help("The asset scale you want to use for your payments (default: 18)") - .default_value("18"), - Arg::with_name("poll_frequency") - .long("poll_frequency") - .help("The frequency in milliseconds at which the engine will check the blockchain about the confirmation status of a tx") - .default_value("5000"), - Arg::with_name("watch_incoming") - .long("watch_incoming") - .help("Launch a blockchain watcher that listens for incoming transactions and notifies the connector upon sufficient confirmations") - .default_value("true"), - ]) - ] - ); - - match app.clone().get_matches().subcommand() { - ("ethereum-ledger", Some(matches)) => { - let settlement_port = - value_t!(matches, "port", u16).expect("port for settlement engine required"); - // TODO make compatible with - // https://github.com/tendermint/signatory to have HSM sigs - let private_key: String = value_t!(matches, "key", String).unwrap(); - let ethereum_endpoint: String = value_t!(matches, "ethereum_endpoint", String).unwrap(); - let token_address = value_t!(matches, "token_address", String).unwrap(); - let token_address = if token_address.len() == 20 { - Some(EthAddress::from_str(&token_address).unwrap()) - } else { - None + .setting(AppSettings::SubcommandsNegateReqs) + .args(&[ + Arg::with_name("config") + .takes_value(true) + .index(1) + .help("Name of config file (in JSON, TOML, YAML, or INI format)"), + Arg::with_name("port") + .long("port") + .short("p") + .takes_value(true) + .default_value("3000") + .help("Port to listen for settlement requests on"), + Arg::with_name("key") + .long("key") + .takes_value(true) + .required(true) + .help("private key for settlement account"), + Arg::with_name("ethereum_endpoint") + .long("ethereum_endpoint") + .takes_value(true) + .default_value("http://127.0.0.1:8545") + .help("Ethereum node endpoint. For example, the address of `ganache`"), + Arg::with_name("token_address") + .long("token_address") + .takes_value(true) + .default_value("") + .help("The address of the ERC20 token to be used for settlement (defaults to sending ETH if no token address is provided)"), + Arg::with_name("connector_url") + .long("connector_url") + .takes_value(true) + .help("Connector Settlement API endpoint") + .default_value("http://127.0.0.1:7771"), + Arg::with_name("redis_uri") + .long("redis_uri") + .takes_value(true) + .default_value("redis://127.0.0.1:6379") + .help("Redis database to add the account to"), + Arg::with_name("chain_id") + .long("chain_id") + .takes_value(true) + .default_value("1") + .help("The chain id so that the signer calculates the v value of the sig appropriately"), + Arg::with_name("confirmations") + .long("confirmations") + .takes_value(true) + .default_value("6") + .help("The number of confirmations the engine will wait for a transaction's inclusion before it notifies the node of its success"), + Arg::with_name("asset_scale") + .long("asset_scale") + .takes_value(true) + .default_value("18") + .help("The asset scale you want to use for your payments"), + Arg::with_name("poll_frequency") + .long("poll_frequency") + .takes_value(true) + .default_value("5000") + .help("The frequency in milliseconds at which the engine will check the blockchain about the confirmation status of a tx"), + Arg::with_name("watch_incoming") + .long("watch_incoming") + .default_value("true") + .help("Launch a blockchain watcher that listens for incoming transactions and notifies the connector upon sufficient confirmations"), + ]) + ]); + + let mut config = get_env_config("ilp"); + if let Ok(path) = merge_config_file(app.clone(), &mut config) { + set_app_env(&config, &mut app, &path,path.len()); + } + + let matches = app.clone().get_matches(); + let runner = Runner::new(); + match matches.subcommand() { + ("ethereum-ledger", Some(ethereum_ledger_matches)) => { + merge_args(&mut config, ðereum_ledger_matches); + runner.run(get_or_error(config.try_into::())); + } + ("", None) => app.print_help().unwrap(), + _ => unreachable!(), + } +} + +fn merge_config_file(mut app: App, config: &mut Config) -> Result, ()> { + // not to cause `required fields error`. + reset_required(&mut app); + let matches = app.get_matches_safe(); + if matches.is_err() { + // if app could not get any appropriate match, just return not to show help etc. + return Err(()); + } + let matches = &matches.unwrap(); + let mut path = Vec::::new(); + let subcommand = get_deepest_command(matches, &mut path); + if let Some(config_path) = subcommand.value_of("config") { + let file_config = config::File::with_name(config_path); + let file_config = file_config.collect().unwrap(); + + // if the key is not defined in the given config already, set it to the config + // because the original values override the ones from the config file + for (k, v) in file_config { + if config.get_str(&k).is_err() { + config.set(&k, v).unwrap(); + } + } + } + Ok(path) +} + +fn merge_args(config: &mut Config, matches: &ArgMatches) { + for (key, value) in &matches.args { + if config.get_str(key).is_ok() { + continue; + } + if value.vals.is_empty() { + // flag + config.set(key, Value::new(None, true)).unwrap(); + } else { + // value + config + .set(key, Value::new(None, value.vals[0].to_str().unwrap())) + .unwrap(); + } + } +} + +// retrieve Config from a certain prefix +// if the prefix is `ilp`, `address` is resolved to `ilp_address` +fn get_env_config(prefix: &str) -> Config { + let mut config = Config::new(); + config + .merge(config::Environment::with_prefix(prefix)) + .unwrap(); + + if prefix.to_lowercase() == "ilp" { + if let Ok(value) = config.get_str("address") { + config.set("ilp_address", value).unwrap(); + } + } + + config +} + +// sets env value into each optional value +// only applied to the specified last command +fn set_app_env(env_config: &Config, app: &mut App, path: &Vec, depth: usize) { + if depth == 1 { + for item in &mut app.p.opts { + if let Ok(value) = env_config.get_str(&item.b.name.to_lowercase()) { + item.v.env = Some((&OsStr::new(item.b.name), Some(OsString::from(value)))); + } + } + return; + } + for subcommand in &mut app.p.subcommands { + if subcommand.get_name() == path[path.len() - depth] { + set_app_env(env_config, subcommand, path, depth - 1); + } + } +} + +fn get_deepest_command<'a>(matches: &'a ArgMatches, path: &mut Vec) -> &'a ArgMatches<'a> { + let (name, subcommand_matches) = matches.subcommand(); + path.push(name.to_string()); + if let Some(matches) = subcommand_matches { + return get_deepest_command(matches, path); + } + matches +} + +fn reset_required(app: &mut App) { + app.p.required.clear(); + for subcommand in &mut app.p.subcommands { + reset_required(subcommand); + } +} + +fn get_or_error(item: Result) -> T { + match item { + Ok(item) => item, + Err(error) => { + match error { + ConfigError::Message(message) => eprintln!("Configuration error: {:?}", message), + _ => eprintln!("{:?}", error), }; - let connector_url: String = value_t!(matches, "connector_url", String).unwrap(); - let redis_uri = value_t!(matches, "redis_uri", String).expect("redis_uri is required"); - let redis_uri = Url::parse(&redis_uri).expect("redis_uri is not a valid URI"); - let chain_id = value_t!(matches, "chain_id", u8).unwrap(); - let confirmations = value_t!(matches, "confirmations", u8).unwrap(); - let asset_scale = value_t!(matches, "asset_scale", u8).unwrap(); - let poll_frequency = value_t!(matches, "poll_frequency", u64).unwrap(); - let watch_incoming = value_t!(matches, "watch_incoming", bool).unwrap(); - - tokio::run(run_ethereum_engine( - redis_uri, - ethereum_endpoint, - settlement_port, - private_key, - chain_id, - confirmations, - asset_scale, - poll_frequency, - connector_url, - token_address, - watch_incoming, - )); + std::process::exit(1); } - _ => app.print_help().unwrap(), } } + +struct Runner {} + +impl Runner { + fn new () -> Runner { + Runner {} + } +} + +trait Runnable { + fn run(&self, opt: T); +} + +impl Runnable for Runner { + fn run(&self, opt: EthereumLedgerOpt) { + let token_address: String = opt.token_address.clone(); + let token_address = if token_address.len() == 20 { + Some(EthAddress::from_str(&token_address).unwrap()) + } else { + None + }; + let redis_uri = Url::parse(&opt.redis_uri).expect("redis_uri is not a valid URI"); + + // TODO make key compatible with + // https://github.com/tendermint/signatory to have HSM sigs + + tokio::run(run_ethereum_engine( + redis_uri, + opt.ethereum_endpoint.clone(), + opt.port, + opt.key.clone(), + opt.chain_id, + opt.confirmations, + opt.asset_scale, + opt.poll_frequency, + opt.connector_url.clone(), + token_address, + opt.watch_incoming, + )); + } +} + +#[derive(Deserialize, Clone)] +struct EthereumLedgerOpt { + port: u16, + key: String, + ethereum_endpoint: String, + token_address: String, + connector_url: String, + redis_uri: String, + // Although the length of `chain_id` seems to be not limited on its specs, + // u8 seems sufficient at this point. + chain_id: u8, + confirmations: u8, + asset_scale: u8, + poll_frequency: u64, + watch_incoming: bool, +} diff --git a/crates/interledger/Cargo.toml b/crates/interledger/Cargo.toml index de00f7738..922cde154 100644 --- a/crates/interledger/Cargo.toml +++ b/crates/interledger/Cargo.toml @@ -6,6 +6,7 @@ description = "Interledger client library" license = "Apache-2.0" edition = "2018" repository = "https://github.com/interledger-rs/interledger-rs" +default-run = "interledger" [lib] name = "interledger" diff --git a/crates/interledger/src/cli.rs b/crates/interledger/src/cli.rs index 7f7cb6c9d..13a1e1a2d 100644 --- a/crates/interledger/src/cli.rs +++ b/crates/interledger/src/cli.rs @@ -49,7 +49,7 @@ pub fn send_spsp_payment_btp( receiver: &str, amount: u64, quiet: bool, -) -> impl Future { +) -> impl Future + Send { let receiver = receiver.to_string(); let btp_server = parse_btp_url(btp_server).unwrap(); let account = AccountBuilder::new(LOCAL_ILP_ADDRESS.clone(), LOCAL_USERNAME.clone()) @@ -118,7 +118,7 @@ pub fn send_spsp_payment_http( receiver: &str, amount: u64, quiet: bool, -) -> impl Future { +) -> impl Future + Send { let receiver = receiver.to_string(); let url = Url::parse(http_server).expect("Cannot parse HTTP URL"); let account = if let Some(token) = url.password() { diff --git a/crates/interledger/src/main.rs b/crates/interledger/src/main.rs index c3e0d16e9..2aadcb426 100644 --- a/crates/interledger/src/main.rs +++ b/crates/interledger/src/main.rs @@ -1,18 +1,19 @@ use base64; -use clap::value_t; -use clap::{App, Arg, ArgGroup, SubCommand}; -use config; +use clap::{crate_version, App, AppSettings, Arg, ArgGroup, ArgMatches, SubCommand}; +use config::Value; +use config::{Config, ConfigError, Source}; use futures::future::Future; use hex; use interledger::{cli::*, node::*}; use interledger_ildcp::IldcpResponseBuilder; use interledger_packet::Address; use interledger_service::Username; +use serde::Deserialize; +use std::ffi::{OsStr, OsString}; use std::str::FromStr; use tokio; use url::Url; -#[allow(clippy::cognitive_complexity)] pub fn main() { env_logger::init(); @@ -21,15 +22,24 @@ pub fn main() { random_token(), random_token() ); + let mut app = App::new("interledger") .about("Blazing fast Interledger CLI written in Rust") + .version(crate_version!()) + .setting(AppSettings::SubcommandsNegateReqs) + .after_help("") .subcommands(vec![ SubCommand::with_name("spsp") .about("Client and Server for the Simple Payment Setup Protocol (SPSP)") + .setting(AppSettings::SubcommandsNegateReqs) .subcommands(vec![ SubCommand::with_name("server") .about("Run an SPSP Server that automatically accepts incoming money") .args(&[ + Arg::with_name("config") + .takes_value(true) + .index(1) + .help("Name of config file (in JSON, TOML, YAML, or INI format)"), Arg::with_name("port") .long("port") .short("p") @@ -46,10 +56,12 @@ pub fn main() { Arg::with_name("ilp_address") .long("ilp_address") .takes_value(true) + .required(true) .help("The server's ILP address (Required for ilp_over_http)"), Arg::with_name("incoming_auth_token") .long("incoming_auth_token") .takes_value(true) + .required(true) .help("Token that must be used to authenticate incoming requests (Required for ilp_over_http)"), Arg::with_name("quiet") .long("quiet") @@ -59,6 +71,10 @@ pub fn main() { SubCommand::with_name("pay") .about("Send an SPSP payment") .args(&[ + Arg::with_name("config") + .takes_value(true) + .index(1) + .help("Name of config file (in JSON, TOML, YAML, or INI format)"), Arg::with_name("btp_server") .long("btp_server") .takes_value(true) @@ -84,84 +100,143 @@ pub fn main() { .help("Suppress log output"), ]), ]), - SubCommand::with_name("moneyd") - .about("Run a local connector that exposes a BTP server with open signup") - .subcommand(SubCommand::with_name("local") - .about("Run locally without connecting to a remote connector") - .args(&[ - Arg::with_name("port") - .long("port") - .default_value("7768") - .help("Port to listen for BTP connections on"), - Arg::with_name("ilp_address") - .long("ilp_address") - .default_value("private.local"), - Arg::with_name("asset_code") - .long("asset_code") - .default_value("XYZ"), - Arg::with_name("asset_scale") - .long("asset_scale") - .default_value("9"), - ]) - ), - SubCommand::with_name("node") - .about("Run an Interledger node (sender, connector, receiver bundle)") - .arg(Arg::with_name("config") - .long("config") - .short("c") + SubCommand::with_name("moneyd") + .about("Run a local connector that exposes a BTP server with open signup") + .setting(AppSettings::SubcommandsNegateReqs) + .subcommand(SubCommand::with_name("local") + .about("Run locally without connecting to a remote connector") + .args(&[ + Arg::with_name("config") + .takes_value(true) + .index(1) + .help("Name of config file (in JSON, TOML, YAML, or INI format)"), + Arg::with_name("port") + .long("port") + .short("p") + .default_value("7768") + .help("Port to listen for BTP connections on"), + Arg::with_name("ilp_address") + .long("ilp_address") + .default_value("private.local"), + Arg::with_name("asset_code") + .long("asset_code") + .default_value("XYZ"), + Arg::with_name("asset_scale") + .long("asset_scale") + .help("Scale of the asset this account's balance is denominated in (a scale of 2 means that 100.50 will be represented as 10050) Refer to https://bit.ly/2ZlOy9n") + .default_value("9"), + ]) + ), + SubCommand::with_name("node") + .about("Run an Interledger node (sender, connector, receiver bundle)") + .setting(AppSettings::SubcommandsNegateReqs) + .args(&[ + Arg::with_name("config") + .takes_value(true) + .index(1) + .help("Name of config file (in JSON, TOML, YAML, or INI format)"), + Arg::with_name("ilp_address") + .long("ilp_address") + .takes_value(true) + .required(true) + .help("ILP Address of this account"), + Arg::with_name("secret_seed") + .long("secret_seed") + .takes_value(true) + .required(true) + .help("Root secret used to derive encryption keys. This MUST NOT be changed after once you started up the node. You could obtain randomly generated one using `openssl rand -hex 32`"), + Arg::with_name("admin_auth_token") + .long("admin_auth_token") + .takes_value(true) + .required(true) + .help("HTTP Authorization token for the node admin (sent as a Bearer token). Refer to: https://bit.ly/2Lk4xgF https://bit.ly/2XaUiBb"), + Arg::with_name("redis_connection") + .long("redis_connection") + .takes_value(true) + .default_value("redis://127.0.0.1:6379") + .help("Redis URI (for example, \"redis://127.0.0.1:6379\" or \"unix:/tmp/redis.sock\")"), + Arg::with_name("http_address") + .long("http_address") + .takes_value(true) + .help("IP address and port to listen for HTTP connections. This is used for both the API and ILP over HTTP packets. ILP over HTTP is a means to transfer ILP packets instead of BTP connections"), + Arg::with_name("settlement_address") + .long("settlement_address") .takes_value(true) - .help("Name of config file (in JSON, TOML, YAML, or INI format)")) - .subcommand(SubCommand::with_name("accounts") - .subcommand(SubCommand::with_name("add") + .help("IP address and port to listen for the Settlement Engine API"), + Arg::with_name("btp_address") + .long("btp_address") + .takes_value(true) + .help("IP address and port to listen for BTP connections"), + Arg::with_name("default_spsp_account") + .long("default_spsp_account") + .takes_value(true) + .help("When SPSP payments are sent to the root domain, the payment pointer is resolved to /.well-known/pay. This value determines which account those payments will be sent to."), + Arg::with_name("route_broadcast_interval") + .long("route_broadcast_interval") + .takes_value(true) + .help("Interval, defined in milliseconds, on which the node will broadcast routing information to other nodes using CCP. Defaults to 30000ms (30 seconds)."), + ]) + .subcommand(SubCommand::with_name("accounts") + .setting(AppSettings::SubcommandsNegateReqs) + .subcommand(SubCommand::with_name("add") .args(&[ + Arg::with_name("config") + .takes_value(true) + .index(1) + .help("Name of config file (in JSON, TOML, YAML, or INI format)"), Arg::with_name("redis_uri") .long("redis_uri") - .help("Redis database to add the account to") - .default_value("redis://127.0.0.1:6379"), + .default_value("redis://127.0.0.1:6379") + .help("Redis database to add the account to"), Arg::with_name("server_secret") .long("server_secret") - .help("Cryptographic seed used to derive keys") .takes_value(true) - .required(true), + .required(true) + .help("Cryptographic seed used to derive keys"), Arg::with_name("ilp_address") .long("ilp_address") - .help("ILP Address of this account") .takes_value(true) - .required(true), + .required(true) + .help("ILP Address of this account"), + Arg::with_name("username") + .long("username") + .takes_value(true) + .required(true) + .help("Username of this account"), Arg::with_name("asset_code") .long("asset_code") - .help("Asset that this account's balance is denominated in") .takes_value(true) - .required(true), + .required(true) + .help("Asset that this account's balance is denominated in"), Arg::with_name("asset_scale") .long("asset_scale") - .help("Scale of the asset this account's balance is denominated in (a scale of 2 means that 100.50 will be represented as 10050)") .takes_value(true) - .required(true), + .required(true) + .help("Scale of the asset this account's balance is denominated in (a scale of 2 means that 100.50 will be represented as 10050) Refer to https://bit.ly/2ZlOy9n"), Arg::with_name("btp_incoming_token") .long("btp_incoming_token") - .help("BTP token this account will use to connect") - .takes_value(true), + .takes_value(true) + .help("BTP token this account will use to connect"), Arg::with_name("btp_uri") .long("btp_uri") - .help("URI of a BTP server or moneyd that this account should use to connect") - .takes_value(true), + .takes_value(true) + .help("URI of a BTP server or moneyd that this account should use to connect"), Arg::with_name("http_url") - .help("URL of the ILP-Over-HTTP endpoint that should be used when sending outgoing requests to this account") .long("http_url") - .takes_value(true), + .takes_value(true) + .help("URL of the ILP-Over-HTTP endpoint that should be used when sending outgoing requests to this account"), Arg::with_name("http_incoming_token") .long("http_incoming_token") - .help("Bearer token this account will use to authenticate HTTP requests sent to this server") - .takes_value(true), + .takes_value(true) + .help("Bearer token this account will use to authenticate HTTP requests sent to this server"), Arg::with_name("settle_threshold") .long("settle_threshold") - .help("Threshold, denominated in the account's asset and scale, at which an outgoing settlement should be sent") - .takes_value(true), + .takes_value(true) + .help("Threshold, denominated in the account's asset and scale, at which an outgoing settlement should be sent"), Arg::with_name("settle_to") .long("settle_to") - .help("The amount that should be left after a settlement is triggered and sent (a negative value indicates that more should be sent than what is already owed)") - .takes_value(true), + .takes_value(true) + .help("The amount that should be left after a settlement is triggered and sent (a negative value indicates that more should be sent than what is already owed)"), Arg::with_name("send_routes") .long("send_routes") .help("Whether to broadcast routes to this account"), @@ -170,203 +245,372 @@ pub fn main() { .help("Whether to accept route broadcasts from this account"), Arg::with_name("routing_relation") .long("routing_relation") - .help("Either 'Parent', 'Peer', or 'Child' to indicate our relationship to this account (used for routing)") - .default_value("Child"), + .default_value("Child") + .help("Either 'Parent', 'Peer', or 'Child' to indicate our relationship to this account (used for routing)"), Arg::with_name("min_balance") .long("min_balance") - .help("Minimum balance this account is allowed to have (can be negative)") - .default_value("0"), + .default_value("0") + .help("Minimum balance this account is allowed to have (can be negative)"), Arg::with_name("round_trip_time") .long("round_trip_time") - .help("The estimated amount of time (in milliseconds) we expect it to take to send a message to this account and receive the response") - .default_value("500"), + .default_value("500") + .help("The estimated amount of time (in milliseconds) we expect it to take to send a message to this account and receive the response"), Arg::with_name("packets_per_minute_limit") .long("packets_per_minute_limit") - .help("Number of outgoing Prepare packets per minute this account can send. Defaults to no limit") - .takes_value(true), + .takes_value(true) + .help("Number of outgoing Prepare packets per minute this account can send. Defaults to no limit"), Arg::with_name("amount_per_minute_limit") .long("amount_per_minute_limit") - .help("Total amount of value this account can send per minute. Defaults to no limit") - .takes_value(true), - ]))), + .takes_value(true) + .help("Total amount of value this account can send per minute. Defaults to no limit"), + ]) + ) + ) ]); - match app.clone().get_matches().subcommand() { - ("spsp", Some(matches)) => match matches.subcommand() { - ("server", Some(matches)) => { - let port = value_t!(matches, "port", u16).expect("Invalid port"); - let quiet = matches.is_present("quiet"); - if matches.is_present("ilp_over_http") { - let client_address = - value_t!(matches, "ilp_address", String).expect("ilp_address is required"); - let client_address = Address::from_str(&client_address).unwrap(); - let auth_token = value_t!(matches, "incoming_auth_token", String) - .expect("incoming_auth_token is required"); - let ildcp_info = IldcpResponseBuilder { - client_address: &client_address, - asset_code: "", - asset_scale: 0, - } - .build(); - tokio::run(run_spsp_server_http( - ildcp_info, - ([127, 0, 0, 1], port).into(), - auth_token, - quiet, - )); - } else { - let btp_server = value_t!(matches, "btp_server", String) - .expect("BTP Server URL is required"); - tokio::run(run_spsp_server_btp( - &btp_server, - ([0, 0, 0, 0], port).into(), - quiet, - )); - } - } - ("pay", Some(matches)) => { - let receiver = value_t!(matches, "receiver", String).expect("Receiver is required"); - let amount = value_t!(matches, "amount", u64).expect("Invalid amount"); - let quiet = matches.is_present("quiet"); + let mut config = get_env_config("ilp"); + if let Ok(path) = merge_config_file(app.clone(), &mut config) { + set_app_env(&config, &mut app, &path,path.len()); + } - // Check for http_server first because btp_server has the default value of connecting to moneyd - if let Ok(http_server) = value_t!(matches, "http_server", String) { - tokio::run(send_spsp_payment_http( - &http_server, - &receiver, - amount, - quiet, - )); - } else if let Ok(btp_server) = value_t!(matches, "btp_server", String) { - tokio::run(send_spsp_payment_btp(&btp_server, &receiver, amount, quiet)); - } else { - panic!("Must specify either btp_server or http_server"); - } + let matches = app.clone().get_matches(); + let runner = Runner::new(); + match matches.subcommand() { + ("spsp", Some(spsp_matches)) => match spsp_matches.subcommand() { + ("server", Some(spsp_server_matches)) => { + merge_args(&mut config, &spsp_server_matches); + runner.run(get_or_error(config.try_into::())); + } + ("pay", Some(spsp_pay_matches)) => { + merge_args(&mut config, &spsp_pay_matches); + runner.run(get_or_error(config.try_into::())); } - _ => app.print_help().unwrap(), + _ => println!("{}", spsp_matches.usage()), }, - ("moneyd", Some(matches)) => match matches.subcommand() { - ("local", Some(matches)) => { - let btp_port = value_t!(matches, "port", u16).expect("btp_port is required"); - let ilp_address = - value_t!(matches, "ilp_address", String).expect("ilp_address is required"); - let ilp_address = Address::from_str(&ilp_address).unwrap(); - let asset_code = - value_t!(matches, "asset_code", String).expect("asset_code is required"); - let asset_scale = - value_t!(matches, "asset_scale", u8).expect("asset_scale is required"); - - let ildcp_info = IldcpResponseBuilder { - client_address: &ilp_address, - asset_code: &asset_code, - asset_scale, - } - .build(); - tokio::run(run_moneyd_local( - ([127, 0, 0, 1], btp_port).into(), - ildcp_info, - )); + ("moneyd", Some(moneyd_matches)) => match moneyd_matches.subcommand() { + ("local", Some(moneyd_local_matches)) => { + merge_args(&mut config, &moneyd_local_matches); + runner.run(get_or_error(config.try_into::())); } - _ => app.print_help().unwrap(), + _ => println!("{}", moneyd_matches.usage()), }, - ("node", Some(matches)) => match matches.subcommand() { - ("accounts", Some(matches)) => match matches.subcommand() { - ("add", Some(matches)) => { - let (http_endpoint, http_outgoing_token) = - if let Some(url) = matches.value_of("http_url") { - let url = Url::parse(url).expect("Invalid URL"); - let auth = if !url.username().is_empty() { - Some(format!( - "Basic {}", - base64::encode(&format!( - "{}:{}", - url.username(), - url.password().unwrap_or("") - )) - )) - } else if let Some(password) = url.password() { - Some(format!("Bearer {}", password)) - } else { - None - }; - (Some(url.to_string()), auth) - } else { - (None, None) - }; - let redis_uri = - value_t!(matches, "redis_uri", String).expect("redis_uri is required"); - let redis_uri = Url::parse(&redis_uri).expect("redis_uri is not a valid URI"); - let server_secret: [u8; 32] = { - let encoded: String = value_t!(matches, "server_secret", String).unwrap(); - let mut server_secret = [0; 32]; - let decoded = - hex::decode(encoded).expect("server_secret must be hex-encoded"); - assert_eq!(decoded.len(), 32, "server_secret must be 32 bytes"); - server_secret.clone_from_slice(&decoded); - server_secret - }; - let account = AccountDetails { - ilp_address: Address::from_str( - &value_t!(matches, "ilp_address", String).unwrap(), - ) - .unwrap(), - username: Username::from_str( - &value_t!(matches, "username", String).unwrap(), - ) - .unwrap(), - asset_code: value_t!(matches, "asset_code", String).unwrap(), - asset_scale: value_t!(matches, "asset_scale", u8).unwrap(), - btp_incoming_token: matches - .value_of("btp_incoming_token") - .map(|s| s.to_string()), - btp_uri: matches.value_of("btp_uri").map(|s| s.to_string()), - http_incoming_token: matches - .value_of("http_incoming_token") - .map(|s| format!("Bearer {}", s)), - http_outgoing_token, - http_endpoint, - max_packet_amount: u64::max_value(), - min_balance: value_t!(matches, "min_balance", i64).ok(), - settle_threshold: value_t!(matches, "settle_threshold", i64).ok(), - settle_to: value_t!(matches, "settle_to", i64).ok(), - send_routes: matches.is_present("send_routes"), - receive_routes: matches.is_present("receive_routes"), - routing_relation: value_t!(matches, "routing_relation", String).ok(), - round_trip_time: value_t!(matches, "round_trip_time", u32).ok(), - packets_per_minute_limit: value_t!( - matches, - "packets_per_minute_limit", - u32 - ) - .ok(), - amount_per_minute_limit: value_t!(matches, "amount_per_minute_limit", u64) - .ok(), - settlement_engine_url: None, - }; - tokio::run( - insert_account_redis(redis_uri, &server_secret, account) - .and_then(move |_| Ok(())), - ); + ("node", Some(node_matches)) => match node_matches.subcommand() { + ("accounts", Some(node_accounts_matches)) => match node_accounts_matches.subcommand() { + ("add", Some(node_accounts_add_matches)) => { + merge_args(&mut config, &node_accounts_add_matches); + runner.run(get_or_error(config.try_into::())); } - _ => app.print_help().unwrap(), + _ => println!("{}", node_accounts_matches.usage()), }, _ => { - let mut node_config = config::Config::new(); - if let Some(config_path) = matches.value_of("config") { - node_config - .merge(config::File::with_name(config_path)) - .unwrap(); - } - node_config - .merge(config::Environment::with_prefix("ILP")) - .unwrap(); - - let node: InterledgerNode = node_config - .try_into() - .expect("Must provide config file name or config environment variables"); - node.run(); + merge_args(&mut config, &node_matches); + get_or_error(config.try_into::()).run(); } }, - _ => app.print_help().unwrap(), + ("", None) => app.print_help().unwrap(), + _ => unreachable!(), + } +} + +fn merge_config_file(mut app: App, config: &mut Config) -> Result, ()> { + // not to cause `required fields error`. + reset_required(&mut app); + let matches = app.get_matches_safe(); + if matches.is_err() { + // if app could not get any appropriate match, just return not to show help etc. + return Err(()); + } + let matches = &matches.unwrap(); + let mut path = Vec::::new(); + let subcommand = get_deepest_command(matches, &mut path); + if let Some(config_path) = subcommand.value_of("config") { + let file_config = config::File::with_name(config_path); + let file_config = file_config.collect().unwrap(); + + // if the key is not defined in the given config already, set it to the config + // because the original values override the ones from the config file + for (k, v) in file_config { + if config.get_str(&k).is_err() { + config.set(&k, v).unwrap(); + } + } + } + Ok(path) +} + +fn merge_args(config: &mut Config, matches: &ArgMatches) { + for (key, value) in &matches.args { + if config.get_str(key).is_ok() { + continue; + } + if value.vals.is_empty() { + // flag + config.set(key, Value::new(None, true)).unwrap(); + } else { + // value + config + .set(key, Value::new(None, value.vals[0].to_str().unwrap())) + .unwrap(); + } + } +} + +// retrieve Config from a certain prefix +// if the prefix is `ilp`, `address` is resolved to `ilp_address` +fn get_env_config(prefix: &str) -> Config { + let mut config = Config::new(); + config + .merge(config::Environment::with_prefix(prefix)) + .unwrap(); + + if prefix.to_lowercase() == "ilp" { + if let Ok(value) = config.get_str("address") { + config.set("ilp_address", value).unwrap(); + } + } + + config +} + +// sets env value into each optional value +// only applied to the specified last command +fn set_app_env(env_config: &Config, app: &mut App, path: &Vec, depth: usize) { + if depth == 1 { + for item in &mut app.p.opts { + if let Ok(value) = env_config.get_str(&item.b.name.to_lowercase()) { + item.v.env = Some((&OsStr::new(item.b.name), Some(OsString::from(value)))); + } + } + return; + } + for subcommand in &mut app.p.subcommands { + if subcommand.get_name() == path[path.len() - depth] { + set_app_env(env_config, subcommand, path, depth - 1); + } + } +} + +fn get_deepest_command<'a>(matches: &'a ArgMatches, path: &mut Vec) -> &'a ArgMatches<'a> { + let (name, subcommand_matches) = matches.subcommand(); + path.push(name.to_string()); + if let Some(matches) = subcommand_matches { + return get_deepest_command(matches, path); + } + matches +} + +fn reset_required(app: &mut App) { + app.p.required.clear(); + for subcommand in &mut app.p.subcommands { + reset_required(subcommand); + } +} + +fn get_or_error(item: Result) -> T { + match item { + Ok(item) => item, + Err(error) => { + match error { + ConfigError::Message(message) => eprintln!("Configuration error: {:?}", message), + _ => eprintln!("{:?}", error), + }; + std::process::exit(1); + } + } +} + +struct Runner {} + +impl Runner { + fn new () -> Runner { + Runner {} + } +} + +trait Runnable { + fn run(&self, opt: T); +} + +impl Runnable for Runner { + fn run(&self, opt: SpspServerOpt) { + if opt.ilp_over_http { + let ildcp_info = IldcpResponseBuilder { + client_address: &Address::from_str(&opt.ilp_address).unwrap(), + asset_code: "", + asset_scale: 0, + } + .build(); + tokio::run(run_spsp_server_http( + ildcp_info, + ([127, 0, 0, 1], opt.port).into(), + opt.incoming_auth_token.clone(), + opt.quiet, + )); + } else { + tokio::run(run_spsp_server_btp( + &opt.btp_server, + ([0, 0, 0, 0], opt.port).into(), + opt.quiet, + )); + } + } +} + +impl Runnable for Runner { + fn run(&self, opt: SpspPayOpt) { + // Check for http_server first because btp_server has the default value of connecting to moneyd + if let Some(http_server) = &opt.http_server { + tokio::run(send_spsp_payment_http( + http_server, + &opt.receiver, + opt.amount, + opt.quiet, + )); + } else if let Some(btp_server) = &opt.btp_server { + tokio::run(send_spsp_payment_btp( + btp_server, + &opt.receiver, + opt.amount, + opt.quiet, + )); + } else { + panic!("Must specify either btp_server or http_server"); + } + } +} + +impl Runnable for Runner { + fn run(&self, opt: MoneydLocalOpt) { + let ildcp_info = IldcpResponseBuilder { + client_address: &Address::from_str(&opt.ilp_address).unwrap(), + asset_code: &opt.asset_code, + asset_scale: opt.asset_scale, + } + .build(); + tokio::run(run_moneyd_local( + ([127, 0, 0, 1], opt.port).into(), + ildcp_info, + )); } } + +impl Runnable for Runner { + fn run(&self, opt: NodeAccountsAddOpt) { + let (http_endpoint, http_outgoing_token) = if let Some(url) = &opt.http_url { + let url = Url::parse(url).expect("Invalid URL"); + let auth = if !url.username().is_empty() { + Some(format!( + "Basic {}", + base64::encode(&format!( + "{}:{}", + url.username(), + url.password().unwrap_or("") + )) + )) + } else if let Some(password) = url.password() { + Some(format!("Bearer {}", password)) + } else { + None + }; + (Some(url.to_string()), auth) + } else { + (None, None) + }; + let redis_uri = Url::parse(&opt.redis_uri).expect("redis_uri is not a valid URI"); + let server_secret: [u8; 32] = { + let mut server_secret = [0; 32]; + let decoded = + hex::decode(&opt.server_secret).expect("server_secret must be hex-encoded"); + assert_eq!(decoded.len(), 32, "server_secret must be 32 bytes"); + server_secret.clone_from_slice(&decoded); + server_secret + }; + let account = AccountDetails { + ilp_address: Address::from_str(&opt.ilp_address).unwrap(), + username: Username::from_str(&opt.username).unwrap(), + asset_code: opt.asset_code.clone(), + asset_scale: opt.asset_scale, + btp_incoming_token: opt.btp_incoming_token.clone(), + btp_uri: opt.btp_uri.clone(), + http_incoming_token: opt + .http_incoming_token + .clone() + .map(|s| format!("Bearer {}", s)), + http_outgoing_token, + http_endpoint, + max_packet_amount: u64::max_value(), + min_balance: Some(opt.min_balance), + settle_threshold: opt.settle_threshold, + settle_to: opt.settle_to, + send_routes: opt.send_routes, + receive_routes: opt.receive_routes, + routing_relation: Some(opt.routing_relation.clone()), + round_trip_time: Some(opt.round_trip_time), + packets_per_minute_limit: opt.packets_per_minute_limit, + amount_per_minute_limit: opt.amount_per_minute_limit, + settlement_engine_url: None, + }; + tokio::run( + insert_account_redis(redis_uri, &server_secret, account).and_then(move |_| Ok(())), + ); + } +} + +#[derive(Deserialize, Clone)] +struct SpspServerOpt { + port: u16, + btp_server: String, + #[serde(default = "default_as_false")] + ilp_over_http: bool, + ilp_address: String, + incoming_auth_token: String, + #[serde(default = "default_as_false")] + quiet: bool, +} + +#[derive(Deserialize, Clone)] +struct SpspPayOpt { + btp_server: Option, + http_server: Option, + receiver: String, + amount: u64, + #[serde(default = "default_as_false")] + quiet: bool, +} + +#[derive(Deserialize, Clone)] +struct MoneydLocalOpt { + port: u16, + ilp_address: String, + asset_code: String, + asset_scale: u8, +} + +#[derive(Deserialize, Clone)] +struct NodeAccountsAddOpt { + redis_uri: String, + server_secret: String, + ilp_address: String, + username: String, + asset_code: String, + asset_scale: u8, + btp_incoming_token: Option, + btp_uri: Option, + http_url: Option, + http_incoming_token: Option, + settle_threshold: Option, + settle_to: Option, + #[serde(default = "default_as_false")] + send_routes: bool, + #[serde(default = "default_as_false")] + receive_routes: bool, + routing_relation: String, + min_balance: i64, + round_trip_time: u32, + packets_per_minute_limit: Option, + amount_per_minute_limit: Option, +} + +fn default_as_false() -> bool { + false +} diff --git a/crates/interledger/src/node.rs b/crates/interledger/src/node.rs index dbb39c3eb..b08340e60 100644 --- a/crates/interledger/src/node.rs +++ b/crates/interledger/src/node.rs @@ -83,8 +83,6 @@ where #[derive(Deserialize, Clone)] pub struct InterledgerNode { /// ILP address of the node - // Rename this one because the env vars are prefixed with "ILP_" - #[serde(alias = "address")] #[serde(deserialize_with = "deserialize_string_to_address")] pub ilp_address: Address, /// Root secret used to derive encryption keys @@ -111,7 +109,7 @@ pub struct InterledgerNode { /// When SPSP payments are sent to the root domain, the payment pointer is resolved /// to /.well-known/pay. This value determines which account those payments /// will be sent to. - pub default_spsp_account: Option, + pub default_spsp_account: Option, /// Interval, defined in milliseconds, on which the node will broadcast routing /// information to other nodes using CCP. Defaults to 30000ms (30 seconds). pub route_broadcast_interval: Option, @@ -135,7 +133,7 @@ impl InterledgerNode { let ilp_address_clone = ilp_address.clone(); let ilp_address_clone2 = ilp_address.clone(); let admin_auth_token = self.admin_auth_token.clone(); - let default_spsp_account = self.default_spsp_account; + let default_spsp_account = self.default_spsp_account.clone(); let redis_addr = self.redis_connection.addr.clone(); let route_broadcast_interval = self.route_broadcast_interval; @@ -249,7 +247,7 @@ impl InterledgerNode { incoming_service.clone(), ); if let Some(account_id) = default_spsp_account { - api.default_spsp_account(format!("{}", account_id)); + api.default_spsp_account(account_id); } let listener = TcpListener::bind(&http_address) .expect("Unable to bind to HTTP address"); diff --git a/docs/operating-manuals/README.md b/docs/operating-manuals/README.md new file mode 100644 index 000000000..866fd88c6 --- /dev/null +++ b/docs/operating-manuals/README.md @@ -0,0 +1,70 @@ +# Interledger.rs Operating Manuals + +## Initial Set up + +### Top Level Commands +Currently we have 2 top level commands. + +```bash +# Interledger Node +cargo run -- node + +# Ethereum Ledger Settlement Engine +cargo run --package interledger-settlement-engines -- ethereum-ledger +``` + +### Types of Parameters + +Please use `--help` option to see what kind of parameters are available. For example, + +```bash +# shows the top level command help +cargo run -- --help + +# shows the subcommand level help of `node` command +cargo run -- node --help +``` + +### Specifying Parameters + +Interledger.rs commands such as `node` and `ethereum-ledger` accept configuration options in the following ways: + +1. Command line arguments +1. Configuration files +1. Environment variables + +```bash # +# 1. +# passing by command line arguments +# --{parameter name} {value} +cargo run -- node --ilp_address example.alice + +# 1. +# passing by a configuration file in JSON, HJSON, TOML, YAML, or INI format +# note that the first argument after subcommands such as `node` is considered as a configuration file +cargo run -- node config.yml + +# 2. +# passing as environment variables +# {parameter name (typically in capital)}={value} +# note that the parameter names MUST begin with a prefix of "ILP_" e.g. ILP_SECRET_SEED +ILP_ADDRESS=example.alice \ +ILP_OTHER_PARAMETER=other_value \ +cargo run -- node +``` + +The commands prioritize parameters in the following order (high to low). + +- Environment variables +- Configuration files +- Command line arguments + +This means that environment variables override configuration file settings, configuration file settings override command line arguments. + +You can specify these 3 at the same time. + +```bash +ILP_ADDRESS=example.alice \ +cargo run -- node alice.yaml \ +--admin_auth_token 26931aa8c117726b2c25c9be2c52ca24d26eda5782fe9a39984db7dc602dcf0c +``` diff --git a/examples/eth-settlement/README.md b/examples/eth-settlement/README.md index fd6824c42..5995d2f6b 100644 --- a/examples/eth-settlement/README.md +++ b/examples/eth-settlement/README.md @@ -132,7 +132,6 @@ cargo run --package interledger-settlement-engines -- ethereum-ledger \ --ethereum_endpoint http://127.0.0.1:8545 \ --connector_url http://127.0.0.1:7771 \ --redis_uri redis://127.0.0.1:6379/0 \ ---watch_incoming true \ --port 3000 \ &> logs/node-alice-settlement-engine.log & @@ -144,7 +143,6 @@ cargo run --package interledger-settlement-engines -- ethereum-ledger \ --ethereum_endpoint http://127.0.0.1:8545 \ --connector_url http://127.0.0.1:8771 \ --redis_uri redis://127.0.0.1:6379/1 \ ---watch_incoming true \ --port 3001 \ &> logs/node-bob-settlement-engine.log & ``` diff --git a/examples/eth_xrp_three_nodes/README.md b/examples/eth_xrp_three_nodes/README.md index 6634d6dfc..c965d5163 100755 --- a/examples/eth_xrp_three_nodes/README.md +++ b/examples/eth_xrp_three_nodes/README.md @@ -193,7 +193,6 @@ cargo run --package interledger-settlement-engines -- ethereum-ledger \ --connector_url http://127.0.0.1:7771 \ --redis_uri redis://127.0.0.1:6379/0 \ --asset_scale 6 \ ---watch_incoming true \ --port 3000 \ &> logs/node-alice-settlement-engine-eth.log & @@ -206,7 +205,6 @@ cargo run --package interledger-settlement-engines -- ethereum-ledger \ --connector_url http://127.0.0.1:8771 \ --redis_uri redis://127.0.0.1:6380/0 \ --asset_scale 6 \ ---watch_incoming true \ --port 3001 \ &> logs/node-bob-settlement-engine-eth.log &