Skip to content

Commit

Permalink
feat(main.rs etc.): cli improvement
Browse files Browse the repository at this point in the history
Every command accepts arguments, config file settings, env vars. `interledger` and
`interledger-settlement-engines` are almost consistent.

Signed-off-by: Taiga Nakayama <dora@dora-gt.jp>

interledger#171, interledger#113, interledger#206, interledger#194, interledger#215
  • Loading branch information
dora-gt committed Aug 29, 2019
1 parent 35a57f0 commit b408c51
Show file tree
Hide file tree
Showing 9 changed files with 801 additions and 331 deletions.
1 change: 1 addition & 0 deletions crates/interledger-settlement-engines/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
336 changes: 248 additions & 88 deletions crates/interledger-settlement-engines/src/main.rs
Original file line number Diff line number Diff line change
@@ -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, &ethereum_ledger_matches);
runner.run(get_or_error(config.try_into::<EthereumLedgerOpt>()));
}
("", None) => app.print_help().unwrap(),
_ => unreachable!(),
}
}

fn merge_config_file(mut app: App, config: &mut Config) -> Result<Vec<String>, ()> {
// 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::<String>::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<String>, 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<String>) -> &'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<T>(item: Result<T, ConfigError>) -> 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<T> {
fn run(&self, opt: T);
}

impl Runnable<EthereumLedgerOpt> 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,
}
1 change: 1 addition & 0 deletions crates/interledger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions crates/interledger/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub fn send_spsp_payment_btp(
receiver: &str,
amount: u64,
quiet: bool,
) -> impl Future<Item = (), Error = ()> {
) -> impl Future<Item = (), Error = ()> + 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())
Expand Down Expand Up @@ -118,7 +118,7 @@ pub fn send_spsp_payment_http(
receiver: &str,
amount: u64,
quiet: bool,
) -> impl Future<Item = (), Error = ()> {
) -> impl Future<Item = (), Error = ()> + 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() {
Expand Down
Loading

0 comments on commit b408c51

Please sign in to comment.