Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
43 changes: 39 additions & 4 deletions crates/icp-cli/src/commands/cycles/balance.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
use icp::context::Context;
use icp::{agent, context::GetAgentError, identity, network};

use crate::commands::token;
use crate::operations::token::balance::{GetBalanceError, get_balance};

#[derive(Debug, thiserror::Error)]
pub(crate) enum CommandError {
#[error(transparent)]
Balance(#[from] token::balance::CommandError),
Project(#[from] icp::LoadError),

#[error(transparent)]
Identity(#[from] identity::LoadError),

#[error(transparent)]
Access(#[from] network::AccessError),

#[error(transparent)]
Agent(#[from] agent::CreateAgentError),

#[error(transparent)]
GetAgent(#[from] GetAgentError),

#[error(transparent)]
GetBalance(#[from] GetBalanceError),
}

pub(crate) async fn exec(
ctx: &Context,
args: &token::balance::BalanceArgs,
) -> Result<(), CommandError> {
token::balance::exec(ctx, "cycles", args)
.await
.map_err(Into::into)
let selections = args.token_command_args.selections();

// Agent
let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;

// Get the balance from the ledger
let balance_info = get_balance(&agent, "cycles").await?;

// Output information
let _ = ctx.term.write_line(&format!(
"Balance: {} {}",
balance_info.amount, balance_info.symbol
));

Ok(())
}
165 changes: 17 additions & 148 deletions crates/icp-cli/src/commands/cycles/mint.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
use bigdecimal::{BigDecimal, ToPrimitive};
use candid::{Decode, Encode};
use bigdecimal::BigDecimal;
use clap::Args;
use ic_agent::AgentError;
use ic_ledger_types::{
AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferError, TransferResult,
};
use icp::{agent, context::GetAgentError, identity, network};
use icp_canister_interfaces::{
cycles_ledger::CYCLES_LEDGER_BLOCK_FEE,
cycles_minting_canister::{
CYCLES_MINTING_CANISTER_PRINCIPAL, ConversionRateResponse, MEMO_MINT_CYCLES,
NotifyMintArgs, NotifyMintErr, NotifyMintResponse,
},
icp_ledger::{ICP_LEDGER_BLOCK_FEE_E8S, ICP_LEDGER_PRINCIPAL},
};

use icp::context::Context;

use crate::commands::args::TokenCommandArgs;
use crate::operations::token::mint::{MintCyclesError, mint_cycles};

#[derive(Debug, Args)]
pub(crate) struct MintArgs {
Expand Down Expand Up @@ -47,38 +35,22 @@ pub(crate) enum CommandError {
#[error(transparent)]
Agent(#[from] agent::CreateAgentError),

#[error("Failed to get identity principal: {message}")]
Principal { message: String },

#[error("Failed to talk to {canister} canister: {source}")]
CanisterError {
canister: String,
source: AgentError,
},

#[error("ICP amount overflow. Specify less tokens.")]
IcpAmountOverflow,

#[error("Failed ICP ledger transfer: {src:?}")]
TransferError { src: TransferError },
#[error(transparent)]
GetAgent(#[from] GetAgentError),

#[error("Insufficient funds: {required} ICP required, {available} ICP available.")]
InsufficientFunds {
required: BigDecimal,
available: BigDecimal,
},
#[error(transparent)]
MintCycles(#[from] MintCyclesError),

#[error("No amount specified. Use --icp or --cycles.")]
NoAmountSpecified,

#[error("Failed to notify mint cycles: {src:?}")]
NotifyMintError { src: NotifyMintErr },

#[error(transparent)]
GetAgent(#[from] GetAgentError),
}

pub(crate) async fn exec(ctx: &Context, args: &MintArgs) -> Result<(), CommandError> {
// Validate args
if args.icp.is_none() && args.cycles.is_none() {
return Err(CommandError::NoAmountSpecified);
}

let selections = args.token_command_args.selections();

// Agent
Expand All @@ -90,116 +62,13 @@ pub(crate) async fn exec(ctx: &Context, args: &MintArgs) -> Result<(), CommandEr
)
.await?;

// Prepare deposit
let user_principal = agent
.get_principal()
.map_err(|e| CommandError::Principal { message: e })?;

let icp_e8s_to_deposit = if let Some(icp_amount) = &args.icp {
(icp_amount * 100_000_000_u64)
.to_u64()
.ok_or(CommandError::IcpAmountOverflow)?
} else if let Some(cycles_amount) = args.cycles {
let cmc_response = agent
.query(
&CYCLES_MINTING_CANISTER_PRINCIPAL,
"get_icp_xdr_conversion_rate",
)
.with_arg(Encode!(&()).expect("Failed to encode get ICP XDR conversion rate args"))
.call()
.await
.map_err(|e| CommandError::CanisterError {
canister: "cmc".to_string(),
source: e,
})?;

let cmc_response =
Decode!(&cmc_response, ConversionRateResponse).expect("CMC response type changed");
let cycles_per_e8s = cmc_response.data.xdr_permyriad_per_icp as u128;
let cycles_plus_fees = cycles_amount + CYCLES_LEDGER_BLOCK_FEE;
let e8s_to_deposit = cycles_plus_fees.div_ceil(cycles_per_e8s);

e8s_to_deposit
.to_u64()
.ok_or(CommandError::IcpAmountOverflow)?
} else {
return Err(CommandError::NoAmountSpecified);
};

let account_id = AccountIdentifier::new(
&CYCLES_MINTING_CANISTER_PRINCIPAL,
&Subaccount::from(user_principal),
);
let memo = Memo(MEMO_MINT_CYCLES);
let transfer_args = TransferArgs {
memo,
amount: Tokens::from_e8s(icp_e8s_to_deposit),
fee: Tokens::from_e8s(ICP_LEDGER_BLOCK_FEE_E8S),
from_subaccount: None,
to: account_id,
created_at_time: None,
};

let transfer_result = agent
.update(&ICP_LEDGER_PRINCIPAL, "transfer")
.with_arg(Encode!(&transfer_args).expect("Failed to encode transfer args"))
.call_and_wait()
.await
.map_err(|e| CommandError::CanisterError {
canister: "ICP ledger".to_string(),
source: e,
})?;
let transfer_response =
Decode!(&transfer_result, TransferResult).expect("ICP ledger transfer result type changed");
let block_index = match transfer_response {
Ok(block_index) => block_index,
Err(err) => match err {
TransferError::TxDuplicate { duplicate_of } => duplicate_of,
TransferError::InsufficientFunds { balance } => {
let required =
BigDecimal::new((icp_e8s_to_deposit + ICP_LEDGER_BLOCK_FEE_E8S).into(), 8);
let available = BigDecimal::new(balance.e8s().into(), 8);
return Err(CommandError::InsufficientFunds {
required,
available,
});
}
err => {
return Err(CommandError::TransferError { src: err });
}
},
};

let notify_response = agent
.update(&CYCLES_MINTING_CANISTER_PRINCIPAL, "notify_mint_cycles")
.with_arg(
Encode!(&NotifyMintArgs {
block_index,
deposit_memo: None,
to_subaccount: None,
})
.expect("Failed to encode notify mint cycles args"),
)
.call_and_wait()
.await
.map_err(|e| CommandError::CanisterError {
canister: "cmc".to_string(),
source: e,
})?;
let notify_response = Decode!(&notify_response, NotifyMintResponse)
.expect("Notify mint cycles response type changed");
let minted = match notify_response {
NotifyMintResponse::Ok(ok) => ok,
NotifyMintResponse::Err(err) => {
return Err(CommandError::NotifyMintError { src: err });
}
};

// display
let deposited = BigDecimal::new((minted.minted - CYCLES_LEDGER_BLOCK_FEE).into(), 12);
let new_balance = BigDecimal::new(minted.balance.into(), 12);
// Execute mint operation
let mint_info = mint_cycles(&agent, args.icp.as_ref(), args.cycles).await?;

// Display results
let _ = ctx.term.write_line(&format!(
"Minted {deposited} TCYCLES to your account, new balance: {new_balance} TCYCLES."
"Minted {} TCYCLES to your account, new balance: {} TCYCLES.",
mint_info.deposited, mint_info.new_balance
));

Ok(())
Expand Down
96 changes: 10 additions & 86 deletions crates/icp-cli/src/commands/token/balance.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use bigdecimal::BigDecimal;
use candid::{Decode, Encode, Nat, Principal};
use clap::Args;
use ic_agent::AgentError;
use icp::{agent, context::GetAgentError, identity, network};
use icrc_ledger_types::icrc1::account::Account;

use icp::context::Context;

use crate::commands::{args::TokenCommandArgs, token::TOKEN_LEDGER_CIDS};
use crate::commands::args::TokenCommandArgs;
use crate::operations::token::balance::{GetBalanceError, get_balance};

#[derive(Args, Clone, Debug)]
pub(crate) struct BalanceArgs {
Expand All @@ -29,17 +26,11 @@ pub(crate) enum CommandError {
#[error(transparent)]
Agent(#[from] agent::CreateAgentError),

#[error("failed to get identity principal: {err}")]
Principal { err: String },

#[error(transparent)]
Query(#[from] AgentError),

#[error(transparent)]
Candid(#[from] candid::Error),
GetAgent(#[from] GetAgentError),

#[error(transparent)]
GetAgent(#[from] GetAgentError),
GetBalance(#[from] GetBalanceError),
}

/// Check the token balance of a given identity
Expand All @@ -63,81 +54,14 @@ pub(crate) async fn exec(
)
.await?;

// Obtain ledger address
let cid = match TOKEN_LEDGER_CIDS.get(token) {
// Given token matched known token names
Some(cid) => cid.to_string(),

// Given token is not known, indicating it's either already a canister id
// or is simply a name of a token we do not know of
None => token.to_string(),
};

// Parse the canister id
let cid = Principal::from_text(cid).map_err(|err| CommandError::Principal {
err: err.to_string(),
})?;

// Perform the required ledger calls
let (balance, decimals, symbol) = tokio::join!(
//
// Obtain token balance
async {
// Convert identity to sender principal
let owner = agent
.get_principal()
.map_err(|err| CommandError::Principal { err })?;

// Specify sub-account
let subaccount = None;

// Perform query
let resp = agent
.query(&cid, "icrc1_balance_of")
.with_arg(Encode!(&Account { owner, subaccount }).expect("failed to encode arg"))
.await?;

// Decode response
Ok::<_, CommandError>(Decode!(&resp, Nat)?)
},
//
// Obtain the number of decimals the token uses
async {
// Perform query
let resp = agent
.query(&cid, "icrc1_decimals")
.with_arg(Encode!(&()).expect("failed to encode arg"))
.await?;

// Decode response
Ok::<_, CommandError>(Decode!(&resp, u8)?)
},
//
// Obtain the symbol of the token
async {
// Perform query
let resp = agent
.query(&cid, "icrc1_symbol")
.with_arg(Encode!(&()).expect("failed to encode arg"))
.await?;

// Decode response
Ok::<_, CommandError>(Decode!(&resp, String)?)
},
);

// Check for errors
let (Nat(balance), decimals, symbol) = (
balance?, //
decimals? as i64, //
symbol?, //
);

// Calculate amount
let amount = BigDecimal::from_biguint(balance, decimals);
// Get the balance from the ledger
let balance_info = get_balance(&agent, token).await?;

// Output information
let _ = ctx.term.write_line(&format!("Balance: {amount} {symbol}"));
let _ = ctx.term.write_line(&format!(
"Balance: {} {}",
balance_info.amount, balance_info.symbol
));

Ok(())
}
Loading