From a90965c550610b84ab7f47f23a3743614b944cc1 Mon Sep 17 00:00:00 2001 From: Prithvi Shahi Date: Tue, 13 Apr 2021 08:37:36 -0700 Subject: [PATCH 1/6] feat: add ledger subcommand --- Cargo.lock | 18 +- src/dfx/Cargo.toml | 4 +- src/dfx/src/commands/ledger/account_id.rs | 17 ++ src/dfx/src/commands/ledger/balance.rs | 51 ++++ .../src/commands/ledger/create_canister.rs | 114 +++++++++ src/dfx/src/commands/ledger/mod.rs | 47 ++++ src/dfx/src/commands/ledger/transfer.rs | 76 ++++++ src/dfx/src/commands/mod.rs | 3 + src/dfx/src/lib/mod.rs | 1 + .../src/lib/nns_types/account_identifier.rs | 189 +++++++++++++++ src/dfx/src/lib/nns_types/icpts.rs | 223 ++++++++++++++++++ src/dfx/src/lib/nns_types/mod.rs | 59 +++++ src/dfx/src/util/clap/validators.rs | 13 + 13 files changed, 813 insertions(+), 2 deletions(-) create mode 100644 src/dfx/src/commands/ledger/account_id.rs create mode 100644 src/dfx/src/commands/ledger/balance.rs create mode 100644 src/dfx/src/commands/ledger/create_canister.rs create mode 100644 src/dfx/src/commands/ledger/mod.rs create mode 100644 src/dfx/src/commands/ledger/transfer.rs create mode 100644 src/dfx/src/lib/nns_types/account_identifier.rs create mode 100644 src/dfx/src/lib/nns_types/icpts.rs create mode 100644 src/dfx/src/lib/nns_types/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 63686572d7..67be897090 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1105,6 +1105,7 @@ dependencies = [ "chrono", "clap", "console 0.7.7", + "crc32fast", "crossbeam", "ctrlc", "delay", @@ -1136,6 +1137,7 @@ dependencies = [ "regex", "reqwest 0.10.10", "ring", + "rust_decimal", "rustls 0.18.1", "semver", "serde", @@ -1707,6 +1709,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hostname" @@ -1932,7 +1937,7 @@ dependencies = [ [[package]] name = "ic-types" version = "0.1.2" -source = "git+https://github.com/dfinity/agent-rs.git?branch=next#5472fb610faa6cc1f0c12c82a745e435603fae08" +source = "git+https://github.com/dfinity/agent-rs.git?branch=next#b9c0b28c2dfd7fbd2a654874f8fd9a9773fa0c8d" dependencies = [ "base32", "crc32fast", @@ -3405,6 +3410,17 @@ dependencies = [ "crossbeam-utils 0.8.3", ] +[[package]] +name = "rust_decimal" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7f5b8840fb1f83869a3e1dfd06d93db79ea05311ac5b42b8337d3371caa4f1" +dependencies = [ + "arrayvec", + "num-traits", + "serde", +] + [[package]] name = "rustc_version" version = "0.2.3" diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 74cc63e92d..daa0b0a5ad 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -26,6 +26,7 @@ candid = { version = "0.6.20", features = [ "random" ] } chrono = "0.4.9" clap = "3.0.0-beta.2" console = "0.7.7" +crc32fast = "1.2.0" crossbeam = "0.7.3" ctrlc = { version = "3.1.6", features = [ "termination" ] } delay = "0.3.1" @@ -33,7 +34,7 @@ dialoguer = "0.6.2" erased-serde = "0.3.10" flate2 = "1.0.11" futures = "0.3.5" -hex = "0.4.2" +hex = {version = "0.4.2", features = ["serde"] } indicatif = "0.13.0" lazy-init = "0.5.0" lazy_static = "1.4.0" @@ -51,6 +52,7 @@ regex = "1.3.1" ring = "0.16.11" reqwest = { version = "0.10.4", features = [ "blocking", "json", "rustls-tls" ] } rustls = "0.18.0" +rust_decimal = "1.10.3" semver = "0.9.0" serde = "1.0" serde_bytes = "0.11.2" diff --git a/src/dfx/src/commands/ledger/account_id.rs b/src/dfx/src/commands/ledger/account_id.rs new file mode 100644 index 0000000000..c32904b23d --- /dev/null +++ b/src/dfx/src/commands/ledger/account_id.rs @@ -0,0 +1,17 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::AccountIdentifier; + +use clap::Clap; + +/// Prints the selected identity's AccountIdentifier. +#[derive(Clap)] +pub struct AccountIdOpts {} + +pub async fn exec(env: &dyn Environment, _opts: AccountIdOpts) -> DfxResult { + let sender = env + .get_selected_identity_principal() + .expect("Selected identity not instantiated."); + println!("{}", AccountIdentifier::new(sender, None)); + Ok(()) +} diff --git a/src/dfx/src/commands/ledger/balance.rs b/src/dfx/src/commands/ledger/balance.rs new file mode 100644 index 0000000000..ce1499f213 --- /dev/null +++ b/src/dfx/src/commands/ledger/balance.rs @@ -0,0 +1,51 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::AccountIdentifier; +use crate::lib::nns_types::icpts::ICPTs; +use crate::lib::nns_types::AccountBalanceArgs; +use crate::lib::nns_types::LEDGER_CANISTER_ID; + +use anyhow::anyhow; +use candid::{Decode, Encode}; +use clap::Clap; +use ic_types::principal::Principal; +use std::str::FromStr; + +const ACCOUNT_BALANCE_METHOD: &str = "account_balance_dfx"; + +/// Prints the account balance of the user +#[derive(Clap)] +pub struct BalanceOpts { + /// Specifies an AccountIdentifier to get the balance of + of: Option, +} + +pub async fn exec(env: &dyn Environment, opts: BalanceOpts) -> DfxResult { + let sender = env + .get_selected_identity_principal() + .expect("Selected identity not instantiated."); + let acc_id = opts + .of + .map_or(Ok(AccountIdentifier::new(sender, None)), |v| { + AccountIdentifier::from_str(&v) + }) + .map_err(|err| anyhow!(err))?; + let agent = env + .get_agent() + .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; + let canister_id = Principal::from_text(LEDGER_CANISTER_ID)?; + + let result = agent + .query(&canister_id, ACCOUNT_BALANCE_METHOD) + .with_arg(Encode!(&AccountBalanceArgs { + account: acc_id.to_string() + })?) + .call() + .await?; + + let balance = Decode!(&result, ICPTs)?; + + println!("{}", balance); + + Ok(()) +} diff --git a/src/dfx/src/commands/ledger/create_canister.rs b/src/dfx/src/commands/ledger/create_canister.rs new file mode 100644 index 0000000000..cbb661595b --- /dev/null +++ b/src/dfx/src/commands/ledger/create_canister.rs @@ -0,0 +1,114 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::{AccountIdentifier, Subaccount}; +use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; +use crate::lib::nns_types::{ + BlockHeight, CreateCanisterResult, Memo, NotifyCanisterArgs, SendArgs, + TransactionNotificationResult, CYCLE_MINTER_CANISTER_ID, LEDGER_CANISTER_ID, +}; +use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::waiter::waiter_with_timeout; +use crate::util::clap::validators::icpts_amount_validator; +use crate::util::expiry_duration; + +use anyhow::anyhow; +use candid::{Decode, Encode}; +use clap::{ArgSettings, Clap}; +use ic_types::principal::Principal; +use std::str::FromStr; + +const SEND_METHOD: &str = "send_dfx"; +const NOTIFY_METHOD: &str = "notify_dfx"; +const MEMO_CREATE_CANISTER: u64 = 1095062083_u64; + +/// Create a canister from ICP +#[derive(Clap)] +pub struct CreateCanisterOpts { + /// ICP to account for the fee and the rest to mint as a cycle deposit + #[clap(long, validator(icpts_amount_validator))] + amount: String, + + /// Transaction fee, default is 137 Doms. + #[clap(long, validator(icpts_amount_validator), setting = ArgSettings::Hidden)] + fee: Option, + + /// Specify the controller of the new canister + #[clap(long)] + controller: String, + + /// Max fee + #[clap(long, validator(icpts_amount_validator), setting = ArgSettings::Hidden)] + max_fee: Option, +} + +pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult { + let amount = ICPTs::from_str(&opts.amount).map_err(|err| anyhow!(err))?; + + let fee = opts.fee.map_or(Ok(TRANSACTION_FEE), |v| { + ICPTs::from_str(&v).map_err(|err| anyhow!(err)) + })?; + + // validated by memo_validator + let memo = Memo(MEMO_CREATE_CANISTER); + + let agent = env + .get_agent() + .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; + + fetch_root_key_if_needed(env).await?; + + let ledger_canister_id = Principal::from_text(LEDGER_CANISTER_ID)?; + + let cycle_minter_id = Principal::from_text(CYCLE_MINTER_CANISTER_ID)?; + + let to_subaccount = Some(Subaccount::from(&Principal::from_text(opts.controller)?)); + let to = AccountIdentifier::new(cycle_minter_id.clone(), to_subaccount); + + let result = agent + .update(&ledger_canister_id, SEND_METHOD) + .with_arg(Encode!(&SendArgs { + memo, + amount, + fee, + from_subaccount: None, + to, + created_at_time: None, + })?) + .call_and_wait(waiter_with_timeout(expiry_duration())) + .await?; + + let block_height = Decode!(&result, BlockHeight)?; + println!("Transfer sent at BlockHeight: {}", block_height); + + let max_fee = opts + .max_fee + .map_or(ICPTs::new(0, 0).map_err(|err| anyhow!(err)), |v| { + ICPTs::from_str(&v).map_err(|err| anyhow!(err)) + })?; + + let result = agent + .update(&ledger_canister_id, NOTIFY_METHOD) + .with_arg(Encode!(&NotifyCanisterArgs { + block_height, + max_fee, + from_subaccount: None, + to_canister: cycle_minter_id, + to_subaccount, + })?) + .call_and_wait(waiter_with_timeout(expiry_duration())) + .await?; + + let result = Decode!(&result, TransactionNotificationResult)?; + + let result = Decode!(&result.0, CreateCanisterResult)?; + + match result { + Ok(v) => { + println!("Canister created with id: {:?}", v.to_text()); + } + Err((msg, maybe_height)) => { + println!("Error: {}\nMaybe BlockHeight:{:?}", msg, maybe_height); + } + }; + Ok(()) +} diff --git a/src/dfx/src/commands/ledger/mod.rs b/src/dfx/src/commands/ledger/mod.rs new file mode 100644 index 0000000000..d58f12ca14 --- /dev/null +++ b/src/dfx/src/commands/ledger/mod.rs @@ -0,0 +1,47 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::provider::create_agent_environment; + +use clap::Clap; +use tokio::runtime::Runtime; + +mod account_id; +mod balance; +mod create_canister; +// mod topup; +mod transfer; + +/// Ledger commands. +#[derive(Clap)] +#[clap(name("ledger"))] +pub struct LedgerOpts { + /// Override the compute network to connect to. By default, the local network is used. + #[clap(long)] + network: Option, + + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + AccountId(account_id::AccountIdOpts), + Balance(balance::BalanceOpts), + CreateCanister(create_canister::CreateCanisterOpts), + // TopUp(topup::TopUpOpts), + Transfer(transfer::TransferOpts), +} + +pub fn exec(env: &dyn Environment, opts: LedgerOpts) -> DfxResult { + let agent_env = create_agent_environment(env, opts.network.clone())?; + let runtime = Runtime::new().expect("Unable to create a runtime"); + runtime.block_on(async { + match opts.subcmd { + SubCommand::AccountId(v) => account_id::exec(&agent_env, v).await, + SubCommand::Balance(v) => balance::exec(&agent_env, v).await, + SubCommand::CreateCanister(v) => create_canister::exec(&agent_env, v).await, + // SubCommand::TopUp(v) => topup::exec(&agent_env, v).await, + SubCommand::Transfer(v) => transfer::exec(&agent_env, v).await, + } + }) +} diff --git a/src/dfx/src/commands/ledger/transfer.rs b/src/dfx/src/commands/ledger/transfer.rs new file mode 100644 index 0000000000..d9d5699348 --- /dev/null +++ b/src/dfx/src/commands/ledger/transfer.rs @@ -0,0 +1,76 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::AccountIdentifier; +use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; +use crate::lib::nns_types::{BlockHeight, Memo, SendArgs, LEDGER_CANISTER_ID}; +use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::waiter::waiter_with_timeout; +use crate::util::clap::validators::{icpts_amount_validator, memo_validator}; +use crate::util::expiry_duration; + +use anyhow::anyhow; +use candid::{Decode, Encode}; +use clap::{ArgSettings, Clap}; +use ic_types::principal::Principal; +use std::str::FromStr; + +const SEND_METHOD: &str = "send_dfx"; + +/// Transfer ICP from the user to the destination AccountIdentifier +#[derive(Clap)] +pub struct TransferOpts { + /// ICPs to transfer + #[clap(long, validator(icpts_amount_validator))] + amount: String, + + /// Specify a numeric memo for this transaction. + #[clap(long, validator(memo_validator))] + memo: String, + + /// Transaction fee, default is 137 Doms. + #[clap(long, validator(icpts_amount_validator), setting = ArgSettings::Hidden)] + fee: Option, + + /// AccountIdentifier of transfer destination. + #[clap(long)] + to: String, +} + +pub async fn exec(env: &dyn Environment, opts: TransferOpts) -> DfxResult { + let amount = ICPTs::from_str(&opts.amount).map_err(|err| anyhow!(err))?; + + let fee = opts.fee.map_or(Ok(TRANSACTION_FEE), |v| { + ICPTs::from_str(&v).map_err(|err| anyhow!(err)) + })?; + + // validated by memo_validator + let memo = Memo(opts.memo.parse::().unwrap()); + + let to = AccountIdentifier::from_str(&opts.to).map_err(|err| anyhow!(err))?; + + let agent = env + .get_agent() + .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; + + fetch_root_key_if_needed(env).await?; + + let canister_id = Principal::from_text(LEDGER_CANISTER_ID)?; + + let result = agent + .update(&canister_id, SEND_METHOD) + .with_arg(Encode!(&SendArgs { + memo, + amount, + fee, + from_subaccount: None, + to, + created_at_time: None, + })?) + .call_and_wait(waiter_with_timeout(expiry_duration())) + .await?; + + let block_height = Decode!(&result, BlockHeight)?; + println!("Transfer sent at BlockHeight: {}", block_height); + + Ok(()) +} diff --git a/src/dfx/src/commands/mod.rs b/src/dfx/src/commands/mod.rs index 983c49cfb1..6f3f9bdd3e 100644 --- a/src/dfx/src/commands/mod.rs +++ b/src/dfx/src/commands/mod.rs @@ -11,6 +11,7 @@ mod config; mod deploy; mod identity; mod language_service; +mod ledger; mod new; mod ping; mod replica; @@ -31,6 +32,7 @@ pub enum Command { Identity(identity::IdentityOpt), #[clap(name("_language-service"))] LanguageServices(language_service::LanguageServiceOpts), + Ledger(ledger::LedgerOpts), New(new::NewOpts), Ping(ping::PingOpts), Replica(replica::ReplicaOpts), @@ -51,6 +53,7 @@ pub fn exec(env: &dyn Environment, cmd: Command) -> DfxResult { Command::Deploy(v) => deploy::exec(env, v), Command::Identity(v) => identity::exec(env, v), Command::LanguageServices(v) => language_service::exec(env, v), + Command::Ledger(v) => ledger::exec(env, v), Command::New(v) => new::exec(env, v), Command::Ping(v) => ping::exec(env, v), Command::Replica(v) => replica::exec(env, v), diff --git a/src/dfx/src/lib/mod.rs b/src/dfx/src/lib/mod.rs index 8f8ea7c0f4..a205ceee0b 100644 --- a/src/dfx/src/lib/mod.rs +++ b/src/dfx/src/lib/mod.rs @@ -11,6 +11,7 @@ pub mod logger; pub mod manifest; pub mod models; pub mod network; +pub mod nns_types; pub mod operations; pub mod package_arguments; pub mod progress_bar; diff --git a/src/dfx/src/lib/nns_types/account_identifier.rs b/src/dfx/src/lib/nns_types/account_identifier.rs new file mode 100644 index 0000000000..efb96c6cc8 --- /dev/null +++ b/src/dfx/src/lib/nns_types/account_identifier.rs @@ -0,0 +1,189 @@ +// DISCLAIMER: +// Do not modify this file arbitrarily. +// The contents are borrowed from: +// dfinity-lab/dfinity@f468897c57a5a0d4785b90c94935255d1a2f7d4c +// https://github.com/dfinity-lab/dfinity/blob/master/rs/rosetta-api/canister/src/account_identifier.rs + +use candid::CandidType; +use ic_types::principal::Principal; +use openssl::sha::Sha224; +use serde::{de, de::Error, Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +const SUB_ACCOUNT_ZERO: Subaccount = Subaccount([0; 32]); +const ACCOUNT_DOMAIN_SEPERATOR: &[u8] = b"\x0Aaccount-id"; + +/// While this is backed by an array of length 28, it's canonical representation +/// is a hex string of length 64. The first 8 characters are the CRC-32 encoded +/// hash of the following 56 characters of hex. Both, upper and lower case +/// characters are valid in the input string and can even be mixed. +/// +/// When it is encoded or decoded it will always be as a string to make it +/// easier to use from DFX. +#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AccountIdentifier { + pub hash: [u8; 28], +} + +impl AccountIdentifier { + pub fn new(account: Principal, sub_account: Option) -> AccountIdentifier { + let mut hash = Sha224::new(); + hash.update(ACCOUNT_DOMAIN_SEPERATOR); + hash.update(account.as_slice()); + + let sub_account = sub_account.unwrap_or(SUB_ACCOUNT_ZERO); + hash.update(&sub_account.0[..]); + + AccountIdentifier { + hash: hash.finish(), + } + } + + pub fn from_hex(hex_str: &str) -> Result { + let hex: Vec = hex::decode(hex_str).map_err(|e| e.to_string())?; + Self::from_slice(&hex[..]) + } + + /// Goes from the canonical format (with checksum) encoded in bytes rather + /// than hex to AccountIdentifier + pub fn from_slice(v: &[u8]) -> Result { + // Trim this down when we reach rust 1.48 + let hex: Box<[u8; 32]> = match v.to_vec().into_boxed_slice().try_into() { + Ok(h) => h, + Err(_) => { + let hex_str = hex::encode(v); + return Err(format!( + "{} has a length of {} but we expected a length of 64", + hex_str, + hex_str.len() + )); + } + }; + check_sum(*hex) + } + + pub fn to_hex(&self) -> String { + hex::encode(self.to_vec()) + } + + pub fn to_vec(&self) -> Vec { + [&self.generate_checksum()[..], &self.hash[..]].concat() + } + + pub fn generate_checksum(&self) -> [u8; 4] { + let mut hasher = crc32fast::Hasher::new(); + hasher.update(&self.hash); + hasher.finalize().to_be_bytes() + } +} + +impl Display for AccountIdentifier { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.to_hex().fmt(f) + } +} + +impl FromStr for AccountIdentifier { + type Err = String; + + fn from_str(s: &str) -> Result { + AccountIdentifier::from_hex(s) + } +} + +impl Serialize for AccountIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_hex().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for AccountIdentifier { + // This is the canonical way to read a this from string + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + D::Error: de::Error, + { + let hex: [u8; 32] = hex::serde::deserialize(deserializer)?; + check_sum(hex).map_err(D::Error::custom) + } +} + +fn check_sum(hex: [u8; 32]) -> Result { + // Get the checksum provided + let found_checksum = &hex[0..4]; + + // Copy the hash into a new array + let mut hash = [0; 28]; + hash.copy_from_slice(&hex[4..32]); + + let account_id = AccountIdentifier { hash }; + let expected_checksum = account_id.generate_checksum(); + + // Check the generated checksum matches + if expected_checksum == found_checksum { + Ok(account_id) + } else { + Err(format!( + "Checksum failed for {}, expected check bytes {} but found {}", + hex::encode(&hex[..]), + hex::encode(expected_checksum), + hex::encode(found_checksum), + )) + } +} + +impl CandidType for AccountIdentifier { + // The type expected for account identifier is + fn _ty() -> candid::types::Type { + String::_ty() + } + + fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> + where + S: candid::types::Serializer, + { + self.to_hex().idl_serialize(serializer) + } +} + +/// Subaccounts are arbitrary 32-byte values. +#[derive(CandidType, Deserialize, Clone, Hash, Debug, PartialEq, Eq, Copy)] +#[serde(transparent)] +pub struct Subaccount(pub [u8; 32]); + +impl Subaccount { + #[allow(dead_code)] + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + pub fn new_from_canister_id( + canister_id: &Principal, + ) -> Result { + Subaccount::try_from(canister_id.as_slice()) + } +} + +impl From<&Principal> for Subaccount { + fn from(principal_id: &Principal) -> Self { + let mut subaccount = [0; std::mem::size_of::()]; + let principal_id = principal_id.as_slice(); + subaccount[0] = principal_id.len().try_into().unwrap(); + subaccount[1..1 + principal_id.len()].copy_from_slice(principal_id); + Subaccount(subaccount) + } +} + +impl TryFrom<&[u8]> for Subaccount { + type Error = std::array::TryFromSliceError; + + fn try_from(slice: &[u8]) -> Result { + slice.try_into().map(Subaccount) + } +} diff --git a/src/dfx/src/lib/nns_types/icpts.rs b/src/dfx/src/lib/nns_types/icpts.rs new file mode 100644 index 0000000000..3afe24142e --- /dev/null +++ b/src/dfx/src/lib/nns_types/icpts.rs @@ -0,0 +1,223 @@ +// DISCLAIMER: +// Do not modify this file arbitrarily. +// The contents are borrowed from: +// dfinity-lab/dfinity@f468897c57a5a0d4785b90c94935255d1a2f7d4c +// https://github.com/dfinity-lab/dfinity/blob/master/rs/rosetta-api/canister/src/icpts.rs + +use candid::CandidType; +use core::ops::{Add, AddAssign, Sub, SubAssign}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +#[derive( + Serialize, + Deserialize, + CandidType, + Clone, + Copy, + Hash, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, +)] +pub struct ICPTs { + /// Number of 10^-8 ICPs. + /// Named because the equivalent part of a Bitcoin is called a Satoshi + e8s: u64, +} + +pub const DECIMAL_PLACES: u32 = 8; +/// How many times can ICPs be divided +pub const ICP_SUBDIVIDABLE_BY: u64 = 100_000_000; + +pub const TRANSACTION_FEE: ICPTs = ICPTs { e8s: 137 }; + +#[allow(dead_code)] +pub const MIN_BURN_AMOUNT: ICPTs = TRANSACTION_FEE; + +#[allow(dead_code)] +impl ICPTs { + /// The maximum value of this construct is 2^64-1 Doms or Roughly 184 + /// Billion ICPTs + pub const MAX: Self = ICPTs { e8s: u64::MAX }; + + /// Construct a new instance of ICPTs. + /// This function will not allow you use more than 1 ICPTs worth of Doms. + pub fn new(icpt: u64, e8s: u64) -> Result { + static CONSTRUCTION_FAILED: &str = + "Constructing ICP failed because the underlying u64 overflowed"; + + let icp_part = icpt + .checked_mul(ICP_SUBDIVIDABLE_BY) + .ok_or_else(|| CONSTRUCTION_FAILED.to_string())?; + if e8s >= ICP_SUBDIVIDABLE_BY { + return Err(format!( + "You've added too many Doms, make sure there are less than {}", + ICP_SUBDIVIDABLE_BY + )); + } + let e8s = icp_part + .checked_add(e8s) + .ok_or_else(|| CONSTRUCTION_FAILED.to_string())?; + Ok(Self { e8s }) + } + + pub const ZERO: Self = ICPTs { e8s: 0 }; + + /// ``` + /// # use ledger_canister::ICPTs; + /// let icpt = ICPTs::from_icpts(12).unwrap(); + /// assert_eq!(icpt.unpack(), (12, 0)) + /// ``` + pub fn from_icpts(icp: u64) -> Result { + Self::new(icp, 0) + } + + /// Construct ICPTs from Doms, 10E8 Doms == 1 ICP + /// ``` + /// # use ledger_canister::ICPTs; + /// let icpt = ICPTs::from_e8s(1200000200); + /// assert_eq!(icpt.unpack(), (12, 200)) + /// ``` + pub const fn from_e8s(e8s: u64) -> Self { + ICPTs { e8s } + } + + /// Gets the total number of whole ICPTs + /// ``` + /// # use ledger_canister::ICPTs; + /// let icpt = ICPTs::new(12, 200).unwrap(); + /// assert_eq!(icpt.get_icpts(), 12) + /// ``` + pub fn get_icpts(self) -> u64 { + self.e8s / ICP_SUBDIVIDABLE_BY + } + + /// Gets the total number of Doms + /// ``` + /// # use ledger_canister::ICPTs; + /// let icpt = ICPTs::new(12, 200).unwrap(); + /// assert_eq!(icpt.get_e8s(), 1200000200) + /// ``` + pub fn get_e8s(self) -> u64 { + self.e8s + } + + /// Gets the total number of Doms not part of a whole ICPT + /// The returned amount is always in the half-open interval [0, 1 ICP). + /// ``` + /// # use ledger_canister::ICPTs; + /// let icpt = ICPTs::new(12, 200).unwrap(); + /// assert_eq!(icpt.get_remainder_e8s(), 200) + /// ``` + pub fn get_remainder_e8s(self) -> u64 { + self.e8s % ICP_SUBDIVIDABLE_BY + } + + /// This returns the number of ICPTs and Doms + /// ``` + /// # use ledger_canister::ICPTs; + /// let icpt = ICPTs::new(12, 200).unwrap(); + /// assert_eq!(icpt.unpack(), (12, 200)) + /// ``` + pub fn unpack(self) -> (u64, u64) { + (self.get_icpts(), self.get_remainder_e8s()) + } +} + +impl Add for ICPTs { + type Output = Result; + + /// This returns a result, in normal operation this should always return Ok + /// because of the cap in the total number of ICP, but when dealing with + /// money it's better to be safe than sorry + fn add(self, other: Self) -> Self::Output { + let e8s = self.e8s.checked_add(other.e8s).ok_or_else(|| { + format!( + "Add ICP {} + {} failed because the underlying u64 overflowed", + self.e8s, other.e8s + ) + })?; + Ok(Self { e8s }) + } +} + +impl AddAssign for ICPTs { + fn add_assign(&mut self, other: Self) { + *self = (*self + other).expect("+= panicked"); + } +} + +impl Sub for ICPTs { + type Output = Result; + + fn sub(self, other: Self) -> Self::Output { + let e8s = self.e8s.checked_sub(other.e8s).ok_or_else(|| { + format!( + "Subtracting ICP {} - {} failed because the underlying u64 underflowed", + self.e8s, other.e8s + ) + })?; + Ok(Self { e8s }) + } +} + +impl SubAssign for ICPTs { + fn sub_assign(&mut self, other: Self) { + *self = (*self - other).expect("-= panicked"); + } +} + +/// ``` +/// # use ledger_canister::ICPTs; +/// let icpt = ICPTs::new(12, 200).unwrap(); +/// let s = format!("{}", icpt); +/// assert_eq!(&s[..], "12.00000200 ICP") +/// ``` +impl fmt::Display for ICPTs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}.{:08} ICP", + self.get_icpts(), + self.get_remainder_e8s() + ) + } +} + +impl FromStr for ICPTs { + type Err = String; + + fn from_str(s: &str) -> Result { + match Decimal::from_str(s) { + Ok(amount) => { + if amount.scale() > DECIMAL_PLACES { + return Err("Doms only go to e8".to_string()); + } + let icpts = match amount.trunc().to_string().parse::() { + Ok(v) => v, + Err(e) => return Err(format!("{}", e)), + }; + let e8s = match amount.fract().to_string().as_str() { + "0" => 0_u64, + e8s => { + let e8s = &e8s.to_string()[2..e8s.to_string().len()]; + let amount = e8s.chars().enumerate().fold(0, |amount, (idx, val)| { + amount + + (10_u64.pow(DECIMAL_PLACES - 1 - (idx as u32)) + * (val.to_digit(10).unwrap() as u64)) + }); + amount as u64 + } + }; + ICPTs::new(icpts, e8s) + } + Err(e) => Err(format!("Decimal conversion error: {}", e)), + } + } +} diff --git a/src/dfx/src/lib/nns_types/mod.rs b/src/dfx/src/lib/nns_types/mod.rs new file mode 100644 index 0000000000..4e554d15f8 --- /dev/null +++ b/src/dfx/src/lib/nns_types/mod.rs @@ -0,0 +1,59 @@ +use candid::CandidType; +use ic_types::principal::Principal; +use serde::{Deserialize, Serialize}; + +pub mod account_identifier; +pub mod icpts; + +pub const CYCLE_MINTER_CANISTER_ID: &str = "rkp4c-7iaaa-aaaaa-aaaca-cai"; +pub const LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; + +#[derive(Deserialize, CandidType)] +pub struct TransactionNotificationResult(pub Vec); + +/// The result of create_canister transaction notification. In case of +/// an error, contains the index of the refund block. +pub type CreateCanisterResult = Result)>; + +/// Position of a block in the chain. The first block has position 0. +pub type BlockHeight = u64; + +#[derive( + Serialize, Deserialize, CandidType, Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, +)] +pub struct Memo(pub u64); + +impl Default for Memo { + fn default() -> Memo { + Memo(0) + } +} + +#[derive(CandidType)] +pub struct AccountBalanceArgs { + pub account: String, +} + +#[derive(CandidType)] +pub struct TimeStamp { + pub timestamp_nanos: u64, +} + +#[derive(CandidType)] +pub struct SendArgs { + pub memo: Memo, + pub amount: icpts::ICPTs, + pub fee: icpts::ICPTs, + pub from_subaccount: Option, + pub to: account_identifier::AccountIdentifier, + pub created_at_time: Option, +} + +#[derive(CandidType)] +pub struct NotifyCanisterArgs { + pub block_height: BlockHeight, + pub max_fee: icpts::ICPTs, + pub from_subaccount: Option, + pub to_canister: Principal, + pub to_subaccount: Option, +} diff --git a/src/dfx/src/util/clap/validators.rs b/src/dfx/src/util/clap/validators.rs index 1961be7997..bae9e82da7 100644 --- a/src/dfx/src/util/clap/validators.rs +++ b/src/dfx/src/util/clap/validators.rs @@ -1,4 +1,6 @@ +use crate::lib::nns_types::icpts::ICPTs; use humanize_rs::bytes::{Bytes, Unit}; +use std::str::FromStr; pub fn is_request_id(v: &str) -> Result<(), String> { // A valid Request Id starts with `0x` and is a series of 64 hexadecimals. @@ -17,6 +19,17 @@ pub fn is_request_id(v: &str) -> Result<(), String> { } } +pub fn icpts_amount_validator(icpts: &str) -> Result<(), String> { + ICPTs::from_str(icpts).map(|_| ()) +} + +pub fn memo_validator(memo: &str) -> Result<(), String> { + if memo.parse::().is_ok() { + return Ok(()); + } + Err("Must be a non negative amount.".to_string()) +} + pub fn cycle_amount_validator(cycles: &str) -> Result<(), String> { if cycles.parse::().is_ok() { return Ok(()); From ad7d876bc3671a9bd056b220dca499cde11c6f90 Mon Sep 17 00:00:00 2001 From: Prithvi Shahi Date: Fri, 30 Apr 2021 10:54:55 -0700 Subject: [PATCH 2/6] update error message --- src/dfx/src/lib/nns_types/icpts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dfx/src/lib/nns_types/icpts.rs b/src/dfx/src/lib/nns_types/icpts.rs index 3afe24142e..d416460be2 100644 --- a/src/dfx/src/lib/nns_types/icpts.rs +++ b/src/dfx/src/lib/nns_types/icpts.rs @@ -197,7 +197,7 @@ impl FromStr for ICPTs { match Decimal::from_str(s) { Ok(amount) => { if amount.scale() > DECIMAL_PLACES { - return Err("Doms only go to e8".to_string()); + return Err("e8s can only be specified to the 8th decimal.".to_string()); } let icpts = match amount.trunc().to_string().parse::() { Ok(v) => v, From 9d8948e2b8876f59e8d11d5b8b054a131e87347a Mon Sep 17 00:00:00 2001 From: Prithvi Shahi Date: Fri, 30 Apr 2021 13:27:43 -0700 Subject: [PATCH 3/6] convert to eprint --- src/dfx/src/commands/ledger/create_canister.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dfx/src/commands/ledger/create_canister.rs b/src/dfx/src/commands/ledger/create_canister.rs index cbb661595b..5b41b83abf 100644 --- a/src/dfx/src/commands/ledger/create_canister.rs +++ b/src/dfx/src/commands/ledger/create_canister.rs @@ -107,7 +107,7 @@ pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult println!("Canister created with id: {:?}", v.to_text()); } Err((msg, maybe_height)) => { - println!("Error: {}\nMaybe BlockHeight:{:?}", msg, maybe_height); + eprintln!("Error: {}\nBlockHeight:{:?}", msg, maybe_height); } }; Ok(()) From cd0634f6ae50d43ab424cc8a4467a1ec6b7a3c62 Mon Sep 17 00:00:00 2001 From: Prithvi Shahi Date: Fri, 30 Apr 2021 19:49:36 -0700 Subject: [PATCH 4/6] update transaction notification result type --- src/dfx/src/commands/ledger/create_canister.rs | 15 +++++++-------- src/dfx/src/lib/nns_types/mod.rs | 12 ++++++------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/dfx/src/commands/ledger/create_canister.rs b/src/dfx/src/commands/ledger/create_canister.rs index 5b41b83abf..9c771b160a 100644 --- a/src/dfx/src/commands/ledger/create_canister.rs +++ b/src/dfx/src/commands/ledger/create_canister.rs @@ -3,8 +3,8 @@ use crate::lib::error::DfxResult; use crate::lib::nns_types::account_identifier::{AccountIdentifier, Subaccount}; use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; use crate::lib::nns_types::{ - BlockHeight, CreateCanisterResult, Memo, NotifyCanisterArgs, SendArgs, - TransactionNotificationResult, CYCLE_MINTER_CANISTER_ID, LEDGER_CANISTER_ID, + BlockHeight, CyclesResponse, Memo, NotifyCanisterArgs, SendArgs, CYCLE_MINTER_CANISTER_ID, + LEDGER_CANISTER_ID, }; use crate::lib::root_key::fetch_root_key_if_needed; use crate::lib::waiter::waiter_with_timeout; @@ -98,17 +98,16 @@ pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult .call_and_wait(waiter_with_timeout(expiry_duration())) .await?; - let result = Decode!(&result, TransactionNotificationResult)?; - - let result = Decode!(&result.0, CreateCanisterResult)?; + let result = Decode!(&result, CyclesResponse)?; match result { - Ok(v) => { + CyclesResponse::CanisterCreated(v) => { println!("Canister created with id: {:?}", v.to_text()); } - Err((msg, maybe_height)) => { - eprintln!("Error: {}\nBlockHeight:{:?}", msg, maybe_height); + CyclesResponse::Refunded(msg, maybe_block_height) => { + println!("Refunded with message: {} at {:?}", msg, maybe_block_height); } + CyclesResponse::ToppedUp(()) => unreachable!(), }; Ok(()) } diff --git a/src/dfx/src/lib/nns_types/mod.rs b/src/dfx/src/lib/nns_types/mod.rs index 4e554d15f8..635d35c849 100644 --- a/src/dfx/src/lib/nns_types/mod.rs +++ b/src/dfx/src/lib/nns_types/mod.rs @@ -8,12 +8,12 @@ pub mod icpts; pub const CYCLE_MINTER_CANISTER_ID: &str = "rkp4c-7iaaa-aaaaa-aaaca-cai"; pub const LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; -#[derive(Deserialize, CandidType)] -pub struct TransactionNotificationResult(pub Vec); - -/// The result of create_canister transaction notification. In case of -/// an error, contains the index of the refund block. -pub type CreateCanisterResult = Result)>; +#[derive(CandidType, Deserialize)] +pub enum CyclesResponse { + CanisterCreated(Principal), + ToppedUp(()), + Refunded(String, Option), +} /// Position of a block in the chain. The first block has position 0. pub type BlockHeight = u64; From d04627d085acf25ac9a27b547552c6f8703889d2 Mon Sep 17 00:00:00 2001 From: Prithvi Shahi Date: Mon, 3 May 2021 19:14:46 -0700 Subject: [PATCH 5/6] top up --- .../src/commands/ledger/create_canister.rs | 76 ++++--------- src/dfx/src/commands/ledger/mod.rs | 104 +++++++++++++++++- src/dfx/src/commands/ledger/top_up.rs | 77 +++++++++++++ .../src/lib/nns_types/account_identifier.rs | 8 +- src/dfx/src/lib/nns_types/icpts.rs | 4 +- src/dfx/src/lib/nns_types/mod.rs | 5 + src/dfx/src/util/clap/validators.rs | 9 +- 7 files changed, 214 insertions(+), 69 deletions(-) create mode 100644 src/dfx/src/commands/ledger/top_up.rs diff --git a/src/dfx/src/commands/ledger/create_canister.rs b/src/dfx/src/commands/ledger/create_canister.rs index 9c771b160a..5ae43b2860 100644 --- a/src/dfx/src/commands/ledger/create_canister.rs +++ b/src/dfx/src/commands/ledger/create_canister.rs @@ -1,34 +1,37 @@ +use crate::commands::ledger::{get_icpts_from_args, send_and_notify}; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::nns_types::account_identifier::{AccountIdentifier, Subaccount}; +use crate::lib::nns_types::account_identifier::Subaccount; use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; -use crate::lib::nns_types::{ - BlockHeight, CyclesResponse, Memo, NotifyCanisterArgs, SendArgs, CYCLE_MINTER_CANISTER_ID, - LEDGER_CANISTER_ID, -}; -use crate::lib::root_key::fetch_root_key_if_needed; -use crate::lib::waiter::waiter_with_timeout; -use crate::util::clap::validators::icpts_amount_validator; -use crate::util::expiry_duration; +use crate::lib::nns_types::{CyclesResponse, Memo}; + +use crate::util::clap::validators::{e8s_validator, icpts_amount_validator}; use anyhow::anyhow; -use candid::{Decode, Encode}; use clap::{ArgSettings, Clap}; use ic_types::principal::Principal; use std::str::FromStr; -const SEND_METHOD: &str = "send_dfx"; -const NOTIFY_METHOD: &str = "notify_dfx"; const MEMO_CREATE_CANISTER: u64 = 1095062083_u64; /// Create a canister from ICP #[derive(Clap)] pub struct CreateCanisterOpts { - /// ICP to account for the fee and the rest to mint as a cycle deposit + /// ICP to mint into cycles and deposit into destination canister + /// Can be specified as a Decimal with the fractional portion up to 8 decimal places + /// i.e. 100.012 #[clap(long, validator(icpts_amount_validator))] - amount: String, + amount: Option, + + /// Specify ICP as a whole number, helpful for use in conjunction with `--e8s` + #[clap(long, validator(e8s_validator), conflicts_with("amount"))] + icp: Option, - /// Transaction fee, default is 137 Doms. + /// Specify e8s as a whole number, helpful for use in conjunction with `--icp` + #[clap(long, validator(e8s_validator), conflicts_with("amount"))] + e8s: Option, + + /// Transaction fee, default is 10000 Doms. #[clap(long, validator(icpts_amount_validator), setting = ArgSettings::Hidden)] fee: Option, @@ -42,7 +45,7 @@ pub struct CreateCanisterOpts { } pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult { - let amount = ICPTs::from_str(&opts.amount).map_err(|err| anyhow!(err))?; + let amount = get_icpts_from_args(opts.amount, opts.icp, opts.e8s)?; let fee = opts.fee.map_or(Ok(TRANSACTION_FEE), |v| { ICPTs::from_str(&v).map_err(|err| anyhow!(err)) @@ -51,34 +54,7 @@ pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult // validated by memo_validator let memo = Memo(MEMO_CREATE_CANISTER); - let agent = env - .get_agent() - .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; - - fetch_root_key_if_needed(env).await?; - - let ledger_canister_id = Principal::from_text(LEDGER_CANISTER_ID)?; - - let cycle_minter_id = Principal::from_text(CYCLE_MINTER_CANISTER_ID)?; - let to_subaccount = Some(Subaccount::from(&Principal::from_text(opts.controller)?)); - let to = AccountIdentifier::new(cycle_minter_id.clone(), to_subaccount); - - let result = agent - .update(&ledger_canister_id, SEND_METHOD) - .with_arg(Encode!(&SendArgs { - memo, - amount, - fee, - from_subaccount: None, - to, - created_at_time: None, - })?) - .call_and_wait(waiter_with_timeout(expiry_duration())) - .await?; - - let block_height = Decode!(&result, BlockHeight)?; - println!("Transfer sent at BlockHeight: {}", block_height); let max_fee = opts .max_fee @@ -86,19 +62,7 @@ pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult ICPTs::from_str(&v).map_err(|err| anyhow!(err)) })?; - let result = agent - .update(&ledger_canister_id, NOTIFY_METHOD) - .with_arg(Encode!(&NotifyCanisterArgs { - block_height, - max_fee, - from_subaccount: None, - to_canister: cycle_minter_id, - to_subaccount, - })?) - .call_and_wait(waiter_with_timeout(expiry_duration())) - .await?; - - let result = Decode!(&result, CyclesResponse)?; + let result = send_and_notify(env, memo, amount, fee, to_subaccount, max_fee).await?; match result { CyclesResponse::CanisterCreated(v) => { diff --git a/src/dfx/src/commands/ledger/mod.rs b/src/dfx/src/commands/ledger/mod.rs index d58f12ca14..045f858fe0 100644 --- a/src/dfx/src/commands/ledger/mod.rs +++ b/src/dfx/src/commands/ledger/mod.rs @@ -1,14 +1,30 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::{AccountIdentifier, Subaccount}; +use crate::lib::nns_types::icpts::ICPTs; +use crate::lib::nns_types::{ + BlockHeight, CyclesResponse, Memo, NotifyCanisterArgs, SendArgs, CYCLE_MINTER_CANISTER_ID, + LEDGER_CANISTER_ID, +}; use crate::lib::provider::create_agent_environment; +use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::waiter::waiter_with_timeout; +use crate::util::expiry_duration; +use anyhow::anyhow; +use candid::{Decode, Encode}; use clap::Clap; +use ic_types::principal::Principal; +use std::str::FromStr; use tokio::runtime::Runtime; +const SEND_METHOD: &str = "send_dfx"; +const NOTIFY_METHOD: &str = "notify_dfx"; + mod account_id; mod balance; mod create_canister; -// mod topup; +mod top_up; mod transfer; /// Ledger commands. @@ -28,7 +44,7 @@ enum SubCommand { AccountId(account_id::AccountIdOpts), Balance(balance::BalanceOpts), CreateCanister(create_canister::CreateCanisterOpts), - // TopUp(topup::TopUpOpts), + TopUp(top_up::TopUpOpts), Transfer(transfer::TransferOpts), } @@ -40,8 +56,90 @@ pub fn exec(env: &dyn Environment, opts: LedgerOpts) -> DfxResult { SubCommand::AccountId(v) => account_id::exec(&agent_env, v).await, SubCommand::Balance(v) => balance::exec(&agent_env, v).await, SubCommand::CreateCanister(v) => create_canister::exec(&agent_env, v).await, - // SubCommand::TopUp(v) => topup::exec(&agent_env, v).await, + SubCommand::TopUp(v) => top_up::exec(&agent_env, v).await, SubCommand::Transfer(v) => transfer::exec(&agent_env, v).await, } }) } + +fn get_icpts_from_args( + amount: Option, + icp: Option, + e8s: Option, +) -> DfxResult { + if amount.is_none() { + let icp = match icp { + Some(s) => { + // validated by e8s_validator + let icps = s.parse::().unwrap(); + ICPTs::from_icpts(icps).map_err(|err| anyhow!(err))? + } + None => ICPTs::from_e8s(0), + }; + let icp_from_e8s = match e8s { + Some(s) => { + // validated by e8s_validator + let e8s = s.parse::().unwrap(); + ICPTs::from_e8s(e8s) + } + None => ICPTs::from_e8s(0), + }; + let amount = icp + icp_from_e8s; + Ok(amount.map_err(|err| anyhow!(err))?) + } else { + Ok(ICPTs::from_str(&amount.unwrap()) + .map_err(|err| anyhow!("Could not add ICPs and e8s: {}", err))?) + } +} + +async fn send_and_notify( + env: &dyn Environment, + memo: Memo, + amount: ICPTs, + fee: ICPTs, + to_subaccount: Option, + max_fee: ICPTs, +) -> DfxResult { + let ledger_canister_id = Principal::from_text(LEDGER_CANISTER_ID)?; + + let cycle_minter_id = Principal::from_text(CYCLE_MINTER_CANISTER_ID)?; + + let agent = env + .get_agent() + .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; + + fetch_root_key_if_needed(env).await?; + + let to = AccountIdentifier::new(cycle_minter_id.clone(), to_subaccount); + + let result = agent + .update(&ledger_canister_id, SEND_METHOD) + .with_arg(Encode!(&SendArgs { + memo, + amount, + fee, + from_subaccount: None, + to, + created_at_time: None, + })?) + .call_and_wait(waiter_with_timeout(expiry_duration())) + .await?; + + let block_height = Decode!(&result, BlockHeight)?; + println!("Transfer sent at BlockHeight: {}", block_height); + + let result = agent + .update(&ledger_canister_id, NOTIFY_METHOD) + .with_arg(Encode!(&NotifyCanisterArgs { + block_height, + max_fee, + from_subaccount: None, + to_canister: cycle_minter_id, + to_subaccount, + })?) + .call_and_wait(waiter_with_timeout(expiry_duration())) + .await?; + + let result = Decode!(&result, CyclesResponse)?; + Ok(result) +} diff --git a/src/dfx/src/commands/ledger/top_up.rs b/src/dfx/src/commands/ledger/top_up.rs new file mode 100644 index 0000000000..262ed33f60 --- /dev/null +++ b/src/dfx/src/commands/ledger/top_up.rs @@ -0,0 +1,77 @@ +use crate::commands::ledger::{get_icpts_from_args, send_and_notify}; +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::Subaccount; +use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; +use crate::lib::nns_types::{CyclesResponse, Memo}; + +use crate::util::clap::validators::{e8s_validator, icpts_amount_validator}; + +use anyhow::anyhow; +use clap::{ArgSettings, Clap}; +use ic_types::principal::Principal; +use std::str::FromStr; + +const MEMO_TOP_UP_CANISTER: u64 = 1347768404_u64; + +/// Top up a canister with cycles minted from ICP +#[derive(Clap)] +pub struct TopUpOpts { + /// ICP to mint into cycles and deposit into destination canister + /// Can be specified as a Decimal with the fractional portion up to 8 decimal places + /// i.e. 100.012 + #[clap(long, validator(icpts_amount_validator))] + amount: Option, + + /// Specify ICP as a whole number, helpful for use in conjunction with `--e8s` + #[clap(long, validator(e8s_validator), conflicts_with("amount"))] + icp: Option, + + /// Specify e8s as a whole number, helpful for use in conjunction with `--icp` + #[clap(long, validator(e8s_validator), conflicts_with("amount"))] + e8s: Option, + + /// Transaction fee, default is 10000 Doms. + #[clap(long, validator(icpts_amount_validator), setting = ArgSettings::Hidden)] + fee: Option, + + /// Specify the canister id to top up + #[clap(long)] + canister: String, + + /// Max fee + #[clap(long, validator(icpts_amount_validator), setting = ArgSettings::Hidden)] + max_fee: Option, +} + +pub async fn exec(env: &dyn Environment, opts: TopUpOpts) -> DfxResult { + let amount = get_icpts_from_args(opts.amount, opts.icp, opts.e8s)?; + + let fee = opts.fee.map_or(Ok(TRANSACTION_FEE), |v| { + ICPTs::from_str(&v).map_err(|err| anyhow!(err)) + })?; + + // validated by memo_validator + let memo = Memo(MEMO_TOP_UP_CANISTER); + + let to_subaccount = Some(Subaccount::from(&Principal::from_text(opts.canister)?)); + + let max_fee = opts + .max_fee + .map_or(ICPTs::new(0, 0).map_err(|err| anyhow!(err)), |v| { + ICPTs::from_str(&v).map_err(|err| anyhow!(err)) + })?; + + let result = send_and_notify(env, memo, amount, fee, to_subaccount, max_fee).await?; + + match result { + CyclesResponse::ToppedUp(()) => { + println!("Canister was topped up!"); + } + CyclesResponse::Refunded(msg, maybe_block_height) => { + println!("Refunded with message: {} at {:?}", msg, maybe_block_height); + } + CyclesResponse::CanisterCreated(_) => unreachable!(), + }; + Ok(()) +} diff --git a/src/dfx/src/lib/nns_types/account_identifier.rs b/src/dfx/src/lib/nns_types/account_identifier.rs index efb96c6cc8..c264074d5c 100644 --- a/src/dfx/src/lib/nns_types/account_identifier.rs +++ b/src/dfx/src/lib/nns_types/account_identifier.rs @@ -1,7 +1,7 @@ // DISCLAIMER: // Do not modify this file arbitrarily. // The contents are borrowed from: -// dfinity-lab/dfinity@f468897c57a5a0d4785b90c94935255d1a2f7d4c +// dfinity-lab/dfinity@25999dd54d29c24edb31483801bddfd8c1d780c8 // https://github.com/dfinity-lab/dfinity/blob/master/rs/rosetta-api/canister/src/account_identifier.rs use candid::CandidType; @@ -162,12 +162,6 @@ impl Subaccount { pub fn to_vec(&self) -> Vec { self.0.to_vec() } - - pub fn new_from_canister_id( - canister_id: &Principal, - ) -> Result { - Subaccount::try_from(canister_id.as_slice()) - } } impl From<&Principal> for Subaccount { diff --git a/src/dfx/src/lib/nns_types/icpts.rs b/src/dfx/src/lib/nns_types/icpts.rs index d416460be2..927a60e186 100644 --- a/src/dfx/src/lib/nns_types/icpts.rs +++ b/src/dfx/src/lib/nns_types/icpts.rs @@ -1,7 +1,7 @@ // DISCLAIMER: // Do not modify this file arbitrarily. // The contents are borrowed from: -// dfinity-lab/dfinity@f468897c57a5a0d4785b90c94935255d1a2f7d4c +// dfinity-lab/dfinity@25999dd54d29c24edb31483801bddfd8c1d780c8 // https://github.com/dfinity-lab/dfinity/blob/master/rs/rosetta-api/canister/src/icpts.rs use candid::CandidType; @@ -35,7 +35,7 @@ pub const DECIMAL_PLACES: u32 = 8; /// How many times can ICPs be divided pub const ICP_SUBDIVIDABLE_BY: u64 = 100_000_000; -pub const TRANSACTION_FEE: ICPTs = ICPTs { e8s: 137 }; +pub const TRANSACTION_FEE: ICPTs = ICPTs { e8s: 10000 }; #[allow(dead_code)] pub const MIN_BURN_AMOUNT: ICPTs = TRANSACTION_FEE; diff --git a/src/dfx/src/lib/nns_types/mod.rs b/src/dfx/src/lib/nns_types/mod.rs index 635d35c849..ab53fcd8a3 100644 --- a/src/dfx/src/lib/nns_types/mod.rs +++ b/src/dfx/src/lib/nns_types/mod.rs @@ -1,3 +1,8 @@ +// DISCLAIMER: +// Do not modify this file arbitrarily. +// The contents are borrowed from: +// dfinity-lab/dfinity@25999dd54d29c24edb31483801bddfd8c1d780c8 + use candid::CandidType; use ic_types::principal::Principal; use serde::{Deserialize, Serialize}; diff --git a/src/dfx/src/util/clap/validators.rs b/src/dfx/src/util/clap/validators.rs index 9dfd70ce3a..a0598e709e 100644 --- a/src/dfx/src/util/clap/validators.rs +++ b/src/dfx/src/util/clap/validators.rs @@ -19,6 +19,13 @@ pub fn is_request_id(v: &str) -> Result<(), String> { } } +pub fn e8s_validator(e8s: &str) -> Result<(), String> { + if e8s.parse::().is_ok() { + return Ok(()); + } + Err("Must specify a non negative whole number.".to_string()) +} + pub fn icpts_amount_validator(icpts: &str) -> Result<(), String> { ICPTs::from_str(icpts).map(|_| ()) } @@ -27,7 +34,7 @@ pub fn memo_validator(memo: &str) -> Result<(), String> { if memo.parse::().is_ok() { return Ok(()); } - Err("Must be a non negative amount.".to_string()) + Err("Must specify a non negative whole number.".to_string()) } pub fn cycle_amount_validator(cycles: &str) -> Result<(), String> { From 479dc849934a8211c2676cd872c97d0bcf0e9b73 Mon Sep 17 00:00:00 2001 From: Prithvi Shahi Date: Mon, 3 May 2021 23:07:06 -0700 Subject: [PATCH 6/6] incorporate review comments --- src/dfx/src/commands/ledger/balance.rs | 7 ++++--- src/dfx/src/commands/ledger/create_canister.rs | 5 ++--- src/dfx/src/commands/ledger/top_up.rs | 5 ++--- src/dfx/src/lib/nns_types/icpts.rs | 16 ++++++++-------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/dfx/src/commands/ledger/balance.rs b/src/dfx/src/commands/ledger/balance.rs index ce1499f213..51a6d1619b 100644 --- a/src/dfx/src/commands/ledger/balance.rs +++ b/src/dfx/src/commands/ledger/balance.rs @@ -26,9 +26,10 @@ pub async fn exec(env: &dyn Environment, opts: BalanceOpts) -> DfxResult { .expect("Selected identity not instantiated."); let acc_id = opts .of - .map_or(Ok(AccountIdentifier::new(sender, None)), |v| { - AccountIdentifier::from_str(&v) - }) + .map_or_else( + || Ok(AccountIdentifier::new(sender, None)), + |v| AccountIdentifier::from_str(&v), + ) .map_err(|err| anyhow!(err))?; let agent = env .get_agent() diff --git a/src/dfx/src/commands/ledger/create_canister.rs b/src/dfx/src/commands/ledger/create_canister.rs index 5ae43b2860..486671ae50 100644 --- a/src/dfx/src/commands/ledger/create_canister.rs +++ b/src/dfx/src/commands/ledger/create_canister.rs @@ -58,9 +58,8 @@ pub async fn exec(env: &dyn Environment, opts: CreateCanisterOpts) -> DfxResult let max_fee = opts .max_fee - .map_or(ICPTs::new(0, 0).map_err(|err| anyhow!(err)), |v| { - ICPTs::from_str(&v).map_err(|err| anyhow!(err)) - })?; + .map_or(ICPTs::new(0, 0), |v| ICPTs::from_str(&v)) + .map_err(|err| anyhow!(err))?; let result = send_and_notify(env, memo, amount, fee, to_subaccount, max_fee).await?; diff --git a/src/dfx/src/commands/ledger/top_up.rs b/src/dfx/src/commands/ledger/top_up.rs index 262ed33f60..f6bac79351 100644 --- a/src/dfx/src/commands/ledger/top_up.rs +++ b/src/dfx/src/commands/ledger/top_up.rs @@ -58,9 +58,8 @@ pub async fn exec(env: &dyn Environment, opts: TopUpOpts) -> DfxResult { let max_fee = opts .max_fee - .map_or(ICPTs::new(0, 0).map_err(|err| anyhow!(err)), |v| { - ICPTs::from_str(&v).map_err(|err| anyhow!(err)) - })?; + .map_or(ICPTs::new(0, 0), |v| ICPTs::from_str(&v)) + .map_err(|err| anyhow!(err))?; let result = send_and_notify(env, memo, amount, fee, to_subaccount, max_fee).await?; diff --git a/src/dfx/src/lib/nns_types/icpts.rs b/src/dfx/src/lib/nns_types/icpts.rs index 927a60e186..7ca03dbfa6 100644 --- a/src/dfx/src/lib/nns_types/icpts.rs +++ b/src/dfx/src/lib/nns_types/icpts.rs @@ -2,7 +2,7 @@ // Do not modify this file arbitrarily. // The contents are borrowed from: // dfinity-lab/dfinity@25999dd54d29c24edb31483801bddfd8c1d780c8 -// https://github.com/dfinity-lab/dfinity/blob/master/rs/rosetta-api/canister/src/icpts.rs +// https://github.com/dfinity-lab/dfinity/blob/master/rs/rosetta-api/ledger_canister/src/icpts.rs use candid::CandidType; use core::ops::{Add, AddAssign, Sub, SubAssign}; @@ -42,12 +42,12 @@ pub const MIN_BURN_AMOUNT: ICPTs = TRANSACTION_FEE; #[allow(dead_code)] impl ICPTs { - /// The maximum value of this construct is 2^64-1 Doms or Roughly 184 + /// The maximum value of this construct is 2^64-1 e8s or Roughly 184 /// Billion ICPTs pub const MAX: Self = ICPTs { e8s: u64::MAX }; /// Construct a new instance of ICPTs. - /// This function will not allow you use more than 1 ICPTs worth of Doms. + /// This function will not allow you use more than 1 ICPTs worth of e8s. pub fn new(icpt: u64, e8s: u64) -> Result { static CONSTRUCTION_FAILED: &str = "Constructing ICP failed because the underlying u64 overflowed"; @@ -57,7 +57,7 @@ impl ICPTs { .ok_or_else(|| CONSTRUCTION_FAILED.to_string())?; if e8s >= ICP_SUBDIVIDABLE_BY { return Err(format!( - "You've added too many Doms, make sure there are less than {}", + "You've added too many e8s, make sure there are less than {}", ICP_SUBDIVIDABLE_BY )); } @@ -78,7 +78,7 @@ impl ICPTs { Self::new(icp, 0) } - /// Construct ICPTs from Doms, 10E8 Doms == 1 ICP + /// Construct ICPTs from e8s, 10E8 e8s == 1 ICP /// ``` /// # use ledger_canister::ICPTs; /// let icpt = ICPTs::from_e8s(1200000200); @@ -98,7 +98,7 @@ impl ICPTs { self.e8s / ICP_SUBDIVIDABLE_BY } - /// Gets the total number of Doms + /// Gets the total number of e8s /// ``` /// # use ledger_canister::ICPTs; /// let icpt = ICPTs::new(12, 200).unwrap(); @@ -108,7 +108,7 @@ impl ICPTs { self.e8s } - /// Gets the total number of Doms not part of a whole ICPT + /// Gets the total number of e8s not part of a whole ICPT /// The returned amount is always in the half-open interval [0, 1 ICP). /// ``` /// # use ledger_canister::ICPTs; @@ -119,7 +119,7 @@ impl ICPTs { self.e8s % ICP_SUBDIVIDABLE_BY } - /// This returns the number of ICPTs and Doms + /// This returns the number of ICPTs and e8s /// ``` /// # use ledger_canister::ICPTs; /// let icpt = ICPTs::new(12, 200).unwrap();