Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: accept more ways to specify cycle and e8s amounts #3452

Merged
merged 6 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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.
Expand Down
100 changes: 89 additions & 11 deletions src/dfx/src/util/clap/parsers.rs
Original file line number Diff line number Diff line change
@@ -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<Decimal, String> {
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<String, String> {
// A valid Request Id starts with `0x` and is a series of 64 hexadecimals.
Expand All @@ -18,20 +46,25 @@ pub fn request_id_parser(v: &str) -> Result<String, String> {
}
}

pub fn e8s_parser(e8s: &str) -> Result<u64, String> {
e8s.parse::<u64>()
.map_err(|_| "Must specify a non negative whole number.".to_string())
pub fn e8s_parser(input: &str) -> Result<u64, String> {
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<u64, String> {
memo.parse::<u64>()
.map_err(|_| "Must specify a non negative whole number.".to_string())
}

pub fn cycle_amount_parser(cycles: &str) -> Result<u128, String> {
cycles
.parse::<u128>()
.map_err(|_| "Must be a non negative amount.".to_string())
pub fn cycle_amount_parser(input: &str) -> Result<u128, String> {
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<PathBuf, String> {
Expand All @@ -52,9 +85,15 @@ pub fn file_or_stdin_parser(path: &str) -> Result<PathBuf, String> {
}
}

pub fn trillion_cycle_amount_parser(cycles: &str) -> Result<u128, String> {
format!("{}000000000000", cycles).parse::<u128>()
.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<u128, String> {
if let Ok(cycles) = format!("{}000000000000", input.replace('_', "")).parse::<u128>() {
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<u64, String> {
Expand Down Expand Up @@ -130,3 +169,42 @@ pub fn hsm_key_id_parser(key_id: &str) -> Result<String, String> {
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));
}
Loading