terra_rust_cli/
cli_helpers.rs

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/// your terra swiss army knife
16#[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    // Terra cli Client daemon
28    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    // Terra cli Client daemon
37    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    // Wallet name
48    #[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}
335/// expand json with environmental values
336
337pub 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                //  missing_env = Some("SENDER".to_string());
361                "###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}
425/// convert a input parameter into json.
426/// input can either be a json string, a file, or '-' to read stdin.
427///
428pub 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}
445/// convert a input parameter into json, expanding the JSON returned with environment variables
446/// input can either be a json string, a file, or '-' to read stdin.
447///
448pub 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    /*
639    // this won't work on other machines
640    #[test]
641    pub fn test_account() -> anyhow::Result<()> {
642        let secp = secp256k1::Secp256k1::new();
643        let wallet = Wallet::new("rpc")?;
644        assert_eq!(
645            "abc terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp def",
646            expand_block(
647                "abc ###A:test2### def",
648                None,
649                &secp,
650                Some(wallet.clone()),
651                None
652            )?
653        );
654        assert_eq!(
655            "abc terravaloper17lmam6zguazs5q5u6z5mmx76uj63gldnskxuaj def",
656            expand_block("abc ###O:test2### def", None, &secp, Some(wallet), None)?
657        );
658        Ok(())
659    }
660
661     */
662    #[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}