diff --git a/Cargo.lock b/Cargo.lock index ef29ace3..6a3822cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,56 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -596,7 +646,7 @@ dependencies = [ "ckb-types", "ckb-util", "clap", - "clap_generate", + "clap_complete", "colored", "console", "crossbeam-channel", @@ -1076,44 +1126,52 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.0-beta.1" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "860643c53f980f0d38a5e25dfab6c3c93b2cb3aa1fe192643d17a293c6c41936" +checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" dependencies = [ - "atty", - "bitflags 1.3.2", + "clap_builder", "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", +] + +[[package]] +name = "clap_builder" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", "strsim", - "termcolor", - "textwrap", - "unicode-width", - "vec_map", +] + +[[package]] +name = "clap_complete" +version = "4.5.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +dependencies = [ + "clap", ] [[package]] name = "clap_derive" -version = "3.2.25" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ - "heck 0.4.1", - "proc-macro-error", + "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.87", ] [[package]] -name = "clap_generate" -version = "3.0.0-beta.1" +name = "clap_lex" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "188d8b137fa922515d549cf17ece165ddbc71bf21e723e81d84406958dcdef67" -dependencies = [ - "clap", -] +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "clipboard-win" @@ -1133,6 +1191,12 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "colored" version = "1.9.4" @@ -1154,7 +1218,7 @@ dependencies = [ "lazy_static", "libc", "unicode-width", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1289,7 +1353,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ "nix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1489,7 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1524,7 +1588,7 @@ checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" dependencies = [ "cfg-if 1.0.0", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1787,12 +1851,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -1854,7 +1912,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2235,9 +2293,15 @@ checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi 0.3.9", "libc", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.8.2" @@ -2525,7 +2589,7 @@ dependencies = [ "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2680,6 +2744,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opaque-debug" version = "0.2.3" @@ -2746,12 +2816,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" - [[package]] name = "parity-multiaddr" version = "0.4.1" @@ -3468,7 +3532,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3511,7 +3575,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "utf8parse", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3552,7 +3616,7 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3838,7 +3902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3866,9 +3930,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -3885,7 +3949,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.87", @@ -3948,7 +4012,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3972,15 +4036,6 @@ dependencies = [ "redox_termios", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.63" @@ -4079,7 +4134,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -4351,12 +4406,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" @@ -4519,7 +4568,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -4582,6 +4631,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" version = "0.2.0" @@ -4621,6 +4676,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 23f48b2c..686dc631 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,8 @@ bitcoin = { git="https://github.com/rust-bitcoin/rust-bitcoin", rev="436de8ef12a faster-hex = "0.6" env_logger = "0.6" crossbeam-channel = "0.5.8" -clap = "=3.0.0-beta.1" -clap_generate = "=3.0.0-beta.1" +clap = { version = "4.5.0", features = ["cargo", "deprecated", "string", "derive"] } +clap_complete = "4.5.0" serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_json = "1.0" diff --git a/Makefile b/Makefile index 5bd9e248..cc8e15f0 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ ci: fmt clippy test security-audit check-crates check-licenses git diff --exit-code Cargo.lock integration: - bash devtools/ci/integration.sh v0.203.0 $(ARGS) + bash devtools/ci/integration.sh v0.204.0 $(ARGS) integration-spec: @if [ -z "$(SPEC)" ]; then \ @@ -25,7 +25,7 @@ integration-spec: echo "Example: make integration-spec SPEC=deploy_type_id"; \ exit 1; \ fi - bash devtools/ci/integration.sh v0.203.0 --spec=$(SPEC) + bash devtools/ci/integration.sh v0.204.0 --spec=$(SPEC) prod: ## Build binary with release profile. cargo build --locked --release diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..7dc92c2d --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,34 @@ +use clap::Parser; + +use crate::utils::arg_parser::{ArgParser, UrlParser}; + +fn parse_url(input: &str) -> Result { + UrlParser.validate(input).map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "ckb-cli")] +pub struct CliArgs { + /// CKB RPC server url. + /// The default value is http://127.0.0.1:8114 + /// You may also use some public available nodes, check the list of public nodes: + /// https://github.com/nervosnetwork/ckb/wiki/Public-JSON-RPC-nodes + #[arg(long, value_parser = parse_url)] + pub url: Option, + + /// Select output format + #[arg(long = "output-format", id = "output-format", value_parser = ["yaml", "json"], default_value = "yaml", global = true)] + pub output_format: String, + + /// Do not highlight(color) output json + #[arg(long = "no-color", id = "no-color", global = true)] + pub no_color: bool, + + /// Display request parameters + #[arg(long, global = true)] + pub debug: bool, + + /// This is a local only subcommand, do not check alerts and get network type + #[arg(long = "local-only", id = "local-only", global = true)] + pub local_only: bool, +} diff --git a/src/interactive.rs b/src/interactive.rs index c91e6164..19bcb3b4 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -19,6 +19,7 @@ use crate::subcommands::{ UtilSubCommand, WalletSubCommand, }; use crate::utils::{ + arg_parser::ArgMatchesExt, completer::CkbCompleter, config::GlobalConfig, genesis_info::GenesisInfo, @@ -34,7 +35,7 @@ pub struct InteractiveEnv { config: GlobalConfig, config_file: PathBuf, history_file: PathBuf, - parser: clap::App<'static>, + parser: clap::Command, plugin_mgr: PluginManager, key_store: KeyStore, rpc_client: HttpRpcClient, @@ -126,8 +127,8 @@ impl InteractiveEnv { } for (cmd_name, description) in &plugin_sub_cmds { parser = parser.subcommand( - // FIXME: when clap updated add `clap::AppSettings::DisableHelpFlags` back - clap::App::new(cmd_name.as_str()).about(description.as_str()), + // FIXME: when clap updated add `clap::CommandSettings::DisableHelpFlags` back + clap::Command::new(cmd_name.clone()).about(description.clone()), ); } @@ -218,7 +219,7 @@ impl InteractiveEnv { fn handle_command( &mut self, - parser: &clap::App, + parser: &clap::Command, line: &str, env_regex: &Regex, ) -> Result { @@ -251,7 +252,7 @@ impl InteractiveEnv { match parser.clone().try_get_matches_from(args) { Ok(matches) => match matches.subcommand() { - ("config", Some(m)) => { + Some(("config", m)) => { if let Some(url) = m.value_of("url") { self.config.set_url(url.to_string()); self.rpc_client = HttpRpcClient::new(self.config.get_url().to_string()); @@ -288,35 +289,35 @@ impl InteractiveEnv { .map_err(|err| format!("save config file failed: {:?}", err))?; Ok(()) } - ("set", Some(m)) => { + Some(("set", m)) => { let key = m.value_of("key").unwrap().to_owned(); let value = m.value_of("value").unwrap().to_owned(); self.config.set(key, serde_json::Value::String(value)); Ok(()) } - ("get", Some(m)) => { + Some(("get", m)) => { let key = m.value_of("key"); println!("{}", self.config.get(key).render(format, color)); Ok(()) } - ("info", _) => { + Some(("info", _)) => { self.config.print(false); Ok(()) } - ("rpc", Some(sub_matches)) => { + Some(("rpc", sub_matches)) => { check_alerts(&mut self.rpc_client); let output = RpcSubCommand::new(&mut self.rpc_client, &mut self.raw_rpc_client) .process(sub_matches, debug)?; output.print(format, color); Ok(()) } - ("account", Some(sub_matches)) => { + Some(("account", sub_matches)) => { let output = AccountSubCommand::new(&mut self.plugin_mgr, &mut self.key_store) .process(sub_matches, debug)?; output.print(format, color); Ok(()) } - ("mock-tx", Some(sub_matches)) => { + Some(("mock-tx", sub_matches)) => { let genesis_info = self.genesis_info().ok(); let output = MockTxSubCommand::new( &mut self.rpc_client, @@ -327,7 +328,7 @@ impl InteractiveEnv { output.print(format, color); Ok(()) } - ("tx", Some(sub_matches)) => { + Some(("tx", sub_matches)) => { let genesis_info = self.genesis_info().ok(); let output = TxSubCommand::new(&mut self.rpc_client, &mut self.plugin_mgr, genesis_info) @@ -335,24 +336,24 @@ impl InteractiveEnv { output.print(format, color); Ok(()) } - ("util", Some(sub_matches)) => { + Some(("util", sub_matches)) => { let output = UtilSubCommand::new(&mut self.rpc_client, &mut self.plugin_mgr) .process(sub_matches, debug)?; output.print(format, color); Ok(()) } - ("plugin", Some(sub_matches)) => { + Some(("plugin", sub_matches)) => { let output = PluginSubCommand::new(&mut self.plugin_mgr).process(sub_matches, debug)?; output.print(format, color); Ok(()) } - ("molecule", Some(sub_matches)) => { + Some(("molecule", sub_matches)) => { let output = MoleculeSubCommand::new().process(sub_matches, debug)?; output.print(format, color); Ok(()) } - ("wallet", Some(sub_matches)) => { + Some(("wallet", sub_matches)) => { let genesis_info = self.genesis_info()?; let output = WalletSubCommand::new( &mut self.rpc_client, @@ -363,7 +364,7 @@ impl InteractiveEnv { output.print(format, color); Ok(()) } - ("dao", Some(sub_matches)) => { + Some(("dao", sub_matches)) => { let genesis_info = self.genesis_info()?; let output = DAOSubCommand::new( &mut self.rpc_client, @@ -374,7 +375,7 @@ impl InteractiveEnv { output.print(format, color); Ok(()) } - ("sudt", Some(sub_matches)) => { + Some(("sudt", sub_matches)) => { let genesis_info = self.genesis_info()?; let output = SudtSubCommand::new( &mut self.rpc_client, @@ -385,7 +386,7 @@ impl InteractiveEnv { output.print(format, color); Ok(()) } - ("deploy", Some(sub_matches)) => { + Some(("deploy", sub_matches)) => { let genesis_info = self.genesis_info()?; let output = DeploySubCommand::new( &mut self.rpc_client, @@ -396,7 +397,7 @@ impl InteractiveEnv { output.print(format, color); Ok(()) } - ("exit", _) => { + Some(("exit", _)) => { return Ok(true); } _ => Ok(()), diff --git a/src/main.rs b/src/main.rs index 4ae6346f..7b6748dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::process; use ckb_build_info::Version; use clap::crate_version; -use clap::{App, AppSettings, Arg}; +use clap::{Arg, ColorChoice, Command, CommandFactory}; use interactive::InteractiveEnv; use plugin::PluginManager; @@ -16,15 +16,17 @@ use subcommands::{ MockTxSubCommand, MoleculeSubCommand, PluginSubCommand, PubSubCommand, RpcSubCommand, SudtSubCommand, TxSubCommand, UtilSubCommand, WalletSubCommand, }; +use utils::arg::ArgValidatorExt; use utils::other::get_genesis_info; use utils::{ - arg_parser::{ArgParser, UrlParser}, + arg_parser::{ArgMatchesExt, ArgParser, UrlParser}, config::GlobalConfig, other::{check_alerts, get_key_store, get_network_type}, printer::{ColorWhen, OutputFormat}, rpc::{HttpRpcClient, RawHttpRpcClient}, }; +mod cli; mod interactive; mod plugin; #[allow(clippy::mutable_key_type)] @@ -120,51 +122,51 @@ async fn main() -> Result<(), io::Error> { })?; let mut plugin_mgr = PluginManager::init(&ckb_cli_dir, ckb_url).unwrap(); let result = match matches.subcommand() { - ("rpc", Some(sub_matches)) => match sub_matches.subcommand() { - ("subscribe", Some(sub_sub_matches)) => { + Some(("rpc", sub_matches)) => match sub_matches.subcommand() { + Some(("subscribe", sub_sub_matches)) => { PubSubCommand::new(output_format, color).process(sub_sub_matches, debug) } _ => { RpcSubCommand::new(&mut rpc_client, &mut raw_rpc_client).process(sub_matches, debug) } }, - ("account", Some(sub_matches)) => { + Some(("account", sub_matches)) => { AccountSubCommand::new(&mut plugin_mgr, &mut key_store).process(sub_matches, debug) } - ("mock-tx", Some(sub_matches)) => { + Some(("mock-tx", sub_matches)) => { MockTxSubCommand::new(&mut rpc_client, &mut plugin_mgr, None) .process(sub_matches, debug) } - ("tx", Some(sub_matches)) => { + Some(("tx", sub_matches)) => { TxSubCommand::new(&mut rpc_client, &mut plugin_mgr, None).process(sub_matches, debug) } - ("util", Some(sub_matches)) => { + Some(("util", sub_matches)) => { UtilSubCommand::new(&mut rpc_client, &mut plugin_mgr).process(sub_matches, debug) } - ("server", Some(sub_matches)) => { + Some(("server", sub_matches)) => { ApiServerSubCommand::new(&mut rpc_client, plugin_mgr, None).process(sub_matches, debug) } - ("plugin", Some(sub_matches)) => { + Some(("plugin", sub_matches)) => { PluginSubCommand::new(&mut plugin_mgr).process(sub_matches, debug) } - ("molecule", Some(sub_matches)) => MoleculeSubCommand::new().process(sub_matches, debug), - ("wallet", Some(sub_matches)) => { + Some(("molecule", sub_matches)) => MoleculeSubCommand::new().process(sub_matches, debug), + Some(("wallet", sub_matches)) => { WalletSubCommand::new(&mut rpc_client, &mut plugin_mgr, None) .process(sub_matches, debug) } - ("dao", Some(sub_matches)) => { + Some(("dao", sub_matches)) => { get_genesis_info(&None, &mut rpc_client).and_then(|genesis_info| { DAOSubCommand::new(&mut rpc_client, &mut plugin_mgr, genesis_info) .process(sub_matches, debug) }) } - ("sudt", Some(sub_matches)) => { + Some(("sudt", sub_matches)) => { get_genesis_info(&None, &mut rpc_client).and_then(|genesis_info| { SudtSubCommand::new(&mut rpc_client, &mut plugin_mgr, genesis_info) .process(sub_matches, debug) }) } - ("deploy", Some(sub_matches)) => { + Some(("deploy", sub_matches)) => { get_genesis_info(&None, &mut rpc_client).and_then(|genesis_info| { DeploySubCommand::new(&mut rpc_client, &mut plugin_mgr, genesis_info) .process(sub_matches, debug) @@ -226,13 +228,15 @@ pub fn get_version() -> Version { } } -pub fn build_cli<'a>(version_short: &'a str, version_long: &'a str) -> App<'a> { - App::new("ckb-cli") +pub fn build_cli(version_short: &str, version_long: &str) -> Command { + let version_short: &'static str = Box::leak(version_short.to_owned().into_boxed_str()); + let version_long: &'static str = Box::leak(version_long.to_owned().into_boxed_str()); + let mut cmd = cli::CliArgs::command(); + cmd = cmd .version(version_short) .long_version(version_long) - .global_setting(AppSettings::ColoredHelp) - .global_setting(AppSettings::DeriveDisplayOrder) - .subcommand(RpcSubCommand::subcommand().subcommand(PubSubCommand::subcommand())) + .color(ColorChoice::Auto); + cmd.subcommand(RpcSubCommand::subcommand().subcommand(PubSubCommand::subcommand())) .subcommand(AccountSubCommand::subcommand("account")) .subcommand(MockTxSubCommand::subcommand("mock-tx")) .subcommand(TxSubCommand::subcommand("tx")) @@ -244,100 +248,60 @@ pub fn build_cli<'a>(version_short: &'a str, version_long: &'a str) -> App<'a> { .subcommand(DAOSubCommand::subcommand()) .subcommand(SudtSubCommand::subcommand("sudt")) .subcommand(DeploySubCommand::subcommand("deploy")) - .arg( - - Arg::with_name("url") - .long("url") - .takes_value(true) - .validator(|input| UrlParser.validate(input)) - .about( - r#"CKB RPC server url. -The default value is http://127.0.0.1:8114 -You may also use some public available nodes, check the list of public nodes: https://github.com/nervosnetwork/ckb/wiki/Public-JSON-RPC-nodes"#, - ), - ) - .arg( - Arg::with_name("output-format") - .long("output-format") - .takes_value(true) - .possible_values(&["yaml", "json"]) - .default_value("yaml") - .global(true) - .about("Select output format"), - ) - .arg( - Arg::with_name("no-color") - .long("no-color") - .global(true) - .about("Do not highlight(color) output json"), - ) - .arg( - Arg::with_name("debug") - .long("debug") - .global(true) - .about("Display request parameters"), - ) - .arg( - Arg::with_name("local-only") - .long("local-only") - .global(true) - .about("This is a local only subcommand, do not check alerts and get network type"), - ) } -pub fn build_interactive() -> App<'static> { - App::new("interactive") +pub fn build_interactive() -> Command { + Command::new("interactive") .version(crate_version!()) - .global_setting(AppSettings::NoBinaryName) - .global_setting(AppSettings::ColoredHelp) - .global_setting(AppSettings::DeriveDisplayOrder) - .global_setting(AppSettings::DisableVersion) + .no_binary_name(true) + .color(ColorChoice::Auto) + .disable_version_flag(true) .subcommand( - App::new("config") + Command::new("config") .about("Config environment") .arg( - Arg::with_name("url") + Arg::new("url") .long("url") .validator(|input| UrlParser.validate(input)) - .takes_value(true) - .about( + .num_args(1) + .help( r#"CKB RPC server url. The default value is http://127.0.0.1:8114 You may also use some public available nodes, check the list of public nodes: https://github.com/nervosnetwork/ckb/wiki/Public-JSON-RPC-nodes"#, ), ) .arg( - Arg::with_name("color") + Arg::new("color") .long("color") - .about("Switch color for rpc interface"), + .help("Switch color for rpc interface"), ) .arg( - Arg::with_name("debug") + Arg::new("debug") .long("debug") - .about("Switch debug mode"), + .help("Switch debug mode"), ) .arg( - Arg::with_name("output-format") + Arg::new("output-format") .long("output-format") - .takes_value(true) - .possible_values(&["yaml", "json"]) + .num_args(1) + .value_parser(["yaml", "json"]) .default_value("yaml") - .about("Select output format"), + .help("Select output format"), ) .arg( - Arg::with_name("completion_style") + Arg::new("completion_style") .long("completion_style") - .about("Switch completion style"), + .help("Switch completion style"), ) .arg( - Arg::with_name("edit_style") + Arg::new("edit_style") .long("edit_style") - .about("Switch edit style"), + .help("Switch edit style"), ), ) - .subcommand(App::new("info").about("Display global variables")) + .subcommand(Command::new("info").about("Display global variables")) .subcommand( - App::new("exit") + Command::new("exit") .visible_alias("quit") .about("Exit the interactive interface"), ) diff --git a/src/subcommands/account.rs b/src/subcommands/account.rs index b26ea69f..e75c3d76 100644 --- a/src/subcommands/account.rs +++ b/src/subcommands/account.rs @@ -9,13 +9,12 @@ use bitcoin::bip32::DerivationPath; use ckb_sdk::{Address, AddressPayload, NetworkType}; use ckb_signer::{Key, KeyStore, MasterPrivKey}; use ckb_types::{packed::Script, prelude::*, H160, H256}; -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; use faster_hex::hex_string; use super::{CliSubCommand, Output}; use crate::plugin::PluginManager; use crate::utils::{ - arg::lock_arg, arg_parser::{ ArgParser, ExtendedPrivkeyPathParser, FilePathParser, FixedHashParser, FromStrParser, HexParser, PrivkeyPathParser, PrivkeyWrapper, @@ -28,6 +27,167 @@ pub struct AccountSubCommand<'a> { key_store: &'a mut KeyStore, } +const ACCOUNT_LIST_LONG_ABOUT: &str = "List all accounts. There are two kinds of account item indicated by `source` field:\n\n When `source` is \"Local File System\" means the account is stored in json keystore file, the output fields are:\n * lock_arg: The blake2b160 hash of the public key.\n * lock_hash: The lock script hash of secp256k1_blake160_sighash_all lock (See [1]).\n * has_ckb_pubkey_derivation_root_path: The CKB public key derivation root path (m/44'/309'/0') is stored so that password is not required to do public key derivation.\n * address: The Mainnet/Testnet addresses of secp256k1_blake160_sighash_all lock (See [1]).\n\n When `source` is \"[plugin]: xxx_keysotre_plugin\" means the account is stored in keystore plugin (Ledger plugin like [2]). If the account metadata is imported by `ckb-cli account import-from-plugin` the output fields are just like \"Local File System\". If the account is not imported, the output fields are:\n * account-id: The account id used to import the account metadata from plugin.\n\n[1]: https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/secp256k1_blake160_sighash_all.c\n[2]: https://github.com/obsidiansystems/ckb-plugin-ledger"; + +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_extended_privkey_path(input: &str) -> Result { + ExtendedPrivkeyPathParser + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_derivation_path(input: &str) -> Result { + FromStrParser::::new() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_account_id(input: &str) -> Result { + let hex = HexParser.parse(input)?; + if hex.is_empty() { + Err("empty account id is not allowed".to_string()) + } else { + Ok(input.to_string()) + } +} + +fn parse_file_path_exists(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_lock_arg(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "account", about = "Manage accounts")] +pub struct AccountCmd { + #[command(subcommand)] + pub command: AccountSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum AccountSubcommands { + /// List all accounts + #[command(long_about = ACCOUNT_LIST_LONG_ABOUT)] + List(AccountListArgs), + /// Create a new account and print related information. + New, + /// Import an unencrypted private key from and create a new account. + Import(AccountImportArgs), + /// Import an account from keystore plugin + ImportFromPlugin(AccountImportFromPluginArgs), + /// Import key from encrypted keystore json file and create a new account. + ImportKeystore(AccountImportKeystoreArgs), + /// Update password of an account + Update(AccountLockArgArgs), + /// Upgrade an account to latest json format + Upgrade(AccountLockArgArgs), + /// Export master private key and chain code as hex plain text (USE WITH YOUR OWN RISK) + Export(AccountExportArgs), + /// Show BIP-32 Extended Public Key in Base58Check format (with xpub prefix) + BitcoinXpub(AccountDeriveArgs), + /// Extended receiving/change Addresses (see: BIP-44) + Bip44Addresses(AccountBip44Args), + /// Extended address (see: BIP-44) + ExtendedAddress(AccountDeriveArgs), + /// Print information about how to remove an account + Remove(AccountLockArgArgs), +} + +#[derive(Args, Debug)] +pub struct AccountListArgs { + /// Only show CKB mainnet address + #[arg(long = "only-mainnet-address", id = "only-mainnet-address")] + pub only_mainnet_address: bool, + /// Only show CKB testnet address + #[arg(long = "only-testnet-address", id = "only-testnet-address")] + pub only_testnet_address: bool, +} + +#[derive(Args, Debug)] +pub struct AccountImportArgs { + /// The privkey is assumed to contain an unencrypted private key in hexadecimal format. (only read first line) + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "extended-privkey-path", value_parser = parse_privkey_path)] + pub privkey_path: Option, + /// Extended private key path (include master private key and chain code) + #[arg(long = "extended-privkey-path", id = "extended-privkey-path", required_unless_present = "privkey-path", value_parser = parse_extended_privkey_path)] + pub extended_privkey_path: Option, +} + +#[derive(Args, Debug)] +pub struct AccountImportFromPluginArgs { + /// The account id (hex format, can be found in account list) + #[arg(long = "account-id", id = "account-id", value_parser = parse_account_id)] + pub account_id: String, +} + +#[derive(Args, Debug)] +pub struct AccountImportKeystoreArgs { + /// The keystore file path (json format) + #[arg(long, value_parser = parse_file_path_exists)] + pub path: String, +} + +#[derive(Args, Debug)] +pub struct AccountLockArgArgs { + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg)] + pub lock_arg: String, +} + +#[derive(Args, Debug)] +pub struct AccountExportArgs { + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg)] + pub lock_arg: String, + /// Output extended private key path (PrivKey + ChainCode) + #[arg(long = "extended-privkey-path", id = "extended-privkey-path")] + pub extended_privkey_path: String, +} + +#[derive(Args, Debug)] +pub struct AccountDeriveArgs { + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg)] + pub lock_arg: String, + /// The derivation key path + #[arg(long = "path", id = "path", value_parser = parse_derivation_path)] + pub path: String, +} + +#[derive(Args, Debug)] +pub struct AccountBip44Args { + #[arg( + long = "from-receiving-index", + id = "from-receiving-index", + default_value = "0" + )] + pub from_receiving_index: u32, + #[arg( + long = "receiving-length", + id = "receiving-length", + default_value = "20" + )] + pub receiving_length: u32, + #[arg( + long = "from-change-index", + id = "from-change-index", + default_value = "0" + )] + pub from_change_index: u32, + #[arg(long = "change-length", id = "change-length", default_value = "10")] + pub change_length: u32, + #[arg(long, default_value = "mainnet", value_parser = ["mainnet", "testnet"])] + pub network: String, + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg)] + pub lock_arg: String, +} + impl<'a> AccountSubCommand<'a> { pub fn new( plugin_mgr: &'a mut PluginManager, @@ -39,171 +199,21 @@ impl<'a> AccountSubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_privkey_path = Arg::with_name("privkey-path") - .long("privkey-path") - .takes_value(true); - let arg_extended_privkey_path = Arg::with_name("extended-privkey-path") - .long("extended-privkey-path") - .takes_value(true) - .about("Extended private key path (include master private key and chain code)"); - let arg_derive_path = Arg::with_name("path") - .long("path") - .takes_value(true) - .validator(|input| FromStrParser::::new().validate(input)) - .about("The derivation key path"); - App::new(name) - .about("Manage accounts") - .subcommands(vec![ - App::new("list") - .arg( - Arg::with_name("only-mainnet-address") - .long("only-mainnet-address") - .about("Only show CKB mainnet address") - ) - .arg( - Arg::with_name("only-testnet-address") - .long("only-testnet-address") - .about("Only show CKB testnet address") - ) - .about("List all accounts") - .long_about("List all accounts. There are two kinds of account item indicated by `source` field: - - When `source` is \"Local File System\" means the account is stored in json keystore file, the output fields are: - * lock_arg: The blake2b160 hash of the public key. - * lock_hash: The lock script hash of secp256k1_blake160_sighash_all lock (See [1]). - * has_ckb_pubkey_derivation_root_path: The CKB public key derivation root path (m/44'/309'/0') is stored so that password is not required to do public key derivation. - * address: The Mainnet/Testnet addresses of secp256k1_blake160_sighash_all lock (See [1]). - - When `source` is \"[plugin]: xxx_keysotre_plugin\" means the account is stored in keystore plugin (Ledger plugin like [2]). If the account metadata is imported by `ckb-cli account import-from-plugin` the output fields are just like \"Local File System\". If the account is not imported, the output fields are: - * account-id: The account id used to import the account metadata from plugin. - -[1]: https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/secp256k1_blake160_sighash_all.c -[2]: https://github.com/obsidiansystems/ckb-plugin-ledger"), - App::new("new").about("Create a new account and print related information."), - App::new("import") - .about("Import an unencrypted private key from and create a new account.") - .arg( - arg_privkey_path - .clone() - .required_unless("extended-privkey-path") - .validator(|input| PrivkeyPathParser.validate(input)) - .about("The privkey is assumed to contain an unencrypted private key in hexadecimal format. (only read first line)") - ) - .arg(arg_extended_privkey_path - .clone() - .required_unless("privkey-path") - .validator(|input| ExtendedPrivkeyPathParser.validate(input)) - ), - App::new("import-from-plugin") - .about("Import an account from keystore plugin") - .arg( - Arg::with_name("account-id") - .long("account-id") - .takes_value(true) - .required(true) - .validator(|input| { - let hex = HexParser.parse(input)?; - if hex.is_empty() { - Err("empty account id is not allowed".to_string()) - } else { - Ok(()) - } - }) - .about("The account id (hex format, can be found in account list)") - ), - App::new("import-keystore") - .about("Import key from encrypted keystore json file and create a new account.") - .arg( - Arg::with_name("path") - .long("path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("The keystore file path (json format)") - ), - App::new("update") - .about("Update password of an account") - .arg(lock_arg().required(true)), - App::new("upgrade") - .about("Upgrade an account to latest json format") - .arg(lock_arg().required(true)), - App::new("export") - .about("Export master private key and chain code as hex plain text (USE WITH YOUR OWN RISK)") - .arg(lock_arg().required(true)) - .arg( - arg_extended_privkey_path - .clone() - .required(true) - .about("Output extended private key path (PrivKey + ChainCode)") - ), - App::new("bitcoin-xpub") - .about("Show BIP-32 Extended Public Key in Base58Check format (with xpub prefix)") - .arg(lock_arg().required(true)) - .arg(arg_derive_path.clone().required(true)), - App::new("bip44-addresses") - .about("Extended receiving/change Addresses (see: BIP-44)") - .arg( - Arg::with_name("from-receiving-index") - .long("from-receiving-index") - .takes_value(true) - .default_value("0") - .validator(|input| FromStrParser::::default().validate(input)) - .about("Start from receiving path index") - ) - .arg( - Arg::with_name("receiving-length") - .long("receiving-length") - .takes_value(true) - .default_value("20") - .validator(|input| FromStrParser::::default().validate(input)) - .about("Receiving addresses length") - ) - .arg( - Arg::with_name("from-change-index") - .long("from-change-index") - .takes_value(true) - .default_value("0") - .validator(|input| FromStrParser::::default().validate(input)) - .about("Start from change path index") - ) - .arg( - Arg::with_name("change-length") - .long("change-length") - .takes_value(true) - .default_value("10") - .validator(|input| FromStrParser::::default().validate(input)) - .about("Change addresses length") - ) - .arg( - Arg::with_name("network") - .long("network") - .takes_value(true) - .default_value("mainnet") - .possible_values(&["mainnet", "testnet"]) - .about("The network type") - ) - .arg(lock_arg().required(true)), - App::new("extended-address") - .about("Extended address (see: BIP-44)") - .arg(lock_arg().required(true)) - .arg(arg_derive_path), - App::new("remove") - .about("Print information about how to remove an account") - .arg(lock_arg().required(true)), - ]) + pub fn subcommand(name: &'static str) -> Command { + AccountCmd::command().name(name) } } impl CliSubCommand for AccountSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - match matches.subcommand() { - ("list", Some(m)) => { + let cmd = AccountCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + AccountSubcommands::List(args) => { let mut accounts = self.plugin_mgr.keystore_handler().list_account()?; // Sort by file path name accounts.sort_by(|a, b| a.1.cmp(&b.1)); - let only_mainnet_address = m.is_present("only-mainnet-address"); - let only_testnet_address = m.is_present("only-testnet-address"); + let only_mainnet_address = args.only_mainnet_address; + let only_testnet_address = args.only_testnet_address; let partial_fields = only_mainnet_address || only_testnet_address; self.key_store .refresh_dir() @@ -254,7 +264,7 @@ impl CliSubCommand for AccountSubCommand<'_> { .collect::>(); Ok(Output::new_output(resp)) } - ("new", _) => { + AccountSubcommands::New => { eprintln!("Your new account is locked with a password. Please give a password. Do not forget this password."); let password = read_password(true, None)?; let lock_arg = self @@ -271,9 +281,11 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("import", Some(m)) => { - let secp_key: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; + AccountSubcommands::Import(args) => { + let secp_key: Option = match args.privkey_path.as_ref() { + Some(path) => Some(PrivkeyPathParser.parse(path)?), + None => None, + }; let password = Some(read_password(false, None)?); let master_privkey = if let Some(secp_key) = secp_key { // Default chain code is [255u8; 32] @@ -281,9 +293,11 @@ impl CliSubCommand for AccountSubCommand<'_> { data[0..32].copy_from_slice(&secp_key[..]); MasterPrivKey::from_bytes(data).map_err(|err| err.to_string())? } else { - let master_privkey: MasterPrivKey = - ExtendedPrivkeyPathParser.from_matches(m, "extended-privkey-path")?; - master_privkey + let extended_privkey_path = args + .extended_privkey_path + .as_ref() + .ok_or_else(|| " is required".to_string())?; + ExtendedPrivkeyPathParser.parse(extended_privkey_path)? }; let lock_arg = self @@ -298,8 +312,8 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("import-from-plugin", Some(m)) => { - let account_id: Vec = HexParser.from_matches(m, "account-id")?; + AccountSubcommands::ImportFromPlugin(args) => { + let account_id: Vec = HexParser.parse(&args.account_id)?; let password = if self.plugin_mgr.keystore_require_password() { Some(read_password(false, None)?) } else { @@ -317,8 +331,8 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("import-keystore", Some(m)) => { - let path: PathBuf = FilePathParser::new(true).from_matches(m, "path")?; + AccountSubcommands::ImportKeystore(args) => { + let path: PathBuf = FilePathParser::new(true).parse(&args.path)?; let old_password = read_password(false, Some("Decrypt password"))?; let new_password = Some(read_password(false, None)?); @@ -341,9 +355,8 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("update", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; + AccountSubcommands::Update(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; let old_password = read_password(false, Some("Old password"))?; let new_passsword = read_password(true, Some("New password"))?; self.plugin_mgr.keystore_handler().update_password( @@ -353,19 +366,17 @@ impl CliSubCommand for AccountSubCommand<'_> { )?; Ok(Output::new_success()) } - ("upgrade", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; + AccountSubcommands::Upgrade(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; let password = read_password(false, None)?; self.key_store .upgrade(&lock_arg, password.as_bytes()) .map_err(|err| err.to_string())?; Ok(Output::new_success()) } - ("export", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; - let key_path = m.value_of("extended-privkey-path").unwrap(); + AccountSubcommands::Export(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; + let key_path = args.extended_privkey_path.as_str(); let password = Some(read_password(false, None)?); if Path::new(key_path).exists() { @@ -407,12 +418,11 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_error(resp)) } - ("bitcoin-xpub", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; + AccountSubcommands::BitcoinXpub(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; let password = read_password(false, None)?; let path: DerivationPath = - FromStrParser::::new().from_matches(m, "path")?; + FromStrParser::::new().parse(&args.path)?; let extended_pubkey = self .key_store .extended_pubkey_with_password(&lock_arg, &path, password.as_bytes()) @@ -422,18 +432,13 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("bip44-addresses", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; - let from_receiving_index: u32 = - FromStrParser::::default().from_matches(m, "from-receiving-index")?; - let receiving_length: u32 = - FromStrParser::::default().from_matches(m, "receiving-length")?; - let from_change_index: u32 = - FromStrParser::::default().from_matches(m, "from-change-index")?; - let change_length: u32 = - FromStrParser::::default().from_matches(m, "change-length")?; - let network = match m.value_of("network").expect("network argument") { + AccountSubcommands::Bip44Addresses(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; + let from_receiving_index = args.from_receiving_index; + let receiving_length = args.receiving_length; + let from_change_index = args.from_change_index; + let change_length = args.change_length; + let network = match args.network.as_str() { "mainnet" => NetworkType::Mainnet, "testnet" => NetworkType::Testnet, _ => unreachable!(), @@ -475,13 +480,10 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("extended-address", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; - let root_key_path = self.plugin_mgr.root_key_path(lock_arg.clone())?; - let path: DerivationPath = FromStrParser::::new() - .from_matches_opt(m, "path")? - .unwrap_or(root_key_path); + AccountSubcommands::ExtendedAddress(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; + let path: DerivationPath = + FromStrParser::::new().parse(&args.path)?; let password = if self.plugin_mgr.keystore_require_password() { Some(read_password(false, None)?) @@ -500,9 +502,8 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("remove", Some(m)) => { - let lock_arg: H160 = - FixedHashParser::::default().from_matches(m, "lock-arg")?; + AccountSubcommands::Remove(args) => { + let lock_arg: H160 = FixedHashParser::::default().parse(&args.lock_arg)?; let filepath = self .key_store .get_filepath(&lock_arg) @@ -513,7 +514,6 @@ impl CliSubCommand for AccountSubCommand<'_> { }); Ok(Output::new_output(resp)) } - _ => Err(Self::subcommand("account").generate_usage()), } } } diff --git a/src/subcommands/api_server.rs b/src/subcommands/api_server.rs index 2f02bc63..3964854b 100644 --- a/src/subcommands/api_server.rs +++ b/src/subcommands/api_server.rs @@ -6,7 +6,7 @@ use std::time::Duration; use ckb_crypto::secp::SECP256K1; use ckb_sdk::{Address, AddressPayload, HumanCapacity, NetworkType}; use ckb_types::{bytes::Bytes, packed::Script, prelude::*, H256}; -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Command, CommandFactory, FromArgMatches, Parser}; use jsonrpc_core::{Error as RpcError, ErrorCode as RpcErrorCode, IoHandler, Result as RpcResult}; use jsonrpc_derive::rpc; use jsonrpc_http_server::{Server, ServerBuilder}; @@ -17,13 +17,33 @@ use serde::{Deserialize, Serialize}; use super::{CliSubCommand, Output, TransferArgs, WalletSubCommand}; use crate::plugin::PluginManager; use crate::utils::{ - arg, arg_parser::{AddressParser, ArgParser, FromStrParser, PrivkeyPathParser, PrivkeyWrapper}, genesis_info::GenesisInfo, other::{get_genesis_info, get_network_type}, rpc::HttpRpcClient, }; +fn parse_listen_addr(input: &str) -> Result { + FromStrParser::::new() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "server", about = "Start advanced API server")] +pub struct ApiServerCmd { + /// Rpc server listen address (when --privkey-path is given ip MUST be 127.0.0.1) + #[arg(long, default_value = "127.0.0.1:3000", value_parser = parse_listen_addr)] + pub listen: String, + /// Private key file path (only read first line) + #[arg(long = "privkey-path", id = "privkey-path", value_parser = parse_privkey_path)] + pub privkey_path: Option, +} + pub struct ApiServerSubCommand<'a> { rpc_client: &'a mut HttpRpcClient, plugin_mgr: Option, @@ -43,30 +63,16 @@ impl<'a> ApiServerSubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - App::new(name) - .about("Start advanced API server") - .arg( - Arg::with_name("listen") - .long("listen") - .takes_value(true) - .required(true) - .default_value("127.0.0.1:3000") - .validator(|input| FromStrParser::::new().validate(input)) - .about("Rpc server listen address (when --privkey-path is given ip MUST be 127.0.0.1)"), - ) - .arg( - arg::privkey_path() - .about("Private key file path (only read first line)") - ) + pub fn subcommand(name: &'static str) -> Command { + ApiServerCmd::command().name(name) } } impl CliSubCommand for ApiServerSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - let listen_addr: SocketAddr = - FromStrParser::::new().from_matches(matches, "listen")?; - let privkey_path: Option = matches.value_of("privkey-path").map(Into::into); + let cmd = ApiServerCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + let listen_addr: SocketAddr = FromStrParser::::new().parse(&cmd.listen)?; + let privkey_path = cmd.privkey_path; let network_result = get_network_type(self.rpc_client); if privkey_path.is_some() && listen_addr.ip() != IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) { @@ -76,8 +82,8 @@ impl CliSubCommand for ApiServerSubCommand<'_> { )); } let privkey_opt: Option = privkey_path - .clone() - .map(|input| PrivkeyPathParser.parse(&input)) + .as_ref() + .map(|input| PrivkeyPathParser.parse(input)) .transpose()?; let network = match network_result { Ok(network) => network, diff --git a/src/subcommands/dao/command.rs b/src/subcommands/dao/command.rs index 4094d9a7..dd91016c 100644 --- a/src/subcommands/dao/command.rs +++ b/src/subcommands/dao/command.rs @@ -1,49 +1,142 @@ use crate::subcommands::dao::util::{calculate_dao_maximum_withdraw, send_transaction}; use crate::subcommands::{CliSubCommand, DAOSubCommand, Output}; use crate::utils::{ - arg, arg_parser::{ AddressParser, ArgParser, CapacityParser, FixedHashParser, FromStrParser, OutPointParser, PrivkeyPathParser, PrivkeyWrapper, }, - other::{get_address, get_network_type}, + other::get_network_type, }; use ckb_crypto::secp::SECP256K1; use ckb_sdk::{Address, AddressPayload, HumanCapacity, NetworkType}; use ckb_types::{packed::Script, H160}; -use clap::{App, Arg, ArgMatches}; +use clap::{ + ArgAction, ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, +}; use std::collections::HashSet; +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_address(input: &str) -> Result { + AddressParser::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_capacity(input: &str) -> Result { + CapacityParser.validate(input).map(|_| input.to_string()) +} + +fn parse_out_point(input: &str) -> Result { + OutPointParser.validate(input).map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command( + name = "dao", + about = "Deposit / prepare / withdraw / query NervosDAO balance (with local index) / key utils" +)] +pub struct DaoCmd { + #[command(subcommand)] + pub command: DaoSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum DaoSubcommands { + /// Deposit capacity into NervosDAO + Deposit(DaoDepositArgs), + /// Prepare specified cells from NervosDAO + Prepare(DaoOutPointArgs), + /// Withdraw specified cells from NervosDAO + Withdraw(DaoOutPointArgs), + /// Query NervosDAO deposited capacity by address + QueryDepositedCells(DaoAddressArgs), + /// Query NervosDAO prepared capacity by address + QueryPreparedCells(DaoAddressArgs), +} + +#[derive(Args, Debug)] +pub struct DaoTransactArgs { + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "from-account", value_parser = parse_privkey_path)] + pub privkey_path: Option, + #[arg( + long = "from-account", + id = "from-account", + required_unless_present = "privkey-path" + )] + pub from_account: Option, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000")] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_capacity)] + pub max_tx_fee: Option, +} + +#[derive(Args, Debug)] +pub struct DaoDepositArgs { + #[command(flatten)] + pub tx: DaoTransactArgs, + #[arg(long, value_parser = parse_capacity)] + pub capacity: String, +} + +#[derive(Args, Debug)] +pub struct DaoOutPointArgs { + #[command(flatten)] + pub tx: DaoTransactArgs, + #[arg(long = "out-point", id = "out-point", action = ArgAction::Append, num_args = 1.., value_parser = parse_out_point)] + pub out_point: Vec, +} + +#[derive(Args, Debug)] +pub struct DaoAddressArgs { + #[arg(long, value_parser = parse_address)] + pub address: String, +} + impl CliSubCommand for DAOSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, debug: bool) -> Result { let network_type = get_network_type(self.rpc_client)?; - match matches.subcommand() { - ("deposit", Some(m)) => { - let args = TransactArgs::from_matches(m, network_type)?; - let capacity: u64 = CapacityParser.from_matches(m, "capacity")?; - let transaction = self.deposit(&args, capacity)?; + let cmd = DaoCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + DaoSubcommands::Deposit(args) => { + let tx_args = TransactArgs::from_dao_args(&args.tx, network_type)?; + let capacity: u64 = CapacityParser.parse(&args.capacity)?.into(); + let transaction = self.deposit(&tx_args, capacity)?; send_transaction(self.rpc_client, transaction, debug) } - ("prepare", Some(m)) => { - let args = TransactArgs::from_matches(m, network_type)?; - let out_points = OutPointParser.from_matches_vec(m, "out-point")?; + DaoSubcommands::Prepare(args) => { + let tx_args = TransactArgs::from_dao_args(&args.tx, network_type)?; + let out_points = args + .out_point + .iter() + .map(|value| OutPointParser.parse(value)) + .collect::, String>>()?; if out_points.len() != out_points.iter().collect::>().len() { return Err("Duplicated out-points".to_string()); } - let transaction = self.prepare(&args, out_points)?; + let transaction = self.prepare(&tx_args, out_points)?; send_transaction(self.rpc_client, transaction, debug) } - ("withdraw", Some(m)) => { - let args = TransactArgs::from_matches(m, network_type)?; - let out_points = OutPointParser.from_matches_vec(m, "out-point")?; + DaoSubcommands::Withdraw(args) => { + let tx_args = TransactArgs::from_dao_args(&args.tx, network_type)?; + let out_points = args + .out_point + .iter() + .map(|value| OutPointParser.parse(value)) + .collect::, String>>()?; if out_points.len() != out_points.iter().collect::>().len() { return Err("Duplicated out-points".to_string()); } - let transaction = self.withdraw(&args, out_points)?; + let transaction = self.withdraw(&tx_args, out_points)?; send_transaction(self.rpc_client, transaction, debug) } - ("query-deposited-cells", Some(m)) => { - let address_payload = get_address(Some(network_type), m)?; + DaoSubcommands::QueryDepositedCells(args) => { + let address = AddressParser::new_sighash() + .set_network(network_type) + .parse(&args.address)?; + let address_payload = address.payload().clone(); let cells = self.query_deposit_cells(Script::from(&address_payload))?; let total_capacity = cells.iter().map(|live| live.capacity).sum::(); let resp = serde_json::json!({ @@ -54,8 +147,11 @@ impl CliSubCommand for DAOSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("query-prepared-cells", Some(m)) => { - let address_payload = get_address(Some(network_type), m)?; + DaoSubcommands::QueryPreparedCells(args) => { + let address = AddressParser::new_sighash() + .set_network(network_type) + .parse(&args.address)?; + let address_payload = address.payload().clone(); let cells = self.query_prepare_cells(Script::from(&address_payload))?; let maximum_withdraws: Vec<_> = cells .iter() @@ -73,35 +169,13 @@ impl CliSubCommand for DAOSubCommand<'_> { }); Ok(Output::new_output(resp)) } - _ => Err(Self::subcommand().generate_usage()), } } } impl DAOSubCommand<'_> { - pub fn subcommand() -> App<'static> { - App::new("dao") - .about("Deposit / prepare / withdraw / query NervosDAO balance (with local index) / key utils") - .subcommands(vec![ - App::new("deposit") - .about("Deposit capacity into NervosDAO") - .args(TransactArgs::args()) - .arg(arg::capacity().required(true)), - App::new("prepare") - .about("Prepare specified cells from NervosDAO") - .args(TransactArgs::args()) - .arg(arg::out_point().required(true).multiple(true)), - App::new("withdraw") - .about("Withdraw specified cells from NervosDAO") - .args(TransactArgs::args()) - .arg(arg::out_point().required(true).multiple(true)), - App::new("query-deposited-cells") - .about("Query NervosDAO deposited capacity by address") - .arg(arg::address()), - App::new("query-prepared-cells") - .about("Query NervosDAO prepared capacity by address") - .arg(arg::address()) - ]) + pub fn subcommand() -> Command { + DaoCmd::command() } } @@ -113,39 +187,45 @@ pub struct TransactArgs { } impl TransactArgs { - fn from_matches(m: &ArgMatches, network_type: NetworkType) -> Result { - let privkey: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; + fn from_dao_args(args: &DaoTransactArgs, network_type: NetworkType) -> Result { + let privkey: Option = args + .privkey_path + .as_ref() + .map(|path| PrivkeyPathParser.parse(path)) + .transpose()?; let address = if let Some(privkey) = privkey.as_ref() { let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, privkey); let payload = AddressPayload::from_pubkey(&pubkey); Address::new(network_type, payload, false) } else { - let account: H160 = FixedHashParser::::default() - .from_matches_opt(m, "from-account") - .or_else(|err| { - let result: Result, String> = AddressParser::new_sighash() - .set_network(network_type) - .from_matches_opt(m, "from-account"); - result - .map(|address_opt| { - address_opt - .map(|address| H160::from_slice(&address.payload().args()).unwrap()) - }) - .map_err(|_| format!("Invalid value for '--from-account': {}", err)) - })? - .ok_or_else(|| { - // It's a bug of clap, otherwise if is not given must required. - // The bug only happen when put before . - String::from(" or is required!") - })?; + let account: H160 = if let Some(from_account) = args.from_account.as_ref() { + FixedHashParser::::default() + .parse(from_account) + .or_else(|err| { + AddressParser::new_sighash() + .set_network(network_type) + .parse(from_account) + .map(|address| H160::from_slice(&address.payload().args()).unwrap()) + .map_err(|_| format!("Invalid value for '--from-account': {}", err)) + })? + } else { + return Err(String::from( + " or is required!", + )); + }; let payload = AddressPayload::from_pubkey_hash(account); Address::new(network_type, payload, false) }; - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; - - let force_small_change_as_fee = - FromStrParser::::default().from_matches_opt(m, "max-tx-fee")?; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; + let force_small_change_as_fee = args + .max_tx_fee + .as_ref() + .map(|value| { + FromStrParser::::default() + .parse(value) + .map(Into::into) + }) + .transpose()?; Ok(Self { privkey, address, @@ -153,13 +233,4 @@ impl TransactArgs { force_small_change_as_fee, }) } - - fn args<'a>() -> Vec> { - vec![ - arg::privkey_path().required_unless(arg::from_account().get_name()), - arg::from_account().required_unless(arg::privkey_path().get_name()), - arg::fee_rate(), - arg::max_tx_fee(), - ] - } } diff --git a/src/subcommands/deploy/mod.rs b/src/subcommands/deploy/mod.rs index dbd5ed41..e8754d6d 100644 --- a/src/subcommands/deploy/mod.rs +++ b/src/subcommands/deploy/mod.rs @@ -16,12 +16,11 @@ use ckb_sdk::{ Address, HumanCapacity, }; use ckb_types::{bytes::Bytes, core::ScriptHashType, packed, prelude::*, H160, H256}; -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; use super::{CliSubCommand, Output, ALLOW_ZERO_LOCK_HELP_MSG}; use crate::plugin::PluginManager; use crate::utils::{ - arg, arg_parser::{ AddressParser, ArgParser, DirPathParser, FilePathParser, FixedHashParser, FromStrParser, PrivkeyPathParser, PrivkeyWrapper, @@ -60,6 +59,122 @@ pub struct DeploySubCommand<'a> { genesis_info: GenesisInfo, } +fn parse_sighash_address(input: &str) -> Result { + AddressParser::new_sighash() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_file_path_exists(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_file_path_optional(input: &str) -> Result { + FilePathParser::new(false) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_dir_path_exists(input: &str) -> Result { + DirPathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_fee_rate(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "deploy", about = "Deploy contract binaries")] +pub struct DeployCmd { + #[command(subcommand)] + pub command: DeploySubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum DeploySubcommands { + /// Generate cell/dep_group deploy transaction, then use `ckb-cli tx` sub-command to sign mutlsig inputs and send the transaction + GenTxs(DeployGenTxsArgs), + /// Sign cell/dep_group transactions (support offline sign) + SignTxs(DeploySignTxsArgs), + /// Explain cell transaction and dep_group transaction + ExplainTxs(DeployExplainTxsArgs), + /// Send cell/dep_group transactions and write results to migration directory + ApplyTxs(DeployApplyTxsArgs), + /// Initialize default deployment config (format: toml) + InitConfig(DeployInitConfigArgs), +} + +#[derive(Args, Debug)] +pub struct DeployGenTxsArgs { + /// Collect cells from this address (sighash address) + #[arg(long = "from-address", id = "from-address", value_parser = parse_sighash_address)] + pub from_address: String, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_fee_rate)] + pub fee_rate: String, + #[arg(long = "deployment-config", id = "deployment-config", value_parser = parse_file_path_exists)] + pub deployment_config: String, + #[arg(long = "info-file", id = "info-file", value_parser = parse_file_path_optional)] + pub info_file: String, + #[arg(long = "migration-dir", id = "migration-dir", value_parser = parse_dir_path_exists)] + pub migration_dir: String, + #[arg(long = "zero-lock", id = "zero-lock", help = ALLOW_ZERO_LOCK_HELP_MSG)] + pub zero_lock: bool, + /// Sign the cell/dep_group transaction add signatures to info-file now + #[arg(long = "sign-now", id = "sign-now")] + pub sign_now: bool, +} + +#[derive(Args, Debug)] +pub struct DeploySignTxsArgs { + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "from-account", value_parser = parse_privkey_path)] + pub privkey_path: Option, + #[arg( + long = "from-account", + id = "from-account", + required_unless_present = "privkey-path" + )] + pub from_account: Option, + #[arg(long = "info-file", id = "info-file", value_parser = parse_file_path_exists)] + pub info_file: String, + #[arg(long = "zero-lock", id = "zero-lock", help = ALLOW_ZERO_LOCK_HELP_MSG)] + pub zero_lock: bool, + /// Sign and add signatures + #[arg(long = "add-signatures", id = "add-signatures")] + pub add_signatures: bool, +} + +#[derive(Args, Debug)] +pub struct DeployExplainTxsArgs { + #[arg(long = "info-file", id = "info-file", value_parser = parse_file_path_exists)] + pub info_file: String, +} + +#[derive(Args, Debug)] +pub struct DeployApplyTxsArgs { + #[arg(long = "info-file", id = "info-file", value_parser = parse_file_path_exists)] + pub info_file: String, + #[arg(long = "migration-dir", id = "migration-dir", value_parser = parse_dir_path_exists)] + pub migration_dir: String, + #[arg(long = "zero-lock", id = "zero-lock", help = ALLOW_ZERO_LOCK_HELP_MSG)] + pub zero_lock: bool, +} + +#[derive(Args, Debug)] +pub struct DeployInitConfigArgs { + #[arg(long = "deployment-config", id = "deployment-config", value_parser = parse_file_path_optional)] + pub deployment_config: String, +} + impl<'a> DeploySubCommand<'a> { pub fn new( rpc_client: &'a mut HttpRpcClient, @@ -73,92 +188,26 @@ impl<'a> DeploySubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_info_file = Arg::with_name("info-file") - .long("info-file") - .required(true) - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("File path for saving deploy cell/dep_group transactions and metadata (format: json)"); - let arg_migration_dir = Arg::with_name("migration-dir") - .long("migration-dir") - .required(true) - .takes_value(true) - .validator(|input| DirPathParser::new(true).validate(input)) - .about("Migration directory for saving json format migration files"); - let arg_deployment = Arg::with_name("deployment-config") - .long("deployment-config") - .required(true) - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("deployment config file path (.toml)"); - let arg_allow_zero_lock = Arg::with_name("zero-lock") - .long("zero-lock") - .about(ALLOW_ZERO_LOCK_HELP_MSG); - App::new(name) - .about("Deploy contract binaries") - .subcommands(vec![ - App::new("gen-txs") - .about("Generate cell/dep_group deploy transaction, then use `ckb-cli tx` sub-command to sign mutlsig inputs and send the transaction") - .arg( - Arg::with_name("from-address") - .long("from-address") - .required(true) - .takes_value(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("Collect cells from this address (sighash address)") - ) - .arg(arg::fee_rate().required(true)) - .arg(arg_deployment.clone()) - .arg(arg_info_file.clone().validator(|input| FilePathParser::new(false).validate(input))) - .arg(arg_migration_dir.clone()) - .arg(arg_allow_zero_lock.clone()) - .arg( - Arg::with_name("sign-now") - .long("sign-now") - .about("Sign the cell/dep_group transaction add signatures to info-file now"), - ), - App::new("sign-txs") - .arg(arg::privkey_path().required_unless(arg::from_account().get_name())) - .arg(arg::from_account().required_unless(arg::privkey_path().get_name())) - .arg(arg_info_file.clone()) - .arg(arg_allow_zero_lock.clone()) - .arg( - Arg::with_name("add-signatures") - .long("add-signatures") - .about("Sign and add signatures"), - ) - .about("Sign cell/dep_group transactions (support offline sign)"), - App::new("explain-txs") - .arg(arg_info_file.clone()) - .about("Explain cell transaction and dep_group transaction"), - App::new("apply-txs") - .arg(arg_info_file.clone()) - .arg(arg_migration_dir) - .arg(arg_allow_zero_lock) - .about("Send cell/dep_group transactions and write results to migration directory"), - App::new("init-config") - .arg(arg_deployment.validator(|input| FilePathParser::new(false).validate(input))) - .about("Initialize default deployment config (format: toml)") - ]) + pub fn subcommand(name: &'static str) -> Command { + DeployCmd::command().name(name) } } impl CliSubCommand for DeploySubCommand<'_> { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - match matches.subcommand() { - ("gen-txs", Some(m)) => { + let cmd = DeployCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + DeploySubcommands::GenTxs(args) => { let network = get_network_type(self.rpc_client)?; let from_address: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "from-address")?; - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; + .parse(&args.from_address)?; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; let deployment_config: PathBuf = - FilePathParser::new(true).from_matches(m, "deployment-config")?; - let migration_dir: PathBuf = - DirPathParser::new(true).from_matches(m, "migration-dir")?; - let info_file: PathBuf = FilePathParser::new(false).from_matches(m, "info-file")?; - let allow_zero_lock: bool = m.is_present("zero-lock"); + FilePathParser::new(true).parse(&args.deployment_config)?; + let migration_dir: PathBuf = DirPathParser::new(true).parse(&args.migration_dir)?; + let info_file: PathBuf = FilePathParser::new(false).parse(&args.info_file)?; + let allow_zero_lock: bool = args.zero_lock; if info_file.exists() { return Err(format!("Output info-file already exists: {:?}", info_file)); @@ -308,7 +357,7 @@ impl CliSubCommand for DeploySubCommand<'_> { explain_txs(&info).map_err(|err| err.to_string())?; // Sign if required - if m.is_present("sign-now") { + if args.sign_now { let account = H160::from_slice(from_address.payload().args().as_ref()).unwrap(); let signer = { let handler = self.plugin_mgr.keystore_handler(); @@ -359,12 +408,16 @@ impl CliSubCommand for DeploySubCommand<'_> { .map_err(|err| err.to_string())?; Ok(Output::new_success()) } - ("sign-txs", Some(m)) => { - let info_file: PathBuf = FilePathParser::new(true).from_matches(m, "info-file")?; - let privkey_opt: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; - let account_opt: Option = m - .value_of("from-account") + DeploySubcommands::SignTxs(args) => { + let info_file: PathBuf = FilePathParser::new(true).parse(&args.info_file)?; + let privkey_opt: Option = args + .privkey_path + .as_ref() + .map(|value| PrivkeyPathParser.parse(value)) + .transpose()?; + let account_opt: Option = args + .from_account + .as_ref() .map(|input| { FixedHashParser::::default() .parse(input) @@ -451,17 +504,17 @@ impl CliSubCommand for DeploySubCommand<'_> { info, self.rpc_client, signer_fn, - m.is_present("add-signatures"), - m.is_present("zero-lock"), + args.add_signatures, + args.zero_lock, ) }) .map_err(|err| err.to_string())?; Ok(Output::new_output(all_signatures)) } - ("explain-txs", Some(m)) => { + DeploySubcommands::ExplainTxs(args) => { // * Report cell transaction summary // * Report dep_group transaction summary - let info_file: PathBuf = FilePathParser::new(false).from_matches(m, "info-file")?; + let info_file: PathBuf = FilePathParser::new(false).parse(&args.info_file)?; let file = fs::File::open(info_file).map_err(|err| err.to_string())?; let info: IntermediumInfo = @@ -471,17 +524,16 @@ impl CliSubCommand for DeploySubCommand<'_> { Ok(Output::new_success()) } - ("apply-txs", Some(m)) => { - let info_file: PathBuf = FilePathParser::new(false).from_matches(m, "info-file")?; - let migration_dir: PathBuf = - DirPathParser::new(true).from_matches(m, "migration-dir")?; + DeploySubcommands::ApplyTxs(args) => { + let info_file: PathBuf = FilePathParser::new(false).parse(&args.info_file)?; + let migration_dir: PathBuf = DirPathParser::new(true).parse(&args.migration_dir)?; let file = fs::File::open(info_file).map_err(|err| err.to_string())?; let info: IntermediumInfo = serde_json::from_reader(&file).map_err(|err| err.to_string())?; let skip_check = false; - let allow_zero_lock: bool = m.is_present("zero-lock"); + let allow_zero_lock: bool = args.zero_lock; let (cell_tx_opt, dep_group_tx_opt) = { let mut live_cell_cache: HashMap< @@ -569,9 +621,9 @@ impl CliSubCommand for DeploySubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("init-config", Some(m)) => { + DeploySubcommands::InitConfig(args) => { let deployment_config: PathBuf = - FilePathParser::new(false).from_matches(m, "deployment-config")?; + FilePathParser::new(false).parse(&args.deployment_config)?; if deployment_config.exists() { return Err(format!( @@ -587,7 +639,6 @@ impl CliSubCommand for DeploySubCommand<'_> { .map_err(|err| err.to_string())?; Ok(Output::new_success()) } - _ => Err(Self::subcommand("deploy").generate_usage()), } } } diff --git a/src/subcommands/mock_tx.rs b/src/subcommands/mock_tx.rs index 5c0c9f2b..977bb1a4 100644 --- a/src/subcommands/mock_tx.rs +++ b/src/subcommands/mock_tx.rs @@ -19,12 +19,11 @@ use ckb_types::{ prelude::*, H160, H256, }; -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; use super::{tx::ReprTxHelper, CliSubCommand, Output}; use crate::plugin::PluginManager; use crate::utils::{ - arg::lock_arg, arg_parser::{ArgParser, FilePathParser, FixedHashParser}, genesis_info::GenesisInfo, mock_tx_helper::MockTransactionHelper, @@ -33,6 +32,83 @@ use crate::utils::{ tx_helper::TxHelper, }; +fn parse_file_path_exists(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_file_path_optional(input: &str) -> Result { + FilePathParser::new(false) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_tx_hash(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_lock_arg(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "mock-tx", about = "Handle mock transactions (verify/send)")] +pub struct MockTxCmd { + #[command(subcommand)] + pub command: MockTxSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum MockTxSubcommands { + /// Print mock transaction template + Template(MockTxTemplateArgs), + /// Complete the mock transaction + Complete(MockTxCompleteArgs), + /// Dump all on-chain data(inputs/cell_deps/header_deps) into mock_info + Dump(MockTxDumpArgs), + /// Verify a mock transaction in local + Verify(MockTxFileArgs), + /// Complete then send a transaction + Send(MockTxFileArgs), +} + +#[derive(Args, Debug)] +pub struct MockTxTemplateArgs { + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg)] + pub lock_arg: Option, + #[arg(long = "output-file", id = "output-file", value_parser = parse_file_path_optional)] + pub output_file: Option, +} + +#[derive(Args, Debug)] +pub struct MockTxCompleteArgs { + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_file_path_exists)] + pub tx_file: String, + #[arg(long = "output-file", id = "output-file", value_parser = parse_file_path_optional)] + pub output_file: Option, +} + +#[derive(Args, Debug)] +pub struct MockTxFileArgs { + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_file_path_exists)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct MockTxDumpArgs { + #[arg(long = "tx-hash", id = "tx-hash", value_parser = parse_tx_hash, required_unless_present = "tx-file", conflicts_with = "tx-file")] + pub tx_hash: Option, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_file_path_exists, required_unless_present = "tx-hash", conflicts_with = "tx-hash")] + pub tx_file: Option, + #[arg(long = "output-file", id = "output-file", value_parser = parse_file_path_optional)] + pub output_file: String, +} + pub struct MockTxSubCommand<'a> { rpc_client: &'a mut HttpRpcClient, plugin_mgr: &'a mut PluginManager, @@ -52,74 +128,18 @@ impl<'a> MockTxSubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_tx_file = Arg::with_name("tx-file") - .long("tx-file") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("Mock transaction data file (format: json)"); - let arg_output_file = Arg::with_name("output-file") - .long("output-file") - .takes_value(true) - .validator(|input| FilePathParser::new(false).validate(input)) - .about("Completed mock transaction data file (format: json)"); - App::new(name) - .about("Handle mock transactions (verify/send)") - .subcommands(vec![ - App::new("template") - .about("Print mock transaction template") - .arg(lock_arg().required(true).clone().required(false)) - .arg(arg_output_file.clone().about("Save to a output file")), - App::new("complete") - .about("Complete the mock transaction") - .arg(arg_tx_file.clone()) - .arg( - arg_output_file - .clone() - .about("Completed mock transaction data file (format: json)"), - ), - App::new("dump") - .about("Dump all on-chain data(inputs/cell_deps/header_deps) into mock_info") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required_unless("tx-file") - .conflicts_with("tx-file") - .about("The hash of transaction which is on the chain"), - ) - .arg( - arg_tx_file - .clone() - .required_unless("tx-hash") - .conflicts_with("tx-hash") - .about("CKB transaction data file or `ckb-cli tx` subcommand json file (format: json)"), - ) - .arg( - arg_output_file - .clone() - .required(true) - .about("Dumped mock transaction data file (format: json)"), - ), - App::new("verify") - .about("Verify a mock transaction in local") - .arg(arg_tx_file.clone()), - App::new("send") - .about("Complete then send a transaction") - .arg(arg_tx_file.clone()), - ]) + pub fn subcommand(name: &'static str) -> Command { + MockTxCmd::command().name(name) } } impl CliSubCommand for MockTxSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - let mut complete_tx = |m: &ArgMatches, + let mut complete_tx = |tx_file: &str, complete: bool, verify: bool| -> Result<(MockTransaction, u64), String> { - let path: PathBuf = FilePathParser::new(true).from_matches(m, "tx-file")?; + let path: PathBuf = FilePathParser::new(true).parse(tx_file)?; let mut content = String::new(); let mut file = fs::File::open(path).map_err(|err| err.to_string())?; file.read_to_string(&mut content) @@ -156,11 +176,12 @@ impl CliSubCommand for MockTxSubCommand<'_> { Ok((mock_tx, cycle)) }; - let output_tx = |m: &ArgMatches, + let output_tx = |output_file: Option<&String>, mock_tx: &MockTransaction| -> Result, String> { - let output_opt: Option = - FilePathParser::new(false).from_matches_opt(m, "output-file")?; + let output_opt: Option = output_file + .map(|path| FilePathParser::new(false).parse(path)) + .transpose()?; let repr_mock_tx = ReprMockTransaction::from(mock_tx.clone()); if let Some(output) = output_opt { let mut out_file = fs::File::create(output).map_err(|err| err.to_string())?; @@ -177,10 +198,14 @@ impl CliSubCommand for MockTxSubCommand<'_> { } }; - match matches.subcommand() { - ("template", Some(m)) => { - let lock_arg_opt: Option = - FixedHashParser::::default().from_matches_opt(m, "lock-arg")?; + let cmd = MockTxCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + MockTxSubcommands::Template(args) => { + let lock_arg_opt: Option = args + .lock_arg + .as_ref() + .map(|value| FixedHashParser::::default().parse(value)) + .transpose()?; let lock_arg = lock_arg_opt.unwrap_or_default(); let genesis_info = get_genesis_info(&self.genesis_info, self.rpc_client)?; @@ -236,16 +261,16 @@ impl CliSubCommand for MockTxSubCommand<'_> { let mut helper = MockTransactionHelper::new(&mut mock_tx); helper.fill_deps(&genesis_info, |_| unreachable!())?; } - if let Some(output) = output_tx(m, &mock_tx)? { + if let Some(output) = output_tx(args.output_file.as_ref(), &mock_tx)? { Ok(Output::new_output(output)) } else { Ok(Output::new_success()) } } - ("complete", Some(m)) => { - let (mock_tx, _cycle) = complete_tx(m, true, false)?; + MockTxSubcommands::Complete(args) => { + let (mock_tx, _cycle) = complete_tx(&args.tx_file, true, false)?; let tx_hash: H256 = mock_tx.core_transaction().hash().unpack(); - if let Some(repr_mock_tx) = output_tx(m, &mock_tx)? { + if let Some(repr_mock_tx) = output_tx(args.output_file.as_ref(), &mock_tx)? { let mut value = serde_json::to_value(repr_mock_tx).unwrap(); value["tx-hash"] = serde_json::json!(tx_hash); Ok(Output::new_output(value)) @@ -256,13 +281,18 @@ impl CliSubCommand for MockTxSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("dump", Some(m)) => { - let output_path: PathBuf = - FilePathParser::new(false).from_matches(m, "output-file")?; - let tx_hash_opt: Option = - FixedHashParser::::default().from_matches_opt(m, "tx-hash")?; - let tx_file_opt: Option = - FilePathParser::new(true).from_matches_opt(m, "tx-file")?; + MockTxSubcommands::Dump(args) => { + let output_path: PathBuf = FilePathParser::new(false).parse(&args.output_file)?; + let tx_hash_opt: Option = args + .tx_hash + .as_ref() + .map(|value| FixedHashParser::::default().parse(value)) + .transpose()?; + let tx_file_opt: Option = args + .tx_file + .as_ref() + .map(|value| FilePathParser::new(true).parse(value)) + .transpose()?; let src_tx: json_types::Transaction = if let Some(path) = tx_file_opt { let mut content = String::new(); @@ -381,8 +411,8 @@ impl CliSubCommand for MockTxSubCommand<'_> { .map_err(|err| err.to_string())?; Ok(Output::new_success()) } - ("verify", Some(m)) => { - let (mock_tx, cycle) = complete_tx(m, false, true)?; + MockTxSubcommands::Verify(args) => { + let (mock_tx, cycle) = complete_tx(&args.tx_file, false, true)?; let tx_hash: H256 = mock_tx.core_transaction().hash().unpack(); let resp = serde_json::json!({ "tx-hash": tx_hash, @@ -390,8 +420,8 @@ impl CliSubCommand for MockTxSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("send", Some(m)) => { - let (mock_tx, _cycle) = complete_tx(m, false, true)?; + MockTxSubcommands::Send(args) => { + let (mock_tx, _cycle) = complete_tx(&args.tx_file, false, true)?; let resp = self .rpc_client .send_transaction( @@ -401,7 +431,6 @@ impl CliSubCommand for MockTxSubCommand<'_> { .map_err(|err| format!("Send transaction error: {}", err))?; Ok(Output::new_output(resp)) } - _ => Err(Self::subcommand("mock-tx").generate_usage()), } } } diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs index e48c246e..54f989e8 100644 --- a/src/subcommands/mod.rs +++ b/src/subcommands/mod.rs @@ -32,7 +32,7 @@ use clap::{Arg, ArgMatches}; use serde::Serialize; use crate::utils::{ - arg_parser::{ArgParser, FixedHashParser}, + arg_parser::{ArgMatchesExt, ArgParser, FixedHashParser}, printer::{OutputFormat, Printable}, }; @@ -101,24 +101,25 @@ Key Considerations: - No Recovery Mechanism: If vulnerabilities or defects exist in the script, there is no way to upgrade, patch, or revoke it. - Use with Caution: Thoroughly audit and test the script before deployment. This option is recommended only for scenarios requiring absolute finality, where script behavior must remain tamper-proof indefinitely."; -fn arg_multisig_code_hash() -> Arg<'static> { - let arg_multisig_code_hash = Arg::with_name("multisig-code-hash") - .long("multisig-code-hash") - .takes_value(true) - .multiple(false) - .required(true) - .possible_values(&[ - // legacy code hash - "legacy", - "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", - // V2 code hash - "v2", - "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", - ]) - .about("Specifies the multisig code hash to use:\n - v2(default): `0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29`. \n - legacy(deprecated): `0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8` is NOT recommended for use.\n\n"); +#[allow(dead_code)] +fn arg_multisig_code_hash() -> Arg { + let arg_multisig_code_hash = Arg::new("multisig-code-hash") + .long("multisig-code-hash") + .num_args(1) + .required(true) + .value_parser([ + // legacy code hash + "legacy", + "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + // V2 code hash + "v2", + "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", + ]) + .help("Specifies the multisig code hash to use:\n - v2(default): `0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29`. \n - legacy(deprecated): `0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8` is NOT recommended for use.\n\n"); arg_multisig_code_hash } +#[allow(dead_code)] fn arg_get_multisig_code_hash(m: &ArgMatches) -> Result { match m.value_of("multisig-code-hash").unwrap() { "legacy" => Ok(MultisigScript::Legacy.script_id().code_hash), diff --git a/src/subcommands/molecule.rs b/src/subcommands/molecule.rs index 6749ee6a..caa6e2aa 100644 --- a/src/subcommands/molecule.rs +++ b/src/subcommands/molecule.rs @@ -6,12 +6,83 @@ use std::path::PathBuf; use ckb_hash::blake2b_256; use ckb_jsonrpc_types::{self as json_types, JsonBytes}; use ckb_types::{bytes::Bytes, packed, prelude::*, H256}; -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; use serde_derive::{Deserialize, Serialize}; use super::{CliSubCommand, Output}; use crate::utils::arg_parser::{ArgParser, FilePathParser, HexFilePathParser, HexParser}; +fn parse_hex(input: &str) -> Result { + HexParser.validate(input).map(|_| input.to_string()) +} + +fn parse_hex_file_path(input: &str) -> Result { + HexFilePathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_file_path_exists(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_file_path_optional(input: &str) -> Result { + FilePathParser::new(false) + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "molecule", about = "Molecule encode/decode utilities")] +pub struct MoleculeCmd { + #[command(subcommand)] + pub command: MoleculeSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum MoleculeSubcommands { + /// Decode molecule type from binary + Decode(MoleculeDecodeArgs), + /// Encode molecule type from json to binary + Encode(MoleculeEncodeArgs), + /// Print default json structure of certain molecule type + Default(MoleculeDefaultArgs), +} + +#[derive(Args, Debug)] +pub struct MoleculeDecodeArgs { + /// The molecule type name defined in blockchain.mol (and extra OutPointVec) + #[arg(long)] + pub r#type: String, + /// Binary data hex format + #[arg(long = "binary-hex", id = "binary-hex", required_unless_present = "hex-binary-path", value_parser = parse_hex)] + pub binary_hex: Option, + /// The hex binary file path of molecule data + #[arg(long = "hex-binary-path", id = "hex-binary-path", required_unless_present = "binary-hex", value_parser = parse_hex_file_path)] + pub hex_binary_path: Option, +} + +#[derive(Args, Debug)] +pub struct MoleculeEncodeArgs { + /// The molecule type name defined in blockchain.mol (and extra OutPointVec) + #[arg(long)] + pub r#type: String, + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path_exists)] + pub json_path: String, + /// Serialize output type + #[arg(long = "output-type", id = "output-type", default_value = "binary", value_parser = ["binary", "hash"])] + pub output_type: String, +} + +#[derive(Args, Debug)] +pub struct MoleculeDefaultArgs { + /// The molecule type name defined in blockchain.mol (and extra OutPointVec) + #[arg(long)] + pub r#type: String, + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path_optional)] + pub json_path: Option, +} + pub struct MoleculeSubCommand {} impl MoleculeSubCommand { @@ -19,73 +90,23 @@ impl MoleculeSubCommand { MoleculeSubCommand {} } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_type = Arg::with_name("type") - .long("type") - .takes_value(true) - .required(true) - .about("The molecule type name defined in blockchain.mol (and extra OutPointVec)"); - let arg_binary_hex = Arg::with_name("binary-hex") - .long("binary-hex") - .required_unless("hex-binary-path") - .takes_value(true) - .validator(|input| HexParser.validate(input)) - .about("Binary data hex format"); - let arg_hex_binary_path = Arg::with_name("hex-binary-path") - .long("hex-binary-path") - .required_unless("binary-hex") - .takes_value(true) - .validator(|input| HexFilePathParser.validate(input)) - .about("The hex binary file path of molecule data"); - - let arg_json_path = Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)); - let arg_serialize_output_type = Arg::with_name("output-type") - .long("output-type") - .takes_value(true) - .default_value("binary") - .possible_values(&["binary", "hash"]) - .about("Serialize output type"); - - App::new(name) - .about("Molecule encode/decode utilities") - .subcommands(vec![ - App::new("decode") - .about("Decode molecule type from binary") - .arg(arg_type.clone()) - .arg(arg_binary_hex) - .arg(arg_hex_binary_path), - App::new("encode") - .about("Encode molecule type from json to binary") - .arg(arg_type.clone()) - .arg(arg_json_path.clone()) - .arg(arg_serialize_output_type), - App::new("default") - .about("Print default json structure of certain molecule type") - .arg(arg_type.clone()) - .arg( - arg_json_path - .clone() - .required(false) - .validator(|input| FilePathParser::new(false).validate(input)), - ), - ]) + pub fn subcommand(name: &'static str) -> Command { + MoleculeCmd::command().name(name) } } impl CliSubCommand for MoleculeSubCommand { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - match matches.subcommand() { - ("decode", Some(m)) => { - let type_name = m.value_of("type").unwrap(); - let binary_opt: Option> = HexParser.from_matches_opt(m, "binary-hex")?; - let binary = if let Some(binary) = binary_opt { - binary + let cmd = MoleculeCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + MoleculeSubcommands::Decode(args) => { + let type_name = args.r#type.as_str(); + let binary = if let Some(binary_hex) = args.binary_hex.as_ref() { + HexParser.parse(binary_hex)? + } else if let Some(hex_binary_path) = args.hex_binary_path.as_ref() { + HexFilePathParser.parse(hex_binary_path)? } else { - HexFilePathParser.from_matches(m, "hex-binary-path")? + return Err(" or is required".to_string()); }; match type_name { "Uint32" => packed::Uint32::from_slice(&binary) @@ -141,10 +162,10 @@ impl CliSubCommand for MoleculeSubCommand { _ => Err(format!("Unsupported molecule type name: {}", type_name)), } } - ("encode", Some(m)) => { - let type_name = m.value_of("type").unwrap(); - let output_type = m.value_of("output-type").unwrap(); - let json_path: PathBuf = FilePathParser::new(true).from_matches(m, "json-path")?; + MoleculeSubcommands::Encode(args) => { + let type_name = args.r#type.as_str(); + let output_type = args.output_type.as_str(); + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let binary_result = match type_name { @@ -206,10 +227,13 @@ impl CliSubCommand for MoleculeSubCommand { }; Ok(Output::new_output(serde_json::Value::String(output))) } - ("default", Some(m)) => { - let type_name = m.value_of("type").unwrap(); - let json_path: Option = - FilePathParser::new(false).from_matches_opt(m, "json-path")?; + MoleculeSubcommands::Default(args) => { + let type_name = args.r#type.as_str(); + let json_path: Option = args + .json_path + .as_ref() + .map(|path| FilePathParser::new(false).parse(path)) + .transpose()?; if let Some(path) = json_path.as_ref() { if path.exists() { return Err(format!("File exists: {:?}", path)); @@ -252,7 +276,6 @@ impl CliSubCommand for MoleculeSubCommand { Ok(Output::new_output(value)) } } - _ => Err(Self::subcommand("molecule").generate_usage()), } } } diff --git a/src/subcommands/plugin.rs b/src/subcommands/plugin.rs index df12b136..869e8f74 100644 --- a/src/subcommands/plugin.rs +++ b/src/subcommands/plugin.rs @@ -1,10 +1,54 @@ -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; use std::path::PathBuf; use super::{CliSubCommand, Output}; use crate::plugin::PluginManager; use crate::utils::arg_parser::{ArgParser, FilePathParser}; +fn parse_plugin_binary_path(input: &str) -> Result { + FilePathParser::new(true).parse(input) +} + +#[derive(Parser, Debug)] +#[command(name = "plugin", about = "ckb-cli plugin management")] +pub struct PluginCmd { + #[command(subcommand)] + pub command: PluginSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum PluginSubcommands { + /// Active a plugin (at most one keystore/indexer role plugin can be actived) + Active(PluginNameArg), + /// Deactive a plugin + Deactive(PluginNameArg), + /// List all plugins + List, + /// Show the detail information of a plugin + Info(PluginNameArg), + /// Install a plugin, will active it immediately by default + Install(PluginInstallArgs), + /// Uninstall a plugin, deactive it then remove the binary file + Uninstall(PluginNameArg), +} + +#[derive(Args, Debug)] +pub struct PluginNameArg { + /// Plugin name + #[arg(long)] + pub name: String, +} + +#[derive(Args, Debug)] +pub struct PluginInstallArgs { + /// The binary file path of the plugin + #[arg(long = "binary-path", id = "binary-path", value_parser = parse_plugin_binary_path)] + pub binary_path: PathBuf, + /// Install the plugin but not active it + #[arg(long)] + pub inactive: bool, +} + pub struct PluginSubCommand<'a> { plugin_mgr: &'a mut PluginManager, } @@ -14,69 +58,32 @@ impl<'a> PluginSubCommand<'a> { PluginSubCommand { plugin_mgr } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_plugin_name = Arg::with_name("name") - .long("name") - .required(true) - .takes_value(true) - .about("Plugin name"); - App::new(name) - .about("ckb-cli plugin management") - .subcommands(vec![ - App::new("active") - .about( - "Active a plugin (at most one keystore/indexer role plugin can be actived)", - ) - .arg(arg_plugin_name.clone()), - App::new("deactive") - .about("Deactive a plugin") - .arg(arg_plugin_name.clone()), - App::new("list").about("List all plugins"), - App::new("info") - .about("Show the detail information of a plugin") - .arg(arg_plugin_name.clone()), - App::new("install") - .about("Install a plugin, will active it immediately by default") - .arg( - Arg::with_name("binary-path") - .long("binary-path") - .required(true) - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("The binary file path of the plugin"), - ) - .arg( - Arg::with_name("inactive") - .long("inactive") - .about("Install the plugin but not active it"), - ), - App::new("uninstall") - .about("Uninstall a plugin, deactive it then remove the binary file") - .arg(arg_plugin_name.clone()), - ]) + pub fn subcommand(name: &'static str) -> Command { + PluginCmd::command().name(name) } } impl CliSubCommand for PluginSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - match matches.subcommand() { - ("active", Some(m)) => { - let name = m.value_of("name").unwrap(); + let cmd = PluginCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + PluginSubcommands::Active(args) => { + let name = args.name.as_str(); self.plugin_mgr.active(name)?; Ok(Output::new_output(serde_json::json!(format!( "Plugin {} is actived!", name )))) } - ("deactive", Some(m)) => { - let name = m.value_of("name").unwrap(); + PluginSubcommands::Deactive(args) => { + let name = args.name.as_str(); self.plugin_mgr.deactive(name)?; Ok(Output::new_output(serde_json::json!(format!( "Plugin {} is deactived!", name )))) } - ("list", Some(_)) => { + PluginSubcommands::List => { let resp = self .plugin_mgr .plugins() @@ -91,8 +98,8 @@ impl CliSubCommand for PluginSubCommand<'_> { .collect::>(); Ok(Output::new_output(resp)) } - ("info", Some(m)) => { - let name = m.value_of("name").unwrap(); + PluginSubcommands::Info(args) => { + let name = args.name.as_str(); if let Some((plugin, config)) = self.plugin_mgr.plugins().get(name) { let resp = serde_json::json!({ "name": config.name, @@ -106,9 +113,9 @@ impl CliSubCommand for PluginSubCommand<'_> { Err(format!("Plugin {} not found", name)) } } - ("install", Some(m)) => { - let path: PathBuf = FilePathParser::new(true).from_matches(m, "binary-path")?; - let active = !m.is_present("inactive"); + PluginSubcommands::Install(args) => { + let path: PathBuf = args.binary_path; + let active = !args.inactive; let config = self.plugin_mgr.install(path, active)?; let resp = serde_json::json!({ "name": config.name, @@ -117,15 +124,14 @@ impl CliSubCommand for PluginSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("uninstall", Some(m)) => { - let name = m.value_of("name").unwrap(); + PluginSubcommands::Uninstall(args) => { + let name = args.name.as_str(); self.plugin_mgr.uninstall(name)?; Ok(Output::new_output(serde_json::json!(format!( "Plugin {} uninstalled!", name )))) } - _ => Err(Self::subcommand("plugin").generate_usage()), } } } diff --git a/src/subcommands/pubsub.rs b/src/subcommands/pubsub.rs index 2e964b99..04684a19 100644 --- a/src/subcommands/pubsub.rs +++ b/src/subcommands/pubsub.rs @@ -1,6 +1,8 @@ use ckb_jsonrpc_types::{BlockView, HeaderView, PoolTransactionEntry, PoolTransactionReject}; use ckb_sdk::pubsub::Client; -use clap::{App, Arg, ArgMatches}; +use clap::{ + ArgAction, ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, +}; use futures::StreamExt; use std::io; use std::net::SocketAddr; @@ -10,6 +12,57 @@ use super::{CliSubCommand, Output}; use crate::utils::arg_parser::{ArgParser, SocketParser}; use crate::OutputFormat; +fn parse_socket(input: &str) -> Result { + SocketParser.validate(input).map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "subscribe", about = "Subscribe to TCP interface of node")] +pub struct PubSubCmd { + #[command(subcommand)] + pub command: PubSubSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum PubSubSubcommands { + /// Subscribe to new block header notification + NewTipHeader(PubSubTcpArgs), + /// Subscribe to new block notification + NewTipBlock(PubSubTcpArgs), + /// Subscribe to new transaction notification + NewTransaction(PubSubTcpArgs), + /// Subscribe to new proposed transaction notification + ProposedTransaction(PubSubTcpArgs), + /// Subscribe to rejected transaction notification + RejectedTransaction(PubSubTcpArgs), + /// Subscribe topic list + List(PubSubListArgs), +} + +#[derive(Args, Debug)] +pub struct PubSubTcpArgs { + #[arg(long, value_parser = parse_socket)] + pub tcp: String, +} + +#[derive(Args, Debug)] +pub struct PubSubListArgs { + #[arg(long, value_parser = parse_socket)] + pub tcp: String, + #[arg( + short = 't', + value_parser = [ + "new_tip_header", + "new_tip_block", + "new_transaction", + "proposed_transaction", + "rejected_transaction", + ], + action = ArgAction::Append + )] + pub topics: Vec, +} + macro_rules! block_on { ($addr:ident, $topic:expr, $output:ty, $format:expr, $color:expr) => {{ let rt = tokio::runtime::Runtime::new().unwrap(); @@ -36,58 +89,17 @@ impl PubSubCommand { PubSubCommand { format, color } } - pub fn subcommand() -> App<'static> { - let arg = Arg::with_name("tcp") - .long("tcp") - .takes_value(true) - .required(true) - .validator(|input| SocketParser.validate(input)) - .about("RPC pubsub server socket, like \"127.0.0.1:18114\""); - - let multi_arg = Arg::with_name("topics") - .short('t') - .takes_value(true) - .required(true) - .possible_values(&[ - "new_tip_header", - "new_tip_block", - "new_transaction", - "proposed_transaction", - "rejected_transaction", - ]) - .multiple(true) - .about("Optional multiple topic subscriptions "); - - App::new("subscribe") - .about("Subscribe to TCP interface of node") - .subcommands(vec![ - App::new("new_tip_header") - .arg(arg.clone()) - .about("Subscribe to new block header notification"), - App::new("new_tip_block") - .arg(arg.clone()) - .about("Subscribe to new block notification"), - App::new("new_transaction") - .arg(arg.clone()) - .about("Subscribe to new transaction notification"), - App::new("proposed_transaction") - .arg(arg.clone()) - .about("Subscribe to new proposed transaction notification"), - App::new("rejected_transaction") - .arg(arg.clone()) - .about("Subscribe to rejected transaction notification"), - App::new("list") - .args(vec![arg, multi_arg]) - .about("Subscribe topic list"), - ]) + pub fn subcommand() -> Command { + PubSubCmd::command() } } impl CliSubCommand for PubSubCommand { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - match matches.subcommand() { - ("new_tip_header", Some(m)) => { - let tcp: SocketAddr = SocketParser.from_matches(m, "tcp")?; + let cmd = PubSubCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + PubSubSubcommands::NewTipHeader(args) => { + let tcp: SocketAddr = SocketParser.parse(&args.tcp)?; let ret = block_on!( tcp, ["new_tip_header"].iter(), @@ -97,8 +109,8 @@ impl CliSubCommand for PubSubCommand { ); ret.map_err(|e| e.to_string()) } - ("new_tip_block", Some(m)) => { - let tcp: SocketAddr = SocketParser.from_matches(m, "tcp")?; + PubSubSubcommands::NewTipBlock(args) => { + let tcp: SocketAddr = SocketParser.parse(&args.tcp)?; let ret = block_on!( tcp, ["new_tip_block"].iter(), @@ -108,8 +120,8 @@ impl CliSubCommand for PubSubCommand { ); ret.map_err(|e| e.to_string()) } - ("new_transaction", Some(m)) => { - let tcp: SocketAddr = SocketParser.from_matches(m, "tcp")?; + PubSubSubcommands::NewTransaction(args) => { + let tcp: SocketAddr = SocketParser.parse(&args.tcp)?; let ret = block_on!( tcp, ["new_transaction"].iter(), @@ -119,8 +131,8 @@ impl CliSubCommand for PubSubCommand { ); ret.map_err(|e| e.to_string()) } - ("proposed_transaction", Some(m)) => { - let tcp: SocketAddr = SocketParser.from_matches(m, "tcp")?; + PubSubSubcommands::ProposedTransaction(args) => { + let tcp: SocketAddr = SocketParser.parse(&args.tcp)?; let ret = block_on!( tcp, ["proposed_transaction"].iter(), @@ -130,8 +142,8 @@ impl CliSubCommand for PubSubCommand { ); ret.map_err(|e| e.to_string()) } - ("rejected_transaction", Some(m)) => { - let tcp: SocketAddr = SocketParser.from_matches(m, "tcp")?; + PubSubSubcommands::RejectedTransaction(args) => { + let tcp: SocketAddr = SocketParser.parse(&args.tcp)?; let ret = block_on!( tcp, ["rejected_transaction"].iter(), @@ -141,13 +153,17 @@ impl CliSubCommand for PubSubCommand { ); ret.map_err(|e| e.to_string()) } - ("list", Some(m)) => { - let tcp: SocketAddr = SocketParser.from_matches(m, "tcp")?; - let list: Vec<_> = m.values_of("topics").unwrap().collect(); - let ret = block_on!(tcp, list.iter(), ListOutput, self.format, self.color); + PubSubSubcommands::List(args) => { + let tcp: SocketAddr = SocketParser.parse(&args.tcp)?; + let ret = block_on!( + tcp, + args.topics.iter().map(String::as_str), + ListOutput, + self.format, + self.color + ); ret.map_err(|e| e.to_string()) } - _ => Err(Self::subcommand().generate_usage()), } } } diff --git a/src/subcommands/rpc.rs b/src/subcommands/rpc.rs index 0f524c08..4f5e4a48 100644 --- a/src/subcommands/rpc.rs +++ b/src/subcommands/rpc.rs @@ -3,7 +3,9 @@ use ckb_jsonrpc_types::{ }; use ckb_types::packed::{CellOutput, OutPoint}; use ckb_types::{bytes::Bytes, packed, prelude::*, H256}; -use clap::{App, Arg, ArgMatches}; +use clap::{ + ArgAction, ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, +}; use ipnetwork::IpNetwork; use multiaddr::Multiaddr; use serde_derive::{Deserialize, Serialize}; @@ -30,6 +32,364 @@ pub struct RpcSubCommand<'a> { raw_rpc_client: &'a mut RawHttpRpcClient, } +fn parse_h256(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_u64(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_u32(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_ip_network(input: &str) -> Result { + FromStrParser::::new() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_multiaddr(input: &str) -> Result { + FromStrParser::::new() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_duration(input: &str) -> Result { + DurationParser.validate(input).map(|_| input.to_string()) +} + +fn parse_file_path(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_hex(input: &str) -> Result { + HexParser.validate(input).map(|_| input.to_string()) +} + +fn parse_fee_rate_target(input: &str) -> Result { + FeeRateStatisticsTargetParser {} + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "rpc", about = "Invoke RPC call to node")] +pub struct RpcCmd { + #[arg(long = "raw-data", global = true)] + pub raw_data: bool, + #[command(subcommand)] + pub command: RpcSubcommands, +} + +#[derive(Subcommand, Debug)] +#[command(rename_all = "snake_case")] +pub enum RpcSubcommands { + GetBlock(RpcGetBlockArgs), + GetBlockByNumber(RpcGetBlockByNumberArgs), + GetBlockHash(RpcGetBlockHashArgs), + GetCurrentEpoch, + GetEpochByNumber(RpcGetEpochByNumberArgs), + GetHeader(RpcGetHeaderArgs), + GetHeaderByNumber(RpcGetHeaderByNumberArgs), + GetLiveCell(RpcGetLiveCellArgs), + GetTipBlockNumber, + GetTipHeader(RpcGetTipHeaderArgs), + GetTransaction(RpcGetTransactionArgs), + GetTransactionProof(RpcGetTransactionProofArgs), + VerifyTransactionProof(RpcVerifyTransactionProofArgs), + GetForkBlock(RpcGetForkBlockArgs), + GetConsensus, + GetBlockMedianTime(RpcGetBlockMedianTimeArgs), + GetBlockEconomicState(RpcGetBlockEconomicStateArgs), + EstimateCycles(RpcEstimateCyclesArgs), + GetFeeRateStatics(RpcGetFeeRateStaticsArgs), + GetFeeRateStatistics(RpcGetFeeRateStatisticsArgs), + GetDeploymentsInfo, + GetTransactionAndWitnessProof(RpcGetTransactionAndWitnessProofArgs), + VerifyTransactionAndWitnessProof(RpcVerifyTransactionAndWitnessProofArgs), + GetBannedAddresses, + GetPeers, + LocalNodeInfo, + SetBan(RpcSetBanArgs), + SyncState, + SetNetworkActive(RpcSetNetworkActiveArgs), + AddNode(RpcAddNodeArgs), + RemoveNode(RpcRemoveNodeArgs), + ClearBannedAddresses, + PingPeers, + RemoveTransaction(RpcRemoveTransactionArgs), + TxPoolInfo, + ClearTxVerifyQueue, + TestTxPoolAccept(RpcTestTxPoolAcceptArgs), + ClearTxPool, + GetRawTxPool(RpcGetRawTxPoolArgs), + TxPoolReady, + GetBlockchainInfo, + SendAlert(RpcSendAlertArgs), + NotifyTransaction(RpcNotifyTransactionArgs), + Truncate(RpcTruncateArgs), + GenerateBlock, + GenerateEpochs(RpcGenerateEpochsArgs), + GetIndexerTip, + GetCells(RpcGetCellsArgs), + GetTransactions(RpcGetTransactionsArgs), + GetCellsCapacity(RpcGetCellsCapacityArgs), +} + +#[derive(Args, Debug)] +pub struct RpcGetBlockArgs { + #[arg(long = "hash", id = "hash", value_parser = parse_h256)] + pub hash: String, + #[arg(long = "with-cycles", id = "with-cycles")] + pub with_cycles: bool, + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetBlockByNumberArgs { + #[arg(long = "number", id = "number", value_parser = parse_u64)] + pub number: String, + #[arg(long = "with-cycles", id = "with-cycles")] + pub with_cycles: bool, + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetBlockHashArgs { + #[arg(long = "number", id = "number", value_parser = parse_u64)] + pub number: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetEpochByNumberArgs { + #[arg(long = "number", id = "number", value_parser = parse_u64)] + pub number: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetHeaderArgs { + #[arg(long = "hash", id = "hash", value_parser = parse_h256)] + pub hash: String, + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetHeaderByNumberArgs { + #[arg(long = "number", id = "number", value_parser = parse_u64)] + pub number: String, + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetLiveCellArgs { + #[arg(long = "tx-hash", id = "tx-hash", value_parser = parse_h256)] + pub tx_hash: String, + #[arg(long = "index", id = "index", value_parser = parse_u32)] + pub index: String, + #[arg(long = "include-tx-pool", id = "include-tx-pool")] + pub include_tx_pool: bool, + #[arg(long = "with-data", id = "with-data")] + pub with_data: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetTipHeaderArgs { + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetTransactionArgs { + #[arg(long = "hash", id = "hash", value_parser = parse_h256)] + pub hash: String, + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetTransactionProofArgs { + #[arg(long = "tx-hash", id = "tx-hash", action = ArgAction::Append, num_args = 1.., value_parser = parse_h256)] + pub tx_hash: Vec, + #[arg(long = "block-hash", id = "block-hash", value_parser = parse_h256)] + pub block_hash: Option, +} + +#[derive(Args, Debug)] +pub struct RpcVerifyTransactionProofArgs { + #[arg(long = "tx-proof-path", id = "tx-proof-path", value_parser = parse_file_path)] + pub tx_proof_path: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetForkBlockArgs { + #[arg(long = "hash", id = "hash", value_parser = parse_h256)] + pub hash: String, + #[arg(long = "packed", id = "packed")] + pub packed: bool, +} + +#[derive(Args, Debug)] +pub struct RpcGetBlockMedianTimeArgs { + #[arg(long = "hash", id = "hash", value_parser = parse_h256)] + pub hash: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetBlockEconomicStateArgs { + #[arg(long = "hash", id = "hash", value_parser = parse_h256)] + pub hash: String, +} + +#[derive(Args, Debug)] +pub struct RpcEstimateCyclesArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetFeeRateStaticsArgs { + #[arg(long = "target", id = "target", value_parser = parse_fee_rate_target)] + pub target: Option, +} + +#[derive(Args, Debug)] +pub struct RpcGetFeeRateStatisticsArgs { + #[arg(long = "target", id = "target", value_parser = parse_fee_rate_target)] + pub target: Option, +} + +#[derive(Args, Debug)] +pub struct RpcGetTransactionAndWitnessProofArgs { + #[arg(long = "tx-hash", id = "tx-hash", action = ArgAction::Append, num_args = 1.., value_parser = parse_h256)] + pub tx_hash: Vec, + #[arg(long = "block-hash", id = "block-hash", value_parser = parse_h256)] + pub block_hash: Option, +} + +#[derive(Args, Debug)] +pub struct RpcVerifyTransactionAndWitnessProofArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, +} + +#[derive(Args, Debug)] +pub struct RpcSetBanArgs { + #[arg(long = "address", id = "address", value_parser = parse_ip_network)] + pub address: String, + #[arg(long = "command", id = "command", value_parser = ["insert", "delete"])] + pub command: String, + #[arg(long = "ban_time", id = "ban_time", default_value = "24h", value_parser = parse_duration)] + pub ban_time: String, + #[arg(long = "reason", id = "reason")] + pub reason: Option, +} + +#[derive(Args, Debug)] +pub struct RpcSetNetworkActiveArgs { + #[arg(long = "state", id = "state", value_parser = ["enable", "disable"])] + pub state: String, +} + +#[derive(Args, Debug)] +pub struct RpcAddNodeArgs { + #[arg(long = "peer-id", id = "peer-id")] + pub peer_id: String, + #[arg(long = "address", id = "address", value_parser = parse_multiaddr)] + pub address: String, +} + +#[derive(Args, Debug)] +pub struct RpcRemoveNodeArgs { + #[arg(long = "peer-id", id = "peer-id")] + pub peer_id: String, +} + +#[derive(Args, Debug)] +pub struct RpcRemoveTransactionArgs { + #[arg(long = "tx-hash", id = "tx-hash", value_parser = parse_h256)] + pub tx_hash: String, +} + +#[derive(Args, Debug)] +pub struct RpcTestTxPoolAcceptArgs { + #[arg(long = "tx-file", id = "tx-file")] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetRawTxPoolArgs { + #[arg(long = "verbose", id = "verbose")] + pub verbose: bool, +} + +#[derive(Args, Debug)] +pub struct RpcSendAlertArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, +} + +#[derive(Args, Debug)] +pub struct RpcNotifyTransactionArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, +} + +#[derive(Args, Debug)] +pub struct RpcTruncateArgs { + #[arg(long = "tip-hash", id = "tip-hash", value_parser = parse_h256)] + pub tip_hash: String, +} + +#[derive(Args, Debug)] +pub struct RpcGenerateEpochsArgs { + #[arg(long = "num-epochs", id = "num-epochs")] + pub num_epochs: String, +} + +#[derive(Args, Debug)] +pub struct RpcGetCellsArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, + #[arg(long = "order", id = "order", value_parser = ["asc", "desc"])] + pub order: String, + #[arg(long = "limit", id = "limit", value_parser = parse_u64)] + pub limit: String, + #[arg(long = "after", id = "after", value_parser = parse_hex)] + pub after: Option, +} + +#[derive(Args, Debug)] +pub struct RpcGetTransactionsArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, + #[arg(long = "order", id = "order", value_parser = ["asc", "desc"])] + pub order: String, + #[arg(long = "limit", id = "limit", value_parser = parse_u64)] + pub limit: String, + #[arg(long = "after", id = "after", value_parser = parse_hex)] + pub after: Option, +} + +#[derive(Args, Debug)] +pub struct RpcGetCellsCapacityArgs { + #[arg(long = "json-path", id = "json-path", value_parser = parse_file_path)] + pub json_path: String, +} + impl<'a> RpcSubCommand<'a> { pub fn new( rpc_client: &'a mut HttpRpcClient, @@ -41,415 +401,21 @@ impl<'a> RpcSubCommand<'a> { } } - pub fn subcommand() -> App<'static> { - let arg_hash = Arg::with_name("hash") - .long("hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required(true); - let arg_number = Arg::with_name("number") - .long("number") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .required(true) - .about("Block number"); - let arg_peer_id = Arg::with_name("peer-id") - .long("peer-id") - .takes_value(true) - .required(true) - .about("Node's peer id"); - let with_cycles = Arg::with_name("with-cycles") - .long("with-cycles") - .about("get block info with cycles"); - let packed = Arg::with_name("packed") - .long("packed") - .about("returns a 0x-prefixed hex string"); - - App::new("rpc") - .about("Invoke RPC call to node") - .arg( - Arg::with_name("raw-data") - .long("raw-data") - .global(true) - .about("Output raw jsonrpc data") - ) - .subcommands(vec![ - // [Chain] - App::new("get_block") - .about("Get block content by hash") - .arg(arg_hash.clone().about("Block hash")) - .arg(with_cycles.clone()) - .arg(packed.clone()), - App::new("get_block_by_number") - .about("Get block content by block number") - .arg(arg_number.clone()) - .arg(with_cycles.clone()) - .arg(packed.clone()), - App::new("get_block_hash") - .about("Get block hash by block number") - .arg(arg_number.clone()), - App::new("get_current_epoch").about("Get current epoch information"), - App::new("get_epoch_by_number") - .about("Get epoch information by epoch number") - .arg(arg_number.clone().about("Epoch number")), - App::new("get_header") - .about("Get block header content by hash") - .arg(arg_hash.clone().about("Block hash")) - .arg(packed.clone()), - App::new("get_header_by_number") - .about("Get block header by block number") - .arg(arg_number.clone()) - .arg(packed.clone()), - App::new("get_live_cell") - .about("Get live cell (live means unspent)") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required(true) - .about("Tx hash"), - ) - .arg( - Arg::with_name("index") - .long("index") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .required(true) - .about("Output index"), - ) - .arg( - Arg::with_name("include-tx-pool") - .long("include-tx-pool") - .about("Weather to check live cell in tx-pool") - ) - .arg( - Arg::with_name("with-data") - .long("with-data") - .about("Get live cell with data") - ), - App::new("get_tip_block_number").about("Get tip block number"), - App::new("get_tip_header").about("Get tip header") - .arg(packed.clone()), - App::new("get_transaction") - .about("Get transaction content by transaction hash") - .arg(arg_hash.clone().about("Tx hash")) - .arg(packed.clone()), - App::new("get_transaction_proof") - .about("Returns a Merkle proof that transactions are included in a block") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .multiple(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .about("Transaction hashes, all transactions must be in the same block") - ) - .arg( - Arg::with_name("block-hash") - .long("block-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .about("Looks for transactions in the block with this hash") - ), - App::new("verify_transaction_proof") - .about("Verifies that a proof points to transactions in a block, returning the transaction hashes it commits to") - .arg( - Arg::with_name("tx-proof-path") - .long("tx-proof-path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("File path of proof generated by `get_transaction_proof` (JSON format)") - ), - App::new("get_fork_block") - .about("Returns the information about a fork block by hash") - .arg(arg_hash.clone().about("The fork block hash")) - .arg(packed.clone()), - App::new("get_consensus") - .about("Return various consensus parameters"), - App::new("get_block_median_time") - .about("Returns the past median time by block hash") - .arg(arg_hash.clone().about("A median time is calculated for a consecutive block sequence. `block_hash` indicates the highest block of the sequence")), - App::new("get_block_economic_state") - .about("Returns increased issuance, miner reward, and the total transaction fee of a block") - .arg(arg_hash.clone().about("Specifies the block hash which rewards should be analyzed")), - App::new("estimate_cycles") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("Transaction content (json format, see rpc estimate_cycles)") - ) - .about("estimate_cycles run a transaction and return the execution consumed cycles."), - App::new("get_fee_rate_statics") - .arg( - Arg::with_name("target") - .long("target") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .about("[Deprecated! please use get_fee_rate_statistics] Specify the number (1 - 101) of confirmed blocks to be counted. If the number is even, automatically add one. Default is 21.") - ) - .about("[Deprecated! please use get_fee_rate_statistics] Returns the fee_rate statistics of confirmed blocks on the chain."), - App::new("get_fee_rate_statistics") - .arg( - Arg::with_name("target") - .long("target") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .about("Specify the number (1 - 101) of confirmed blocks to be counted. If the number is even, automatically add one. Default is 21.") - ) - .about("Returns the fee_rate statistics of confirmed blocks on the chain."), - App::new("get_deployments_info").about("Returns the information about all deployments"), - App::new("get_transaction_and_witness_proof") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .multiple(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .about("Transaction hashes") - ) - .arg( - Arg::with_name("block-hash") - .long("block-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .about("Looks for transactions in the block with this hash") - ).about("Returns a Merkle proof that transactions and witnesses are included in a block"), - App::new("verify_transaction_and_witness_proof") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("File path of proof which is a `TransactionAndWitnessProof` (JSON format)") - ) - .about("Verifies that a proof points to transactions in a block, returning the transaction hashes it commits to"), - // [Net] - App::new("get_banned_addresses").about("Get all banned IPs/Subnets"), - App::new("get_peers").about("Get connected peers"), - App::new("local_node_info").about("Get local node information"), - App::new("set_ban") - .arg( - Arg::with_name("address") - .long("address") - .takes_value(true) - .validator(|input| FromStrParser::::new().validate(input)) - .required(true) - .about("The IP/Subnet with an optional netmask (default is /32 = single IP)") - ) - .arg( - Arg::with_name("command") - .long("command") - .takes_value(true) - .possible_values(&["insert", "delete"]) - .required(true) - .about("`insert` to insert an IP/Subnet to the list, `delete` to delete an IP/Subnet from the list") - ) - .arg( - Arg::with_name("ban_time") - .long("ban_time") - .takes_value(true) - .validator(|input| DurationParser.validate(input)) - .required(true) - .default_value("24h") - .about("How long the IP is banned") - ) - .arg( - Arg::with_name("reason") - .long("reason") - .takes_value(true) - .about("Ban reason, optional parameter") - ) - .about("Insert or delete an IP/Subnet from the banned list"), - App::new("sync_state").about("Returns sync state of this node"), - App::new("set_network_active") - .arg( - Arg::with_name("state") - .long("state") - .takes_value(true) - .possible_values(&["enable", "disable"]) - .required(true) - .about("The network state to set") - ) - .about("Disable/enable all p2p network activity"), - App::new("add_node") - .arg(arg_peer_id.clone()) - .arg( - Arg::with_name("address") - .long("address") - .takes_value(true) - .validator(|input| FromStrParser::::new().validate(input)) - .required(true) - .about("Target node's address (multiaddr)") - ) - .about("Connect to a node"), - App::new("remove_node") - .arg(arg_peer_id.clone()) - .about("Disconnect a node"), - App::new("clear_banned_addresses").about("Clears all banned IPs/Subnets"), - App::new("ping_peers").about("Requests that a ping is sent to all connected peers, to measure ping time"), - // [Pool] - App::new("remove_transaction") - .about("Removes a transaction and all transactions which depends on it from tx pool if it exists") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required(true) - .about("Hash of a transaction"), - ), - App::new("tx_pool_info").about("Get transaction pool information"), - App::new("clear_tx_verify_queue").about("Clear TxPool verify_queue"), - App::new("test_tx_pool_accept") - .about("Test if transaction can be accepted by Tx Pool") - .arg( - Arg::with_name("tx-file").long("tx-file").takes_value(true).required(true).about("transaction data file(format json)") - ), - App::new("clear_tx_pool").about("Removes all transactions from the transaction pool"), - App::new("get_raw_tx_pool") - .about("Returns all transaction ids in tx pool as a json array of string transaction ids") - .arg(Arg::with_name("verbose").long("verbose").about("True for a json object, false for array of transaction ids")), - App::new("tx_pool_ready").about("Returns whether tx-pool service is started, ready for request"), - // [`Stats`] - App::new("get_blockchain_info").about("Get chain information"), - // [Alert] - App::new("send_alert") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("The alert message (json format)") - ) - .about("Sends an alert"), - // [`IntegrationTest`] - App::new("notify_transaction") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .required(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("[TEST ONLY] Transaction content (json format, see rpc send_transaction)") - ) - .about("[TEST ONLY] Notify transaction"), - App::new("truncate") - .arg( - Arg::with_name("tip-hash") - .long("tip-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required(true) - .about("Target tip block hash") - ) - .about("[TEST ONLY] Truncate blocks to target tip block"), - App::new("generate_block") - .about("[TEST ONLY] Generate an empty block"), - App::new("generate_epochs") - .arg( - Arg::with_name("num-epochs") - .long("num-epochs") - .takes_value(true) - .required(true) - .about("The number of epochs to generate.") - ) - .about("[TEST ONLY] Generate epochs"), - // [`Indexer`] - App::new("get_indexer_tip").about("Returns the indexed tip"), - App::new("get_cells") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .required(true) - .about("Indexer search key")) - .arg( - Arg::with_name("order") - .long("order") - .takes_value(true) - .possible_values(&["asc", "desc"]) - .required(true) - .about("Indexer search order") - ) - .arg( - Arg::with_name("limit") - .long("limit") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .required(true) - .about("Limit the number of results") - ) - .arg( - Arg::with_name("after") - .long("after") - .takes_value(true) - .validator(|input| HexParser.validate(input)) - .about("Pagination parameter") - ) - .about("Returns the live cells collection by the lock or type script"), - App::new("get_transactions") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .required(true) - .about("Indexer search key")) - .arg( - Arg::with_name("order") - .long("order") - .takes_value(true) - .possible_values(&["asc", "desc"]) - .required(true) - .about("Indexer search order") - ) - .arg( - Arg::with_name("limit") - .long("limit") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .required(true) - .about("Limit the number of results") - ) - .arg( - Arg::with_name("after") - .long("after") - .takes_value(true) - .validator(|input| HexParser.validate(input)) - .about("Pagination parameter") - ) - .about("Returns the transactions collection by the lock or type script"), - App::new("get_cells_capacity") - .arg( - Arg::with_name("json-path") - .long("json-path") - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .required(true) - .about("Indexer search key")) - .about("Returns the live cells capacity by the lock or type script"), - ]) + pub fn subcommand() -> Command { + RpcCmd::command() } } impl CliSubCommand for RpcSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, _debug: bool) -> Result { - let is_raw_data = matches.is_present("raw-data"); - match matches.subcommand() { + let cmd = RpcCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + let is_raw_data = cmd.raw_data; + match cmd.command { // [Chain] - ("get_block", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let with_cycles = m.is_present("with-cycles"); - let packed = m.is_present("packed"); - let hash: H256 = FixedHashParser::::default().from_matches(m, "hash")?; + RpcSubcommands::GetBlock(args) => { + let with_cycles = args.with_cycles; + let packed = args.packed; + let hash: H256 = FixedHashParser::::default().parse(&args.hash)?; if is_raw_data { let verbose = if packed { @@ -493,11 +459,10 @@ impl CliSubCommand for RpcSubCommand<'_> { } } } - ("get_block_by_number", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let with_cycles = m.is_present("with-cycles"); - let packed = m.is_present("packed"); - let number: u64 = FromStrParser::::default().from_matches(m, "number")?; + RpcSubcommands::GetBlockByNumber(args) => { + let with_cycles = args.with_cycles; + let packed = args.packed; + let number: u64 = FromStrParser::::default().parse(&args.number)?; if is_raw_data { let verbose = if packed { @@ -544,14 +509,13 @@ impl CliSubCommand for RpcSubCommand<'_> { } } } - ("get_block_hash", Some(m)) => { - let number: u64 = FromStrParser::::default().from_matches(m, "number")?; + RpcSubcommands::GetBlockHash(args) => { + let number: u64 = FromStrParser::::default().parse(&args.number)?; let resp = self.rpc_client.get_block_hash(number).map(OptionH256)?; Ok(Output::new_output(resp)) } - ("get_current_epoch", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetCurrentEpoch => { if is_raw_data { let resp = self .raw_rpc_client @@ -563,9 +527,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_epoch_by_number", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let number: u64 = FromStrParser::::default().from_matches(m, "number")?; + RpcSubcommands::GetEpochByNumber(args) => { + let number: u64 = FromStrParser::::default().parse(&args.number)?; if is_raw_data { let resp = self @@ -582,10 +545,9 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_header", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let packed = m.is_present("packed"); - let hash: H256 = FixedHashParser::::default().from_matches(m, "hash")?; + RpcSubcommands::GetHeader(args) => { + let packed = args.packed; + let hash: H256 = FixedHashParser::::default().parse(&args.hash)?; if is_raw_data { if packed { @@ -614,10 +576,9 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_header_by_number", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let packed = m.is_present("packed"); - let number: u64 = FromStrParser::::default().from_matches(m, "number")?; + RpcSubcommands::GetHeaderByNumber(args) => { + let packed = args.packed; + let number: u64 = FromStrParser::::default().parse(&args.number)?; if is_raw_data { if packed { @@ -649,13 +610,11 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_live_cell", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let include_tx_pool = m.is_present("include-tx-pool"); - let tx_hash: H256 = - FixedHashParser::::default().from_matches(m, "tx-hash")?; - let index: u32 = FromStrParser::::default().from_matches(m, "index")?; - let with_data = m.is_present("with-data"); + RpcSubcommands::GetLiveCell(args) => { + let include_tx_pool = args.include_tx_pool; + let tx_hash: H256 = FixedHashParser::::default().parse(&args.tx_hash)?; + let index: u32 = FromStrParser::::default().parse(&args.index)?; + let with_data = args.with_data; let out_point = packed::OutPoint::new_builder() .tx_hash(tx_hash.pack()) @@ -687,8 +646,7 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_tip_block_number", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetTipBlockNumber => { if is_raw_data { let resp = self .raw_rpc_client @@ -703,9 +661,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_tip_header", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let packed = m.is_present("packed"); + RpcSubcommands::GetTipHeader(args) => { + let packed = args.packed; if is_raw_data { if packed { let resp = self @@ -730,10 +687,9 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_transaction", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let packed = m.is_present("packed"); - let hash: H256 = FixedHashParser::::default().from_matches(m, "hash")?; + RpcSubcommands::GetTransaction(args) => { + let packed = args.packed; + let hash: H256 = FixedHashParser::::default().parse(&args.hash)?; if is_raw_data { let verbosity = if packed { Some("0x0") } else { None }; @@ -757,12 +713,17 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_transaction_proof", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let tx_hashes: Vec = - FixedHashParser::::default().from_matches_vec(m, "tx-hash")?; - let block_hash: Option = - FixedHashParser::::default().from_matches_opt(m, "block-hash")?; + RpcSubcommands::GetTransactionProof(args) => { + let tx_hashes: Vec = args + .tx_hash + .iter() + .map(|value| FixedHashParser::::default().parse(value)) + .collect::, String>>()?; + let block_hash: Option = args + .block_hash + .as_ref() + .map(|value| FixedHashParser::::default().parse(value)) + .transpose()?; if is_raw_data { let resp = self @@ -777,9 +738,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("verify_transaction_proof", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let path: PathBuf = FilePathParser::new(true).from_matches(m, "tx-proof-path")?; + RpcSubcommands::VerifyTransactionProof(args) => { + let path: PathBuf = FilePathParser::new(true).parse(&args.tx_proof_path)?; let content = fs::read_to_string(path).map_err(|err| err.to_string())?; if is_raw_data { @@ -797,10 +757,9 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_fork_block", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let packed = m.is_present("packed"); - let hash: H256 = FixedHashParser::::default().from_matches(m, "hash")?; + RpcSubcommands::GetForkBlock(args) => { + let packed = args.packed; + let hash: H256 = FixedHashParser::::default().parse(&args.hash)?; if is_raw_data { if packed { @@ -823,8 +782,7 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_consensus", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetConsensus => { if is_raw_data { let resp = self .raw_rpc_client @@ -836,9 +794,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_block_median_time", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let hash: H256 = FixedHashParser::::default().from_matches(m, "hash")?; + RpcSubcommands::GetBlockMedianTime(args) => { + let hash: H256 = FixedHashParser::::default().parse(&args.hash)?; if is_raw_data { let resp = self @@ -855,9 +812,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_block_economic_state", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let hash: H256 = FixedHashParser::::default().from_matches(m, "hash")?; + RpcSubcommands::GetBlockEconomicState(args) => { + let hash: H256 = FixedHashParser::::default().parse(&args.hash)?; if is_raw_data { let resp = self @@ -874,9 +830,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("estimate_cycles", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let json_path: PathBuf = FilePathParser::new(true).from_matches(m, "json-path")?; + RpcSubcommands::EstimateCycles(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let tx: Transaction = serde_json::from_str(&content).map_err(|err| err.to_string())?; @@ -891,10 +846,12 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_fee_rate_statics", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let target: Option = - FeeRateStatisticsTargetParser {}.from_matches_opt(m, "target")?; + RpcSubcommands::GetFeeRateStatics(args) => { + let target: Option = args + .target + .as_ref() + .map(|value| FeeRateStatisticsTargetParser {}.parse(value)) + .transpose()?; if is_raw_data { let resp = self @@ -907,10 +864,12 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_fee_rate_statistics", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let target: Option = - FeeRateStatisticsTargetParser {}.from_matches_opt(m, "target")?; + RpcSubcommands::GetFeeRateStatistics(args) => { + let target: Option = args + .target + .as_ref() + .map(|value| FeeRateStatisticsTargetParser {}.parse(value)) + .transpose()?; if is_raw_data { let resp = self @@ -923,9 +882,7 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_deployments_info", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - + RpcSubcommands::GetDeploymentsInfo => { if is_raw_data { let resp = self .raw_rpc_client @@ -937,12 +894,17 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_transaction_and_witness_proof", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let tx_hashes: Vec = - FixedHashParser::::default().from_matches_vec(m, "tx-hash")?; - let block_hash: Option = - FixedHashParser::::default().from_matches_opt(m, "block-hash")?; + RpcSubcommands::GetTransactionAndWitnessProof(args) => { + let tx_hashes: Vec = args + .tx_hash + .iter() + .map(|value| FixedHashParser::::default().parse(value)) + .collect::, String>>()?; + let block_hash: Option = args + .block_hash + .as_ref() + .map(|value| FixedHashParser::::default().parse(value)) + .transpose()?; if is_raw_data { let resp = self @@ -957,10 +919,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("verify_transaction_and_witness_proof", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - - let json_path: PathBuf = FilePathParser::new(true).from_matches(m, "json-path")?; + RpcSubcommands::VerifyTransactionAndWitnessProof(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let tx_and_witness_proof: rpc_types::TransactionAndWitnessProof = @@ -979,8 +939,7 @@ impl CliSubCommand for RpcSubCommand<'_> { } } // [Net] - ("get_banned_addresses", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetBannedAddresses => { if is_raw_data { let resp = self .raw_rpc_client @@ -993,8 +952,7 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_peers", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetPeers => { if is_raw_data { let resp = self .raw_rpc_client @@ -1007,8 +965,7 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("local_node_info", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::LocalNodeInfo => { if is_raw_data { let resp = self .raw_rpc_client @@ -1020,12 +977,11 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("set_ban", Some(m)) => { - let address: IpNetwork = - FromStrParser::::new().from_matches(m, "address")?; - let ban_time: Duration = DurationParser.from_matches(m, "ban_time")?; - let command = m.value_of("command").map(|v| v.to_string()).unwrap(); - let reason = m.value_of("reason").map(|v| v.to_string()); + RpcSubcommands::SetBan(args) => { + let address: IpNetwork = FromStrParser::::new().parse(&args.address)?; + let ban_time: Duration = DurationParser.parse(&args.ban_time)?; + let command = args.command.clone(); + let reason = args.reason.clone(); let absolute = Some(false); let ban_time = Some(ban_time.as_secs() * 1000); @@ -1038,8 +994,7 @@ impl CliSubCommand for RpcSubCommand<'_> { )?; Ok(Output::new_success()) } - ("sync_state", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::SyncState => { if is_raw_data { let resp = self .raw_rpc_client @@ -1051,40 +1006,37 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("set_network_active", Some(m)) => { - let state = m.value_of("state").unwrap() == "enable"; + RpcSubcommands::SetNetworkActive(args) => { + let state = args.state == "enable"; self.rpc_client.set_network_active(state)?; Ok(Output::new_success()) } - ("add_node", Some(m)) => { - let peer_id = m.value_of("peer-id").map(|v| v.to_string()).unwrap(); - let address: Multiaddr = - FromStrParser::::new().from_matches(m, "address")?; + RpcSubcommands::AddNode(args) => { + let peer_id = args.peer_id.clone(); + let address: Multiaddr = FromStrParser::::new().parse(&args.address)?; self.rpc_client.add_node(peer_id, address.to_string())?; Ok(Output::new_success()) } - ("remove_node", Some(m)) => { - let peer_id = m.value_of("peer-id").map(|v| v.to_string()).unwrap(); + RpcSubcommands::RemoveNode(args) => { + let peer_id = args.peer_id.clone(); self.rpc_client.remove_node(peer_id)?; Ok(Output::new_success()) } - ("clear_banned_addresses", _) => { + RpcSubcommands::ClearBannedAddresses => { self.rpc_client.clear_banned_addresses()?; Ok(Output::new_success()) } - ("ping_peers", _) => { + RpcSubcommands::PingPeers => { self.rpc_client.ping_peers()?; Ok(Output::new_success()) } // [Pool] - ("remove_transaction", Some(m)) => { - let tx_hash: H256 = - FixedHashParser::::default().from_matches(m, "tx-hash")?; + RpcSubcommands::RemoveTransaction(args) => { + let tx_hash: H256 = FixedHashParser::::default().parse(&args.tx_hash)?; let resp = self.rpc_client.remove_transaction(tx_hash)?; Ok(Output::new_output(resp)) } - ("tx_pool_info", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::TxPoolInfo => { if is_raw_data { let resp = self .raw_rpc_client @@ -1096,8 +1048,7 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("clear_tx_verify_queue", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::ClearTxVerifyQueue => { if is_raw_data { self.raw_rpc_client .clear_tx_verify_queue() @@ -1108,8 +1059,8 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(())) } } - ("test_tx_pool_accept", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(false).from_matches(m, "tx-file")?; + RpcSubcommands::TestTxPoolAccept(args) => { + let tx_file: PathBuf = FilePathParser::new(false).parse(&args.tx_file)?; let mut live_cell_cache: HashMap<(OutPoint, bool), (CellOutput, Bytes)> = Default::default(); @@ -1131,7 +1082,6 @@ impl CliSubCommand for RpcSubCommand<'_> { let tx_view = helper.build_tx(&mut get_live_cell, true)?; let tx = tx_view.data(); - let is_raw_data = is_raw_data || m.is_present("raw-data"); if is_raw_data { let resp = self .raw_rpc_client @@ -1143,17 +1093,16 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("clear_tx_pool", _) => { + RpcSubcommands::ClearTxPool => { self.rpc_client.clear_tx_pool()?; Ok(Output::new_success()) } - ("tx_pool_ready", _) => { + RpcSubcommands::TxPoolReady => { let resp = self.rpc_client.tx_pool_ready()?; Ok(Output::new_output(resp)) } - ("get_raw_tx_pool", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); - let verbose = m.is_present("verbose"); + RpcSubcommands::GetRawTxPool(args) => { + let verbose = args.verbose; if is_raw_data { let resp = self .raw_rpc_client @@ -1166,8 +1115,7 @@ impl CliSubCommand for RpcSubCommand<'_> { } } // [Stats] - ("get_blockchain_info", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetBlockchainInfo => { if is_raw_data { let resp = self .raw_rpc_client @@ -1180,41 +1128,39 @@ impl CliSubCommand for RpcSubCommand<'_> { } } // [Alert] - ("send_alert", Some(m)) => { - let json_path: PathBuf = FilePathParser::new(true).from_matches(m, "json-path")?; + RpcSubcommands::SendAlert(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let alert: Alert = serde_json::from_str(&content).map_err(|err| err.to_string())?; self.rpc_client.send_alert(alert)?; Ok(Output::new_success()) } // [IntegrationTest] - ("notify_transaction", Some(m)) => { - let json_path: PathBuf = FilePathParser::new(true).from_matches(m, "json-path")?; + RpcSubcommands::NotifyTransaction(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let tx: Transaction = serde_json::from_str(&content).map_err(|err| err.to_string())?; let resp = self.rpc_client.notify_transaction(tx.into())?; Ok(Output::new_output(resp)) } - ("truncate", Some(m)) => { + RpcSubcommands::Truncate(args) => { let target_tip_hash: H256 = - FixedHashParser::::default().from_matches(m, "tip-hash")?; + FixedHashParser::::default().parse(&args.tip_hash)?; self.rpc_client.truncate(target_tip_hash)?; Ok(Output::new_success()) } - ("generate_block", Some(_m)) => { + RpcSubcommands::GenerateBlock => { let resp = self.rpc_client.generate_block()?; Ok(Output::new_output(resp)) } - ("generate_epochs", Some(m)) => { - let num_epochs: u64 = - FromStrParser::::default().from_matches(m, "num-epochs")?; + RpcSubcommands::GenerateEpochs(args) => { + let num_epochs: u64 = FromStrParser::::default().parse(&args.num_epochs)?; let resp = self.rpc_client.generate_epochs(num_epochs)?; Ok(Output::new_output(resp)) } // [Indexer] - ("get_indexer_tip", Some(m)) => { - let is_raw_data = is_raw_data || m.is_present("raw-data"); + RpcSubcommands::GetIndexerTip => { if is_raw_data { let resp = self .raw_rpc_client @@ -1226,20 +1172,22 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_cells", Some(m)) => { - let json_path: PathBuf = FilePathParser::new(true) - .from_matches_opt(m, "json-path")? - .expect("json-path is required"); + RpcSubcommands::GetCells(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let search_key = serde_json::from_str(&content).map_err(|err| err.to_string())?; - let order_str = m.value_of("order").expect("order is required"); - let order = parse_order(order_str)?; - let limit: u32 = FromStrParser::::default().from_matches(m, "limit")?; - let after_opt: Option = HexParser - .from_matches_opt::(m, "after")? - .map(JsonBytes::from_bytes); - - let is_raw_data = is_raw_data || m.is_present("raw-data"); + let order = parse_order(&args.order)?; + let limit: u32 = FromStrParser::::default().parse(&args.limit)?; + let after_opt: Option = args + .after + .as_ref() + .map(|value| { + HexParser + .parse(value) + .map(Bytes::from) + .map(JsonBytes::from_bytes) + }) + .transpose()?; if is_raw_data { let resp = self .raw_rpc_client @@ -1253,20 +1201,22 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_transactions", Some(m)) => { - let json_path: PathBuf = FilePathParser::new(true) - .from_matches_opt(m, "json-path")? - .expect("json-path is required"); + RpcSubcommands::GetTransactions(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let search_key = serde_json::from_str(&content).map_err(|err| err.to_string())?; - let order_str = m.value_of("order").expect("order is required"); - let order = parse_order(order_str)?; - let limit: u32 = FromStrParser::::default().from_matches(m, "limit")?; - let after_opt: Option = HexParser - .from_matches_opt::(m, "after")? - .map(JsonBytes::from_bytes); - - let is_raw_data = is_raw_data || m.is_present("raw-data"); + let order = parse_order(&args.order)?; + let limit: u32 = FromStrParser::::default().parse(&args.limit)?; + let after_opt: Option = args + .after + .as_ref() + .map(|value| { + HexParser + .parse(value) + .map(Bytes::from) + .map(JsonBytes::from_bytes) + }) + .transpose()?; if is_raw_data { let resp = self .raw_rpc_client @@ -1283,14 +1233,10 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - ("get_cells_capacity", Some(m)) => { - let json_path: PathBuf = FilePathParser::new(true) - .from_matches_opt(m, "json-path")? - .expect("json-path is required"); + RpcSubcommands::GetCellsCapacity(args) => { + let json_path: PathBuf = FilePathParser::new(true).parse(&args.json_path)?; let content = fs::read_to_string(json_path).map_err(|err| err.to_string())?; let search_key = serde_json::from_str(&content).map_err(|err| err.to_string())?; - - let is_raw_data = is_raw_data || m.is_present("raw-data"); if is_raw_data { let resp = self .raw_rpc_client @@ -1302,7 +1248,6 @@ impl CliSubCommand for RpcSubCommand<'_> { Ok(Output::new_output(resp)) } } - _ => Err(Self::subcommand().generate_usage()), } } } diff --git a/src/subcommands/sudt.rs b/src/subcommands/sudt.rs index 0ebf1895..534708b2 100644 --- a/src/subcommands/sudt.rs +++ b/src/subcommands/sudt.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; -use clap::{App, Arg, ArgMatches}; +use clap::{ + ArgAction, ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, +}; use ckb_jsonrpc_types as json_types; use ckb_sdk::{ @@ -37,7 +39,6 @@ use crate::{ plugin::PluginManager, subcommands::{CliSubCommand, Output}, utils::{ - arg, arg_parser::{ AddressParser, ArgParser, CellDepsParser, FromStrParser, PrivkeyPathParser, PrivkeyWrapper, UdtTargetParser, @@ -67,6 +68,222 @@ struct SudtCommonArgs { debug: bool, } +fn parse_owner(input: &str) -> Result { + AddressParser::new_sighash() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_sender(input: &str) -> Result { + AddressParser::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_capacity_provider(input: &str) -> Result { + AddressParser::new_sighash() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_udt_to(input: &str) -> Result { + UdtTargetParser::new(AddressParser::default()) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_sighash_address(input: &str) -> Result { + AddressParser::new_sighash() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_address(input: &str) -> Result { + AddressParser::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_cell_deps(input: &str) -> Result { + CellDepsParser.validate(input).map(|_| input.to_string()) +} + +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_fee_rate(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_max_tx_fee(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(about = "SUDT issue/transfer operations (currently only support sudt)")] +pub struct SudtCmd { + #[command(subcommand)] + pub command: SudtSubcommands, +} + +#[derive(Subcommand, Debug)] +#[command(rename_all = "kebab-case")] +pub enum SudtSubcommands { + /// Issue SUDT to multiple addresses + Issue(SudtIssueArgs), + /// Transfer SUDT to multiple addresses (all target addresses must have same lock script id) + Transfer(SudtTransferArgs), + /// Get SUDT total amount of an address + GetAmount(SudtGetAmountArgs), + /// Create a SUDT cell with 0 amount and an acp lock script + NewEmptyAcp(SudtNewEmptyAcpArgs), + /// Claim all cheque cells identified by given lock script and type script + ChequeClaim(SudtChequeClaimArgs), + /// Withdraw all cheque cells identified by given lock script and type script + ChequeWithdraw(SudtChequeWithdrawArgs), + /// Build an anyone-can-pay address by sighash address and anyone-can-pay script id. + BuildAcpAddress(SudtBuildAcpAddressArgs), + /// Build a cheque address by cheque script id and receiver+sender address + BuildChequeAddress(SudtBuildChequeAddressArgs), +} + +#[derive(Args, Debug)] +pub struct SudtIssueArgs { + #[arg(long = "owner", id = "owner", value_parser = parse_owner)] + pub owner: String, + #[arg(long = "udt-to", id = "udt-to", action = ArgAction::Append, num_args = 1.., required = true, value_parser = parse_udt_to)] + pub udt_to: Vec, + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "to-acp-address", id = "to-acp-address")] + pub to_acp_address: bool, + #[arg(long = "to-cheque-address", id = "to-cheque-address")] + pub to_cheque_address: bool, + #[arg(long = "privkey-path", id = "privkey-path", action = ArgAction::Append, num_args = 1.., value_parser = parse_privkey_path)] + pub privkey_path: Vec, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_fee_rate)] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_max_tx_fee)] + pub max_tx_fee: Option, +} + +#[derive(Args, Debug)] +pub struct SudtTransferArgs { + #[arg(long = "owner", id = "owner", value_parser = parse_owner)] + pub owner: String, + #[arg(long = "sender", id = "sender", value_parser = parse_sender)] + pub sender: String, + #[arg(long = "udt-to", id = "udt-to", action = ArgAction::Append, num_args = 1.., required = true, value_parser = parse_udt_to)] + pub udt_to: Vec, + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "to-acp-address", id = "to-acp-address")] + pub to_acp_address: bool, + #[arg(long = "to-cheque-address", id = "to-cheque-address")] + pub to_cheque_address: bool, + #[arg(long = "capacity-provider", id = "capacity-provider", value_parser = parse_capacity_provider)] + pub capacity_provider: Option, + #[arg(long = "privkey-path", id = "privkey-path", action = ArgAction::Append, num_args = 1.., value_parser = parse_privkey_path)] + pub privkey_path: Vec, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_fee_rate)] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_max_tx_fee)] + pub max_tx_fee: Option, +} + +#[derive(Args, Debug)] +pub struct SudtGetAmountArgs { + #[arg(long = "owner", id = "owner", value_parser = parse_owner)] + pub owner: String, + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "address", id = "address", value_parser = parse_address)] + pub address: String, +} + +#[derive(Args, Debug)] +pub struct SudtNewEmptyAcpArgs { + #[arg(long = "owner", id = "owner", value_parser = parse_owner)] + pub owner: String, + #[arg(long = "capacity-provider", id = "capacity-provider", value_parser = parse_capacity_provider)] + pub capacity_provider: Option, + #[arg(long = "to", id = "to", value_parser = parse_sighash_address)] + pub to: String, + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "privkey-path", id = "privkey-path", action = ArgAction::Append, num_args = 1.., value_parser = parse_privkey_path)] + pub privkey_path: Vec, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_fee_rate)] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_max_tx_fee)] + pub max_tx_fee: Option, +} + +#[derive(Args, Debug)] +pub struct SudtChequeClaimArgs { + #[arg(long = "owner", id = "owner", value_parser = parse_owner)] + pub owner: String, + #[arg(long = "sender", id = "sender", value_parser = parse_sender)] + pub sender: String, + #[arg(long = "receiver", id = "receiver", value_parser = parse_sighash_address)] + pub receiver: String, + #[arg(long = "capacity-provider", id = "capacity-provider", value_parser = parse_capacity_provider)] + pub capacity_provider: Option, + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "privkey-path", id = "privkey-path", action = ArgAction::Append, num_args = 1.., value_parser = parse_privkey_path)] + pub privkey_path: Vec, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_fee_rate)] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_max_tx_fee)] + pub max_tx_fee: Option, +} + +#[derive(Args, Debug)] +pub struct SudtChequeWithdrawArgs { + #[arg(long = "owner", id = "owner", value_parser = parse_owner)] + pub owner: String, + #[arg(long = "sender", id = "sender", value_parser = parse_sender)] + pub sender: String, + #[arg(long = "receiver", id = "receiver", value_parser = parse_sighash_address)] + pub receiver: String, + #[arg(long = "capacity-provider", id = "capacity-provider", value_parser = parse_capacity_provider)] + pub capacity_provider: Option, + #[arg(long = "to-acp-address", id = "to-acp-address")] + pub to_acp_address: bool, + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "privkey-path", id = "privkey-path", action = ArgAction::Append, num_args = 1.., value_parser = parse_privkey_path)] + pub privkey_path: Vec, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_fee_rate)] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_max_tx_fee)] + pub max_tx_fee: Option, +} + +#[derive(Args, Debug)] +pub struct SudtBuildAcpAddressArgs { + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "sighash-address", id = "sighash-address", value_parser = parse_sighash_address)] + pub sighash_address: String, +} + +#[derive(Args, Debug)] +pub struct SudtBuildChequeAddressArgs { + #[arg(long = "cell-deps", id = "cell-deps", value_parser = parse_cell_deps)] + pub cell_deps: String, + #[arg(long = "receiver", id = "receiver", value_parser = parse_sighash_address)] + pub receiver: String, + #[arg(long = "sender", id = "sender", value_parser = parse_sender)] + pub sender: String, +} + impl<'a> SudtSubCommand<'a> { pub fn new( rpc_client: &'a mut HttpRpcClient, @@ -87,130 +304,8 @@ impl<'a> SudtSubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_udt_to = Arg::with_name("udt-to") - .long("udt-to") - .takes_value(true) - .multiple(true) - .required(true) - .validator(|input| UdtTargetParser::new(AddressParser::default()).validate(input)); - let arg_to_cheque_address = Arg::with_name("to-cheque-address").long("to-cheque-address"); - let arg_receiver = Arg::with_name("receiver") - .long("receiver") - .takes_value(true) - .required(true) - .validator(|input| AddressParser::new_sighash().validate(input)); - - App::new(name) - .about("SUDT issue/transfer operations (currently only support sudt)") - .subcommands(vec![ - App::new("issue") - .about("Issue SUDT to multiple addresses") - .arg(arg_owner()) - .arg( - arg_udt_to.clone() - .about("The issue target, format: {address}:{amount}, the address type can be: [acp, sighash]") - ) - .arg(arg_cell_deps()) - .arg(arg_to_acp_address()) - .arg( - arg_to_cheque_address - .clone() - .about("Treat all addresses in as cheque receiver (sighash address, and the cheque sender is the ), otherwise the address will be used as the lock script of the SUDT cell") - ) - .arg(arg::privkey_path().multiple(true)) - .arg(arg::fee_rate()) - .arg(arg::max_tx_fee()), - App::new("transfer") - .about("Transfer SUDT to multiple addresses (all target addresses must have same lock script id)") - .arg(arg_owner()) - .arg(arg_sender().about("SUDT sender address, the address type can be: [acp, sighash], when address type is `acp` this address will be used to build a sighash lock script for build cheque address or provide capacity, if is not given will also use as capacity provider.")) - .arg( - arg_udt_to - .about("The transfer target, format: {address}:{amount}, the address type can be: [acp, sighash]") - ) - .arg(arg_cell_deps()) - .arg(arg_to_acp_address()) - .arg( - arg_to_cheque_address - .clone() - .about("Treat all addresses in as cheque receiver (sighash address), otherwise the address will be used as the lock script of the SUDT cell. When this flag is presented cell_dep must be given") - ) - .arg(arg_capacity_provider()) - .arg(arg::privkey_path().multiple(true)) - .arg(arg::fee_rate()) - .arg(arg::max_tx_fee()), - App::new("get-amount") - .about("Get SUDT total amount of an address") - .arg(arg_owner()) - .arg(arg_cell_deps()) - .arg( - Arg::with_name("address") - .long("address") - .takes_value(true) - .required(true) - .validator(|input| AddressParser::default().validate(input)) - .about("The target address of those SUDT cells"), - ), - App::new("new-empty-acp") - .about("Create a SUDT cell with 0 amount and an acp lock script") - .arg(arg_owner()) - .arg(arg_capacity_provider()) - .arg( - Arg::with_name("to") - .long("to") - .takes_value(true) - .required(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("The target address (sighash), used to create anyone-can-pay address, if is not given will also use as capacity provider"), - ) - .arg(arg_cell_deps()) - .arg(arg::privkey_path().multiple(true)) - .arg(arg::fee_rate()) - .arg(arg::max_tx_fee()), - App::new("cheque-claim") - .about("Claim all cheque cells identified by given lock script and type script") - .arg(arg_owner()) - .arg(arg_sender().about("The cheque sender address (sighash)")) - .arg( - arg_receiver - .clone() - .about("The cheque receiver address (sighash), for searching an input to save the claimed amount, this address will be used to build anyone-can-pay address, if not given will also be used as capacity provider") - ) - .arg(arg_capacity_provider()) - .arg(arg_cell_deps()) - .arg(arg::privkey_path().multiple(true)) - .arg(arg::fee_rate()) - .arg(arg::max_tx_fee()), - App::new("cheque-withdraw") - .about("Withdraw all cheque cells identified by given lock script and type script") - .arg(arg_owner()) - .arg(arg_sender().about("The cheque sender address (sighash), if not given will use as capacity provider")) - .arg(arg_receiver.clone().about("The cheque receiver address (sighash)")) - .arg(arg_capacity_provider()) - .arg(arg_to_acp_address().about("Withdraw to anyone-can-pay address, will use to build the anyone-can-pay address, the cell must be already exists")) - .arg(arg_cell_deps()) - .arg(arg::privkey_path().multiple(true)) - .arg(arg::fee_rate()) - .arg(arg::max_tx_fee()), - // TODO: move this subcommand to `util` - App::new("build-acp-address") - .about("Build an anyone-can-pay address by sighash address and anyone-can-pay script id.") - .arg(arg_cell_deps()) - .arg( - Arg::with_name("sighash-address") - .long("sighash-address") - .takes_value(true) - .required(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("The sighash address") - ), - App::new("build-cheque-address") - .about("Build a cheque address by cheque script id and receiver+sender address") - .arg(arg_cell_deps()) - .arg(arg_receiver.about("The receiver address")) - .arg(arg_sender()), - ]) + pub fn subcommand(name: &'static str) -> Command { + SudtCmd::command().name(name) } fn issue( @@ -866,27 +961,42 @@ impl<'a> SudtSubCommand<'a> { } } -impl CliSubCommand for SudtSubCommand<'_> { +impl<'a> CliSubCommand for SudtSubCommand<'a> { fn process(&mut self, matches: &ArgMatches, debug: bool) -> Result { let network = get_network_type(self.rpc_client)?; - match matches.subcommand() { - ("issue", Some(m)) => { + let cmd = SudtCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + SudtSubcommands::Issue(args) => { let owner: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "owner")?; - let udt_to_vec: Vec<(Address, u128)> = { - let mut address_parser = AddressParser::default(); - address_parser.set_network(network); - UdtTargetParser::new(address_parser).from_matches_vec(m, "udt-to")? - }; - let privkeys: Vec = - PrivkeyPathParser.from_matches_vec(m, "privkey-path")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; - let force_small_change_as_fee = - FromStrParser::::default().from_matches_opt(m, "max-tx-fee")?; - let to_cheque_address = m.is_present("to-cheque-address"); - let to_acp_address = m.is_present("to-acp-address"); + .parse(&args.owner)?; + let udt_to_vec: Vec<(Address, u128)> = args + .udt_to + .iter() + .map(|value| { + let mut address_parser = AddressParser::default(); + address_parser.set_network(network); + UdtTargetParser::new(address_parser).parse(value) + }) + .collect::, String>>()?; + let privkeys: Vec = args + .privkey_path + .iter() + .map(|value| PrivkeyPathParser.parse(value)) + .collect::, String>>()?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; + let force_small_change_as_fee = args + .max_tx_fee + .as_ref() + .map(|value| { + FromStrParser::::default() + .parse(value) + .map(Into::into) + }) + .transpose()?; + let to_cheque_address = args.to_cheque_address; + let to_acp_address = args.to_acp_address; check_udt_args( &udt_to_vec, @@ -913,29 +1023,49 @@ impl CliSubCommand for SudtSubCommand<'_> { network, ) } - ("transfer", Some(m)) => { + SudtSubcommands::Transfer(args) => { let owner: Address = AddressParser::default() .set_network(network) - .from_matches(m, "owner")?; + .parse(&args.owner)?; let sender: Address = AddressParser::default() .set_network(network) - .from_matches(m, "sender")?; - let udt_to_vec: Vec<(Address, u128)> = { - let mut address_parser = AddressParser::default(); - address_parser.set_network(network); - UdtTargetParser::new(address_parser).from_matches_vec(m, "udt-to")? - }; - let capacity_provider: Option
= AddressParser::new_sighash() - .set_network(network) - .from_matches_opt(m, "capacity-provider")?; - let privkeys: Vec = - PrivkeyPathParser.from_matches_vec(m, "privkey-path")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; - let to_cheque_address = m.is_present("to-cheque-address"); - let to_acp_address = m.is_present("to-acp-address"); - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; - let force_small_change_as_fee = - FromStrParser::::default().from_matches_opt(m, "max-tx-fee")?; + .parse(&args.sender)?; + let udt_to_vec: Vec<(Address, u128)> = args + .udt_to + .iter() + .map(|value| { + let mut address_parser = AddressParser::default(); + address_parser.set_network(network); + UdtTargetParser::new(address_parser).parse(value) + }) + .collect::, String>>()?; + let capacity_provider: Option
= args + .capacity_provider + .as_ref() + .map(|value| { + AddressParser::new_sighash() + .set_network(network) + .parse(value) + }) + .transpose()?; + let privkeys: Vec = args + .privkey_path + .iter() + .map(|value| PrivkeyPathParser.parse(value)) + .collect::, String>>()?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; + let to_cheque_address = args.to_cheque_address; + let to_acp_address = args.to_acp_address; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; + let force_small_change_as_fee = args + .max_tx_fee + .as_ref() + .map(|value| { + FromStrParser::::default() + .parse(value) + .map(Into::into) + }) + .transpose()?; check_udt_args( &udt_to_vec, @@ -964,32 +1094,48 @@ impl CliSubCommand for SudtSubCommand<'_> { network, ) } - ("get-amount", Some(m)) => { + SudtSubcommands::GetAmount(args) => { let owner: Address = AddressParser::default() .set_network(network) - .from_matches(m, "owner")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; + .parse(&args.owner)?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; let address: Address = AddressParser::default() .set_network(network) - .from_matches(m, "address")?; + .parse(&args.address)?; self.get_amount(owner, address, cell_deps) } - ("new-empty-acp", Some(m)) => { + SudtSubcommands::NewEmptyAcp(args) => { let owner: Address = AddressParser::default() .set_network(network) - .from_matches(m, "owner")?; + .parse(&args.owner)?; let to: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "to")?; - let capacity_provider: Option
= AddressParser::new_sighash() - .set_network(network) - .from_matches_opt(m, "capacity-provider")?; - let privkeys: Vec = - PrivkeyPathParser.from_matches_vec(m, "privkey-path")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; - let force_small_change_as_fee = - FromStrParser::::default().from_matches_opt(m, "max-tx-fee")?; + .parse(&args.to)?; + let capacity_provider: Option
= args + .capacity_provider + .as_ref() + .map(|value| { + AddressParser::new_sighash() + .set_network(network) + .parse(value) + }) + .transpose()?; + let privkeys: Vec = args + .privkey_path + .iter() + .map(|value| PrivkeyPathParser.parse(value)) + .collect::, String>>()?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; + let force_small_change_as_fee = args + .max_tx_fee + .as_ref() + .map(|value| { + FromStrParser::::default() + .parse(value) + .map(Into::into) + }) + .transpose()?; self.new_empty_acp( NewAcpArgs { owner, @@ -1006,25 +1152,41 @@ impl CliSubCommand for SudtSubCommand<'_> { network, ) } - ("cheque-claim", Some(m)) => { + SudtSubcommands::ChequeClaim(args) => { let owner: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "owner")?; + .parse(&args.owner)?; let sender: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "sender")?; + .parse(&args.sender)?; let receiver: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "receiver")?; - let capacity_provider: Option
= AddressParser::new_sighash() - .set_network(network) - .from_matches_opt(m, "capacity-provider")?; - let privkeys: Vec = - PrivkeyPathParser.from_matches_vec(m, "privkey-path")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; - let force_small_change_as_fee = - FromStrParser::::default().from_matches_opt(m, "max-tx-fee")?; + .parse(&args.receiver)?; + let capacity_provider: Option
= args + .capacity_provider + .as_ref() + .map(|value| { + AddressParser::new_sighash() + .set_network(network) + .parse(value) + }) + .transpose()?; + let privkeys: Vec = args + .privkey_path + .iter() + .map(|value| PrivkeyPathParser.parse(value)) + .collect::, String>>()?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; + let force_small_change_as_fee = args + .max_tx_fee + .as_ref() + .map(|value| { + FromStrParser::::default() + .parse(value) + .map(Into::into) + }) + .transpose()?; if capacity_provider.as_ref() == Some(&sender) { return Err(" can't be the same with ".to_string()); @@ -1045,26 +1207,42 @@ impl CliSubCommand for SudtSubCommand<'_> { }, ) } - ("cheque-withdraw", Some(m)) => { + SudtSubcommands::ChequeWithdraw(args) => { let owner: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "owner")?; + .parse(&args.owner)?; let sender: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "sender")?; + .parse(&args.sender)?; let receiver: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "receiver")?; - let capacity_provider: Option
= AddressParser::new_sighash() - .set_network(network) - .from_matches_opt(m, "capacity-provider")?; - let to_acp_address = m.is_present("to-acp-address"); - let privkeys: Vec = - PrivkeyPathParser.from_matches_vec(m, "privkey-path")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; - let fee_rate: u64 = FromStrParser::::default().from_matches(m, "fee-rate")?; - let force_small_change_as_fee = - FromStrParser::::default().from_matches_opt(m, "max-tx-fee")?; + .parse(&args.receiver)?; + let capacity_provider: Option
= args + .capacity_provider + .as_ref() + .map(|value| { + AddressParser::new_sighash() + .set_network(network) + .parse(value) + }) + .transpose()?; + let to_acp_address = args.to_acp_address; + let privkeys: Vec = args + .privkey_path + .iter() + .map(|value| PrivkeyPathParser.parse(value)) + .collect::, String>>()?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; + let fee_rate: u64 = FromStrParser::::default().parse(&args.fee_rate)?; + let force_small_change_as_fee = args + .max_tx_fee + .as_ref() + .map(|value| { + FromStrParser::::default() + .parse(value) + .map(Into::into) + }) + .transpose()?; self.cheque_withdraw( WithdrawArgs { owner, @@ -1082,11 +1260,11 @@ impl CliSubCommand for SudtSubCommand<'_> { }, ) } - ("build-acp-address", Some(m)) => { + SudtSubcommands::BuildAcpAddress(args) => { let sighash_addr: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "sighash-address")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; + .parse(&args.sighash_address)?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; let acp_script_id = get_script_id(&cell_deps, CellDepName::Acp)?; let acp_script = Script::new_builder() .code_hash(acp_script_id.code_hash.pack()) @@ -1097,14 +1275,14 @@ impl CliSubCommand for SudtSubCommand<'_> { let acp_addr = Address::new(network, acp_payload, true); Ok(Output::new_output(acp_addr.to_string())) } - ("build-cheque-address", Some(m)) => { + SudtSubcommands::BuildChequeAddress(args) => { let sender: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "sender")?; + .parse(&args.sender)?; let receiver: Address = AddressParser::new_sighash() .set_network(network) - .from_matches(m, "receiver")?; - let cell_deps: CellDeps = CellDepsParser.from_matches(m, "cell-deps")?; + .parse(&args.receiver)?; + let cell_deps: CellDeps = CellDepsParser.parse(&args.cell_deps)?; let cheque_script_id = get_script_id(&cell_deps, CellDepName::Cheque)?; let sender_script_hash = Script::from(&sender).calc_script_hash(); @@ -1121,7 +1299,6 @@ impl CliSubCommand for SudtSubCommand<'_> { let cheque_addr = Address::new(network, cheque_payload, true); Ok(Output::new_output(cheque_addr.to_string())) } - _ => Err(Self::subcommand("sudt").generate_usage()), } } } @@ -1201,43 +1378,6 @@ struct WithdrawArgs { to_acp_address: bool, } -pub fn arg_owner<'a>() -> Arg<'a> { - Arg::with_name("owner") - .long("owner") - .takes_value(true) - .required(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("The owner address of the SUDT cell (the admin address, only sighash address is supported)") -} -pub fn arg_sender<'a>() -> Arg<'a> { - Arg::with_name("sender") - .long("sender") - .takes_value(true) - .required(true) - .validator(|input| AddressParser::default().validate(input)) - .about("Sender address") -} -pub fn arg_capacity_provider<'a>() -> Arg<'a> { - Arg::with_name("capacity-provider") - .long("capacity-provider") - .takes_value(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("Capacity provider address (provide transaction fee or needed capacity)") -} -pub fn arg_to_acp_address<'a>() -> Arg<'a> { - Arg::with_name("to-acp-address") - .long("to-acp-address") - .about("Treat all addresses in as anyone-can-pay address") -} -pub fn arg_cell_deps<'a>() -> Arg<'a> { - Arg::with_name("cell-deps") - .long("cell-deps") - .takes_value(true) - .required(true) - .validator(|input| CellDepsParser.validate(input)) - .about("The cell deps information (for resolve cell_dep by script id or build lock/type script)") -} - pub struct UdtTxBuilder<'a> { pub plugin_mgr: &'a mut PluginManager, pub rpc_client: &'a HttpRpcClient, @@ -1248,7 +1388,7 @@ pub struct UdtTxBuilder<'a> { pub builder: &'a dyn TxBuilder, } -impl UdtTxBuilder<'_> { +impl<'a> UdtTxBuilder<'a> { #[allow(clippy::too_many_arguments)] pub fn build( &mut self, diff --git a/src/subcommands/tx.rs b/src/subcommands/tx.rs index c8b005af..a1ee67c6 100644 --- a/src/subcommands/tx.rs +++ b/src/subcommands/tx.rs @@ -20,17 +20,15 @@ use ckb_types::{ prelude::*, H160, H256, }; -use clap::{App, Arg, ArgMatches}; +use clap::{ + ArgAction, ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand, +}; use faster_hex::hex_string; use serde_derive::{Deserialize, Serialize}; -use super::{ - arg_get_multisig_code_hash, arg_multisig_code_hash, CliSubCommand, Output, - ALLOW_ZERO_LOCK_HELP_MSG, -}; +use super::{CliSubCommand, Output}; use crate::plugin::{KeyStoreHandler, PluginManager, SignTarget}; use crate::utils::{ - arg, arg_parser::{ AddressParser, ArgParser, CapacityParser, FilePathParser, FixedHashParser, FromStrParser, HexParser, PrivkeyPathParser, PrivkeyWrapper, @@ -38,7 +36,7 @@ use crate::utils::{ genesis_info::GenesisInfo, other::{ check_capacity, get_genesis_info, get_live_cell, get_live_cell_with_cache, - get_network_type, get_privkey_signer, get_to_data, read_password, + get_network_type, get_privkey_signer, read_password, }, rpc::HttpRpcClient, tx_helper::{SignerFn, TxHelper}, @@ -50,6 +48,261 @@ pub struct TxSubCommand<'a> { genesis_info: Option, } +fn parse_tx_file(input: &str) -> Result { + FilePathParser::new(false) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_sighash_address(input: &str) -> Result { + AddressParser::new_sighash() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_multisig_address(input: &str) -> Result { + AddressParser::new_multisig(MultisigScript::Legacy) + .validate(input) + .or_else(|_| AddressParser::new_multisig(MultisigScript::V2).validate(input)) + .map(|_| input.to_string()) +} + +fn parse_u8(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_u32(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_u64(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_h256(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_capacity(input: &str) -> Result { + CapacityParser.validate(input).map(|_| input.to_string()) +} + +fn parse_hex(input: &str) -> Result { + HexParser.validate(input).map(|_| input.to_string()) +} + +fn parse_lock_arg_20_28(input: &str) -> Result { + match HexParser.parse(input) { + Ok(data) if data.len() == 20 || data.len() == 28 => Ok(input.to_string()), + Ok(data) => Err(format!("invalid data length: {}", data.len())), + Err(err) => Err(err), + } +} + +fn parse_signature(input: &str) -> Result { + match HexParser.parse(input) { + Ok(data) if data.len() == SECP_SIGNATURE_SIZE => Ok(input.to_string()), + Ok(data) => Err(format!("invalid data length: {}", data.len())), + Err(err) => Err(err), + } +} + +fn parse_multisig_code_hash_value(input: &str) -> Result { + match input { + "legacy" => Ok(MultisigScript::Legacy.script_id().code_hash), + "v2" => Ok(MultisigScript::V2.script_id().code_hash), + _ => FixedHashParser::::default().parse(input), + } +} + +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_from_account(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .or_else(|err| { + AddressParser::default() + .validate(input) + .and_then(|()| AddressParser::new_sighash().validate(input)) + .map_err(|_| err) + }) + .map(|_| input.to_string()) +} + +fn parse_to_data_path(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command(about = "Handle common sighash/multisig transaction")] +pub struct TxCmd { + #[command(subcommand)] + pub command: TxSubcommands, +} + +#[derive(Subcommand, Debug)] +#[command(rename_all = "kebab-case")] +pub enum TxSubcommands { + /// Init a common (sighash/multisig) transaction + Init(TxInitArgs), + /// Add multisig config + AddMultisigConfig(TxAddMultisigConfigArgs), + /// Remove all field items in transaction + ClearField(TxClearFieldArgs), + /// Add cell input (with secp/multisig lock) + AddInput(TxAddInputArgs), + /// Add cell output + AddOutput(TxAddOutputArgs), + /// Add signature + AddSignature(TxAddSignatureArgs), + /// Show detail of this multisig transaction (capacity, tx-fee, etc.) + Info(TxInfoArgs), + /// Sign all sighash/multisig inputs in this transaction + SignInputs(TxSignInputsArgs), + /// Send multisig transaction + Send(TxSendArgs), + /// Build multisig address with multisig config and since(optional) argument + BuildMultisigAddress(TxBuildMultisigAddressArgs), +} + +#[derive(Args, Debug)] +pub struct TxInitArgs { + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct TxAddMultisigConfigArgs { + #[arg(long = "sighash-address", id = "sighash-address", action = ArgAction::Append, num_args = 1.., required = true, value_parser = parse_sighash_address)] + pub sighash_address: Vec, + #[arg(long = "multisig-code-hash", id = "multisig-code-hash", value_parser = [ + "legacy", + "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "v2", + "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", + ])] + pub multisig_code_hash: String, + #[arg(long = "require-first-n", id = "require-first-n", default_value = "0", value_parser = parse_u8)] + pub require_first_n: String, + #[arg(long = "threshold", id = "threshold", default_value = "1", value_parser = parse_u8)] + pub threshold: String, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct TxClearFieldArgs { + #[arg(long = "field", id = "field", value_parser = ["inputs", "outputs", "signatures"])] + pub field: String, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct TxAddInputArgs { + #[arg(long = "tx-hash", id = "tx-hash", value_parser = parse_h256)] + pub tx_hash: String, + #[arg(long = "index", id = "index", value_parser = parse_u32)] + pub index: String, + #[arg(long = "since-absolute-epoch", id = "since-absolute-epoch", value_parser = parse_u64)] + pub since_absolute_epoch: Option, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, + #[arg(long = "skip-check", id = "skip-check")] + pub skip_check: bool, +} + +#[derive(Args, Debug)] +pub struct TxAddOutputArgs { + #[arg(long = "to-sighash-address", id = "to-sighash-address", conflicts_with_all = ["to-short-multisig-address", "to-long-multisig-address"], value_parser = parse_sighash_address)] + pub to_sighash_address: Option, + #[arg(long = "to-short-multisig-address", id = "to-short-multisig-address", conflicts_with = "to-long-multisig-address", value_parser = parse_multisig_address)] + pub to_short_multisig_address: Option, + #[arg(long = "to-long-multisig-address", id = "to-long-multisig-address", value_parser = parse_multisig_address)] + pub to_long_multisig_address: Option, + #[arg(long = "capacity", id = "capacity", value_parser = parse_capacity)] + pub capacity: String, + #[arg(long = "to-data", id = "to-data", value_parser = parse_hex)] + pub to_data: Option, + #[arg(long = "to-data-path", id = "to-data-path", value_parser = parse_to_data_path)] + pub to_data_path: Option, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct TxAddSignatureArgs { + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg_20_28)] + pub lock_arg: String, + #[arg(long = "signature", id = "signature", value_parser = parse_signature)] + pub signature: String, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct TxInfoArgs { + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, +} + +#[derive(Args, Debug)] +pub struct TxSignInputsArgs { + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "from-account", value_parser = parse_privkey_path)] + pub privkey_path: Option, + #[arg(long = "from-account", id = "from-account", required_unless_present = "privkey-path", value_parser = parse_from_account)] + pub from_account: Option, + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, + #[arg(long = "add-signatures", id = "add-signatures")] + pub add_signatures: bool, + #[arg(long = "skip-check", id = "skip-check")] + pub skip_check: bool, +} + +#[derive(Args, Debug)] +pub struct TxSendArgs { + #[arg(long = "tx-file", id = "tx-file", value_parser = parse_tx_file)] + pub tx_file: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", default_value = "1.0", value_parser = parse_capacity)] + pub max_tx_fee: String, + #[arg(long = "skip-check", id = "skip-check")] + pub skip_check: bool, + #[arg(long = "zero-lock", id = "zero-lock")] + pub zero_lock: bool, +} + +#[derive(Args, Debug)] +pub struct TxBuildMultisigAddressArgs { + #[arg(long = "sighash-address", id = "sighash-address", action = ArgAction::Append, num_args = 1.., required = true, value_parser = parse_sighash_address)] + pub sighash_address: Vec, + #[arg(long = "require-first-n", id = "require-first-n", default_value = "0", value_parser = parse_u8)] + pub require_first_n: String, + #[arg(long = "multisig-code-hash", id = "multisig-code-hash", value_parser = [ + "legacy", + "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "v2", + "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", + ])] + pub multisig_code_hash: String, + #[arg(long = "threshold", id = "threshold", default_value = "1", value_parser = parse_u8)] + pub threshold: String, + #[arg(long = "since-absolute-epoch", id = "since-absolute-epoch", value_parser = parse_u64)] + pub since_absolute_epoch: Option, +} + impl<'a> TxSubCommand<'a> { pub fn new( rpc_client: &'a mut HttpRpcClient, @@ -63,196 +316,8 @@ impl<'a> TxSubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_tx_file = Arg::with_name("tx-file") - .long("tx-file") - .takes_value(true) - .validator(|input| FilePathParser::new(false).validate(input)) - .required(true) - .about("Multisig transaction data file (format: json)"); - let arg_sighash_address = Arg::with_name("sighash-address") - .long("sighash-address") - .takes_value(true) - .multiple(true) - .required(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("Normal sighash address"); - let arg_require_first_n = Arg::with_name("require-first-n") - .long("require-first-n") - .takes_value(true) - .default_value("0") - .validator(|input| FromStrParser::::default().validate(input)) - .about("Require first n signatures of corresponding pubkey"); - let arg_threshold = Arg::with_name("threshold") - .long("threshold") - .takes_value(true) - .default_value("1") - .validator(|input| FromStrParser::::default().validate(input)) - .about("Multisig threshold"); - let arg_since_absolute_epoch = Arg::with_name("since-absolute-epoch") - .long("since-absolute-epoch") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .about("Since absolute epoch number"); - let arg_skip_check = Arg::with_name("skip-check") - .long("skip-check") - .about("Send transaction without any check, be cautious to use this flag"); - let arg_allow_zero_lock = Arg::with_name("zero-lock") - .long("zero-lock") - .about(ALLOW_ZERO_LOCK_HELP_MSG); - - App::new(name) - .about("Handle common sighash/multisig transaction") - .subcommands(vec![ - App::new("init") - .about("Init a common (sighash/multisig) transaction") - .arg(arg_tx_file.clone()), - App::new("add-multisig-config") - .about("Add multisig config") - .arg(arg_sighash_address.clone()) - .arg(arg_multisig_code_hash().required(true)) - .arg(arg_require_first_n.clone()) - .arg(arg_threshold.clone()) - .arg(arg_tx_file.clone()), - App::new("clear-field") - .about("Remove all field items in transaction") - .arg( - Arg::with_name("field") - .long("field") - .takes_value(true) - .required(true) - .possible_values(&["inputs", "outputs", "signatures"]) - .about("The transaction field"), - ) - .arg(arg_tx_file.clone()), - App::new("add-input") - .about("Add cell input (with secp/multisig lock)") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required(true) - .about("Transaction hash"), - ) - .arg( - Arg::with_name("index") - .long("index") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .required(true) - .about("Transaction output index"), - ) - .arg(arg_since_absolute_epoch.clone()) - .arg(arg_tx_file.clone()) - .arg(arg_skip_check.clone()), - App::new("add-output") - .about("Add cell output") - .arg( - Arg::with_name("to-sighash-address") - .long("to-sighash-address") - .conflicts_with_all(&[ - "to-short-multisig-address", - "to-long-multisig-address", - ]) - .takes_value(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("To normal sighash address"), - ) - .arg( - Arg::with_name("to-short-multisig-address") - .long("to-short-multisig-address") - .conflicts_with("to-long-multisig-address") - .takes_value(true) - .validator(|input| { - AddressParser::new_multisig(MultisigScript::Legacy) - .validate(input) - .or(AddressParser::new_multisig(MultisigScript::V2) - .validate(input)) - }) - .about("To short multisig address(encode with legacy multisig script)"), - ) - .arg( - Arg::with_name("to-long-multisig-address") - .long("to-long-multisig-address") - .takes_value(true) - .requires("multisig-code-hash") - .validator(|input| { - AddressParser::new_multisig(MultisigScript::Legacy) - .validate(input) - .or(AddressParser::new_multisig(MultisigScript::V2) - .validate(input)) - }) - .about("To long multisig address (special case, include since)"), - ) - .arg(arg::capacity().required(true)) - .arg(arg::to_data()) - .arg(arg::to_data_path()) - .arg(arg_tx_file.clone()), - App::new("add-signature") - .about("Add signature") - .arg( - Arg::with_name("lock-arg") - .long("lock-arg") - .takes_value(true) - .required(true) - .validator(|input| match HexParser.parse(input) { - Ok(ref data) if data.len() == 20 || data.len() == 28 => Ok(()), - Ok(ref data) => Err(format!("invalid data length: {}", data.len())), - Err(err) => Err(err), - }) - .about("The lock_arg of input lock script (20 bytes or 28 bytes)"), - ) - .arg( - Arg::with_name("signature") - .long("signature") - .takes_value(true) - .required(true) - .validator(|input| match HexParser.parse(input) { - Ok(ref data) if data.len() == SECP_SIGNATURE_SIZE => Ok(()), - Ok(ref data) => Err(format!("invalid data length: {}", data.len())), - Err(err) => Err(err), - }) - .about("The signature"), - ) - .arg(arg_tx_file.clone()), - App::new("info") - .about("Show detail of this multisig transaction (capacity, tx-fee, etc.)") - .arg(arg_tx_file.clone()), - App::new("sign-inputs") - .about("Sign all sighash/multisig inputs in this transaction") - .arg(arg::privkey_path().required_unless(arg::from_account().get_name())) - .arg(arg::from_account().required_unless(arg::privkey_path().get_name())) - .arg(arg_tx_file.clone()) - .arg( - Arg::with_name("add-signatures") - .long("add-signatures") - .about("Sign and add signatures"), - ) - .arg(arg_skip_check.clone()), - App::new("send") - .about("Send multisig transaction") - .arg(arg_tx_file.clone()) - .arg( - Arg::with_name("max-tx-fee") - .long("max-tx-fee") - .takes_value(true) - .default_value("1.0") - .validator(|input| CapacityParser.validate(input)) - .about("Max transaction fee (unit: CKB)"), - ) - .arg(arg_skip_check) - .arg(arg_allow_zero_lock), - App::new("build-multisig-address") - .about( - "Build multisig address with multisig config and since(optional) argument", - ) - .arg(arg_sighash_address.clone()) - .arg(arg_require_first_n.clone()) - .arg(arg_multisig_code_hash().required(true)) - .arg(arg_threshold.clone()) - .arg(arg_since_absolute_epoch.clone()), - ]) + pub fn subcommand(name: &'static str) -> Command { + TxCmd::command().name(name) } } @@ -260,27 +325,22 @@ impl CliSubCommand for TxSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, debug: bool) -> Result { let network = get_network_type(self.rpc_client)?; - match matches.subcommand() { - ("init", Some(m)) => { - let tx_file_opt: Option = - FilePathParser::new(false).from_matches_opt(m, "tx-file")?; + let cmd = TxCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + TxSubcommands::Init(args) => { + let tx_file: PathBuf = FilePathParser::new(false).parse(&args.tx_file)?; let helper = TxHelper::default(); let repr = ReprTxHelper::new(helper, network); - if let Some(tx_file) = tx_file_opt { - let mut file = fs::File::create(tx_file).map_err(|err| err.to_string())?; - let content = - serde_json::to_string_pretty(&repr).map_err(|err| err.to_string())?; - file.write_all(content.as_bytes()) - .map_err(|err| err.to_string())?; - Ok(Output::new_success()) - } else { - Ok(Output::new_output(repr)) - } + let mut file = fs::File::create(tx_file).map_err(|err| err.to_string())?; + let content = serde_json::to_string_pretty(&repr).map_err(|err| err.to_string())?; + file.write_all(content.as_bytes()) + .map_err(|err| err.to_string())?; + Ok(Output::new_success()) } - ("clear-field", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(true).from_matches(m, "tx-file")?; - let field = m.value_of("field").unwrap(); + TxSubcommands::ClearField(args) => { + let tx_file: PathBuf = FilePathParser::new(true).parse(&args.tx_file)?; + let field = args.field.as_str(); modify_tx_file(&tx_file, network, |helper| { match field { "inputs" => helper.clear_inputs(), @@ -292,15 +352,17 @@ impl CliSubCommand for TxSubCommand<'_> { })?; Ok(Output::new_success()) } - ("add-input", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(true).from_matches(m, "tx-file")?; - let tx_hash: H256 = - FixedHashParser::::default().from_matches(m, "tx-hash")?; - let index: u32 = FromStrParser::::default().from_matches(m, "index")?; - let since_absolute_epoch_opt: Option = - FromStrParser::::default().from_matches_opt(m, "since-absolute-epoch")?; - - let skip_check: bool = m.is_present("skip-check"); + TxSubcommands::AddInput(args) => { + let tx_file: PathBuf = FilePathParser::new(true).parse(&args.tx_file)?; + let tx_hash: H256 = FixedHashParser::::default().parse(&args.tx_hash)?; + let index: u32 = FromStrParser::::default().parse(&args.index)?; + let since_absolute_epoch_opt: Option = args + .since_absolute_epoch + .as_ref() + .map(|value| FromStrParser::::default().parse(value)) + .transpose()?; + + let skip_check: bool = args.skip_check; let genesis_info = get_genesis_info(&self.genesis_info, self.rpc_client)?; let out_point = OutPoint::new_builder() .tx_hash(tx_hash.pack()) @@ -321,27 +383,45 @@ impl CliSubCommand for TxSubCommand<'_> { Ok(Output::new_success()) } - ("add-output", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(true).from_matches(m, "tx-file")?; - let capacity: u64 = CapacityParser.from_matches(m, "capacity")?; - - let to_sighash_address_opt: Option
= - AddressParser::new_sighash().from_matches_opt(m, "to-sighash-address")?; - let to_short_multisig_address_opt: Option
= - AddressParser::new_multisig(MultisigScript::Legacy) - .from_matches_opt(m, "to-short-multisig-address") - .or_else(|_| { - AddressParser::new_multisig(MultisigScript::V2) - .from_matches_opt(m, "to-short-multisig-address") - })?; - let to_long_multisig_address_opt: Option
= { - AddressParser::new_multisig(MultisigScript::Legacy) - .from_matches_opt(m, "to-long-multisig-address") - .or(AddressParser::new_multisig(MultisigScript::V2) - .from_matches_opt(m, "to-long-multisig-address"))? - }; + TxSubcommands::AddOutput(args) => { + let tx_file: PathBuf = FilePathParser::new(true).parse(&args.tx_file)?; + let capacity: u64 = CapacityParser.parse(&args.capacity)?.into(); + + let to_sighash_address_opt: Option
= args + .to_sighash_address + .as_ref() + .map(|value| AddressParser::new_sighash().parse(value)) + .transpose()?; + let to_short_multisig_address_opt: Option
= args + .to_short_multisig_address + .as_ref() + .map(|value| { + AddressParser::new_multisig(MultisigScript::Legacy) + .parse(value) + .or_else(|_| { + AddressParser::new_multisig(MultisigScript::V2).parse(value) + }) + }) + .transpose()?; + let to_long_multisig_address_opt: Option
= args + .to_long_multisig_address + .as_ref() + .map(|value| { + AddressParser::new_multisig(MultisigScript::Legacy) + .parse(value) + .or_else(|_| { + AddressParser::new_multisig(MultisigScript::V2).parse(value) + }) + }) + .transpose()?; - let to_data = get_to_data(m)?; + let to_data = if let Some(hex) = args.to_data.as_ref() { + Bytes::from(HexParser.parse(hex)?) + } else if let Some(path) = args.to_data_path.as_ref() { + Bytes::from(fs::read(path).map_err(|err| err.to_string())?) + } else { + Bytes::new() + }; check_capacity(capacity, to_data.len())?; if let Some(address) = to_long_multisig_address_opt.as_ref() { let payload = address.payload(); @@ -369,18 +449,19 @@ impl CliSubCommand for TxSubCommand<'_> { Ok(Output::new_success()) } - ("add-signature", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(true).from_matches(m, "tx-file")?; - let lock_arg: Bytes = HexParser.from_matches(m, "lock-arg")?; - let signature: Bytes = HexParser.from_matches(m, "signature")?; + TxSubcommands::AddSignature(args) => { + let tx_file: PathBuf = FilePathParser::new(true).parse(&args.tx_file)?; + let lock_arg: Bytes = Bytes::from(HexParser.parse(&args.lock_arg)?); + let signature: Bytes = Bytes::from(HexParser.parse(&args.signature)?); modify_tx_file(&tx_file, network, |helper| { helper.add_signature(lock_arg, signature) })?; Ok(Output::new_success()) } - ("add-multisig-config", Some(m)) => { - let multisig_lock_code_hash: H256 = arg_get_multisig_code_hash(m)?; + TxSubcommands::AddMultisigConfig(args) => { + let multisig_lock_code_hash: H256 = + parse_multisig_code_hash_value(&args.multisig_code_hash)?; let multisig_script = MultisigScript::try_from(multisig_lock_code_hash.clone()) .map_err(|_err| { format!( @@ -389,13 +470,19 @@ impl CliSubCommand for TxSubCommand<'_> { ) })?; - let tx_file: PathBuf = FilePathParser::new(false).from_matches(m, "tx-file")?; - let sighash_addresses: Vec
= AddressParser::new_sighash() - .set_network(network) - .from_matches_vec(m, "sighash-address")?; + let tx_file: PathBuf = FilePathParser::new(false).parse(&args.tx_file)?; + let sighash_addresses: Vec
= args + .sighash_address + .iter() + .map(|value| { + AddressParser::new_sighash() + .set_network(network) + .parse(value) + }) + .collect::, String>>()?; let require_first_n: u8 = - FromStrParser::::default().from_matches(m, "require-first-n")?; - let threshold: u8 = FromStrParser::::default().from_matches(m, "threshold")?; + FromStrParser::::default().parse(&args.require_first_n)?; + let threshold: u8 = FromStrParser::::default().parse(&args.threshold)?; let sighash_addresses = sighash_addresses .into_iter() @@ -414,8 +501,8 @@ impl CliSubCommand for TxSubCommand<'_> { })?; Ok(Output::new_success()) } - ("info", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(false).from_matches(m, "tx-file")?; + TxSubcommands::Info(args) => { + let tx_file: PathBuf = FilePathParser::new(false).parse(&args.tx_file)?; let mut live_cell_cache: HashMap<(OutPoint, bool), (CellOutput, Bytes)> = Default::default(); @@ -487,12 +574,16 @@ impl CliSubCommand for TxSubCommand<'_> { }); Ok(Output::new_output(resp)) } - ("sign-inputs", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(true).from_matches(m, "tx-file")?; - let privkey_opt: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; - let account_opt: Option = m - .value_of("from-account") + TxSubcommands::SignInputs(args) => { + let tx_file: PathBuf = FilePathParser::new(true).parse(&args.tx_file)?; + let privkey_opt: Option = args + .privkey_path + .as_ref() + .map(|value| PrivkeyPathParser.parse(value)) + .transpose()?; + let account_opt: Option = args + .from_account + .as_ref() .map(|input| { FixedHashParser::::default() .parse(input) @@ -508,7 +599,7 @@ impl CliSubCommand for TxSubCommand<'_> { }) }) .transpose()?; - let skip_check: bool = m.is_present("skip-check"); + let skip_check: bool = args.skip_check; let mut signer = if let Some(privkey) = privkey_opt { get_privkey_signer(privkey) @@ -538,7 +629,7 @@ impl CliSubCommand for TxSubCommand<'_> { let signatures = modify_tx_file(&tx_file, network, |helper| { let signatures = helper.sign_inputs(&mut signer, get_live_cell, skip_check)?; - if m.is_present("add-signatures") { + if args.add_signatures { for (lock_arg, signature) in signatures.clone() { helper.add_signature(lock_arg, signature)?; } @@ -556,11 +647,11 @@ impl CliSubCommand for TxSubCommand<'_> { .collect::>(); Ok(Output::new_output(resp)) } - ("send", Some(m)) => { - let tx_file: PathBuf = FilePathParser::new(false).from_matches(m, "tx-file")?; - let max_tx_fee: u64 = CapacityParser.from_matches(m, "max-tx-fee")?; - let skip_check: bool = m.is_present("skip-check"); - let allow_zero_lock: bool = m.is_present("zero-lock"); + TxSubcommands::Send(args) => { + let tx_file: PathBuf = FilePathParser::new(false).parse(&args.tx_file)?; + let max_tx_fee: u64 = CapacityParser.parse(&args.max_tx_fee)?.into(); + let skip_check: bool = args.skip_check; + let allow_zero_lock: bool = args.zero_lock; let mut live_cell_cache: HashMap<(OutPoint, bool), (CellOutput, Bytes)> = Default::default(); @@ -605,8 +696,9 @@ impl CliSubCommand for TxSubCommand<'_> { .map_err(|err| format!("Send transaction error: {}", err))?; Ok(Output::new_output(resp)) } - ("build-multisig-address", Some(m)) => { - let multisig_lock_code_hash: H256 = arg_get_multisig_code_hash(m)?; + TxSubcommands::BuildMultisigAddress(args) => { + let multisig_lock_code_hash: H256 = + parse_multisig_code_hash_value(&args.multisig_code_hash)?; let multisig_script = MultisigScript::try_from(multisig_lock_code_hash.clone()) .map_err(|_err| { format!( @@ -615,14 +707,23 @@ impl CliSubCommand for TxSubCommand<'_> { ) })?; - let sighash_addresses: Vec
= AddressParser::new_sighash() - .set_network(network) - .from_matches_vec(m, "sighash-address")?; + let sighash_addresses: Vec
= args + .sighash_address + .iter() + .map(|value| { + AddressParser::new_sighash() + .set_network(network) + .parse(value) + }) + .collect::, String>>()?; let require_first_n: u8 = - FromStrParser::::default().from_matches(m, "require-first-n")?; - let threshold: u8 = FromStrParser::::default().from_matches(m, "threshold")?; - let since_absolute_epoch_opt: Option = - FromStrParser::::default().from_matches_opt(m, "since-absolute-epoch")?; + FromStrParser::::default().parse(&args.require_first_n)?; + let threshold: u8 = FromStrParser::::default().parse(&args.threshold)?; + let since_absolute_epoch_opt: Option = args + .since_absolute_epoch + .as_ref() + .map(|value| FromStrParser::::default().parse(value)) + .transpose()?; let sighash_addresses = sighash_addresses .into_iter() @@ -646,7 +747,6 @@ impl CliSubCommand for TxSubCommand<'_> { }); Ok(Output::new_output(resp)) } - _ => Err(Self::subcommand("tx").generate_usage()), } } } diff --git a/src/subcommands/util.rs b/src/subcommands/util.rs index 5276ae18..8c142732 100644 --- a/src/subcommands/util.rs +++ b/src/subcommands/util.rs @@ -1,11 +1,10 @@ use std::fs; use std::io::Read; -use std::path::PathBuf; use bitcoin::bip32::{ChildNumber, DerivationPath}; use chrono::prelude::*; -use clap::{App, Arg, ArgMatches}; -use clap_generate::generators::{Bash, Elvish, Fish, PowerShell, Zsh}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; +use clap_complete::Shell; use eaglesong::EagleSongBuilder; use faster_hex::hex_string; use secp256k1::ecdsa::{RecoverableSignature, RecoveryId, Signature}; @@ -27,16 +26,15 @@ use ckb_types::{ H160, H256, U256, }; -use super::{arg_get_multisig_code_hash, arg_multisig_code_hash, CliSubCommand, Output}; +use super::{CliSubCommand, Output}; use crate::plugin::{PluginManager, SignTarget}; use crate::utils::{ - arg, arg_parser::{ AddressParser, ArgParser, FilePathParser, FixedHashParser, FromStrParser, HexParser, PrivkeyPathParser, PrivkeyWrapper, PubkeyHexParser, }, genesis_info::GenesisInfo, - other::{address_json, get_address, get_network_type, read_password}, + other::{address_json, get_network_type, read_password}, rpc::{ChainInfo, HttpRpcClient}, }; use crate::{build_cli, get_version}; @@ -48,6 +46,275 @@ const FLAG_SINCE_EPOCH_NUMBER: u64 = const EPOCH_LENGTH: u64 = 1800; const BLOCK_PERIOD: u64 = 8 * 1000; // 8 seconds +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_pubkey_hex(input: &str) -> Result { + PubkeyHexParser.validate(input).map(|_| input.to_string()) +} + +fn parse_address(input: &str) -> Result { + AddressParser::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_sighash_address(input: &str) -> Result { + AddressParser::new_sighash() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_sighash_address_mainnet(input: &str) -> Result { + AddressParser::new_sighash() + .set_network(NetworkType::Mainnet) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_hex(input: &str) -> Result { + HexParser.validate(input).map(|_| input.to_string()) +} + +fn parse_message_hash(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_file_path_exists(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_compact_target(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .or_else(|_| { + let trimmed = if input.starts_with("0x") || input.starts_with("0X") { + &input[2..] + } else { + input + }; + u32::from_str_radix(trimmed, 16) + .map(|_| ()) + .map_err(|err| err.to_string()) + }) + .map(|_| input.to_string()) +} + +fn parse_difficulty(input: &str) -> Result { + let trimmed = if input.starts_with("0x") || input.starts_with("0X") { + &input[2..] + } else { + input + }; + U256::from_hex_str(trimmed) + .map(|_| input.to_string()) + .map_err(|err| err.to_string()) +} + +fn parse_u32(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_locktime_rfc3339(input: &str) -> Result { + DateTime::parse_from_rfc3339(input) + .map(|_| input.to_string()) + .map_err(|err| err.to_string()) +} + +#[derive(Parser, Debug)] +#[command(name = "util", about = "Utilities")] +pub struct UtilCmd { + #[command(subcommand)] + pub command: UtilSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum UtilSubcommands { + /// Show public information of a secp256k1 private key (from file) or public key + KeyInfo(UtilKeyInfoArgs), + /// Sign data with secp256k1 signature + SignData(UtilSignDataArgs), + /// Sign message with secp256k1 signature + SignMessage(UtilSignMessageArgs), + /// Verify a compact format signature + VerifySignature(UtilVerifySignatureArgs), + /// Hash binary use eaglesong algorithm + Eaglesong(UtilBinaryHexArgs), + /// Hash binary use blake2b algorithm (personalization: 'ckb-default-hash') + Blake2b(UtilBlake2bArgs), + /// Convert compact target value to difficulty value + CompactToDifficulty(UtilCompactToDifficultyArgs), + /// Convert difficulty value to compact target value + DifficultyToCompact(UtilDifficultyToCompactArgs), + /// Show information about an address + AddressInfo(UtilAddressInfoArgs), + /// Convert address in single signature format to multisig format (only for mainnet genesis cells) + ToGenesisMultisigAddr(UtilToGenesisMultisigAddrArgs), + /// Convert address in single signature format to multisig format + ToMultisigAddr(UtilToMultisigAddrArgs), + /// Query live cell's metadata + CellMeta(UtilCellMetaArgs), + /// Show genesis scripts code hash and cell_deps information + GenesisScripts, + /// Generates completion scripts for your shell + Completions(UtilCompletionsArgs), +} + +#[derive(Args, Debug)] +pub struct UtilKeyInfoArgs { + #[arg(long = "privkey-path", id = "privkey-path", value_parser = parse_privkey_path, conflicts_with = "pubkey")] + pub privkey_path: Option, + #[arg(long, value_parser = parse_pubkey_hex)] + pub pubkey: Option, + #[arg(long, value_parser = parse_address)] + pub address: Option, + #[arg(long = "lock-arg", id = "lock-arg")] + pub lock_arg: Option, +} + +#[derive(Args, Debug)] +pub struct UtilSignDataArgs { + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "from-account", value_parser = parse_privkey_path)] + pub privkey_path: Option, + #[arg( + long = "from-account", + id = "from-account", + required_unless_present = "privkey-path", + conflicts_with = "privkey-path" + )] + pub from_account: Option, + #[arg(long)] + pub recoverable: bool, + #[arg(long = "extended-address", id = "extended-address", conflicts_with = "privkey-path", value_parser = parse_sighash_address)] + pub extended_address: Option, + #[arg(long = "binary-hex", id = "binary-hex", required_unless_present = "utf8-string", conflicts_with = "utf8-string", value_parser = parse_hex)] + pub binary_hex: Option, + #[arg(long = "no-magic-bytes", id = "no-magic-bytes")] + pub no_magic_bytes: bool, + #[arg( + long = "utf8-string", + id = "utf8-string", + required_unless_present = "binary-hex", + conflicts_with = "binary-hex" + )] + pub utf8_string: Option, +} + +#[derive(Args, Debug)] +pub struct UtilSignMessageArgs { + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "from-account", value_parser = parse_privkey_path)] + pub privkey_path: Option, + #[arg( + long = "from-account", + id = "from-account", + required_unless_present = "privkey-path", + conflicts_with = "privkey-path" + )] + pub from_account: Option, + #[arg(long)] + pub recoverable: bool, + #[arg(long = "extended-address", id = "extended-address", conflicts_with = "privkey-path", value_parser = parse_sighash_address)] + pub extended_address: Option, + #[arg(long = "message", id = "message", value_parser = parse_message_hash)] + pub message: String, +} + +#[derive(Args, Debug)] +pub struct UtilVerifySignatureArgs { + #[arg(long, value_parser = parse_pubkey_hex)] + pub pubkey: Option, + #[arg(long = "privkey-path", id = "privkey-path", value_parser = parse_privkey_path, conflicts_with = "pubkey")] + pub privkey_path: Option, + #[arg(long = "from-account", id = "from-account", conflicts_with_all = ["privkey-path", "pubkey"])] + pub from_account: Option, + #[arg(long = "message", id = "message", value_parser = parse_message_hash)] + pub message: String, + #[arg(long = "extended-address", id = "extended-address", conflicts_with = "pubkey", value_parser = parse_sighash_address)] + pub extended_address: Option, + #[arg(long, value_parser = parse_hex)] + pub signature: String, +} + +#[derive(Args, Debug)] +pub struct UtilBinaryHexArgs { + #[arg(long = "binary-hex", id = "binary-hex", value_parser = parse_hex)] + pub binary_hex: String, +} + +#[derive(Args, Debug)] +pub struct UtilBlake2bArgs { + #[arg(long = "binary-hex", id = "binary-hex", value_parser = parse_hex)] + pub binary_hex: Option, + #[arg(long = "binary-path", id = "binary-path", value_parser = parse_file_path_exists)] + pub binary_path: Option, + #[arg(long = "prefix-160", id = "prefix-160")] + pub prefix_160: bool, +} + +#[derive(Args, Debug)] +pub struct UtilCompactToDifficultyArgs { + #[arg(long = "compact-target", id = "compact-target", value_parser = parse_compact_target)] + pub compact_target: String, +} + +#[derive(Args, Debug)] +pub struct UtilDifficultyToCompactArgs { + #[arg(long, value_parser = parse_difficulty)] + pub difficulty: String, +} + +#[derive(Args, Debug)] +pub struct UtilAddressInfoArgs { + #[arg(long, value_parser = parse_address)] + pub address: String, +} + +#[derive(Args, Debug)] +pub struct UtilToGenesisMultisigAddrArgs { + #[arg(long = "sighash-address", id = "sighash-address", value_parser = parse_sighash_address_mainnet)] + pub sighash_address: String, + #[arg(long)] + pub locktime: String, +} + +#[derive(Args, Debug)] +pub struct UtilToMultisigAddrArgs { + #[arg(long = "sighash-address", id = "sighash-address", value_parser = parse_sighash_address)] + pub sighash_address: String, + #[arg(long = "multisig-code-hash", id = "multisig-code-hash", value_parser = [ + "legacy", + "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "v2", + "0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29", + ])] + pub multisig_code_hash: String, + #[arg(long, value_parser = parse_locktime_rfc3339)] + pub locktime: String, +} + +#[derive(Args, Debug)] +pub struct UtilCellMetaArgs { + #[arg(long = "tx-hash", id = "tx-hash", value_parser = parse_message_hash)] + pub tx_hash: String, + #[arg(long, value_parser = parse_u32)] + pub index: String, + #[arg(long = "with-data", id = "with-data")] + pub with_data: bool, +} + +#[derive(Args, Debug)] +pub struct UtilCompletionsArgs { + #[arg(value_parser = ["bash", "zsh", "fish", "elvish", "powershell"])] + pub shell: String, +} + pub struct UtilSubCommand<'a> { rpc_client: &'a mut HttpRpcClient, plugin_mgr: &'a mut PluginManager, @@ -64,259 +331,34 @@ impl<'a> UtilSubCommand<'a> { } } - pub fn subcommand(name: &'static str) -> App<'static> { - let arg_privkey = Arg::with_name("privkey-path") - .long("privkey-path") - .takes_value(true) - .validator(|input| PrivkeyPathParser.validate(input)) - .about("Private key file path (only read first line)"); - let arg_pubkey = Arg::with_name("pubkey") - .long("pubkey") - .takes_value(true) - .validator(|input| PubkeyHexParser.validate(input)) - .about("Public key (hex string, compressed format)"); - let arg_address = Arg::with_name("address") - .long("address") - .takes_value(true) - .validator(|input| AddressParser::default().validate(input)) - .required(true) - .about("Target address (see: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md)"); - - let binary_hex_arg = Arg::with_name("binary-hex") - .long("binary-hex") - .takes_value(true) - .required(true) - .validator(|input| HexParser.validate(input)); - let arg_sighash_address = Arg::with_name("sighash-address") - .long("sighash-address") - .required(true) - .takes_value(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .about("The address in single signature format"); - - let arg_recoverable = Arg::with_name("recoverable") - .long("recoverable") - .about("Sign use recoverable signature"); - - let arg_message = Arg::with_name("message") - .long("message") - .takes_value(true) - .required(true) - .validator(|input| FixedHashParser::::default().validate(input)); - - let arg_extended_address = Arg::with_name("extended-address") - .long("extended-address") - .takes_value(true) - .validator(|input| AddressParser::new_sighash().validate(input)) - .conflicts_with(arg::privkey_path().get_name()) - .about("The address extended from `m/44'/309'/0'` (Search 2000 receiving addresses and 2000 change addresses max)"); - - App::new(name) - .about("Utilities") - .subcommands(vec![ - App::new("key-info") - .about( - "Show public information of a secp256k1 private key (from file) or public key", - ) - .arg(arg_privkey.clone().conflicts_with("pubkey")) - .arg(arg_pubkey.clone().required(false)) - .arg(arg_address.clone().required(false)) - .arg(arg::lock_arg().clone()), - App::new("sign-data") - .about("Sign data with secp256k1 signature ") - .arg(arg::privkey_path().required_unless(arg::from_account().get_name())) - .arg( - arg::from_account() - .required_unless(arg::privkey_path().get_name()) - .conflicts_with(arg::privkey_path().get_name()), - ) - .arg(arg_recoverable.clone()) - .arg(arg_extended_address.clone()) - .arg( - binary_hex_arg - .clone() - .required(false) - .required_unless("utf8-string") - .conflicts_with("utf8-string") - .about("The data to be signed. The input data will be hashed using blake2b with 'ckb-default-hash' personalization first.") - ) - .arg( - Arg::with_name("no-magic-bytes") - .long("no-magic-bytes") - .about("Don't add magic bytes before binary data (magic bytes: \"Nervos Message:\")") - ) - .arg( - Arg::with_name("utf8-string") - .long("utf8-string") - .takes_value(true) - .required_unless(binary_hex_arg.get_name()) - .conflicts_with(binary_hex_arg.get_name()) - .about("The utf-8 string to be signed. The input string will be hashed using blake2b with 'ckb-default-hash' personalization first.") - ), - App::new("sign-message") - .about("Sign message with secp256k1 signature") - .arg(arg::privkey_path().required_unless(arg::from_account().get_name())) - .arg( - arg::from_account() - .required_unless(arg::privkey_path().get_name()) - .conflicts_with(arg::privkey_path().get_name()), - ) - .arg(arg_recoverable.clone()) - .arg(arg_extended_address.clone()) - .arg(arg_message.clone().about("The message to be signed (32 bytes)")), - App::new("verify-signature") - .about("Verify a compact format signature") - .arg(arg::pubkey()) - .arg(arg::privkey_path().conflicts_with(arg::pubkey().get_name())) - .arg( - arg::from_account() - .conflicts_with_all(&[arg::privkey_path().get_name(), arg::pubkey().get_name()]), - ) - .arg(arg_message.clone().about("The message to be verify (32 bytes)")) - .arg( - arg_extended_address - .clone() - .conflicts_with(arg::pubkey().get_name()) - ) - .arg( - Arg::with_name("signature") - .long("signature") - .takes_value(true) - .required(true) - .validator(|input| HexParser.validate(input)) - .about("The compact format signature (support recoverable signature)") - ), - App::new("eaglesong") - .about("Hash binary use eaglesong algorithm") - .arg(binary_hex_arg.clone().about("The binary in hex format to hash")), - App::new("blake2b") - .about("Hash binary use blake2b algorithm (personalization: 'ckb-default-hash')") - .arg(binary_hex_arg.clone().required(false).about("The binary in hex format to hash")) - .arg( - Arg::with_name("binary-path") - .long("binary-path") - .takes_value(true) - .validator(|input| FilePathParser::new(true).validate(input)) - .about("The binary file path") - ) - .arg( - Arg::with_name("prefix-160") - .long("prefix-160") - .about("Only show prefix 160 bits (Example: calculate lock_arg from pubkey)") - ), - App::new("compact-to-difficulty") - .about("Convert compact target value to difficulty value") - .arg(Arg::with_name("compact-target") - .long("compact-target") - .takes_value(true) - .validator(|input| { - FromStrParser::::default() - .validate(input) - .or_else(|_| { - let input = if input.starts_with("0x") || input.starts_with("0X") { - &input[2..] - } else { - input - }; - u32::from_str_radix(input, 16).map(|_| ()).map_err(|err| err.to_string()) - }) - }) - .required(true) - .about("The compact target value") - ), - App::new("difficulty-to-compact") - .about("Convert difficulty value to compact target value") - .arg(Arg::with_name("difficulty") - .long("difficulty") - .takes_value(true) - .validator(|input| { - let input = if input.starts_with("0x") || input.starts_with("0X") { - &input[2..] - } else { - input - }; - U256::from_hex_str(input).map(|_| ()).map_err(|err| err.to_string()) - }) - .required(true) - .about("The difficulty value") - ), - App::new("address-info") - .about("Show information about an address") - .arg(arg_address), - App::new("to-genesis-multisig-addr") - .about("Convert address in single signature format to multisig format (only for mainnet genesis cells)") - .arg( - arg_sighash_address - .clone() - .validator(|input| { - AddressParser::new_sighash() - .set_network(NetworkType::Mainnet) - .validate(input) - })) - .arg( - Arg::with_name("locktime") - .long("locktime") - .required(true) - .takes_value(true) - .about("The locktime in UTC format date. Example: 2022-05-01") - ), - App::new("to-multisig-addr") - .about("Convert address in single signature format to multisig format") - .arg(arg_sighash_address.clone()) - .arg(arg_multisig_code_hash()) - .arg( - Arg::with_name("locktime") - .long("locktime") - .required(true) - .takes_value(true) - .validator(|input| DateTime::parse_from_rfc3339(input).map(|_| ()).map_err(|err| err.to_string())) - .about("The locktime in RFC3339 format. Example: 2014-11-28T21:00:00+00:00") - ), - App::new("cell-meta") - .about("Query live cell's metadata") - .arg( - Arg::with_name("tx-hash") - .long("tx-hash") - .takes_value(true) - .validator(|input| FixedHashParser::::default().validate(input)) - .required(true) - .about("Tx hash"), - ) - .arg( - Arg::with_name("index") - .long("index") - .takes_value(true) - .validator(|input| FromStrParser::::default().validate(input)) - .required(true) - .about("Output index"), - ) - .arg( - Arg::with_name("with-data") - .long("with-data") - .about("Get live cell with data") - ), - App::new("genesis-scripts") - .about("Show genesis scripts code hash and cell_deps information, include: [sighash, multisig, dao, secp256k1_data, type_id], see RFC24 for more details."), - App::new("completions") - .about("Generates completion scripts for your shell") - .arg( - Arg::with_name("shell") - .required(true) - .possible_values(&["bash", "zsh", "fish", "elvish", "powershell"]) - .about("The shell to generate the script for") - ), - ]) + pub fn subcommand(name: &'static str) -> Command { + UtilCmd::command().name(name) + } +} + +fn parse_multisig_code_hash_value(input: &str) -> Result { + match input { + "legacy" => Ok(MultisigScript::Legacy.script_id().code_hash), + "v2" => Ok(MultisigScript::V2.script_id().code_hash), + _ => FixedHashParser::::default().parse(input), } } impl CliSubCommand for UtilSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, debug: bool) -> Result { - match matches.subcommand() { - ("key-info", Some(m)) => { - let privkey_opt: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; - let pubkey_opt: Option = - PubkeyHexParser.from_matches_opt(m, "pubkey")?; + let cmd = UtilCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + UtilSubcommands::KeyInfo(args) => { + let privkey_opt: Option = args + .privkey_path + .as_ref() + .map(|value| PrivkeyPathParser.parse(value)) + .transpose()?; + let pubkey_opt: Option = args + .pubkey + .as_ref() + .map(|value| PubkeyHexParser.parse(value)) + .transpose()?; let pubkey_opt = privkey_opt .map(|privkey| secp256k1::PublicKey::from_secret_key(&SECP256K1, &privkey)) .or(pubkey_opt); @@ -326,7 +368,17 @@ impl CliSubCommand for UtilSubCommand<'_> { let address_payload = match pubkey_opt { Some(pubkey) => AddressPayload::from_pubkey(&pubkey), - None => get_address(None, m)?, + None => { + if let Some(address) = args.address.as_ref() { + AddressParser::default().parse(address)?.payload().clone() + } else if let Some(lock_arg) = args.lock_arg.as_ref() { + let lock_arg: H160 = + FixedHashParser::::default().parse(lock_arg)?; + AddressPayload::from_pubkey_hash(lock_arg) + } else { + return Err("Please give one argument".to_string()); + } + } }; let lock_arg = H160::from_slice(address_payload.args().as_ref()).unwrap(); let old_address = OldAddress::new_default(lock_arg.clone()); @@ -357,33 +409,47 @@ message = "0x" }); Ok(Output::new_output(resp)) } - ("sign-data", Some(m)) => { - let binary_opt: Option> = HexParser.from_matches_opt(m, "binary-hex")?; - let recoverable = m.is_present("recoverable"); - let from_privkey_opt: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; - let from_account_opt: Option = FixedHashParser::::default() - .from_matches_opt(m, "from-account") - .or_else(|err| { - let result: Result, String> = - AddressParser::new_sighash().from_matches_opt(m, "from-account"); - result - .map(|address_opt| { - address_opt.map(|address| { - H160::from_slice(&address.payload().args()).unwrap() - }) + UtilSubcommands::SignData(args) => { + let binary_opt: Option> = args + .binary_hex + .as_ref() + .map(|value| HexParser.parse(value)) + .transpose()?; + let recoverable = args.recoverable; + let from_privkey_opt: Option = args + .privkey_path + .as_ref() + .map(|value| PrivkeyPathParser.parse(value)) + .transpose()?; + let from_account_opt: Option = args + .from_account + .as_ref() + .map(|input| { + FixedHashParser::::default() + .parse(input) + .or_else(|err| { + let result: Result = + AddressParser::new_sighash().parse(input); + result + .map(|address| { + H160::from_slice(&address.payload().args()).unwrap() + }) + .map_err(|_| err) }) - .map_err(|_| err) - })?; - let no_magic_bytes = m.is_present("no-magic-bytes"); + }) + .transpose()?; + let no_magic_bytes = args.no_magic_bytes; let password = if self.plugin_mgr.keystore_require_password() && from_account_opt.is_some() { Some(read_password(false, None)?) } else { None }; - let extended_address_opt: Option
= - AddressParser::new_sighash().from_matches_opt(m, "extended-address")?; + let extended_address_opt: Option
= args + .extended_address + .as_ref() + .map(|value| AddressParser::new_sighash().parse(value)) + .transpose()?; let root_path = if let Some(ref account) = from_account_opt { self.plugin_mgr.root_key_path(account.clone())? } else { @@ -400,11 +466,12 @@ message = "0x" let (mut binary, target) = if let Some(data) = binary_opt { (data.clone(), SignTarget::AnyData(JsonBytes::from_vec(data))) } else { - let utf8_string = m - .value_of("utf8-string") + let utf8_string = args + .utf8_string + .as_ref() .ok_or_else(|| " or is required".to_string())?; let binary = utf8_string.as_bytes().to_vec(); - (binary, SignTarget::AnyString(utf8_string.to_string())) + (binary, SignTarget::AnyString(utf8_string.clone())) }; if !no_magic_bytes { @@ -431,33 +498,42 @@ message = "0x" }); Ok(Output::new_output(result)) } - ("sign-message", Some(m)) => { - let message: H256 = - FixedHashParser::::default().from_matches(m, "message")?; - let recoverable = m.is_present("recoverable"); - let from_privkey_opt: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; - let from_account_opt: Option = FixedHashParser::::default() - .from_matches_opt(m, "from-account") - .or_else(|err| { - let result: Result, String> = - AddressParser::new_sighash().from_matches_opt(m, "from-account"); - result - .map(|address_opt| { - address_opt.map(|address| { - H160::from_slice(&address.payload().args()).unwrap() - }) + UtilSubcommands::SignMessage(args) => { + let message: H256 = FixedHashParser::::default().parse(&args.message)?; + let recoverable = args.recoverable; + let from_privkey_opt: Option = args + .privkey_path + .as_ref() + .map(|value| PrivkeyPathParser.parse(value)) + .transpose()?; + let from_account_opt: Option = args + .from_account + .as_ref() + .map(|input| { + FixedHashParser::::default() + .parse(input) + .or_else(|err| { + let result: Result = + AddressParser::new_sighash().parse(input); + result + .map(|address| { + H160::from_slice(&address.payload().args()).unwrap() + }) + .map_err(|_| err) }) - .map_err(|_| err) - })?; + }) + .transpose()?; let password = if self.plugin_mgr.keystore_require_password() && from_account_opt.is_some() { Some(read_password(false, None)?) } else { None }; - let extended_address_opt: Option
= - AddressParser::new_sighash().from_matches_opt(m, "extended-address")?; + let extended_address_opt: Option
= args + .extended_address + .as_ref() + .map(|value| AddressParser::new_sighash().parse(value)) + .transpose()?; let root_path = if let Some(ref account) = from_account_opt { self.plugin_mgr.root_key_path(account.clone())? @@ -490,29 +566,41 @@ message = "0x" }); Ok(Output::new_output(result)) } - ("verify-signature", Some(m)) => { - let message: H256 = - FixedHashParser::::default().from_matches(m, "message")?; - let signature: Vec = HexParser.from_matches(m, "signature")?; - let pubkey_opt: Option = - PubkeyHexParser.from_matches_opt(m, "pubkey")?; - let from_privkey_opt: Option = - PrivkeyPathParser.from_matches_opt(m, "privkey-path")?; - let from_account_opt: Option = FixedHashParser::::default() - .from_matches_opt(m, "from-account") - .or_else(|err| { - let result: Result, String> = - AddressParser::new_sighash().from_matches_opt(m, "from-account"); - result - .map(|address_opt| { - address_opt.map(|address| { - H160::from_slice(&address.payload().args()).unwrap() - }) + UtilSubcommands::VerifySignature(args) => { + let message: H256 = FixedHashParser::::default().parse(&args.message)?; + let signature: Vec = HexParser.parse(&args.signature)?; + let pubkey_opt: Option = args + .pubkey + .as_ref() + .map(|value| PubkeyHexParser.parse(value)) + .transpose()?; + let from_privkey_opt: Option = args + .privkey_path + .as_ref() + .map(|value| PrivkeyPathParser.parse(value)) + .transpose()?; + let from_account_opt: Option = args + .from_account + .as_ref() + .map(|input| { + FixedHashParser::::default() + .parse(input) + .or_else(|err| { + let result: Result = + AddressParser::new_sighash().parse(input); + result + .map(|address| { + H160::from_slice(&address.payload().args()).unwrap() + }) + .map_err(|_| err) }) - .map_err(|_| err) - })?; - let extended_address_opt: Option
= - AddressParser::new_sighash().from_matches_opt(m, "extended-address")?; + }) + .transpose()?; + let extended_address_opt: Option
= args + .extended_address + .as_ref() + .map(|value| AddressParser::new_sighash().parse(value)) + .transpose()?; let password = if self.plugin_mgr.keystore_require_password() && from_account_opt.is_some() { Some(read_password(false, None)?) @@ -572,30 +660,29 @@ message = "0x" }); Ok(Output::new_output(result)) } - ("eaglesong", Some(m)) => { - let binary: Vec = HexParser.from_matches(m, "binary-hex")?; + UtilSubcommands::Eaglesong(args) => { + let binary: Vec = HexParser.parse(&args.binary_hex)?; let mut builder = EagleSongBuilder::new(); builder.update(&binary); let output_string = format!("{:#x}", H256::from(builder.finalize())); Ok(Output::new_output(serde_json::Value::String(output_string))) } - ("blake2b", Some(m)) => { - let binary: Vec = HexParser - .from_matches_opt(m, "binary-hex")? - .ok_or_else(String::new) - .or_else(|_| -> Result<_, String> { - let path: PathBuf = FilePathParser::new(true) - .from_matches(m, "binary-path") - .map_err(|err| { - format!(" or is required: {}", err) - })?; - let mut data = Vec::new(); - let mut file = fs::File::open(path).map_err(|err| err.to_string())?; - file.read_to_end(&mut data).map_err(|err| err.to_string())?; - Ok(data) + UtilSubcommands::Blake2b(args) => { + let binary: Vec = if let Some(hex) = args.binary_hex.as_ref() { + HexParser.parse(hex)? + } else if let Some(path) = args.binary_path.as_ref() { + let path = FilePathParser::new(true).parse(path).map_err(|err| { + format!(" or is required: {}", err) })?; + let mut data = Vec::new(); + let mut file = fs::File::open(path).map_err(|err| err.to_string())?; + file.read_to_end(&mut data).map_err(|err| err.to_string())?; + data + } else { + return Err(" or is required".to_string()); + }; let hash_data = blake2b_256(binary); - let slice = if m.is_present("prefix-160") { + let slice = if args.prefix_160 { &hash_data[0..20] } else { &hash_data[..] @@ -603,15 +690,16 @@ message = "0x" let output_string = format!("0x{}", hex_string(slice)); Ok(Output::new_output(serde_json::Value::String(output_string))) } - ("compact-to-difficulty", Some(m)) => { + UtilSubcommands::CompactToDifficulty(args) => { let compact_target: u32 = FromStrParser::::default() - .from_matches(m, "compact-target") + .parse(&args.compact_target) .or_else(|_| { - let input = m.value_of("compact-target").unwrap(); - let input = if input.starts_with("0x") || input.starts_with("0X") { - &input[2..] + let input = if args.compact_target.starts_with("0x") + || args.compact_target.starts_with("0X") + { + &args.compact_target[2..] } else { - input + args.compact_target.as_str() }; u32::from_str_radix(input, 16).map_err(|err| err.to_string()) })?; @@ -620,21 +708,21 @@ message = "0x" }); Ok(Output::new_output(resp)) } - ("difficulty-to-compact", Some(m)) => { - let input = m.value_of("difficulty").unwrap(); - let input = if input.starts_with("0x") || input.starts_with("0X") { - &input[2..] - } else { - input - }; + UtilSubcommands::DifficultyToCompact(args) => { + let input = + if args.difficulty.starts_with("0x") || args.difficulty.starts_with("0X") { + &args.difficulty[2..] + } else { + args.difficulty.as_str() + }; let difficulty = U256::from_hex_str(input).map_err(|err| err.to_string())?; let resp = serde_json::json!({ "compact-target": format!("{:#x}", difficulty_to_compact(difficulty)), }); Ok(Output::new_output(resp)) } - ("address-info", Some(m)) => { - let address: Address = AddressParser::default().from_matches(m, "address")?; + UtilSubcommands::AddressInfo(args) => { + let address: Address = AddressParser::default().parse(&args.address)?; if matches!(address.network(), NetworkType::Staging | NetworkType::Dev) && address.payload().is_short_acp() { @@ -665,7 +753,7 @@ message = "0x" .to_string()); Ok(Output::new_output(resp)) } - ("to-genesis-multisig-addr", Some(m)) => { + UtilSubcommands::ToGenesisMultisigAddr(args) => { let chain_info: ChainInfo = self .rpc_client .get_blockchain_info() @@ -674,12 +762,11 @@ message = "0x" return Err("Node is not in mainnet spec".to_owned()); } - let locktime = m.value_of("locktime").unwrap(); + let locktime = args.locktime.as_str(); let address = { - let input = m.value_of("sighash-address").unwrap(); AddressParser::new_sighash() .set_network(NetworkType::Mainnet) - .parse(input)? + .parse(&args.sighash_address)? }; let genesis_timestamp = @@ -707,15 +794,14 @@ message = "0x" } Ok(Output::new_output(serde_json::json!(resp))) } - ("to-multisig-addr", Some(m)) => { - let address: Address = - AddressParser::new_sighash().from_matches(m, "sighash-address")?; - let locktime_timestamp = - DateTime::parse_from_rfc3339(m.value_of("locktime").unwrap()) - .map(|dt| dt.timestamp_millis() as u64) - .map_err(|err| err.to_string())?; + UtilSubcommands::ToMultisigAddr(args) => { + let address: Address = AddressParser::new_sighash().parse(&args.sighash_address)?; + let locktime_timestamp = DateTime::parse_from_rfc3339(&args.locktime) + .map(|dt| dt.timestamp_millis() as u64) + .map_err(|err| err.to_string())?; - let multisig_lock_code_hash: H256 = arg_get_multisig_code_hash(m)?; + let multisig_lock_code_hash: H256 = + parse_multisig_code_hash_value(&args.multisig_code_hash)?; let multisig_script = MultisigScript::try_from(multisig_lock_code_hash.clone()) .map_err(|_err| { @@ -743,11 +829,10 @@ message = "0x" }); Ok(Output::new_output(resp)) } - ("cell-meta", Some(m)) => { - let tx_hash: H256 = - FixedHashParser::::default().from_matches(m, "tx-hash")?; - let index: u32 = FromStrParser::::default().from_matches(m, "index")?; - let with_data = m.is_present("with-data"); + UtilSubcommands::CellMeta(args) => { + let tx_hash: H256 = FixedHashParser::::default().parse(&args.tx_hash)?; + let index: u32 = FromStrParser::::default().parse(&args.index)?; + let with_data = args.with_data; let out_point = packed::OutPoint::new_builder() .tx_hash(tx_hash.pack()) .index(index) @@ -781,7 +866,7 @@ message = "0x" Ok(Output::new_output(resp)) } } - ("genesis-scripts", _) => { + UtilSubcommands::GenesisScripts => { let genesis_block: BlockView = self .rpc_client .get_block_by_number(0)? @@ -827,27 +912,25 @@ message = "0x" }); Ok(Output::new_output(resp)) } - ("completions", Some(m)) => { - let shell = m.value_of("shell").unwrap(); + UtilSubcommands::Completions(args) => { + let shell = args.shell.as_str(); let version = get_version(); let version_short = version.short(); let version_long = version.long(); let mut app = build_cli(&version_short, &version_long); let bin_name = "ckb-cli"; let output = &mut std::io::stdout(); - match shell { - "bash" => clap_generate::generate::(&mut app, bin_name, output), - "zsh" => clap_generate::generate::(&mut app, bin_name, output), - "fish" => clap_generate::generate::(&mut app, bin_name, output), - "elvish" => clap_generate::generate::(&mut app, bin_name, output), - "powershell" => { - clap_generate::generate::(&mut app, bin_name, output) - } + let shell = match shell { + "bash" => Shell::Bash, + "zsh" => Shell::Zsh, + "fish" => Shell::Fish, + "elvish" => Shell::Elvish, + "powershell" => Shell::PowerShell, _ => panic!("Invalid shell: {}", shell), - } + }; + clap_complete::generate(shell, &mut app, bin_name, output); Ok(Output::new_success()) } - _ => Err(Self::subcommand("util").generate_usage()), } } } diff --git a/src/subcommands/wallet.rs b/src/subcommands/wallet.rs index 026db305..22a2f03f 100644 --- a/src/subcommands/wallet.rs +++ b/src/subcommands/wallet.rs @@ -1,7 +1,7 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{collections::HashMap, fs, str::FromStr}; use bitcoin::bip32::DerivationPath; -use clap::{App, Arg, ArgMatches}; +use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; use serde::{Deserialize, Serialize}; use ckb_chain_spec::consensus::TYPE_ID_CODE_HASH; @@ -38,20 +38,157 @@ use plugin_protocol::LiveCellInfo; use super::{CliSubCommand, Output}; use crate::plugin::PluginManager; use crate::utils::{ - arg, arg_parser::{ - AddressParser, ArgParser, CapacityParser, FixedHashParser, FromStrParser, - PrivkeyPathParser, PrivkeyWrapper, + AddressParser, ArgParser, CapacityParser, FilePathParser, FixedHashParser, FromStrParser, + HexParser, PrivkeyPathParser, PrivkeyWrapper, PubkeyHexParser, }, genesis_info::GenesisInfo, other::{ - check_capacity, get_address, get_arg_value, get_genesis_info, get_network_type, - get_to_data, map_tx_builder_error_2_str, read_password, to_live_cell_info, + check_capacity, get_genesis_info, get_network_type, map_tx_builder_error_2_str, + read_password, to_live_cell_info, }, rpc::HttpRpcClient, signer::KeyStoreHandlerSigner, }; +fn parse_privkey_path(input: &str) -> Result { + PrivkeyPathParser.validate(input).map(|_| input.to_string()) +} + +fn parse_address(input: &str) -> Result { + AddressParser::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_lock_arg(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_from_account(input: &str) -> Result { + FixedHashParser::::default() + .validate(input) + .or_else(|err| { + AddressParser::default() + .validate(input) + .and_then(|()| AddressParser::new_sighash().validate(input)) + .map_err(|_| err) + }) + .map(|_| input.to_string()) +} + +fn parse_capacity(input: &str) -> Result { + CapacityParser.validate(input).map(|_| input.to_string()) +} + +fn parse_u64(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_hex(input: &str) -> Result { + HexParser.validate(input).map(|_| input.to_string()) +} + +fn parse_file_path(input: &str) -> Result { + FilePathParser::new(true) + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_u32(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +fn parse_usize(input: &str) -> Result { + FromStrParser::::default() + .validate(input) + .map(|_| input.to_string()) +} + +#[derive(Parser, Debug)] +#[command( + name = "wallet", + about = "Transfer / query balance (with local index) / key utils" +)] +pub struct WalletCmd { + #[command(subcommand)] + pub command: WalletSubcommands, +} + +#[derive(Subcommand, Debug)] +pub enum WalletSubcommands { + /// Transfer capacity to an address (can have data) + Transfer(WalletTransferArgs), + /// Get capacity address or lock arg or pubkey + GetCapacity(WalletGetCapacityArgs), + /// Get live cells by address + GetLiveCells(WalletGetLiveCellsArgs), +} + +#[derive(Args, Debug)] +pub struct WalletTransferArgs { + #[arg(long = "privkey-path", id = "privkey-path", required_unless_present = "from-account", value_parser = parse_privkey_path)] + pub privkey_path: Option, + #[arg(long = "from-account", id = "from-account", required_unless_present = "privkey-path", conflicts_with = "privkey-path", value_parser = parse_from_account)] + pub from_account: Option, + #[arg(long = "from-locked-address", id = "from-locked-address", value_parser = parse_address)] + pub from_locked_address: Option, + #[arg(long = "to-address", id = "to-address", value_parser = parse_address)] + pub to_address: String, + #[arg(long = "to-data", id = "to-data", value_parser = parse_hex)] + pub to_data: Option, + #[arg(long = "to-data-path", id = "to-data-path", value_parser = parse_file_path)] + pub to_data_path: Option, + #[arg(long = "capacity", id = "capacity", value_parser = parse_capacity)] + pub capacity: String, + #[arg(long = "fee-rate", id = "fee-rate", default_value = "1000", value_parser = parse_u64)] + pub fee_rate: String, + #[arg(long = "max-tx-fee", id = "max-tx-fee", value_parser = parse_capacity)] + pub max_tx_fee: Option, + #[arg(long = "derive-receiving-address-length", id = "derive-receiving-address-length", default_value = "1000", value_parser = parse_u32)] + pub derive_receiving_address_length: String, + #[arg(long = "derive-change-address", id = "derive-change-address", conflicts_with = "privkey-path", value_parser = parse_address)] + pub derive_change_address: Option, + #[arg(long = "skip-check-to-address", id = "skip-check-to-address")] + pub skip_check_to_address: bool, + #[arg(long = "type-id", id = "type-id")] + pub type_id: bool, +} + +#[derive(Args, Debug)] +pub struct WalletGetCapacityArgs { + #[arg(long, value_parser = parse_address)] + pub address: Option, + #[arg(long = "pubkey", id = "pubkey")] + pub pubkey: Option, + #[arg(long = "lock-arg", id = "lock-arg", value_parser = parse_lock_arg)] + pub lock_arg: Option, + #[arg(long = "derive-receiving-address-length", id = "derive-receiving-address-length", default_value = "1000", value_parser = parse_u32)] + pub derive_receiving_address_length: String, + #[arg(long = "derive-change-address-length", id = "derive-change-address-length", default_value = "1000", value_parser = parse_u32)] + pub derive_change_address_length: String, + #[arg(long = "derived", id = "derived")] + pub derived: bool, +} + +#[derive(Args, Debug)] +pub struct WalletGetLiveCellsArgs { + #[arg(long, value_parser = parse_address)] + pub address: String, + #[arg(long = "limit", id = "limit", default_value = "15", value_parser = parse_usize)] + pub limit: String, + #[arg(long = "from", id = "from", value_parser = parse_u64)] + pub from: Option, + #[arg(long = "to", id = "to", value_parser = parse_u64)] + pub to: Option, +} + // Max derived change address to search const DERIVE_CHANGE_ADDRESS_MAX_LEN: u32 = 10000; @@ -79,53 +216,8 @@ impl<'a> WalletSubCommand<'a> { Ok(self.genesis_info.clone().unwrap()) } - pub fn subcommand() -> App<'static> { - App::new("wallet") - .about("Transfer / query balance (with local index) / key utils") - .subcommands(vec![ - App::new("transfer") - .about("Transfer capacity to an address (can have data)") - .arg(arg::privkey_path().required_unless(arg::from_account().get_name())) - .arg( - arg::from_account() - .required_unless(arg::privkey_path().get_name()) - .conflicts_with(arg::privkey_path().get_name()), - ) - .arg(arg::from_locked_address()) - .arg(arg::to_address().required(true)) - .arg(arg::to_data()) - .arg(arg::to_data_path()) - .arg(arg::capacity().required(true)) - .arg(arg::fee_rate()) - .arg(arg::max_tx_fee()) - .arg(arg::derive_receiving_address_length()) - .arg( - arg::derive_change_address().conflicts_with(arg::privkey_path().get_name()), - ) - .arg( - Arg::with_name("skip-check-to-address") - .long("skip-check-to-address") - .about("Skip check (default only allow sighash/multisig address), be cautious to use this flag")) - .arg( - Arg::with_name("type-id") - .long("type-id") - .about("Add type id type script to target output cell"), - ), - App::new("get-capacity") - .about("Get capacity address or lock arg or pubkey") - .arg(arg::address()) - .arg(arg::pubkey()) - .arg(arg::lock_arg()) - .arg(arg::derive_receiving_address_length()) - .arg(arg::derive_change_address_length()) - .arg(arg::derived()), - App::new("get-live-cells") - .about("Get live cells by address") - .arg(arg::address()) - .arg(arg::live_cells_limit()) - .arg(arg::from_block_number()) - .arg(arg::to_block_number()) - ]) + pub fn subcommand() -> Command { + WalletCmd::command() } pub fn transfer( @@ -578,28 +670,33 @@ impl<'a> WalletSubCommand<'a> { impl CliSubCommand for WalletSubCommand<'_> { fn process(&mut self, matches: &ArgMatches, debug: bool) -> Result { - match matches.subcommand() { - ("transfer", Some(m)) => { - let to_data = get_to_data(m)?; + let cmd = WalletCmd::from_arg_matches(matches).map_err(|err| err.to_string())?; + match cmd.command { + WalletSubcommands::Transfer(args) => { + let to_data = if let Some(hex) = args.to_data.as_ref() { + Bytes::from(HexParser.parse(hex)?) + } else if let Some(path) = args.to_data_path.as_ref() { + let content = fs::read(path).map_err(|err| err.to_string())?; + Bytes::from(content) + } else { + Bytes::new() + }; let args = TransferArgs { - privkey_path: m.value_of("privkey-path").map(|s| s.to_string()), - from_account: m.value_of("from-account").map(|s| s.to_string()), - from_locked_address: m.value_of("from-locked-address").map(|s| s.to_string()), + privkey_path: args.privkey_path.clone(), + from_account: args.from_account.clone(), + from_locked_address: args.from_locked_address.clone(), password: None, - capacity: get_arg_value(m, "capacity")?, - fee_rate: get_arg_value(m, "fee-rate")?, - force_small_change_as_fee: m.value_of("max-tx-fee").map(|s| s.to_string()), - derive_receiving_address_length: Some(get_arg_value( - m, - "derive-receiving-address-length", - )?), - derive_change_address: m - .value_of("derive-change-address") - .map(|s| s.to_string()), - to_address: get_arg_value(m, "to-address")?, + capacity: args.capacity.clone(), + fee_rate: args.fee_rate.clone(), + force_small_change_as_fee: args.max_tx_fee.clone(), + derive_receiving_address_length: Some( + args.derive_receiving_address_length.clone(), + ), + derive_change_address: args.derive_change_address.clone(), + to_address: args.to_address.clone(), to_data: Some(to_data), - is_type_id: m.is_present("type-id"), - skip_check_to_address: m.is_present("skip-check-to-address"), + is_type_id: args.type_id, + skip_check_to_address: args.skip_check_to_address, }; let tx = self.transfer(args, false)?; if debug { @@ -610,24 +707,30 @@ impl CliSubCommand for WalletSubCommand<'_> { Ok(Output::new_output(tx_hash)) } } - ("get-capacity", Some(m)) => { + WalletSubcommands::GetCapacity(args) => { let network_type = get_network_type(self.rpc_client)?; - let receiving_address_length: u32 = FromStrParser::::default() - .from_matches(m, "derive-receiving-address-length")?; - let change_address_length: u32 = FromStrParser::::default() - .from_matches(m, "derive-change-address-length")?; - let address_payload = if let Some(address_str) = m.value_of("address") { + let receiving_address_length: u32 = + FromStrParser::::default().parse(&args.derive_receiving_address_length)?; + let change_address_length: u32 = + FromStrParser::::default().parse(&args.derive_change_address_length)?; + let address_payload = if let Some(address_str) = args.address.as_ref() { AddressParser::default() .set_network(network_type) .parse(address_str)? .payload() .clone() + } else if let Some(pubkey_str) = args.pubkey.as_ref() { + let pubkey = PubkeyHexParser.parse(pubkey_str)?; + AddressPayload::from_pubkey(&pubkey) + } else if let Some(lock_arg_str) = args.lock_arg.as_ref() { + let lock_arg: H160 = FixedHashParser::::default().parse(lock_arg_str)?; + AddressPayload::from_pubkey_hash(lock_arg) } else { - get_address(Some(network_type), m)? + return Err("Please give one argument".to_string()); }; let mut lock_scripts = vec![Script::from(&address_payload)]; - if m.is_present("derived") { + if args.derived { let lock_arg = H160::from_slice(address_payload.args().as_ref()).unwrap(); let key_set = self @@ -662,17 +765,23 @@ impl CliSubCommand for WalletSubCommand<'_> { } Ok(Output::new_output(resp)) } - ("get-live-cells", Some(m)) => { - let limit: u32 = FromStrParser::::default().from_matches(m, "limit")?; - let from_number_opt: Option = - FromStrParser::::default().from_matches_opt(m, "from")?; - let to_number_opt: Option = - FromStrParser::::default().from_matches_opt(m, "to")?; + WalletSubcommands::GetLiveCells(args) => { + let limit: u32 = FromStrParser::::default().parse(&args.limit)?; + let from_number_opt: Option = args + .from + .as_ref() + .map(|value| FromStrParser::::default().parse(value)) + .transpose()?; + let to_number_opt: Option = args + .to + .as_ref() + .map(|value| FromStrParser::::default().parse(value)) + .transpose()?; let network_type = get_network_type(self.rpc_client)?; let address: Address = AddressParser::default() .set_network(network_type) - .from_matches(m, "address")?; + .parse(&args.address)?; let lock_script = Script::from(address.payload()); let live_cells = self.get_live_cells( lock_script, @@ -697,7 +806,6 @@ impl CliSubCommand for WalletSubCommand<'_> { Ok(Output::new_output(resp)) } - _ => Err(Self::subcommand().generate_usage()), } } } diff --git a/src/utils/arg.rs b/src/utils/arg.rs index bcfd001d..ba9228a0 100644 --- a/src/utils/arg.rs +++ b/src/utils/arg.rs @@ -1,80 +1,100 @@ +#![allow(dead_code)] + use crate::utils::arg_parser::{ AddressParser, ArgParser, CapacityParser, FilePathParser, FixedHashParser, FromStrParser, HexParser, OutPointParser, PrivkeyPathParser, PubkeyHexParser, }; use ckb_types::H160; +use clap::builder::ValueParser; use clap::Arg; -pub fn privkey_path<'a>() -> Arg<'a> { - Arg::with_name("privkey-path") +pub trait ArgValidatorExt { + fn validator(self, validator: F) -> Self + where + F: Fn(&str) -> Result<(), String> + Clone + Send + Sync + 'static; +} + +impl ArgValidatorExt for Arg { + fn validator(self, validator: F) -> Self + where + F: Fn(&str) -> Result<(), String> + Clone + Send + Sync + 'static, + { + self.value_parser(ValueParser::new(move |input: &str| { + validator(input).map(|_| input.to_string()) + })) + } +} + +pub fn privkey_path() -> Arg { + Arg::new("privkey-path") .long("privkey-path") - .takes_value(true) + .num_args(1) .validator(|input| PrivkeyPathParser.validate(input)) - .about("Private key file path (only read first line)") + .help("Private key file path (only read first line)") } -pub fn pubkey<'a>() -> Arg<'a> { - Arg::with_name("pubkey") +pub fn pubkey() -> Arg { + Arg::new("pubkey") .long("pubkey") - .takes_value(true) + .num_args(1) .validator(|input| PubkeyHexParser.validate(input)) - .about("Public key (hex string, compressed format)") + .help("Public key (hex string, compressed format)") } -pub fn address<'a>() -> Arg<'a> { - Arg::with_name("address") +pub fn address() -> Arg { + Arg::new("address") .long("address") - .takes_value(true) + .num_args(1) .validator(|input| AddressParser::default().validate(input)) - .about( + .help( "Target address (see: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md)", ) } -pub fn derive_receiving_address_length<'a>() -> Arg<'a> { - Arg::with_name("derive-receiving-address-length") +pub fn derive_receiving_address_length() -> Arg { + Arg::new("derive-receiving-address-length") .long("derive-receiving-address-length") - .takes_value(true) + .num_args(1) .default_value("1000") .validator(|input| FromStrParser::::default().validate(input)) - .about("Search derived receiving address length") + .help("Search derived receiving address length") } -pub fn derive_change_address_length<'a>() -> Arg<'a> { - Arg::with_name("derive-change-address-length") +pub fn derive_change_address_length() -> Arg { + Arg::new("derive-change-address-length") .long("derive-change-address-length") - .takes_value(true) + .num_args(1) .default_value("1000") .validator(|input| FromStrParser::::default().validate(input)) - .about("Search derived change address length") + .help("Search derived change address length") } -pub fn derive_change_address<'a>() -> Arg<'a> { - Arg::with_name("derive-change-address") +pub fn derive_change_address() -> Arg { + Arg::new("derive-change-address") .long("derive-change-address") - .takes_value(true) + .num_args(1) .validator(|input| AddressParser::default().validate(input)) - .about("Manually specify the last change address (search 10000 addresses max, required keystore password, see: BIP-44)") + .help("Manually specify the last change address (search 10000 addresses max, required keystore password, see: BIP-44)") } -pub fn derived<'a>() -> Arg<'a> { - Arg::with_name("derived") +pub fn derived() -> Arg { + Arg::new("derived") .long("derived") - .about("Search derived address space (search 10000 addresses(change/receiving) max, required keystore password, see: BIP-44)") + .help("Search derived address space (search 10000 addresses(change/receiving) max, required keystore password, see: BIP-44)") } -pub fn lock_arg<'a>() -> Arg<'a> { - Arg::with_name("lock-arg") +pub fn lock_arg() -> Arg { + Arg::new("lock-arg") .long("lock-arg") - .takes_value(true) + .num_args(1) .validator(|input| FixedHashParser::::default().validate(input)) - .about("Lock argument (account identifier, blake2b(pubkey)[0..20])") + .help("Lock argument (account identifier, blake2b(pubkey)[0..20])") } -pub fn from_account<'a>() -> Arg<'a> { - Arg::with_name("from-account") +pub fn from_account() -> Arg { + Arg::new("from-account") .long("from-account") - .takes_value(true) + .num_args(1) .validator(|input| { FixedHashParser::::default() .validate(input) @@ -85,97 +105,97 @@ pub fn from_account<'a>() -> Arg<'a> { .map_err(|_| err) }) }) - .about("The account's lock-arg or sighash address (transfer from this account)") + .help("The account's lock-arg or sighash address (transfer from this account)") } -pub fn from_locked_address<'a>() -> Arg<'a> { - Arg::with_name("from-locked-address") +pub fn from_locked_address() -> Arg { + Arg::new("from-locked-address") .long("from-locked-address") - .takes_value(true) + .num_args(1) .validator(|input| AddressParser::default().validate(input)) - .about("The time locked multisig address to search live cells (which S=0,R=0,M=1,N=1 and have since value)") + .help("The time locked multisig address to search live cells (which S=0,R=0,M=1,N=1 and have since value)") } -pub fn to_address<'a>() -> Arg<'a> { - Arg::with_name("to-address") +pub fn to_address() -> Arg { + Arg::new("to-address") .long("to-address") - .takes_value(true) + .num_args(1) .validator(|input| AddressParser::default().validate(input)) - .about("Target address") + .help("Target address") } -pub fn to_data<'a>() -> Arg<'a> { - Arg::with_name("to-data") +pub fn to_data() -> Arg { + Arg::new("to-data") .long("to-data") - .takes_value(true) + .num_args(1) .validator(|input| HexParser.validate(input)) - .about("Hex data store in target cell (optional)") + .help("Hex data store in target cell (optional)") } -pub fn to_data_path<'a>() -> Arg<'a> { - Arg::with_name("to-data-path") +pub fn to_data_path() -> Arg { + Arg::new("to-data-path") .long("to-data-path") - .takes_value(true) + .num_args(1) .validator(|input| FilePathParser::new(true).validate(input)) - .about("Data binary file path store in target cell (optional)") + .help("Data binary file path store in target cell (optional)") } -pub fn capacity<'a>() -> Arg<'a> { - Arg::with_name("capacity") +pub fn capacity() -> Arg { + Arg::new("capacity") .long("capacity") - .takes_value(true) + .num_args(1) .validator(|input| CapacityParser.validate(input)) - .about("The capacity (unit: CKB, format: 123.335)") + .help("The capacity (unit: CKB, format: 123.335)") } -pub fn fee_rate<'a>() -> Arg<'a> { - Arg::with_name("fee-rate") +pub fn fee_rate() -> Arg { + Arg::new("fee-rate") .long("fee-rate") - .takes_value(true) + .num_args(1) .validator(|input| FromStrParser::::default().validate(input)) .default_value("1000") - .about("The transaction fee rate (unit: shannons/KB)") + .help("The transaction fee rate (unit: shannons/KB)") } /// create an Arg object to receive value of force_small_change_as_fee for CapacityBalancer -pub fn max_tx_fee<'a>() -> Arg<'a> { - Arg::with_name("max-tx-fee") +pub fn max_tx_fee() -> Arg { + Arg::new("max-tx-fee") .long("max-tx-fee") - .takes_value(true) + .num_args(1) .value_name("capacity") - .validator(|input|CapacityParser.validate(input)) - .about("When there is no more inputs for create a change cell to balance the transaction capacity, force the addition capacity as fee, the value is actual maximum transaction fee(unit CKB, example:0.001)") + .validator(|input| CapacityParser.validate(input)) + .help("When there is no more inputs for create a change cell to balance the transaction capacity, force the addition capacity as fee, the value is actual maximum transaction fee(unit CKB, example:0.001)") } -pub fn live_cells_limit<'a>() -> Arg<'a> { - Arg::with_name("limit") +pub fn live_cells_limit() -> Arg { + Arg::new("limit") .long("limit") - .takes_value(true) + .num_args(1) .validator(|input| FromStrParser::::default().validate(input)) .default_value("15") - .about("Get live cells <= limit") + .help("Get live cells <= limit") } -pub fn from_block_number<'a>() -> Arg<'a> { - Arg::with_name("from") +pub fn from_block_number() -> Arg { + Arg::new("from") .long("from") - .takes_value(true) + .num_args(1) .validator(|input| FromStrParser::::default().validate(input)) - .about("From block number (inclusive)") + .help("From block number (inclusive)") } -pub fn to_block_number<'a>() -> Arg<'a> { - Arg::with_name("to") +pub fn to_block_number() -> Arg { + Arg::new("to") .long("to") - .takes_value(true) + .num_args(1) .validator(|input| FromStrParser::::default().validate(input)) - .about("To block number (exclusive)") + .help("To block number (exclusive)") } -pub fn out_point<'a>() -> Arg<'a> { - Arg::with_name("out-point") +pub fn out_point() -> Arg { + Arg::new("out-point") .long("out-point") - .takes_value(true) + .num_args(1) .validator(|input| { OutPointParser.validate(input) }) - .about("out-point to specify a cell. Example: 0xd56ed5d4e8984701714de9744a533413f79604b3b91461e2265614829d2005d1-1") + .help("out-point to specify a cell. Example: 0xd56ed5d4e8984701714de9744a533413f79604b3b91461e2265614829d2005d1-1") } diff --git a/src/utils/arg_parser.rs b/src/utils/arg_parser.rs index 66c69339..b6c965bc 100644 --- a/src/utils/arg_parser.rs +++ b/src/utils/arg_parser.rs @@ -21,6 +21,21 @@ use ckb_types::{core::ScriptHashType, packed::OutPoint, prelude::*, H160, H256}; use crate::utils::cell_dep::CellDeps; +pub trait ArgMatchesExt { + fn value_of(&self, name: &str) -> Option<&str>; + fn is_present(&self, name: &str) -> bool; +} + +impl ArgMatchesExt for ArgMatches { + fn value_of(&self, name: &str) -> Option<&str> { + self.get_one::(name).map(|value| value.as_str()) + } + + fn is_present(&self, name: &str) -> bool { + self.contains_id(name) + } +} + #[allow(clippy::wrong_self_convention)] pub trait ArgParser { fn parse(&self, input: &str) -> Result; @@ -29,6 +44,7 @@ pub trait ArgParser { self.parse(input).map(|_| ()) } + #[allow(dead_code)] fn from_matches>(&self, matches: &ArgMatches, name: &str) -> Result { self.from_matches_option(matches, name, true) .map(Option::unwrap) @@ -48,26 +64,29 @@ pub trait ArgParser { name: &str, required: bool, ) -> Result, String> { - if required && !matches.is_present(name) { + if required && !matches.contains_id(name) { return Err(format!("<{}> is required", name)); } matches - .value_of(name) + .get_one::(name) .map(|input| self.parse(input).map(Into::into)) .transpose() } + #[allow(dead_code)] fn from_matches_vec>( &self, matches: &ArgMatches, name: &str, ) -> Result, String> { matches - .values_of_lossy(name) - .unwrap_or_default() - .into_iter() - .map(|input| self.parse(&input).map(Into::into)) - .collect() + .get_many::(name) + .map(|values| { + values + .map(|input| self.parse(input).map(Into::into)) + .collect() + }) + .unwrap_or_else(|| Ok(Vec::new())) } } @@ -407,6 +426,7 @@ impl AddressParser { self } + #[allow(dead_code)] pub fn set_network_opt(&mut self, network: Option) -> &mut Self { self.network = network; self diff --git a/src/utils/command.rs b/src/utils/command.rs new file mode 100644 index 00000000..95076cb2 --- /dev/null +++ b/src/utils/command.rs @@ -0,0 +1,13 @@ +#![allow(dead_code)] + +use clap::Command; + +pub trait CommandHelpExt { + fn help(self, about: impl Into) -> Self; +} + +impl CommandHelpExt for Command { + fn help(self, about: impl Into) -> Self { + self.about(about.into()) + } +} diff --git a/src/utils/completer.rs b/src/utils/completer.rs index 60d57c09..8c2152a4 100644 --- a/src/utils/completer.rs +++ b/src/utils/completer.rs @@ -27,14 +27,14 @@ static DEFAULT_BREAK_CHARS: [char; 17] = [ static ESCAPE_CHAR: Option = None; #[derive(Helper)] -pub struct CkbCompleter<'a> { - clap_app: clap::App<'a>, +pub struct CkbCompleter { + clap_app: clap::Command, completer: FilenameCompleter, validator: MatchingBracketValidator, } -impl<'a> CkbCompleter<'a> { - pub fn new(clap_app: clap::App<'a>) -> Self { +impl CkbCompleter { + pub fn new(clap_app: clap::Command) -> Self { CkbCompleter { clap_app, completer: FilenameCompleter::new(), @@ -42,7 +42,7 @@ impl<'a> CkbCompleter<'a> { } } - pub fn get_completions(app: &clap::App<'a>, args: &[String]) -> Vec<(String, String)> { + pub fn get_completions(app: &clap::Command, args: &[String]) -> Vec<(String, String)> { let args_set = args.iter().collect::>(); let switched_completions = |short: Option, long: Option<&str>, multiple: bool, required: bool| { @@ -69,7 +69,6 @@ impl<'a> CkbCompleter<'a> { } }; app.get_subcommands() - .iter() .map(|app| { [ vec![(app.get_name().to_owned(), app.get_name().to_owned())], @@ -79,12 +78,14 @@ impl<'a> CkbCompleter<'a> { ] .concat() }) - .chain(app.get_arguments().iter().map(|a| { + .chain(app.get_arguments().map(|a| { switched_completions( a.get_short(), a.get_long(), - a.is_set(clap::ArgSettings::MultipleValues), - a.is_set(clap::ArgSettings::Required), + a.get_num_args() + .map(|r| r.max_values() > 1) + .unwrap_or(false), + a.is_required_set(), ) })) .collect::>>() @@ -92,11 +93,11 @@ impl<'a> CkbCompleter<'a> { } pub fn find_subcommand<'s, Iter: iter::Iterator>( - app: clap::App<'a>, + app: clap::Command, mut prefix_names: iter::Peekable, - ) -> Option> { + ) -> Option { if let Some(name) = prefix_names.next() { - for inner_app in app.get_subcommands().iter() { + for inner_app in app.get_subcommands() { if inner_app.get_name() == name || inner_app.get_all_aliases().any(|alias| alias == name) { @@ -108,7 +109,7 @@ impl<'a> CkbCompleter<'a> { } } } - if prefix_names.peek().is_none() || app.get_subcommands().is_empty() { + if prefix_names.peek().is_none() || app.get_subcommands().next().is_none() { Some(app) } else { None @@ -116,7 +117,7 @@ impl<'a> CkbCompleter<'a> { } } -impl Completer for CkbCompleter<'_> { +impl Completer for CkbCompleter { type Candidate = Pair; fn complete( @@ -192,7 +193,7 @@ impl Completer for CkbCompleter<'_> { } } -impl Hinter for CkbCompleter<'_> { +impl Hinter for CkbCompleter { type Hint = String; fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { @@ -200,7 +201,7 @@ impl Hinter for CkbCompleter<'_> { } } -impl Validator for CkbCompleter<'_> { +impl Validator for CkbCompleter { fn validate( &self, ctx: &mut validate::ValidationContext, @@ -213,7 +214,7 @@ impl Validator for CkbCompleter<'_> { } } -impl Highlighter for CkbCompleter<'_> { +impl Highlighter for CkbCompleter { fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { Owned("\x1b[1m".to_owned() + hint + "\x1b[m") } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e76c3b57..f8c6c074 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ pub mod arg; pub mod arg_parser; pub mod cell_dep; +pub mod command; pub mod completer; pub mod config; pub mod genesis_info; diff --git a/src/utils/other.rs b/src/utils/other.rs index 044da459..15bf8fb6 100644 --- a/src/utils/other.rs +++ b/src/utils/other.rs @@ -6,6 +6,7 @@ use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::utils::arg_parser::ArgMatchesExt; use ckb_hash::{blake2b_256, new_blake2b}; use ckb_jsonrpc_types as rpc_types; use ckb_jsonrpc_types::Status; @@ -69,6 +70,7 @@ pub fn get_key_store(ckb_cli_dir: PathBuf) -> Result { KeyStore::from_dir(keystore_dir, ScryptType::default()).map_err(|err| err.to_string()) } +#[allow(dead_code)] pub fn get_address(network: Option, m: &ArgMatches) -> Result { let address_opt: Option
= AddressParser::new_sighash() .set_network_opt(network) @@ -286,6 +288,7 @@ pub fn check_lack_of_capacity(transaction: &TransactionView) -> Result<(), Strin Ok(()) } +#[allow(dead_code)] pub fn get_to_data(m: &ArgMatches) -> Result { let to_data_opt: Option = HexParser.from_matches_opt(m, "to-data")?; match to_data_opt { @@ -326,6 +329,7 @@ pub fn get_privkey_signer(privkey: PrivkeyWrapper) -> SignerFn { ) } +#[allow(dead_code)] pub fn get_arg_value(matches: &ArgMatches, name: &str) -> Result { matches .value_of(name)