1use crate::errors::TerraRustCLIError;
2use crate::errors::TerraRustCLIError::MissingEnv;
3use clap::{Arg, ArgMatches, Parser};
4use lazy_static::lazy_static;
5use regex::{Captures, Regex};
6use secp256k1::{Context, Secp256k1, Signing};
7use std::collections::HashMap;
8use std::fs::File;
9use std::io::{BufReader, Read};
10use std::path::Path;
11use terra_rust_api::core_types::Coin;
12use terra_rust_api::{GasOptions, PrivateKey, Terra};
13use terra_rust_wallet::Wallet;
14
15#[derive(Parser)]
17
18pub struct Cli<T: clap::FromArgMatches + clap::Subcommand> {
19 #[clap(
20 name = "lcd",
21 env = "TERRARUST_LCD",
22 default_value = "https://lcd.terra.dev",
23 short,
24 long = "lcd-client-url",
25 help = "https://lcd.terra.dev is main-net, https://bombay-lcd.terra.dev"
26 )]
27 pub lcd: String,
29 #[clap(
30 name = "fcd",
31 env = "TERRARUST_FCD",
32 default_value = "https://fcd.terra.dev",
33 long = "fcd-client-url",
34 help = "https://fcd.terra.dev is main-net. currently only used to fetch gas prices"
35 )]
36 pub fcd: String,
38 #[clap(
39 name = "chain",
40 env = "TERRARUST_CHAIN",
41 default_value = "columbus-5",
42 short,
43 long = "chain",
44 help = "bombay-12 is testnet, columbus-5 is main-net"
45 )]
46 pub chain_id: String,
47 #[clap(
49 name = "wallet",
50 env = "TERRARUST_WALLET",
51 default_value = "default",
52 short,
53 long = "wallet",
54 help = "the default wallet to look for keys in"
55 )]
56 pub wallet: String,
57 #[clap(
58 name = "seed",
59 env = "TERRARUST_SEED_PHRASE",
60 default_value = "",
61 short,
62 long = "seed",
63 help = "the seed phrase to use with this private key"
64 )]
65 pub seed: String,
66 #[clap(
67 name = "fees",
68 default_value = "",
69 short,
70 long = "fees",
71 help = "the fees to use. This will override gas parameters if specified."
72 )]
73 pub fees: String,
74 #[clap(
75 name = "gas",
76 default_value = "auto",
77 long = "gas",
78 help = "the gas amount to use 'auto' to estimate"
79 )]
80 pub gas: String,
81 #[clap(
82 name = "gas-prices",
83 env = "TERRARUST_GAS_PRICES",
84 default_value = "auto",
85 long = "gas-prices",
86 help = "the gas price to use to calculate fee. Format is NNNtoken eg. 1000uluna. note we only support a single price for now. if auto. it will use FCD"
87 )]
88 pub gas_price: String,
89 #[clap(
90 name = "gas-denom",
91 env = "TERRARUST_GAS_DENOM",
92 default_value = "ukrw",
93 long = "gas-denom",
94 help = "the denomination/currency to use to pay fee. Format is uXXXX."
95 )]
96 pub gas_price_denom: String,
97 #[clap(
98 name = "gas-adjustment",
99 default_value = "1.4",
100 env = "TERRARUST_GAS_ADJUSTMENT",
101 long = "gas-adjustment",
102 help = "the adjustment to multiply the estimate to calculate the fee"
103 )]
104 pub gas_adjustment: f64,
105 #[clap(short, long, parse(from_flag))]
106 pub debug: std::sync::atomic::AtomicBool,
107 #[clap(subcommand)]
108 pub cmd: T,
109}
110impl<T: clap::FromArgMatches + clap::Subcommand> Cli<T> {
111 pub async fn gas_opts(&self) -> Result<GasOptions, TerraRustCLIError> {
112 if self.gas_price == "auto" {
113 let client = reqwest::Client::new();
114 let gas_opts = GasOptions::create_with_fcd(
115 &client,
116 &self.fcd,
117 &self.gas_price_denom,
118 self.gas_adjustment,
119 )
120 .await?;
121 if let Some(gas_price) = &gas_opts.gas_price {
122 log::info!("Using Gas price of {}", gas_price);
123 }
124
125 Ok(gas_opts)
126 } else {
127 let fees = Coin::parse(&self.fees)?;
128 let gas_str = &self.gas;
129 let (estimate_gas, gas) = if gas_str == "auto" {
130 (true, None)
131 } else {
132 let g = &self.gas.parse::<u64>()?;
133 (false, Some(*g))
134 };
135
136 let gas_price = Coin::parse(&self.gas_price)?;
137 let gas_adjustment = Some(self.gas_adjustment);
138
139 Ok(GasOptions {
140 fees,
141 estimate_gas,
142 gas,
143 gas_price,
144 gas_adjustment,
145 })
146 }
147 }
148}
149#[allow(dead_code)]
150pub fn gen_cli_read_only<'a>(app_name: &'a str, bin_name: &'a str) -> clap::Command<'a> {
151 clap::Command::new(app_name)
152 .bin_name(bin_name)
153 .arg(
154 Arg::new("wallet")
155 .long("wallet")
156 .takes_value(true)
157 .value_name("wallet")
158 .env("TERRARUST_WALLET")
159 .default_value("default")
160 .help("the default wallet to look for keys in"),
161 )
162 .arg(
163 Arg::new("seed")
164 .long("seed")
165 .takes_value(true)
166 .value_name("seed")
167 .env("TERRARUST_SEED_PHRASE")
168 .default_value("")
169 .help("the seed phrase to use with this private key"),
170 )
171 .arg(
172 Arg::new("lcd")
173 .long("lcd")
174 .value_name("lcd")
175 .takes_value(true)
176 .env("TERRARUST_LCD")
177 .default_value("https://lcd.terra.dev")
178 .help("https://lcd.terra.dev is main-net, https://bombay-lcd.terra.dev"),
179 )
180 .arg(
181 Arg::new("fcd")
182 .long("fcd")
183 .value_name("fcd")
184 .takes_value(true)
185 .env("TERRARUST_FCD")
186 .default_value("https://fcd.terra.dev")
187 .help("https://fcd.terra.dev is main-net, https://bombay-fcd.terra.dev"),
188 )
189 .arg(
190 Arg::new("chain")
191 .long("chain")
192 .takes_value(true)
193 .value_name("chain")
194 .env("TERRARUST_CHAIN")
195 .default_value("columbus-5")
196 .help("bombay-12 is testnet, columbus-5 is main-net"),
197 )
198}
199#[allow(dead_code)]
200pub fn gen_cli<'a>(app_name: &'a str, bin_name: &'a str) -> clap::Command<'a> {
201 gen_cli_read_only(app_name,bin_name).args(&[
202 Arg::new("fees").long("fees").takes_value(true).value_name("fees").default_value("").help( "the fees to use. This will override gas parameters if specified."),
203 Arg::new("gas").long("gas").takes_value(true).value_name("gas").default_value("auto").help( "the gas amount to use 'auto' to estimate"),
204 Arg::new("gas-prices").long("gas-prices").takes_value(true).value_name("gas-prices").default_value("auto").help( "the gas price to use to calculate fee. Format is NNNtoken eg. 1000uluna. note we only support a single price for now. if auto. it will use FCD"),
205 Arg::new("gas-denom").long("gas-denom").takes_value(true).value_name("gas-denom").env("TERRARUST_GAS_DENOM").default_value("ukrw").help( "the denomination/currency to use to pay fee. Format is uXXXX."),
206 Arg::new("gas-adjustment").long("gas-adjustment").takes_value(true).value_name("gas-adjustment").default_value("1.4").help( "the adjustment to multiply the estimate to calculate the fee"),
207 Arg::new("sender").long("sender").takes_value(true).value_name("sender").help( "wallet that is sending the command")
208 .env("TERRARUST_SENDER"),
209 Arg::new("phrase")
210 .long("phrase")
211 .takes_value(true)
212 .value_name("phrase")
213 .required(false)
214 .help("the phrase words for the key"),
215 ])
216}
217#[allow(dead_code)]
218pub async fn gas_opts(arg_matches: &ArgMatches) -> Result<GasOptions, TerraRustCLIError> {
219 let gas_price = arg_matches
220 .value_of("gas-prices")
221 .expect("gas-prices should be in the CLI");
222 let gas_adjustment = arg_matches
223 .value_of("gas-adjustment")
224 .unwrap()
225 .parse::<f64>()?;
226 if gas_price == "auto" {
227 let fcd = arg_matches.value_of("fcd").unwrap();
228 let gas_price_denom = arg_matches.value_of("gas-denom").unwrap();
229
230 let client = reqwest::Client::new();
231 let gas_opts =
232 GasOptions::create_with_fcd(&client, fcd, gas_price_denom, gas_adjustment).await?;
233 if let Some(gas_price) = &gas_opts.gas_price {
234 log::info!("Using Gas price of {}", gas_price);
235 }
236
237 Ok(gas_opts)
238 } else {
239 let gas_str = arg_matches.value_of("gas").unwrap();
240 let fees = Coin::parse(arg_matches.value_of("fees").unwrap())?;
241
242 let (estimate_gas, gas) = if gas_str == "auto" {
243 (true, None)
244 } else {
245 let g = &gas_str.parse::<u64>()?;
246 (false, Some(*g))
247 };
248
249 let gas_price = Coin::parse(gas_price)?;
250 let gas_adjustment = Some(gas_adjustment);
251
252 Ok(GasOptions {
253 fees,
254 estimate_gas,
255 gas,
256 gas_price,
257 gas_adjustment,
258 })
259 }
260}
261#[allow(dead_code)]
262pub fn wallet_from_args(cli: &ArgMatches) -> Result<Wallet, TerraRustCLIError> {
263 let wallet = get_arg_value(cli, "wallet")?;
264 Ok(Wallet::create(wallet))
265}
266
267pub fn wallet_opt_from_args(matches: &ArgMatches) -> Option<Wallet> {
268 matches.value_of("wallet").map(Wallet::create)
269}
270
271pub fn seed_from_args(matches: &ArgMatches) -> Option<&str> {
272 if let Some(seed) = matches.value_of("seed") {
273 Some(seed)
274 } else {
275 None
276 }
277}
278
279#[allow(dead_code)]
280pub async fn lcd_from_args(cli: &ArgMatches) -> Result<Terra, TerraRustCLIError> {
281 let gas_opts = gas_opts(cli).await?;
282 let lcd = get_arg_value(cli, "lcd")?;
283 let chain_id = get_arg_value(cli, "chain")?;
284
285 Ok(Terra::lcd_client(lcd, chain_id, &gas_opts, None))
286}
287#[allow(dead_code)]
288pub fn lcd_no_tx_from_args(cli: &ArgMatches) -> Result<Terra, TerraRustCLIError> {
289 let lcd = get_arg_value(cli, "lcd")?;
290 let chain_id = get_arg_value(cli, "chain")?;
291
292 Ok(Terra::lcd_client_no_tx(lcd, chain_id))
293}
294
295pub fn get_private_key<C: Context + Signing>(
296 secp: &Secp256k1<C>,
297 matches: &ArgMatches,
298) -> Result<PrivateKey, TerraRustCLIError> {
299 if let Some(phrase) = matches.value_of("phrase") {
300 if let Some(seed) = matches.value_of("seed") {
301 Ok(PrivateKey::from_words_seed(secp, phrase, seed)?)
302 } else {
303 Ok(PrivateKey::from_words(secp, phrase, 0, 0)?)
304 }
305 } else {
306 let wallet = wallet_from_args(matches)?;
307 let sender = get_arg_value(matches, "sender")?;
308 Ok(wallet.get_private_key(secp, sender, matches.value_of("seed"))?)
309 }
310}
311
312pub fn get_arg_value<'a>(cli: &'a ArgMatches, id: &str) -> Result<&'a str, TerraRustCLIError> {
313 if let Some(val) = cli.value_of(id) {
314 Ok(val)
315 } else {
316 Err(TerraRustCLIError::MissingArgument(id.to_string()))
317 }
318}
319
320fn hack_get_wallet_pub_key<C: secp256k1::Signing + secp256k1::Context>(
321 secp: &Secp256k1<C>,
322 wallet: &Wallet,
323 seed: Option<&str>,
324 key: &str,
325) -> Option<(String, String)> {
326 if let Ok(public_key) = wallet.get_public_key(secp, key, seed) {
327 if let Ok(account) = public_key.account() {
328 if let Ok(operator) = public_key.operator_address() {
329 return Some((account, operator));
330 }
331 }
332 }
333 None
334}
335pub fn expand_block<C: secp256k1::Signing + secp256k1::Context>(
338 in_str: &str,
339 sender_account: Option<String>,
340 secp: &Secp256k1<C>,
341 wallet: Option<Wallet>,
342 seed: Option<&str>,
343 variables: Option<HashMap<String, String>>,
344 fail_if_variable_not_present: bool,
345) -> Result<String, TerraRustCLIError> {
346 lazy_static! {
347 static ref RE: Regex = Regex::new(
348 r"###(E:[a-zA-Z0-9_]*?|A:[a-zA-Z0-9_]*?|O:[a-zA-Z0-9_]*?|V:[a-zA-Z0-9_]*?|SENDER)###"
349 )
350 .expect("unable to compile regex");
351 }
352 let mut missing_env: Option<String> = None;
353
354 let caps = RE.replace_all(in_str, |captures: &Captures| match &captures[1] {
355 "" => String::from("%"),
356 "SENDER" => {
357 if let Some(sender) = sender_account.clone() {
358 sender
359 } else {
360 "###SENDER###".to_string()
362 }
363 }
364 varname => {
365 if varname.starts_with("E:") {
366 let env_var = varname.split_at(2).1;
367 if let Ok(value) = std::env::var(env_var) {
368 value
369 } else {
370 missing_env = Some(env_var.to_string());
371 format!("###_err_{}###", env_var)
372 }
373 } else if let Some(wallet_entry) = wallet.clone() {
374 if varname.starts_with("A:") {
375 let key_name = varname.split_at(2).1;
376 if let Some((account, _operator)) =
377 hack_get_wallet_pub_key(secp, &wallet_entry, seed, key_name)
378 {
379 account
380 } else {
381 missing_env = Some(varname.to_string());
382 format!("###_err_{}###", varname)
383 }
384 } else if varname.starts_with("O:") {
385 let key_name = varname.split_at(2).1;
386 if let Some((_account, operator)) =
387 hack_get_wallet_pub_key(secp, &wallet_entry, seed, key_name)
388 {
389 operator
390 } else {
391 missing_env = Some(varname.to_string());
392 format!("###_err_{}###", varname)
393 }
394 } else if varname.starts_with("V:") {
395 let key_name = varname.split_at(2).1;
396 if let Some(vars) = variables.clone() {
397 if let Some(value) = vars.get(key_name) {
398 value.to_string()
399 } else if fail_if_variable_not_present {
400 missing_env = Some(varname.to_string());
401 format!("###_err_{}###", varname)
402 } else {
403 format!("###{}###", varname)
404 }
405 } else if fail_if_variable_not_present {
406 missing_env = Some(varname.to_string());
407 format!("###_err_{}###", varname)
408 } else {
409 format!("###{}###", varname)
410 }
411 } else {
412 format!("###{}###", varname)
413 }
414 } else {
415 format!("###{}###", varname)
416 }
417 }
418 });
419 if let Some(env) = missing_env {
420 Err(MissingEnv(env))
421 } else {
422 Ok(caps.to_string())
423 }
424}
425pub fn get_json_block(in_str: &str) -> Result<serde_json::Value, TerraRustCLIError> {
429 if in_str.starts_with('{') {
430 Ok(serde_json::from_str::<serde_json::Value>(in_str)?)
431 } else if in_str == "-" {
432 let input = std::io::stdin();
433 let mut input = input.lock();
434 let mut str_buf = String::new();
435 input.read_to_string(&mut str_buf)?;
436
437 Ok(serde_json::from_str(&str_buf)?)
438 } else {
439 let p = Path::new(in_str);
440 let file = File::open(p)?;
441 let rdr = BufReader::new(file);
442 Ok(serde_json::from_reader(rdr)?)
443 }
444}
445pub fn get_json_block_expanded<C: secp256k1::Signing + secp256k1::Context>(
449 in_str: &str,
450 sender: Option<String>,
451 secp: &Secp256k1<C>,
452 wallet: Option<Wallet>,
453 seed: Option<&str>,
454 variables: Option<HashMap<String, String>>,
455 fail_if_variable_not_present: bool,
456) -> Result<serde_json::Value, TerraRustCLIError> {
457 let json = get_json_block(in_str)?;
458 let in_str = serde_json::to_string(&json)?;
459 let out_str = expand_block(
460 &in_str,
461 sender,
462 secp,
463 wallet,
464 seed,
465 variables,
466 fail_if_variable_not_present,
467 )?;
468 Ok(serde_json::from_str(&out_str)?)
469}
470
471#[cfg(test)]
472mod tst {
473 use super::*;
474 use std::env;
475
476 #[test]
477 pub fn test() -> anyhow::Result<()> {
478 let secp = secp256k1::Secp256k1::new();
479 assert_eq!(
480 "mary had a little lamb",
481 expand_block(
482 "mary had a little lamb",
483 Some("sender".into()),
484 &secp,
485 None,
486 None,
487 None,
488 false
489 )?
490 );
491 assert_eq!(
492 "mary had a ###little lamb",
493 expand_block(
494 "mary had a ###little lamb",
495 Some("sender".into()),
496 &secp,
497 None,
498 None,
499 None,
500 false
501 )?
502 );
503 assert_eq!(
504 "mary had a sender lamb",
505 expand_block(
506 "mary had a ###SENDER### lamb",
507 Some("sender".into()),
508 &secp,
509 None,
510 None,
511 None,
512 false
513 )?
514 );
515 env::set_var("FOO", "BAR");
516 assert_eq!(
517 "mary had a BAR lamb",
518 expand_block(
519 "mary had a ###E:FOO### lamb",
520 Some("sender".into()),
521 &secp,
522 None,
523 None,
524 None,
525 false
526 )?
527 );
528 assert_eq!(
529 "mary had a BAR ###lamb",
530 expand_block(
531 "mary had a ###E:FOO### ###lamb",
532 Some("sender".into()),
533 &secp,
534 None,
535 None,
536 None,
537 false
538 )?
539 );
540 assert_eq!(
541 "mary BAR a ###FOO### ###lamb",
542 expand_block(
543 "mary ###E:FOO### a ###FOO### ###lamb",
544 Some("sender".into()),
545 &secp,
546 None,
547 None,
548 None,
549 false
550 )?
551 );
552 assert_eq!(
553 "mary BAR a BAR ###lamb",
554 expand_block(
555 "mary ###E:FOO### a ###E:FOO### ###lamb",
556 Some("sender".into()),
557 &secp,
558 None,
559 None,
560 None,
561 false
562 )?
563 );
564 assert_eq!(
565 "mary BAR sender BAR ###lamb",
566 expand_block(
567 "mary ###E:FOO### ###SENDER### ###E:FOO### ###lamb",
568 Some("sender".into()),
569 &secp,
570 None,
571 None,
572 None,
573 false
574 )?
575 );
576 env::set_var("XYZ", "aXYZc");
577 assert_eq!(
578 "mary BAR sender aXYZc ###lamb",
579 expand_block(
580 "mary ###E:FOO### ###SENDER### ###E:XYZ### ###lamb",
581 Some("sender".into()),
582 &secp,
583 None,
584 None,
585 None,
586 false
587 )?
588 );
589 assert_eq!(
590 "mary BAR sender aXYZc ###E:lamb aXYZc",
591 expand_block(
592 "mary ###E:FOO### ###SENDER### ###E:XYZ### ###E:lamb ###E:XYZ###",
593 Some("sender".into()),
594 &secp,
595 None,
596 None,
597 None,
598 false
599 )?
600 );
601 assert_eq!(
602 "mary BAR xxx aXYZc ###lamb",
603 expand_block(
604 "mary ###E:FOO### xxx ###E:XYZ### ###lamb",
605 None,
606 &secp,
607 None,
608 None,
609 None,
610 false
611 )?
612 );
613 assert!(expand_block(
614 "mary ###E:FOO### ###SENDER### ###E:AAA### ###lamb",
615 Some("sender".into()),
616 &secp,
617 None,
618 None,
619 None,
620 false
621 )
622 .is_err());
623 assert_eq!(
624 "mary ###SENDER### ###lamb aXYZc",
625 expand_block(
626 "mary ###SENDER### ###lamb ###E:XYZ###",
627 None,
628 &secp,
629 None,
630 None,
631 None,
632 false
633 )?
634 );
635 Ok(())
636 }
637
638 #[test]
663 pub fn test_vars() -> anyhow::Result<()> {
664 let secp = secp256k1::Secp256k1::new();
665 let wallet = Wallet::new("rpc")?;
666 env::set_var("XYZ", "aXYZc");
667 let vars: HashMap<String, String> = [("XYZ".to_string(), "def".to_string())]
668 .iter()
669 .cloned()
670 .collect();
671 assert_eq!(
672 "abc aXYZc def def",
673 expand_block(
674 "abc ###E:XYZ### ###V:XYZ### def",
675 None,
676 &secp,
677 Some(wallet.clone()),
678 None,
679 Some(vars.clone()),
680 true
681 )?
682 );
683 assert_eq!(
684 "abc aXYZc ###V:AYZ### def",
685 expand_block(
686 "abc ###E:XYZ### ###V:AYZ### def",
687 None,
688 &secp,
689 Some(wallet.clone()),
690 None,
691 Some(vars.clone()),
692 false
693 )?
694 );
695 assert!(expand_block(
696 "abc ###E:XYZ### ###V:AYZ### def",
697 None,
698 &secp,
699 Some(wallet.clone()),
700 None,
701 Some(vars),
702 true
703 )
704 .is_err());
705
706 Ok(())
707 }
708}