diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b5f3ca3b..9fc52fba8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,18 @@ call it as a query call. This resolves a potential security risk. The message "transaction is a duplicate of another transaction in block ...", previously printed to stdout, is now logged to stderr. This means that the output of `dfx ledger transfer` to stdout will contain only "Transfer sent at block height ". +### feat: accept more ways to specify cycle and e8s amounts + +Underscores (`_`) can now be used to make large numbers more readable. For example: `dfx canister deposit-cycles 1_234_567 mycanister` + +Certain suffixes that replace a number of zeros are now supported. The (case-insensitive) suffixes are: +- `k` for `000`, e.g. `500k` +- `m` for `000_000`, e.g. `5m` +- `b` for `000_000_000`, e.g. `50B` +- `t` for `000_000_000_000`, e.g. `0.3T` + +For cycles an additional `c` or `C` is also acceptable. For example: `dfx canister deposit-cycles 3TC mycanister` + ### feat: added `dfx cycles` command This won't work on mainnet yet, but can work locally after installing the cycles ledger. diff --git a/src/dfx/src/util/clap/parsers.rs b/src/dfx/src/util/clap/parsers.rs index 290e3da662..9b7184dffc 100644 --- a/src/dfx/src/util/clap/parsers.rs +++ b/src/dfx/src/util/clap/parsers.rs @@ -1,5 +1,33 @@ use byte_unit::{Byte, ByteUnit}; -use std::path::PathBuf; +use rust_decimal::Decimal; +use std::{path::PathBuf, str::FromStr}; + +/// Removes `_`, interprets `k`, `m`, `b`, `t` suffix (case-insensitive) +fn decimal_with_suffix_parser(input: &str) -> Result { + let input = input.replace('_', "").to_lowercase(); + let (number, suffix) = if input + .chars() + .last() + .map(|char| char.is_alphabetic()) + .unwrap_or(false) + { + input.split_at(input.len() - 1) + } else { + (input.as_str(), "") + }; + let multiplier: u64 = match suffix { + "" => Ok(1), + "k" => Ok(1_000), + "m" => Ok(1_000_000), + "b" => Ok(1_000_000_000), + "t" => Ok(1_000_000_000_000), + other => Err(format!("Unknown amount specifier: '{}'", other)), + }?; + let number = Decimal::from_str(number).map_err(|err| err.to_string())?; + Decimal::from(multiplier) + .checked_mul(number) + .ok_or_else(|| "Amount too large.".to_string()) +} pub fn request_id_parser(v: &str) -> Result { // A valid Request Id starts with `0x` and is a series of 64 hexadecimals. @@ -18,9 +46,10 @@ pub fn request_id_parser(v: &str) -> Result { } } -pub fn e8s_parser(e8s: &str) -> Result { - e8s.parse::() - .map_err(|_| "Must specify a non negative whole number.".to_string()) +pub fn e8s_parser(input: &str) -> Result { + decimal_with_suffix_parser(input)? + .try_into() + .map_err(|_| "Must specify a non-negative whole number.".to_string()) } pub fn memo_parser(memo: &str) -> Result { @@ -28,10 +57,14 @@ pub fn memo_parser(memo: &str) -> Result { .map_err(|_| "Must specify a non negative whole number.".to_string()) } -pub fn cycle_amount_parser(cycles: &str) -> Result { - cycles - .parse::() - .map_err(|_| "Must be a non negative amount.".to_string()) +pub fn cycle_amount_parser(input: &str) -> Result { + let removed_cycle_suffix = if input.to_lowercase().ends_with('c') { + &input[..input.len() - 1] + } else { + input + }; + + decimal_with_suffix_parser(removed_cycle_suffix)?.try_into().map_err(|_| "Failed to parse amount. Please use digits only or something like 3.5TC, 2t, or 5_000_000.".to_string()) } pub fn file_parser(path: &str) -> Result { @@ -52,9 +85,15 @@ pub fn file_or_stdin_parser(path: &str) -> Result { } } -pub fn trillion_cycle_amount_parser(cycles: &str) -> Result { - format!("{}000000000000", cycles).parse::() - .map_err(|_| "Must be a non negative amount. Currently only accepts whole numbers. Use --cycles otherwise.".to_string()) +pub fn trillion_cycle_amount_parser(input: &str) -> Result { + if let Ok(cycles) = format!("{}000000000000", input.replace('_', "")).parse::() { + Ok(cycles) + } else { + decimal_with_suffix_parser(input)? + .checked_mul(1_000_000_000_000_u64.into()) + .and_then(|total| total.try_into().ok()) + .ok_or_else(|| "Amount too large.".to_string()) + } } pub fn compute_allocation_parser(compute_allocation: &str) -> Result { @@ -130,3 +169,42 @@ pub fn hsm_key_id_parser(key_id: &str) -> Result { Ok(key_id.to_string()) } } + +#[test] +fn test_cycle_amount_parser() { + assert_eq!(cycle_amount_parser("900c"), Ok(900)); + assert_eq!(cycle_amount_parser("9_887K"), Ok(9_887_000)); + assert_eq!(cycle_amount_parser("0.1M"), Ok(100_000)); + assert_eq!(cycle_amount_parser("0.01b"), Ok(10_000_000)); + assert_eq!(cycle_amount_parser("10T"), Ok(10_000_000_000_000)); + assert_eq!(cycle_amount_parser("10TC"), Ok(10_000_000_000_000)); + assert_eq!(cycle_amount_parser("1.23t"), Ok(1_230_000_000_000)); + + assert!(cycle_amount_parser("1ffff").is_err()); + assert!(cycle_amount_parser("1MT").is_err()); + assert!(cycle_amount_parser("-0.1m").is_err()); + assert!(cycle_amount_parser("T100").is_err()); + assert!(cycle_amount_parser("1.1k0").is_err()); + assert!(cycle_amount_parser(&format!("{}0", u128::MAX)).is_err()); +} + +#[test] +fn test_trillion_cycle_amount_parser() { + const TRILLION: u128 = 1_000_000_000_000; + assert_eq!(trillion_cycle_amount_parser("3"), Ok(3 * TRILLION)); + assert_eq!(trillion_cycle_amount_parser("5_555"), Ok(5_555 * TRILLION)); + assert_eq!(trillion_cycle_amount_parser("1k"), Ok(1_000 * TRILLION)); + assert_eq!(trillion_cycle_amount_parser("0.3"), Ok(300_000_000_000)); + assert_eq!(trillion_cycle_amount_parser("0.3k"), Ok(300 * TRILLION)); + + assert!(trillion_cycle_amount_parser("-0.1m").is_err()); + assert!(trillion_cycle_amount_parser("1TC").is_err()); // ambiguous in combination with --t +} + +#[test] +fn test_e8s_parser() { + assert_eq!(e8s_parser("1"), Ok(1)); + assert_eq!(e8s_parser("1_000"), Ok(1_000)); + assert_eq!(e8s_parser("1k"), Ok(1_000)); + assert_eq!(e8s_parser("1M"), Ok(1_000_000)); +}