Skip to content

Commit

Permalink
feat: accept more ways to specify cycle and e8s amounts (#3452)
Browse files Browse the repository at this point in the history
There are more ways to specify cycles amounts than just using plain digits. This PR adds support for `_` as thousand separator, plus shorthands such as `5k` or `3TC` for cycle, trillion cycle, and e8s amounts.
  • Loading branch information
sesi200 authored Nov 27, 2023
1 parent 216923b commit 9ee350b
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 11 deletions.
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));
}

0 comments on commit 9ee350b

Please sign in to comment.