From 058e1b6e1342cab14855cb709609ad97d6868858 Mon Sep 17 00:00:00 2001 From: Yan Liu Date: Sun, 26 Jan 2025 23:29:18 +0100 Subject: [PATCH] refactor: Major cli code refactor --- cli/src/commands/account.rs | 54 ++ cli/src/commands/contract.rs | 281 +++++++ cli/src/commands/keygen.rs | 64 ++ cli/src/commands/ledger.rs | 181 +++++ cli/src/commands/mod.rs | 93 +++ cli/src/commands/np.rs | 160 ++++ cli/src/commands/offering.rs | 25 + cli/src/commands/user.rs | 48 ++ cli/src/contracts/mod.rs | 2 + cli/src/contracts/payment.rs | 66 ++ cli/src/identity/list.rs | 67 ++ cli/src/identity/mod.rs | 31 + cli/src/ledger/data_operations.rs | 165 ++++ cli/src/ledger/metadata.rs | 18 + cli/src/ledger/mod.rs | 57 ++ cli/src/ledger_canister_client.rs | 48 +- cli/src/lib.rs | 1 + cli/src/main.rs | 1164 +---------------------------- cli/src/utils/logger.rs | 10 + cli/src/utils/mod.rs | 4 + cli/src/utils/prompts.rs | 58 ++ common/src/ledger_refresh.rs | 2 +- np-offering/src/lib.rs | 1 - 23 files changed, 1442 insertions(+), 1158 deletions(-) create mode 100644 cli/src/commands/account.rs create mode 100644 cli/src/commands/contract.rs create mode 100644 cli/src/commands/keygen.rs create mode 100644 cli/src/commands/ledger.rs create mode 100644 cli/src/commands/mod.rs create mode 100644 cli/src/commands/np.rs create mode 100644 cli/src/commands/offering.rs create mode 100644 cli/src/commands/user.rs create mode 100644 cli/src/contracts/mod.rs create mode 100644 cli/src/contracts/payment.rs create mode 100644 cli/src/identity/list.rs create mode 100644 cli/src/identity/mod.rs create mode 100644 cli/src/ledger/data_operations.rs create mode 100644 cli/src/ledger/metadata.rs create mode 100644 cli/src/ledger/mod.rs create mode 100644 cli/src/utils/logger.rs create mode 100644 cli/src/utils/mod.rs create mode 100644 cli/src/utils/prompts.rs diff --git a/cli/src/commands/account.rs b/cli/src/commands/account.rs new file mode 100644 index 0000000..d1cee87 --- /dev/null +++ b/cli/src/commands/account.rs @@ -0,0 +1,54 @@ +use crate::argparse::AccountArgs; +use crate::ledger::handle_funds_transfer; +use candid::Principal as IcPrincipal; +use dcc_common::{ + account_balance_get_as_string, DccIdentity, IcrcCompatibleAccount, TokenAmountE9s, + DC_TOKEN_DECIMALS_DIV, +}; +use std::path::PathBuf; + +pub async fn handle_account_command( + account_args: AccountArgs, + network_url: &str, + ledger_canister_id: IcPrincipal, + identity: Option, +) -> Result<(), Box> { + let identity = identity.expect("Identity must be specified for this command, use --identity"); + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + println!("Account Principal ID: {}", dcc_id); + println!( + "Account balance: {} DCT", + account_balance_get_as_string(&dcc_id.as_icrc_compatible_account()) + ); + + if let Some(to_principal_string) = &account_args.transfer_to { + let to_icrc1_account = IcrcCompatibleAccount::from(to_principal_string); + + let transfer_amount_e9s = match &account_args.amount_dct { + Some(value) => { + (value.parse::()? * (DC_TOKEN_DECIMALS_DIV as f64)).round() as TokenAmountE9s + } + None => match &account_args.amount_e9s { + Some(value) => value.parse::()?, + None => { + panic!("You must specify either --amount-dct or --amount-e9s") + } + }, + }; + + println!( + "{}", + handle_funds_transfer( + network_url, + ledger_canister_id, + &dcc_id, + &to_icrc1_account, + transfer_amount_e9s, + ) + .await? + ); + } + + Ok(()) +} diff --git a/cli/src/commands/contract.rs b/cli/src/commands/contract.rs new file mode 100644 index 0000000..1704637 --- /dev/null +++ b/cli/src/commands/contract.rs @@ -0,0 +1,281 @@ +use crate::argparse::ContractCommands; +use crate::contracts::prompt_for_payment_entries; +use crate::utils::prompts::{prompt_bool, prompt_editor, prompt_input}; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use dcc_common::{ContractSignReply, ContractSignRequest, DccIdentity}; +use decent_cloud::ledger_canister_client::LedgerCanister; +use ledger_map::LedgerMap; +use std::path::PathBuf; + +pub async fn handle_contract_command( + contract_args: ContractCommands, + network_url: &str, + ledger_canister_id: candid::Principal, + identity: Option, + ledger_local: LedgerMap, +) -> Result<(), Box> { + match contract_args { + ContractCommands::ListOpen(_list_open_args) => { + println!("Listing all open contracts..."); + // A user may provide the identity (public key), but doesn't have to + let contracts_open = match identity { + Some(name) => { + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&name)).unwrap(); + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id) + .await?; + canister + .contracts_list_pending(&Some(dcc_id.to_bytes_verifying())) + .await + } + None => { + LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .contracts_list_pending(&None) + .await + } + }; + if contracts_open.is_empty() { + println!("No open contracts"); + } else { + for open_contract in contracts_open { + println!( + "{}", + serde_json::to_string_pretty(&open_contract).unwrap_or_default() + ); + } + } + } + ContractCommands::SignRequest(sign_req_args) => { + println!("Request to sign a contract..."); + loop { + println!(); + let i = sign_req_args.interactive; + let identity = prompt_input("Please enter the identity name", &identity, i, false); + let instance_id = prompt_input( + "Please enter the offering id", + &sign_req_args.offering_id, + i, + false, + ); + let requester_ssh_pubkey = prompt_input( + "Please enter your ssh public key, which will be granted access to the contract", + &sign_req_args.requester_ssh_pubkey, + i, + false, + ); + let requester_contact = prompt_input( + "Enter your contact information (this will be public)", + &sign_req_args.requester_contact, + i, + true, + ); + let provider_pubkey_pem = + sign_req_args + .provider_pubkey_pem + .clone() + .unwrap_or_else(|| { + prompt_editor( + "# Enter the provider's public key below, as a PEM string", + i, + ) + .lines() + .map(|line| { + line.split_once('#') + .map(|line| line.0) + .unwrap_or(line) + .trim() + .to_string() + }) + .filter(|line| !line.is_empty()) + .collect::>() + .join("\n") + }); + let provider_dcc_id = + match DccIdentity::new_verifying_from_pem(&provider_pubkey_pem) { + Ok(ident) => ident, + Err(e) => { + eprintln!("ERROR: Failed to parse provider pubkey: {}", e); + continue; + } + }; + let provider_pubkey_bytes = provider_dcc_id.to_bytes_verifying(); + + // Find the offering with the given id, from the provider + let offerings = dcc_common::offerings::do_get_matching_offerings( + &ledger_local, + &format!("instance_types.id = \"{instance_id}\""), + ) + .into_iter() + .filter(|o| o.0.to_bytes_verifying() == provider_pubkey_bytes) + .collect::>(); + + let offering = match offerings.len() { + 0 => { + eprintln!( + "ERROR: No offering found for the provider {provider_dcc_id} and id: {instance_id}" + ); + continue; + } + 1 => &offerings[0].1, + _ => { + eprintln!("ERROR: Provider {provider_dcc_id} has multiple offerings with id: {instance_id}"); + continue; + } + }; + + let payment_entries = prompt_for_payment_entries( + &sign_req_args.payment_entries_json, + offering, + &instance_id, + ); + + let payment_amount_e9s = payment_entries.iter().map(|e| e.amount_e9s).sum(); + + let memo = prompt_input( + "Please enter a memo for the contract (this will be public)", + &sign_req_args.memo, + i, + true, + ); + + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + let requester_pubkey_bytes = dcc_id.to_bytes_verifying(); + let req = ContractSignRequest::new( + &requester_pubkey_bytes, + requester_ssh_pubkey, + requester_contact, + &provider_pubkey_bytes, + instance_id.clone(), + None, + None, + None, + payment_amount_e9s, + payment_entries, + None, + memo.clone(), + ); + println!("The following contract sign request will be sent:"); + println!("{}", serde_json::to_string_pretty(&req)?); + if dialoguer::Confirm::new() + .with_prompt("Is this correct? If yes, press enter to send.") + .default(false) + .show_default(true) + .interact() + .unwrap() + { + let payload_bytes = borsh::to_vec(&req).unwrap(); + let payload_sig_bytes = dcc_id.sign(&payload_bytes)?.to_bytes(); + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id) + .await?; + + match canister + .contract_sign_request( + &requester_pubkey_bytes, + &payload_bytes, + &payload_sig_bytes, + ) + .await + { + Ok(response) => { + println!("Contract sign request successful: {}", response); + break; + } + Err(e) => { + println!("Contract sign request failed: {}", e); + if dialoguer::Confirm::new() + .with_prompt("Do you want to retry?") + .default(true) + .show_default(true) + .interact() + .unwrap() + { + continue; + } else { + break; + } + } + } + } else { + println!("Contract sign request canceled."); + break; + } + } + } + ContractCommands::SignReply(sign_reply_args) => { + println!("Reply to a contract-sign request..."); + loop { + let i = sign_reply_args.interactive; + let identity = prompt_input("Please enter the identity name", &identity, i, false); + let contract_id = prompt_input( + "Please enter the contract id, as a base64 encoded string", + &sign_reply_args.contract_id, + i, + false, + ); + let accept = prompt_bool( + "Do you accept the contract?", + sign_reply_args.sign_accept, + i, + ); + let response_text = prompt_input( + "Please enter a response text for the contract (this will be public)", + &sign_reply_args.response_text, + i, + true, + ); + let response_details = prompt_input( + "Please enter a response details for the contract (this will be public)", + &sign_reply_args.response_details, + i, + true, + ); + + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity)).unwrap(); + let provider_pubkey_bytes = dcc_id.to_bytes_verifying(); + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id) + .await?; + + let contracts_open = canister + .contracts_list_pending(&Some(provider_pubkey_bytes.clone())) + .await; + let open_contract = contracts_open + .iter() + .find(|c| c.contract_id_base64 == contract_id) + .expect("Provided contract id not found"); + + let contract_id_bytes = BASE64.decode(contract_id.as_bytes()).unwrap(); + + let reply = ContractSignReply::new( + open_contract.contract_req.requester_pubkey_bytes().to_vec(), + open_contract.contract_req.request_memo(), + contract_id_bytes, + accept, + &response_text, + &response_details, + ); + + let payload_bytes = borsh::to_vec(&reply).unwrap(); + let signature = dcc_id.sign(&payload_bytes)?.to_vec(); + + match canister + .contract_sign_reply(&provider_pubkey_bytes, &payload_bytes, &signature) + .await + { + Ok(response) => { + println!("Contract sign reply sent successfully: {}", response); + break; + } + Err(e) => { + println!("Error sending contract sign reply: {:?}", e); + } + } + } + } + } + Ok(()) +} diff --git a/cli/src/commands/keygen.rs b/cli/src/commands/keygen.rs new file mode 100644 index 0000000..faa1bf5 --- /dev/null +++ b/cli/src/commands/keygen.rs @@ -0,0 +1,64 @@ +use bip39::{Language, Mnemonic, MnemonicType}; +use dcc_common::DccIdentity; +use log::info; +use std::io::{BufRead, BufReader, Read, Write}; + +use crate::argparse::KeygenArgs; + +pub async fn handle_keygen_command( + keygen_args: KeygenArgs, + identity: Option, +) -> Result<(), Box> { + let identity = identity.expect("Identity must be specified for this command, use --identity"); + let mnemonic = if keygen_args.generate { + let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); + info!("Generated mnemonic: {}", mnemonic); + mnemonic + } else if keygen_args.mnemonic.is_some() { + let mnemonic_string = keygen_args + .mnemonic + .clone() + .unwrap_or_default() + .split_whitespace() + .map(String::from) + .collect::>(); + if mnemonic_string.len() < 12 { + let reader = BufReader::new(std::io::stdin()); + mnemonic_from_stdin(reader, std::io::stdout())? + } else { + mnemonic_from_strings(mnemonic_string)? + } + } else { + panic!("Neither mnemonic nor generate specified"); + }; + + let seed = bip39::Seed::new(&mnemonic, ""); + let dcc_identity = DccIdentity::new_from_seed(seed.as_bytes())?; + info!("Generated identity: {}", dcc_identity); + dcc_identity.save_to_dir(&identity)?; + Ok(()) +} + +pub fn mnemonic_from_strings(words: Vec) -> Result> { + let mnemonic_string = words.join(" "); + Ok(Mnemonic::from_phrase(&mnemonic_string, Language::English)?) +} + +pub fn mnemonic_from_stdin( + mut reader: BufReader, + mut writer: W, +) -> Result> { + let mut words = Vec::new(); + writeln!( + writer, + "Please enter your mnemonic phrase (12 words, one per line):" + )?; + for i in 0..12 { + write!(writer, "Word {}: ", i + 1)?; + writer.flush()?; + let mut word = String::new(); + reader.read_line(&mut word)?; + words.push(word.trim().to_string()); + } + mnemonic_from_strings(words) +} diff --git a/cli/src/commands/ledger.rs b/cli/src/commands/ledger.rs new file mode 100644 index 0000000..7d874da --- /dev/null +++ b/cli/src/commands/ledger.rs @@ -0,0 +1,181 @@ +use crate::argparse::{LedgerLocalArgs, LedgerRemoteCommands}; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use candid::{Decode, Encode}; +use chrono::DateTime; +use dcc_common::{DccIdentity, FundsTransfer, LABEL_DC_TOKEN_TRANSFER}; +use decent_cloud::ledger_canister_client::LedgerCanister; +use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use ledger_map::LedgerMap; +use log::Level; +use std::path::PathBuf; +use tabular::{Row, Table}; + +pub async fn handle_ledger_local_command( + local_args: LedgerLocalArgs, + ledger_local: LedgerMap, +) -> Result<(), Box> { + if local_args.list_entries { + println!("Entries:"); + for entry in ledger_local.iter(None) { + match entry.label() { + LABEL_DC_TOKEN_TRANSFER => { + let transfer_id = BASE64.encode(entry.key()); + let transfer: FundsTransfer = + borsh::from_slice(entry.value()).map_err(|e| e.to_string())?; + println!("[DCTokenTransfer] TransferId {}: {}", transfer_id, transfer); + } + _ => println!("{}", entry), + }; + } + } else if local_args.list_entries_raw { + println!("Raw Entries:"); + for entry in ledger_local.iter_raw() { + let (blk_header, ledger_block) = entry?; + println!("{}", blk_header); + println!("{}", ledger_block) + } + } + Ok(()) +} + +pub async fn handle_ledger_remote_command( + subcmd: LedgerRemoteCommands, + network_url: &str, + ledger_canister_id: candid::Principal, + identity: Option, + ledger_local: LedgerMap, +) -> Result<(), Box> { + let local_ledger_path = ledger_local + .get_file_path() + .expect("Failed to get local ledger path"); + + match subcmd { + LedgerRemoteCommands::DataFetch => { + let canister = + LedgerCanister::new_without_identity(network_url, ledger_canister_id).await?; + return crate::ledger::ledger_data_fetch(&canister, &ledger_local).await; + } + LedgerRemoteCommands::DataPushAuthorize | LedgerRemoteCommands::DataPush => { + let identity = + identity.expect("Identity must be specified for this command, use --identity"); + + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + let push_auth = subcmd == LedgerRemoteCommands::DataPushAuthorize; + + if push_auth { + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id) + .await?; + let args = Encode!(&()).map_err(|e| e.to_string())?; + let result = canister.call_update("data_push_auth", &args).await?; + let response = + Decode!(&result, Result).map_err(|e| e.to_string())??; + + println!("Push auth: {}", response); + } + + // After authorizing, we can push the data + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id).await?; + + return crate::ledger::ledger_data_push(&canister, local_ledger_path).await; + } + LedgerRemoteCommands::Metadata => { + let canister = + LedgerCanister::new_without_identity(network_url, ledger_canister_id).await?; + + #[allow(clippy::literal_string_with_formatting_args)] + let mut table = Table::new("{:<} {:<}") + .with_row(Row::from_cells(["Key", "Value"].iter().cloned())); + + for md_entry in crate::ledger::get_ledger_metadata(&canister).await { + let md_entry_val = match md_entry.1 { + MetadataValue::Nat(v) => v.to_string(), + MetadataValue::Int(v) => v.to_string(), + MetadataValue::Text(v) => v.to_string(), + MetadataValue::Blob(v) => hex::encode(v), + }; + table.add_row(Row::new().with_cell(md_entry.0).with_cell(md_entry_val)); + } + print!("{}", table); + } + LedgerRemoteCommands::GetRegistrationFee => { + let canister = + LedgerCanister::new_without_identity(network_url, ledger_canister_id).await?; + let noargs = Encode!(&()).expect("Failed to encode args"); + let response = canister.call_query("get_registration_fee", &noargs).await?; + let fee_e9s = Decode!(response.as_slice(), u64).map_err(|e| e.to_string())?; + println!( + "Registration fee: {}", + dcc_common::amount_as_string(fee_e9s) + ); + } + LedgerRemoteCommands::GetCheckInNonce => { + let nonce_bytes = LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .get_check_in_nonce() + .await; + println!("{}", hex::encode(nonce_bytes)); + } + LedgerRemoteCommands::GetLogsDebug => { + println!("Ledger canister DEBUG logs:"); + print_logs( + Level::Debug, + &LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .get_logs_debug() + .await?, + )?; + } + LedgerRemoteCommands::GetLogsInfo => { + println!("Ledger canister INFO logs:"); + print_logs( + Level::Info, + &LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .get_logs_info() + .await?, + )?; + } + LedgerRemoteCommands::GetLogsWarn => { + println!("Ledger canister WARN logs:"); + print_logs( + Level::Warn, + &LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .get_logs_warn() + .await?, + )?; + } + LedgerRemoteCommands::GetLogsError => { + println!("Ledger canister ERROR logs:"); + print_logs( + Level::Error, + &LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .get_logs_error() + .await?, + )?; + } + } + + Ok(()) +} + +fn print_logs(log_level: Level, logs_json: &str) -> Result<(), Box> { + for entry in serde_json::from_str::>(logs_json)?.into_iter() { + let timestamp_ns = entry["timestamp"].as_u64().unwrap_or_default(); + let timestamp_s = (timestamp_ns / 1_000_000_000) as i64; + // Create DateTime from the timestamp + let dt = DateTime::from_timestamp(timestamp_s, 0).unwrap_or_default(); + println!( + "{} [{}] - {}", + dt.format("%Y-%m-%dT%H:%M:%S"), + log_level, + entry["message"].as_str().expect("Invalid message field") + ); + } + Ok(()) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs new file mode 100644 index 0000000..539b6c5 --- /dev/null +++ b/cli/src/commands/mod.rs @@ -0,0 +1,93 @@ +mod account; +mod contract; +mod keygen; +mod ledger; +mod np; +mod offering; +mod user; + +use crate::argparse::{Cli, Commands}; +pub use account::handle_account_command; +use candid::Principal as IcPrincipal; +use clap::Parser; +pub use contract::handle_contract_command; +pub use keygen::handle_keygen_command; +pub use ledger::{handle_ledger_local_command, handle_ledger_remote_command}; +use ledger_map::LedgerMap; +pub use np::handle_np_command; +pub use offering::handle_offering_command; +use std::error::Error; +pub use user::handle_user_command; + +pub async fn handle_command( + command: Commands, + ledger_local: LedgerMap, +) -> Result<(), Box> { + let cli = Cli::parse(); + let network: String = cli.network.unwrap_or_else(|| "ic".to_string()); + + let network_url = match network.as_str() { + "local" => "http://127.0.0.1:8000", + "mainnet-eu" | "mainnet-01" | "mainnet-02" | "ic" => "https://icp-api.io", + _ => panic!("unknown network: {}", network), + }; + + let ledger_canister_id = match network.as_str() { + "local" => IcPrincipal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai")?, + "mainnet-eu" => IcPrincipal::from_text("tlvs5-oqaaa-aaaas-aaabq-cai")?, + "mainnet-01" | "ic" => IcPrincipal::from_text("ggi4a-wyaaa-aaaai-actqq-cai")?, + "mainnet-02" => IcPrincipal::from_text("gplx4-aqaaa-aaaai-actra-cai")?, + _ => panic!("unknown network: {}", network), + }; + + let identity = cli.identity; + + match command { + Commands::Keygen(args) => handle_keygen_command(args, identity).await, + Commands::Account(args) => { + handle_account_command(args, network_url, ledger_canister_id, identity).await + } + Commands::Np(args) => { + handle_np_command( + args, + network_url, + ledger_canister_id, + identity, + ledger_local, + ) + .await + } + Commands::User(args) => { + handle_user_command( + args, + network_url, + ledger_canister_id, + identity, + ledger_local, + ) + .await + } + Commands::LedgerLocal(args) => handle_ledger_local_command(args, ledger_local).await, + Commands::LedgerRemote(args) => { + handle_ledger_remote_command( + args, + network_url, + ledger_canister_id, + identity, + ledger_local, + ) + .await + } + Commands::Offering(args) => handle_offering_command(args, ledger_local).await, + Commands::Contract(args) => { + handle_contract_command( + args, + network_url, + ledger_canister_id, + identity, + ledger_local, + ) + .await + } + } +} diff --git a/cli/src/commands/np.rs b/cli/src/commands/np.rs new file mode 100644 index 0000000..cab1de2 --- /dev/null +++ b/cli/src/commands/np.rs @@ -0,0 +1,160 @@ +use crate::argparse::NpCommands; +use crate::identity::{list_identities, list_local_identities, ListIdentityType}; +use dcc_common::DccIdentity; +use decent_cloud::ledger_canister_client::LedgerCanister; +use ledger_map::LedgerMap; +use log::info; +use np_offering::Offering; +use std::{path::PathBuf, time::SystemTime}; + +use crate::ledger::ledger_data_fetch; + +pub async fn handle_np_command( + np_cmd: NpCommands, + network_url: &str, + ledger_canister_id: candid::Principal, + identity: Option, + ledger_local: LedgerMap, +) -> Result<(), Box> { + match np_cmd { + NpCommands::List(list_args) => { + if list_args.only_local { + list_local_identities(list_args.balances)? + } else { + list_identities( + &ledger_local, + ListIdentityType::Providers, + list_args.balances, + )? + } + } + NpCommands::Register => { + let identity = + identity.expect("Identity must be specified for this command, use --identity"); + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + info!("Registering principal: {} as {}", identity, dcc_id); + let pubkey_bytes = dcc_id.to_bytes_verifying(); + let pubkey_signature = dcc_id.sign(pubkey_bytes.as_ref())?; + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id).await?; + let result = canister + .node_provider_register(&pubkey_bytes, pubkey_signature.to_bytes().as_slice()) + .await?; + println!("Register: {}", result); + } + NpCommands::CheckIn(check_in_args) => { + if check_in_args.only_nonce { + let nonce_bytes = + LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await? + .get_check_in_nonce() + .await; + let nonce_string = hex::encode(&nonce_bytes); + + println!("0x{}", nonce_string); + } else { + let identity = + identity.expect("Identity must be specified for this command, use --identity"); + + let dcc_ident = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + // Check the local ledger timestamp + let local_ledger_path = ledger_local + .get_file_path() + .expect("Failed to get local ledger path"); + let local_ledger_file_mtime = local_ledger_path.metadata()?.modified()?; + + // If the local ledger is older than 1 minute, refresh it automatically before proceeding + // If needed, the local ledger can also be refreshed manually from the command line + if local_ledger_file_mtime < SystemTime::now() - std::time::Duration::from_secs(10) + { + info!("Local ledger is older than 1 minute, refreshing..."); + let canister = + LedgerCanister::new_without_identity(network_url, ledger_canister_id) + .await?; + ledger_data_fetch(&canister, &ledger_local).await?; + + dcc_common::refresh_caches_from_ledger(&ledger_local) + .expect("Loading balances from ledger failed"); + } + // The local ledger needs to be refreshed to get the latest nonce + // This provides the incentive to clone and frequently re-fetch the ledger + let nonce_bytes = ledger_local.get_latest_block_hash(); + let nonce_string = hex::encode(&nonce_bytes); + + info!( + "Checking-in provider identity {} ({}), using nonce: {} ({} bytes)", + identity, + dcc_ident, + nonce_string, + nonce_bytes.len() + ); + let check_in_memo = check_in_args.memo.clone().unwrap_or_else(|| { + println!( + "No memo specified, did you know that you can specify one? Try out --memo" + ); + String::new() + }); + let nonce_crypto_signature = dcc_ident.sign(nonce_bytes.as_ref())?; + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_ident) + .await?; + let result = canister + .node_provider_check_in( + &dcc_ident.to_bytes_verifying(), + &check_in_memo, + &nonce_crypto_signature.to_bytes(), + ) + .await + .map_err(|e| format!("Check-in failed: {}", e))?; + info!("Check-in success: {}", result); + } + } + NpCommands::UpdateProfile(update_profile_args) => { + let identity = + identity.expect("Identity must be specified for this command, use --identity"); + + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + let np_profile = np_profile::Profile::new_from_file(&update_profile_args.profile_file)?; + let np_profile_bytes = borsh::to_vec(&np_profile)?; + let crypto_signature = dcc_id.sign(&np_profile_bytes)?; + + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id).await?; + let result = canister + .node_provider_update_profile( + &dcc_id.to_bytes_verifying(), + &np_profile_bytes, + &crypto_signature.to_bytes(), + ) + .await + .map_err(|e| format!("Update profile failed: {}", e))?; + info!("Profile update response: {}", result); + } + NpCommands::UpdateOffering(update_offering_args) => { + let identity = + identity.expect("Identity must be specified for this command, use --identity"); + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + // Offering::new_from_file returns an error if the schema validation fails + let np_offering = Offering::new_from_file(&update_offering_args.offering_file)?; + let np_offering_bytes = np_offering.serialize()?; + let crypto_signature = dcc_id.sign(&np_offering_bytes)?; + + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id).await?; + let result = canister + .node_provider_update_offering( + &dcc_id.to_bytes_verifying(), + &np_offering_bytes, + &crypto_signature.to_bytes(), + ) + .await + .map_err(|e| format!("Update offering failed: {}", e))?; + info!("Offering update response: {}", result); + } + } + Ok(()) +} diff --git a/cli/src/commands/offering.rs b/cli/src/commands/offering.rs new file mode 100644 index 0000000..2504553 --- /dev/null +++ b/cli/src/commands/offering.rs @@ -0,0 +1,25 @@ +use crate::argparse::OfferingCommands; +use dcc_common::offerings::do_get_matching_offerings; +use ledger_map::LedgerMap; + +pub async fn handle_offering_command( + cmd: OfferingCommands, + ledger_local: LedgerMap, +) -> Result<(), Box> { + let query = match cmd { + OfferingCommands::List => "", + OfferingCommands::Query(query_args) => &query_args.query.clone(), + }; + + let offerings = do_get_matching_offerings(&ledger_local, query); + println!("Found {} matching offerings:", offerings.len()); + for (dcc_id, offering) in offerings { + println!( + "{} ==>\n{}", + dcc_id.display_as_ic_and_pem_one_line(), + &offering.as_json_string_pretty().unwrap_or_default() + ); + } + + Ok(()) +} diff --git a/cli/src/commands/user.rs b/cli/src/commands/user.rs new file mode 100644 index 0000000..8db1213 --- /dev/null +++ b/cli/src/commands/user.rs @@ -0,0 +1,48 @@ +use crate::argparse::UserCommands; +use crate::identity::{list_identities, list_local_identities, ListIdentityType}; +use candid::{Decode, Encode}; +use dcc_common::DccIdentity; +use decent_cloud::ledger_canister_client::LedgerCanister; +use ledger_map::LedgerMap; +use std::path::PathBuf; + +pub async fn handle_user_command( + user_cmd: UserCommands, + network_url: &str, + ledger_canister_id: candid::Principal, + identity: Option, + ledger_local: LedgerMap, +) -> Result<(), Box> { + match user_cmd { + UserCommands::List(list_args) => { + if list_args.only_local { + list_local_identities(list_args.balances)? + } else { + list_identities(&ledger_local, ListIdentityType::Users, list_args.balances)? + } + } + UserCommands::Register => { + let identity = + identity.expect("Identity must be specified for this command, use --identity"); + let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; + + let canister = + LedgerCanister::new_with_dcc_id(network_url, ledger_canister_id, &dcc_id).await?; + let pubkey_bytes = dcc_id.to_bytes_verifying(); + let pubkey_signature = dcc_id.sign(&pubkey_bytes)?; + let args = Encode!(&pubkey_bytes, &pubkey_signature.to_bytes())?; + let result = canister.call_update("user_register", &args).await?.to_vec(); + let response = Decode!(&result, Result).map_err(|e| e.to_string())?; + + match response { + Ok(response) => { + println!("Registration successful: {}", response); + } + Err(e) => { + println!("Registration failed: {}", e); + } + } + } + } + Ok(()) +} diff --git a/cli/src/contracts/mod.rs b/cli/src/contracts/mod.rs new file mode 100644 index 0000000..ca9c175 --- /dev/null +++ b/cli/src/contracts/mod.rs @@ -0,0 +1,2 @@ +mod payment; +pub use payment::prompt_for_payment_entries; diff --git a/cli/src/contracts/payment.rs b/cli/src/contracts/payment.rs new file mode 100644 index 0000000..89c3a79 --- /dev/null +++ b/cli/src/contracts/payment.rs @@ -0,0 +1,66 @@ +use dcc_common::{PaymentEntries, PaymentEntry, PaymentEntryWithAmount, TokenAmountE9s}; +use np_offering::Offering; +use std::collections::HashMap; + +pub fn prompt_for_payment_entries( + payment_entries_json: &Option, + offering: &Offering, + instance_id: &str, +) -> Vec { + let pricing: HashMap> = offering.instance_pricing(instance_id); + + let get_total_price = |model: &str, time_period_unit: &str, quantity: u64| -> TokenAmountE9s { + pricing + .get(model) + .and_then(|units| units.get(time_period_unit)) + .map(|amount| { + amount + .replace("_", "") + .parse::() + .expect("Failed to parse the offering price as TokenAmountE9s") + * quantity + }) + .unwrap() + }; + let mut payment_entries: Vec<_> = payment_entries_json + .clone() + .map(|entries| { + entries + .0 + .into_iter() + .map(|e| PaymentEntryWithAmount { + e: e.clone(), + amount_e9s: get_total_price(&e.pricing_model, &e.time_period_unit, e.quantity), + }) + .collect() + }) + .unwrap_or_default(); + + if payment_entries.is_empty() { + let models = pricing.keys().collect::>(); + let model = models[dialoguer::Select::new() + .with_prompt("Please select instance pricing model (ESC to exit)") + .items(&models) + .default(0) + .interact() + .expect("Failed to read input")]; + let units = pricing[model].keys().collect::>(); + let time_period_unit = units[dialoguer::Select::new() + .with_prompt("Please select time period unit") + .items(&units) + .report(true) + .default(0) + .interact() + .expect("Failed to read input")]; + let quantity = dialoguer::Input::::new() + .with_prompt("Please enter the number of units") + .default(1) + .interact() + .expect("Failed to read input"); + payment_entries.push(PaymentEntryWithAmount { + e: PaymentEntry::new(model, time_period_unit, quantity), + amount_e9s: get_total_price(model, time_period_unit, quantity), + }); + } + payment_entries +} diff --git a/cli/src/identity/list.rs b/cli/src/identity/list.rs new file mode 100644 index 0000000..87584f5 --- /dev/null +++ b/cli/src/identity/list.rs @@ -0,0 +1,67 @@ +use dcc_common::{DccIdentity, LABEL_NP_REGISTER, LABEL_USER_REGISTER}; +use ledger_map::LedgerMap; + +use super::println_identity; + +#[derive(PartialEq)] +pub enum ListIdentityType { + Providers, + Users, + All, +} + +pub fn list_local_identities(include_balances: bool) -> Result<(), Box> { + let identities_dir = DccIdentity::identities_dir(); + println!("Available identities at {}:", identities_dir.display()); + let mut identities: Vec<_> = fs_err::read_dir(identities_dir)? + .filter_map(|entry| match entry { + Ok(entry) => Some(entry), + Err(e) => { + eprintln!("Failed to read identity: {}", e); + None + } + }) + .collect(); + + identities.sort_by_key(|identity| identity.file_name()); + + for identity in identities { + let path = identity.path(); + if path.is_dir() { + let identity_name = identity.file_name(); + let identity_name = identity_name.to_string_lossy(); + match DccIdentity::load_from_dir(&path) { + Ok(dcc_identity) => { + print!("{} => ", identity_name); + println_identity(&dcc_identity, include_balances); + } + Err(e) => { + println!("{} => Error: {}", identity_name, e); + } + } + } + } + Ok(()) +} + +pub fn list_identities( + ledger: &LedgerMap, + identity_type: ListIdentityType, + show_balances: bool, +) -> Result<(), Box> { + if identity_type == ListIdentityType::Providers || identity_type == ListIdentityType::All { + println!("\n# Registered providers"); + for entry in ledger.iter(Some(LABEL_NP_REGISTER)) { + let dcc_id = DccIdentity::new_verifying_from_bytes(entry.key()).unwrap(); + println_identity(&dcc_id, show_balances); + } + } + if identity_type == ListIdentityType::Users || identity_type == ListIdentityType::All { + println!("\n# Registered users"); + for entry in ledger.iter(Some(LABEL_USER_REGISTER)) { + let dcc_id = DccIdentity::new_verifying_from_bytes(entry.key()).unwrap(); + println_identity(&dcc_id, show_balances); + } + } + Ok(()) +} diff --git a/cli/src/identity/mod.rs b/cli/src/identity/mod.rs new file mode 100644 index 0000000..38e789d --- /dev/null +++ b/cli/src/identity/mod.rs @@ -0,0 +1,31 @@ +use dcc_common::{account_balance_get_as_string, reputation_get, DccIdentity}; +use ic_agent::identity::BasicIdentity; + +mod list; +pub use list::{list_identities, list_local_identities, ListIdentityType}; + +pub fn println_identity(dcc_id: &DccIdentity, show_balance: bool) { + if show_balance { + println!( + "{}, reputation {}, balance {}", + dcc_id.display_as_ic_and_pem_one_line(), + reputation_get(dcc_id.to_bytes_verifying()), + account_balance_get_as_string(&dcc_id.as_icrc_compatible_account()) + ); + } else { + println!( + "{} reputation {}", + dcc_id.display_as_ic_and_pem_one_line(), + reputation_get(dcc_id.to_bytes_verifying()) + ); + } +} + +pub fn dcc_to_ic_auth(dcc_identity: &DccIdentity) -> Option { + dcc_identity + .signing_key_as_ic_agent_pem_string() + .map(|pem_key| { + let cursor = std::io::Cursor::new(pem_key.as_bytes()); + BasicIdentity::from_pem(cursor).expect("failed to parse pem key") + }) +} diff --git a/cli/src/ledger/data_operations.rs b/cli/src/ledger/data_operations.rs new file mode 100644 index 0000000..f88ecbc --- /dev/null +++ b/cli/src/ledger/data_operations.rs @@ -0,0 +1,165 @@ +use candid::{Decode, Encode}; +use dcc_common::DATA_PULL_BYTES_BEFORE_LEN; +use dcc_common::{cursor_from_data, CursorDirection, LedgerCursor}; +use decent_cloud::ledger_canister_client::LedgerCanister; +use fs_err::OpenOptions; +use ledger_map::{platform_specific::persistent_storage_read, LedgerMap}; +use log::info; +use std::{ + io::{Seek, Write}, + path::PathBuf, +}; + +use super::get_ledger_metadata; + +const PUSH_BLOCK_SIZE: u64 = 1024 * 1024; + +pub async fn ledger_data_fetch( + ledger_canister: &LedgerCanister, + ledger_local: &LedgerMap, +) -> Result<(), Box> { + let ledger_file_path = ledger_local + .get_file_path() + .expect("failed to open the local ledger path"); + + let cursor_local = { + cursor_from_data( + ledger_map::partition_table::get_data_partition().start_lba, + ledger_map::platform_specific::persistent_storage_size_bytes(), + ledger_local.get_next_block_start_pos(), + ledger_local.get_next_block_start_pos(), + ) + }; + + let bytes_before = if cursor_local.position > DATA_PULL_BYTES_BEFORE_LEN as u64 { + let mut buf = vec![0u8; DATA_PULL_BYTES_BEFORE_LEN as usize]; + persistent_storage_read( + cursor_local.position - DATA_PULL_BYTES_BEFORE_LEN as u64, + &mut buf, + )?; + Some(buf) + } else { + None + }; + + info!( + "Fetching data from the Ledger canister {} to {}, with local cursor: {} and bytes before: {:?}", + ledger_canister.canister_id(), + ledger_file_path.display(), + cursor_local, + hex::encode(bytes_before.as_ref().unwrap_or(&vec![])), + ); + let (cursor_remote, data) = ledger_canister + .data_fetch(Some(cursor_local.to_request_string()), bytes_before) + .await?; + let cursor_remote = LedgerCursor::new_from_string(cursor_remote); + let offset_remote = cursor_remote.position; + info!( + "Ledger canister returned position {:0x}, full cursor: {}", + offset_remote, cursor_remote + ); + if offset_remote < cursor_local.position { + return Err(format!( + "Ledger canister has less data than available locally {} < {} bytes", + offset_remote, cursor_local.position + ) + .into()); + } + if data.len() <= 64 { + info!("Data: {} bytes ==> {:?}", data.len(), data); + } else { + info!( + "Data: {} bytes ==> {:?}...", + data.len(), + &data[..64.min(data.len())] + ); + } + let mut ledger_file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&ledger_file_path) + .expect("failed to open the local ledger path"); + let file_size_bytes = ledger_file.metadata().unwrap().len(); + let file_size_bytes_target = offset_remote + data.len() as u64 + 1024 * 1024; + if file_size_bytes < file_size_bytes_target { + ledger_file.set_len(file_size_bytes_target).unwrap(); + ledger_file + .seek(std::io::SeekFrom::Start(offset_remote)) + .unwrap(); + } + if offset_remote + cursor_remote.response_bytes > cursor_local.position { + ledger_file.write_all(&data).unwrap(); + info!( + "Wrote {} bytes at offset 0x{:0x} of file {}", + data.len(), + offset_remote, + ledger_file_path.display() + ); + } + // Set the modified time to the current time, to mark that the data is up-to-date + filetime::set_file_mtime(ledger_file_path, std::time::SystemTime::now().into())?; + + Ok(()) +} + +pub async fn ledger_data_push( + ledger_canister: &LedgerCanister, + local_ledger_path: PathBuf, +) -> Result<(), Box> { + let ledger_local = LedgerMap::new_with_path(Some(vec![]), Some(local_ledger_path)) + .expect("Failed to create LedgerMap"); + let cursor_local = cursor_from_data( + ledger_map::partition_table::get_data_partition().start_lba, + ledger_map::platform_specific::persistent_storage_size_bytes(), + ledger_local.get_next_block_start_pos(), + ledger_local.get_next_block_start_pos(), + ); + + let remote_metadata = get_ledger_metadata(ledger_canister).await; + let cursor_remote: LedgerCursor = remote_metadata.into(); + + if cursor_local.data_end_position <= cursor_remote.data_end_position { + info!("Nothing to push"); + return Ok(()); + } + + info!( + "Data end position local {} remote {} ==> {} bytes to push", + cursor_local.data_end_position, + cursor_remote.data_end_position, + cursor_local.data_end_position - cursor_remote.data_end_position + ); + + let last_i = (cursor_local + .data_end_position + .saturating_sub(cursor_local.data_begin_position)) + / PUSH_BLOCK_SIZE + + 1; + for i in 0..last_i { + let position = (i * PUSH_BLOCK_SIZE).max(cursor_local.data_begin_position); + + let cursor_push = LedgerCursor::new( + cursor_local.data_begin_position, + position, + cursor_local.data_end_position, + CursorDirection::Forward, + i + 1 < last_i, + ); + + let buf_size = + PUSH_BLOCK_SIZE.min(cursor_local.data_end_position.saturating_sub(position)) as usize; + let mut buf = vec![0u8; buf_size]; + persistent_storage_read(position, &mut buf).map_err(|e| e.to_string())?; + info!( + "Pushing block of {} bytes at position {}", + buf_size, position, + ); + let args = Encode!(&cursor_push.to_urlenc_string(), &buf).map_err(|e| e.to_string())?; + let result = ledger_canister.call_update("data_push", &args).await?; + let result = Decode!(&result, Result).map_err(|e| e.to_string())??; + info!("Response from pushing at position {}: {}", position, result); + } + + Ok(()) +} diff --git a/cli/src/ledger/metadata.rs b/cli/src/ledger/metadata.rs new file mode 100644 index 0000000..2e35619 --- /dev/null +++ b/cli/src/ledger/metadata.rs @@ -0,0 +1,18 @@ +use candid::encode_one; +use decent_cloud::ledger_canister_client::LedgerCanister; +use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use std::collections::HashMap; + +pub async fn get_ledger_metadata( + ledger_canister: &LedgerCanister, +) -> HashMap { + let no_args = encode_one(()).expect("Failed to encode empty tuple"); + let response = ledger_canister + .call_query("metadata", &no_args) + .await + .expect("Failed to call ledger canister"); + candid::decode_one::>(&response) + .expect("Failed to decode metadata") + .into_iter() + .collect() +} diff --git a/cli/src/ledger/mod.rs b/cli/src/ledger/mod.rs new file mode 100644 index 0000000..0b7fe0b --- /dev/null +++ b/cli/src/ledger/mod.rs @@ -0,0 +1,57 @@ +mod data_operations; +mod metadata; + +pub use data_operations::{ledger_data_fetch, ledger_data_push}; +pub use metadata::get_ledger_metadata; + +use candid::{Decode, Encode, Nat}; +use dcc_common::{amount_as_string, DccIdentity, IcrcCompatibleAccount, TokenAmountE9s}; +use decent_cloud::ledger_canister_client::LedgerCanister; +use decent_cloud_canister::DC_TOKEN_TRANSFER_FEE_E9S; +use icrc_ledger_types::{ + icrc1::transfer::TransferArg, icrc1::transfer::TransferError as Icrc1TransferError, +}; + +use crate::identity::dcc_to_ic_auth; + +pub async fn handle_funds_transfer( + network_url: &str, + ledger_canister_id: candid::Principal, + from_dcc_id: &DccIdentity, + to_icrc1_account: &IcrcCompatibleAccount, + transfer_amount_e9s: TokenAmountE9s, +) -> Result> { + let from_icrc1_account = from_dcc_id.as_icrc_compatible_account(); + let from_ic_auth = dcc_to_ic_auth(from_dcc_id); + + println!( + "Transferring {} tokens from {} \t to account {}", + amount_as_string(transfer_amount_e9s), + from_icrc1_account, + to_icrc1_account, + ); + + let canister = LedgerCanister::new(ledger_canister_id, from_ic_auth, network_url).await?; + let transfer_args = TransferArg { + amount: transfer_amount_e9s.into(), + fee: Some(DC_TOKEN_TRANSFER_FEE_E9S.into()), + from_subaccount: None, + to: to_icrc1_account.into(), + created_at_time: None, + memo: None, + }; + let args = Encode!(&transfer_args).map_err(|e| e.to_string())?; + let result = canister.call_update("icrc1_transfer", &args).await?; + let response = Decode!(&result, Result).map_err(|e| e.to_string())?; + + match response { + Ok(block_num) => Ok(format!( + "Transfer request successful, will be included in block: {}", + block_num + )), + Err(e) => Err(Box::::from(format!( + "Transfer error: {}", + e + ))), + } +} diff --git a/cli/src/ledger_canister_client.rs b/cli/src/ledger_canister_client.rs index c18c4c0..f3d602a 100644 --- a/cli/src/ledger_canister_client.rs +++ b/cli/src/ledger_canister_client.rs @@ -1,15 +1,17 @@ +use crate::identity::dcc_to_ic_auth; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use borsh::BorshDeserialize; use candid::{Decode, Encode}; use dcc_common::{ - ContractId, ContractReqSerialized, ContractSignRequest, ContractSignRequestPayload, + ContractId, ContractReqSerialized, ContractSignRequest, ContractSignRequestPayload, DccIdentity, }; use ic_agent::{export::Principal, identity::BasicIdentity, Agent}; use serde::Serialize; type ResultString = Result; +#[derive(Debug)] pub struct LedgerCanister { agent: Agent, // wallet_canister_id: Principal, @@ -24,9 +26,9 @@ impl LedgerCanister { canister_id: Principal, identity: Option, // network_name: String, - network_url: String, + network_url: &str, ) -> anyhow::Result { - let agent = Agent::builder().with_url(&network_url); + let agent = Agent::builder().with_url(network_url); let agent = match identity { Some(identity) => agent.with_identity(identity), None => agent, @@ -41,10 +43,48 @@ impl LedgerCanister { // wallet_canister_id, canister_id, // network_name, - // network_url, + // network_url: network_url.to_string(), }) } + pub async fn new_with_identity( + network_url: &str, + canister_id: Principal, + identity: BasicIdentity, + ) -> anyhow::Result { + Self::new( + // wallet_canister_id, + canister_id, + Some(identity), + // network_name, + network_url, + ) + .await + } + + pub async fn new_with_dcc_id( + network_url: &str, + canister_id: Principal, + dcc_id: &DccIdentity, + ) -> anyhow::Result { + let ic_auth = dcc_to_ic_auth(dcc_id).unwrap(); + Self::new_with_identity(network_url, canister_id, ic_auth).await + } + + pub async fn new_without_identity( + network_url: &str, + canister_id: Principal, + ) -> anyhow::Result { + Self::new( + // wallet_canister_id, + canister_id, + None, + // network_name, + network_url, + ) + .await + } + pub fn canister_id(&self) -> &Principal { &self.canister_id } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index f8087e7..713cecd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,3 +1,4 @@ +pub mod identity; pub mod ledger_canister_client; pub mod node_provider; diff --git a/cli/src/main.rs b/cli/src/main.rs index cb0b6a1..529a781 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,50 +1,18 @@ mod argparse; -mod keygen; +mod commands; +mod contracts; +mod identity; +mod ledger; +mod utils; -use argparse::{ - Commands, ContractCommands, LedgerRemoteCommands, NpCommands, OfferingCommands, UserCommands, -}; -// use borsh::{BorshDeserialize, BorshSerialize}; -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; -use bip39::Seed; -use candid::{Decode, Encode, Nat, Principal as IcPrincipal}; -use chrono::DateTime; -use dcc_common::{ - account_balance_get_as_string, amount_as_string, cursor_from_data, - offerings::do_get_matching_offerings, refresh_caches_from_ledger, reputation_get, - CursorDirection, DccIdentity, FundsTransfer, IcrcCompatibleAccount, LedgerCursor, - TokenAmountE9s, DATA_PULL_BYTES_BEFORE_LEN, DC_TOKEN_DECIMALS_DIV, LABEL_DC_TOKEN_TRANSFER, -}; -use dcc_common::{ - ContractSignReply, ContractSignRequest, PaymentEntries, PaymentEntry, PaymentEntryWithAmount, - LABEL_NP_REGISTER, LABEL_USER_REGISTER, -}; -use decent_cloud::ledger_canister_client::LedgerCanister; -use decent_cloud_canister::DC_TOKEN_TRANSFER_FEE_E9S; -use fs_err::OpenOptions; -use ic_agent::identity::BasicIdentity; -use icrc_ledger_types::{ - icrc::generic_metadata_value::MetadataValue, icrc1::transfer::TransferArg, - icrc1::transfer::TransferError as Icrc1TransferError, -}; -use ledger_map::{platform_specific::persistent_storage_read, LedgerMap}; -use log::{info, Level}; -use np_offering::Offering; -use std::time::SystemTime; -use std::{ - collections::HashMap, - io::{self, BufReader, Seek, Write}, - path::PathBuf, -}; -use tabular::{Row, Table}; - -const PUSH_BLOCK_SIZE: u64 = 1024 * 1024; +use argparse::parse_args; +use ledger_map::LedgerMap; +use std::path::PathBuf; #[tokio::main] async fn main() -> Result<(), Box> { - let cli = argparse::parse_args(); - init_logger(cli.verbose); + let cli = parse_args(); + utils::init_logger(cli.verbose); let ledger_path = cli.local_ledger_dir.map(PathBuf::from).unwrap_or_else(|| { dirs::home_dir() @@ -56,1118 +24,10 @@ async fn main() -> Result<(), Box> { let ledger_local = LedgerMap::new_with_path(None, Some(ledger_path)).expect("Failed to load the local ledger"); - refresh_caches_from_ledger(&ledger_local).expect("Failed to get balances"); - - let network = cli.network.unwrap_or_else(|| "ic".to_string()); - - let ledger_canister_id = match network.as_str() { - "local" => IcPrincipal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai")?, - "mainnet-eu" => IcPrincipal::from_text("tlvs5-oqaaa-aaaas-aaabq-cai")?, - "mainnet-01" | "ic" => IcPrincipal::from_text("ggi4a-wyaaa-aaaai-actqq-cai")?, - "mainnet-02" => IcPrincipal::from_text("gplx4-aqaaa-aaaai-actra-cai")?, - _ => panic!("unknown network: {}", network), - }; - let network_url = match network.as_str() { - "local" => "http://127.0.0.1:8000", - "mainnet-eu" | "mainnet-01" | "mainnet-02" | "ic" => "https://ic0.app", - _ => panic!("unknown network: {}", network), - }; - let ledger_canister = |identity| async { - LedgerCanister::new(ledger_canister_id, identity, network_url.to_string()).await - }; - let identity_name = cli.identity.clone(); - - match cli.command { - Commands::Keygen(ref keygen_args) => { - let identity = - identity_name.expect("Identity must be specified for this command, use --identity"); - - let mnemonic = if keygen_args.generate { - let mnemonic = - bip39::Mnemonic::new(bip39::MnemonicType::Words12, bip39::Language::English); - info!("Generated mnemonic: {}", mnemonic); - mnemonic - } else if keygen_args.mnemonic.is_some() { - let mnemonic_string = keygen_args - .mnemonic - .clone() - .unwrap_or_default() - .split_whitespace() - .map(String::from) - .collect::>(); - if mnemonic_string.len() < 12 { - let reader = BufReader::new(io::stdin()); - keygen::mnemonic_from_stdin(reader, io::stdout())? - } else { - keygen::mnemonic_from_strings(mnemonic_string)? - } - } else { - panic!("Neither mnemonic nor generate specified"); - }; - - let seed = Seed::new(&mnemonic, ""); - let dcc_identity = DccIdentity::new_from_seed(seed.as_bytes())?; - info!("Generated identity: {}", dcc_identity); - dcc_identity.save_to_dir(&identity) - } - Commands::Account(ref account_args) => { - let identities_dir = DccIdentity::identities_dir(); - let identity = - identity_name.expect("Identity must be specified for this command, use --identity"); - let dcc_id = DccIdentity::load_from_dir(&identities_dir.join(identity))?; - if account_args.balance { - let ic_account = dcc_id.as_icrc_compatible_account(); - println!( - "Account balance: {}", - account_balance_get_as_string(&ic_account) - ); - return Ok(()); - } - let to_principal_string = &account_args - .transfer_to - .clone() - .expect("You must specify --transfer-to"); - let to_icrc1_account = IcrcCompatibleAccount::from(to_principal_string); - let transfer_amount_e9s = match &account_args.amount_dct { - Some(value) => (value.parse::()? * (DC_TOKEN_DECIMALS_DIV as f64)).round() - as TokenAmountE9s, - None => match &account_args.amount_e9s { - Some(value) => value.parse::()?, - None => { - panic!("You must specify either --amount-dct or --amount-e9s") - } - }, - }; - - println!( - "{}", - handle_funds_transfer( - network_url, - ledger_canister_id, - &dcc_id, - &to_icrc1_account, - transfer_amount_e9s, - ) - .await? - ); - - Ok(()) - } - Commands::Np(ref np_cmd) => { - match np_cmd { - NpCommands::List(list_args) => { - if list_args.only_local { - list_local_identities(list_args.balances)? - } else { - list_identities( - &ledger_local, - ListIdentityType::Providers, - list_args.balances, - )? - } - } - NpCommands::Register => { - let identity = identity_name - .expect("Identity must be specified for this command, use --identity"); - let dcc_ident = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - let ic_auth = dcc_to_ic_auth(&dcc_ident); - - info!("Registering principal: {} as {}", identity, dcc_ident); - let pubkey_bytes = dcc_ident.to_bytes_verifying(); - let pubkey_signature = dcc_ident.sign(pubkey_bytes.as_ref())?; - let result = ledger_canister(ic_auth) - .await? - .node_provider_register( - &pubkey_bytes, - pubkey_signature.to_bytes().as_slice(), - ) - .await?; - println!("Register: {}", result); - } - NpCommands::CheckIn(check_in_args) => { - if check_in_args.only_nonce { - let nonce_bytes = ledger_canister(None).await?.get_check_in_nonce().await; - let nonce_string = hex::encode(&nonce_bytes); - - println!("0x{}", nonce_string); - } else { - let identity = identity_name - .expect("Identity must be specified for this command, use --identity"); - - let dcc_ident = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - let ic_auth = dcc_to_ic_auth(&dcc_ident); - - // Check the local ledger timestamp - let local_ledger_path = ledger_local - .get_file_path() - .expect("Failed to get local ledger path"); - let local_ledger_file_mtime = local_ledger_path.metadata()?.modified()?; - - // If the local ledger is older than 1 minute, refresh it automatically before proceeding - // If needed, the local ledger can also be refreshed manually from the command line - if local_ledger_file_mtime - < SystemTime::now() - std::time::Duration::from_secs(10) - { - info!("Local ledger is older than 1 minute, refreshing..."); - let canister = ledger_canister(None).await?; - ledger_data_fetch(&canister, local_ledger_path).await?; - - refresh_caches_from_ledger(&ledger_local) - .expect("Loading balances from ledger failed"); - } - // The local ledger needs to be refreshed to get the latest nonce - // This provides the incentive to clone and frequently re-fetch the ledger - let nonce_bytes = ledger_local.get_latest_block_hash(); - let nonce_string = hex::encode(&nonce_bytes); - - info!( - "Checking-in provider identity {} ({}), using nonce: {} ({} bytes)", - identity, - dcc_ident, - nonce_string, - nonce_bytes.len() - ); - let check_in_memo = check_in_args.memo.clone().unwrap_or_else(|| { - println!("No memo specified, did you know that you can specify one? Try out --memo"); - String::new() - }); - let nonce_crypto_signature = dcc_ident.sign(nonce_bytes.as_ref())?; - let result = ledger_canister(ic_auth) - .await? - .node_provider_check_in( - &dcc_ident.to_bytes_verifying(), - &check_in_memo, - &nonce_crypto_signature.to_bytes(), - ) - .await - .map_err(|e| format!("Check-in failed: {}", e))?; - info!("Check-in success: {}", result); - } - } - NpCommands::UpdateProfile(update_profile_args) => { - let identity = identity_name - .expect("Identity must be specified for this command, use --identity"); - - let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - let ic_auth = dcc_to_ic_auth(&dcc_id); - - let np_profile = - np_profile::Profile::new_from_file(&update_profile_args.profile_file)?; - let np_profile_bytes = borsh::to_vec(&np_profile)?; - let crypto_signature = dcc_id.sign(&np_profile_bytes)?; - - let result = ledger_canister(ic_auth) - .await? - .node_provider_update_profile( - &dcc_id.to_bytes_verifying(), - &np_profile_bytes, - &crypto_signature.to_bytes(), - ) - .await - .map_err(|e| format!("Update profile failed: {}", e))?; - info!("Profile update response: {}", result); - } - NpCommands::UpdateOffering(update_offering_args) => { - let identity = identity_name - .expect("Identity must be specified for this command, use --identity"); - let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - let ic_auth = dcc_to_ic_auth(&dcc_id); - - // Offering::new_from_file returns an error if the schema validation fails - let np_offering = - np_offering::Offering::new_from_file(&update_offering_args.offering_file)?; - let np_offering_bytes = np_offering.serialize()?; - let crypto_signature = dcc_id.sign(&np_offering_bytes)?; - - let result = ledger_canister(ic_auth) - .await? - .node_provider_update_offering( - &dcc_id.to_bytes_verifying(), - &np_offering_bytes, - &crypto_signature.to_bytes(), - ) - .await - .map_err(|e| format!("Update offering failed: {}", e))?; - info!("Offering update response: {}", result); - } - } - Ok(()) - } - Commands::User(ref user_cmd) => { - match user_cmd { - UserCommands::List(list_args) => { - if list_args.only_local { - list_local_identities(list_args.balances)? - } else { - list_identities(&ledger_local, ListIdentityType::Users, list_args.balances)? - } - } - UserCommands::Register => { - let identity = identity_name - .expect("Identity must be specified for this command, use --identity"); - let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - let ic_auth = dcc_to_ic_auth(&dcc_id); - - let canister = ledger_canister(ic_auth).await?; - let pubkey_bytes = dcc_id.to_bytes_verifying(); - let pubkey_signature = dcc_id.sign(&pubkey_bytes)?; - let args = Encode!(&pubkey_bytes, &pubkey_signature.to_bytes())?; - let result = canister.call_update("user_register", &args).await?; - let response = - Decode!(&result, Result).map_err(|e| e.to_string())?; - - match response { - Ok(response) => { - println!("Registration successful: {}", response); - } - Err(e) => { - println!("Registration failed: {}", e); - } - } - } - } - Ok(()) - } - Commands::LedgerLocal(ref local_args) => { - if local_args.list_entries { - println!("Entries:"); - for entry in ledger_local.iter(None) { - match entry.label() { - LABEL_DC_TOKEN_TRANSFER => { - let transfer_id = BASE64.encode(entry.key()); - let transfer: FundsTransfer = - borsh::from_slice(entry.value()).map_err(|e| e.to_string())?; - println!("[DCTokenTransfer] TransferId {}: {}", transfer_id, transfer); - } - _ => println!("{}", entry), - }; - } - } else if local_args.list_entries_raw { - println!("Raw Entries:"); - for entry in ledger_local.iter_raw() { - let (blk_header, ledger_block) = entry?; - println!("{}", blk_header); - println!("{}", ledger_block) - } - } - Ok(()) - } - Commands::LedgerRemote(ref subcmd) => { - // TODO: Switch to subcommands - let local_ledger_path = ledger_local - .get_file_path() - .expect("Failed to get local ledger path"); - - match subcmd { - LedgerRemoteCommands::DataFetch => { - let canister = ledger_canister(None).await?; - return ledger_data_fetch(&canister, local_ledger_path).await; - } - LedgerRemoteCommands::DataPushAuthorize | LedgerRemoteCommands::DataPush => { - let identity = identity_name - .expect("Identity must be specified for this command, use --identity"); - - let dcc_ident = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - - let push_auth = *subcmd == LedgerRemoteCommands::DataPushAuthorize; - - if push_auth { - let ic_auth = dcc_to_ic_auth(&dcc_ident); - let canister = ledger_canister(ic_auth).await?; - let args = Encode!(&()).map_err(|e| e.to_string())?; - let result = canister.call_update("data_push_auth", &args).await?; - let response = Decode!(&result, Result) - .map_err(|e| e.to_string())??; - - println!("Push auth: {}", response); - } - - // After authorizing, we can push the data - let ic_auth = dcc_to_ic_auth(&dcc_ident); - let canister = ledger_canister(ic_auth).await?; - - return ledger_data_push(&canister, local_ledger_path).await; - } - LedgerRemoteCommands::Metadata => { - let canister = ledger_canister(None).await?; - - let mut table = Table::new("{:<} {:<}"); - table.add_row(Row::new().with_cell("Key").with_cell("Value")); - - for md_entry in get_ledger_metadata(&canister).await { - let md_entry_val = match md_entry.1 { - MetadataValue::Nat(v) => v.to_string(), - MetadataValue::Int(v) => v.to_string(), - MetadataValue::Text(v) => v.to_string(), - MetadataValue::Blob(v) => hex::encode(v), - }; - table.add_row(Row::new().with_cell(md_entry.0).with_cell(md_entry_val)); - } - print!("{}", table); - } - LedgerRemoteCommands::GetRegistrationFee => { - let canister = ledger_canister(None).await?; - let noargs = Encode!(&()).expect("Failed to encode args"); - let response = canister.call_query("get_registration_fee", &noargs).await?; - let fee_e9s = Decode!(response.as_slice(), u64).map_err(|e| e.to_string())?; - println!("Registration fee: {}", amount_as_string(fee_e9s)); - } - LedgerRemoteCommands::GetCheckInNonce => { - let nonce_bytes = ledger_canister(None).await?.get_check_in_nonce().await; - println!("{}", hex::encode(nonce_bytes)); - } - LedgerRemoteCommands::GetLogsDebug => { - println!("Ledger canister DEBUG logs:"); - for entry in serde_json::from_str::>( - &ledger_canister(None).await?.get_logs_debug().await?, - )? - .into_iter() - { - log_with_level(entry, Level::Debug); - } - } - LedgerRemoteCommands::GetLogsInfo => { - println!("Ledger canister INFO logs:"); - for entry in serde_json::from_str::>( - &ledger_canister(None).await?.get_logs_info().await?, - )? - .into_iter() - { - log_with_level(entry, Level::Info); - } - } - LedgerRemoteCommands::GetLogsWarn => { - println!("Ledger canister WARN logs:"); - for entry in serde_json::from_str::>( - &ledger_canister(None).await?.get_logs_warn().await?, - )? - .into_iter() - { - log_with_level(entry, Level::Warn); - } - } - LedgerRemoteCommands::GetLogsError => { - println!("Ledger canister ERROR logs:"); - for entry in serde_json::from_str::>( - &ledger_canister(None).await?.get_logs_error().await?, - )? - .into_iter() - { - log_with_level(entry, Level::Error); - } - } - } - - fn log_with_level(log_entry: serde_json::Value, log_level: Level) { - let timestamp_ns = log_entry["timestamp"].as_u64().unwrap_or_default(); - let timestamp_s = (timestamp_ns / 1_000_000_000) as i64; - // Create DateTime from the timestamp - let dt = DateTime::from_timestamp(timestamp_s, 0).unwrap_or_default(); - println!( - "{} [{}] - {}", - dt.format("%Y-%m-%dT%H:%M:%S"), - log_level, - log_entry["message"] - .as_str() - .expect("Invalid message field") - ); - } - - Ok(()) - } - Commands::Offering(ref cmd) => { - let query = match cmd { - OfferingCommands::List => "", - OfferingCommands::Query(query_args) => &query_args.query, - }; - let offerings = do_get_matching_offerings(&ledger_local, query); - println!("Found {} matching offerings:", offerings.len()); - for (dcc_id, offering) in offerings { - println!( - "{} ==>\n{}", - dcc_id.display_as_ic_and_pem_one_line(), - &offering.as_json_string_pretty().unwrap_or_default() - ); - } - - Ok(()) - } - Commands::Contract(ref contract_args) => match contract_args { - ContractCommands::ListOpen(_list_open_args) => { - println!("Listing all open contracts..."); - // A user may provide the identity (public key), but doesn't have to - let pubkey_bytes = cli.identity.map(|name| { - let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&name)).unwrap(); - dcc_id.to_bytes_verifying() - }); - let canister = ledger_canister(None).await?; - let contracts_open = canister.contracts_list_pending(&pubkey_bytes).await; - if contracts_open.is_empty() { - println!("No open contracts"); - } else { - for open_contract in contracts_open { - println!( - "{}", - serde_json::to_string_pretty(&open_contract).unwrap_or_default() - ); - } - } - Ok(()) - } - ContractCommands::SignRequest(sign_req_args) => { - println!("Request to sign a contract..."); - loop { - println!(); - let i = sign_req_args.interactive; - let identity = - prompt_input("Please enter the identity name", &cli.identity, i, false); - let instance_id = prompt_input( - "Please enter the offering id", - &sign_req_args.offering_id, - i, - false, - ); - let requester_ssh_pubkey=prompt_input( - "Please enter your ssh public key, which will be granted access to the contract", - &sign_req_args.requester_ssh_pubkey, i, false - ); - let requester_contact = prompt_input( - "Enter your contact information (this will be public)", - &sign_req_args.requester_contact, - i, - true, - ); - let provider_pubkey_pem = sign_req_args - .provider_pubkey_pem - .clone() - .unwrap_or_else(|| { - prompt_editor( - "# Enter the provider's public key below, as a PEM string", - i, - ) - .lines() - .map(|line| { - line.split_once('#') - .map(|line| line.0) - .unwrap_or(line) - .trim() - .to_string() - }) - .filter(|line| !line.is_empty()) - .collect::>() - .join("\n") - }); - let provider_dcc_id = - match DccIdentity::new_verifying_from_pem(&provider_pubkey_pem) { - Ok(ident) => ident, - Err(e) => { - eprintln!("ERROR: Failed to parse provider pubkey: {}", e); - continue; - } - }; - let provider_pubkey_bytes = provider_dcc_id.to_bytes_verifying(); - // Find the offering with the given id, from the provider - let offerings = do_get_matching_offerings( - &ledger_local, - &format!("instance_types.id = \"{instance_id}\""), - ) - .into_iter() - .filter(|o| o.0.to_bytes_verifying() == provider_pubkey_bytes) - .collect::>(); - - let offering = match offerings.len() { - 0 => { - eprintln!( - "ERROR: No offering found for the provider {provider_dcc_id} and id: {instance_id}" - ); - continue; - } - 1 => &offerings[0].1, - _ => { - eprintln!("ERROR: Provider {provider_dcc_id} has multiple offerings with id: {instance_id}"); - continue; - } - }; - - let payment_entries = prompt_for_payment_entries( - &sign_req_args.payment_entries_json, - offering, - &instance_id, - ); - - let payment_amount_e9s = payment_entries.iter().map(|e| e.amount_e9s).sum(); - - let memo = prompt_input( - "Please enter a memo for the contract (this will be public)", - &sign_req_args.memo, - i, - true, - ); - - let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity))?; - - let requester_pubkey_bytes = dcc_id.to_bytes_verifying(); - let req = ContractSignRequest::new( - &requester_pubkey_bytes, - requester_ssh_pubkey, - requester_contact, - &provider_pubkey_bytes, - instance_id.clone(), - None, - None, - None, - payment_amount_e9s, - payment_entries, - None, - memo.clone(), - ); - println!("The following contract sign request will be sent:"); - println!("{}", serde_json::to_string_pretty(&req)?); - if dialoguer::Confirm::new() - .with_prompt("Is this correct? If yes, press enter to send.") - .default(false) - .show_default(true) - .interact() - .unwrap() - { - let payload_bytes = borsh::to_vec(&req).unwrap(); - let payload_sig_bytes = dcc_id.sign(&payload_bytes)?.to_bytes(); - let ic_auth = dcc_to_ic_auth(&dcc_id); - let canister = ledger_canister(ic_auth).await?; - - match canister - .contract_sign_request( - &requester_pubkey_bytes, - &payload_bytes, - &payload_sig_bytes, - ) - .await - { - Ok(response) => { - println!("Contract sign request successful: {}", response); - break; - } - Err(e) => { - println!("Contract sign request failed: {}", e); - if dialoguer::Confirm::new() - .with_prompt("Do you want to retry?") - .default(true) - .show_default(true) - .interact() - .unwrap() - { - continue; - } else { - break; - } - } - } - } else { - println!("Contract sign request canceled."); - break; - } - } - Ok(()) - } - ContractCommands::SignReply(sign_reply_args) => { - println!("Reply to a contract-sign request..."); - loop { - let i = sign_reply_args.interactive; - let identity = - prompt_input("Please enter the identity name", &cli.identity, i, false); - let contract_id = prompt_input( - "Please enter the contract id, as a base64 encoded string", - &sign_reply_args.contract_id, - i, - false, - ); - let accept = prompt_bool( - "Do you accept the contract?", - sign_reply_args.sign_accept, - i, - ); - let response_text = prompt_input( - "Please enter a response text for the contract (this will be public)", - &sign_reply_args.response_text, - i, - true, - ); - let response_details = prompt_input( - "Please enter a response details for the contract (this will be public)", - &sign_reply_args.response_details, - i, - true, - ); - - let dcc_id = DccIdentity::load_from_dir(&PathBuf::from(&identity)).unwrap(); - let provider_pubkey_bytes = dcc_id.to_bytes_verifying(); - let ic_auth = dcc_to_ic_auth(&dcc_id); - let canister = ledger_canister(ic_auth).await?; - - let contracts_open = canister - .contracts_list_pending(&Some(provider_pubkey_bytes.clone())) - .await; - let open_contract = contracts_open - .iter() - .find(|c| c.contract_id_base64 == contract_id) - .expect("Provided contract id not found"); - - let contract_id_bytes = BASE64.decode(contract_id.as_bytes()).unwrap(); - - let reply = ContractSignReply::new( - open_contract.contract_req.requester_pubkey_bytes().to_vec(), - open_contract.contract_req.request_memo(), - contract_id_bytes, - accept, - &response_text, - &response_details, - ); - - let payload_bytes = borsh::to_vec(&reply).unwrap(); - let signature = dcc_id.sign(&payload_bytes)?.to_vec(); - - match canister - .contract_sign_reply(&provider_pubkey_bytes, &payload_bytes, &signature) - .await - { - Ok(response) => { - println!("Contract sign reply sent successfully: {}", response); - break; - } - Err(e) => { - println!("Error sending contract sign reply: {:?}", e); - } - } - } - Ok(()) - } - }, - }?; - Ok(()) -} - -fn prompt_input( - prompt_message: &str, - cli_arg_value: &Option, - interactive: bool, - allow_empty: bool, -) -> String { - match cli_arg_value { - Some(value) => value.to_string(), - None => { - if interactive { - dialoguer::Input::::new() - .with_prompt(prompt_message) - .allow_empty(allow_empty) - .show_default(false) - .interact() - .unwrap_or_default() - } else { - panic!("CLI argument required: {}", prompt_message) - } - } - } -} - -fn prompt_bool(prompt_message: &str, cli_arg_value: Option, interactive: bool) -> bool { - match cli_arg_value { - Some(value) => value, - None => { - if interactive { - dialoguer::Confirm::new() - .with_prompt(prompt_message) - .default(false) - .show_default(true) - .interact() - .unwrap_or_default() - } else { - panic!("CLI argument required: {}", prompt_message) - } - } - } -} - -fn prompt_editor(prompt_message: &str, interactive: bool) -> String { - if interactive { - match dialoguer::Editor::new().edit(prompt_message) { - Ok(Some(content)) => content, - Ok(None) => { - println!("No input received."); - String::new() - } - Err(err) => { - eprintln!("Error opening editor: {}", err); - String::new() - } - } - } else { - panic!("CLI argument required: {}", prompt_message); - } -} - -/// We only allow one payment entry at a time, but this can be easily changed later in the CLI -fn prompt_for_payment_entries( - payment_entries_json: &Option, - offering: &Offering, - instance_id: &str, -) -> Vec { - let pricing: HashMap> = offering.instance_pricing(instance_id); - - let get_total_price = |model: &str, time_period_unit: &str, quantity: u64| -> TokenAmountE9s { - pricing - .get(model) - .and_then(|units| units.get(time_period_unit)) - .map(|amount| { - amount - .replace("_", "") - .parse::() - .expect("Failed to parse the offering price as TokenAmountE9s") - * quantity - }) - .unwrap() - }; - let mut payment_entries: Vec<_> = payment_entries_json - .clone() - .map(|entries| { - entries - .0 - .into_iter() - .map(|e| PaymentEntryWithAmount { - e: e.clone(), - amount_e9s: get_total_price(&e.pricing_model, &e.time_period_unit, e.quantity), - }) - .collect() - }) - .unwrap_or_default(); - - if payment_entries.is_empty() { - let models = pricing.keys().collect::>(); - let model = models[dialoguer::Select::new() - .with_prompt("Please select instance pricing model (ESC to exit)") - .items(&models) - .default(0) - .interact() - .expect("Failed to read input")]; - let units = pricing[model].keys().collect::>(); - let time_period_unit = units[dialoguer::Select::new() - .with_prompt("Please select time period unit") - .items(&units) - .report(true) - .default(0) - .interact() - .expect("Failed to read input")]; - let quantity = dialoguer::Input::::new() - .with_prompt("Please enter the number of units") - .default(1) - .interact() - .expect("Failed to read input"); - payment_entries.push(PaymentEntryWithAmount { - e: PaymentEntry::new(model, time_period_unit, quantity), - amount_e9s: get_total_price(model, time_period_unit, quantity), - }); - } - payment_entries -} -fn list_local_identities(include_balances: bool) -> Result<(), Box> { - let identities_dir = DccIdentity::identities_dir(); - println!("Available identities at {}:", identities_dir.display()); - let mut identities: Vec<_> = fs_err::read_dir(identities_dir)? - .filter_map(|entry| match entry { - Ok(entry) => Some(entry), - Err(e) => { - eprintln!("Failed to read identity: {}", e); - None - } - }) - .collect(); + dcc_common::refresh_caches_from_ledger(&ledger_local).expect("Failed to get balances"); - identities.sort_by_key(|identity| identity.file_name()); + commands::handle_command(cli.command, ledger_local).await?; - for identity in identities { - let path = identity.path(); - if path.is_dir() { - let identity_name = identity.file_name(); - let identity_name = identity_name.to_string_lossy(); - match DccIdentity::load_from_dir(&path) { - Ok(dcc_identity) => { - print!("{} => ", identity_name); - println_identity(&dcc_identity, include_balances); - } - Err(e) => { - println!("{} => Error: {}", identity_name, e); - } - } - } - } Ok(()) } - -#[derive(PartialEq)] -enum ListIdentityType { - Providers, - Users, - All, -} - -fn list_identities( - ledger: &LedgerMap, - identity_type: ListIdentityType, - show_balances: bool, -) -> Result<(), Box> { - if identity_type == ListIdentityType::Providers || identity_type == ListIdentityType::All { - println!("\n# Registered providers"); - for entry in ledger.iter(Some(LABEL_NP_REGISTER)) { - let dcc_id = DccIdentity::new_verifying_from_bytes(entry.key()).unwrap(); - println_identity(&dcc_id, show_balances); - } - } - if identity_type == ListIdentityType::Users || identity_type == ListIdentityType::All { - println!("\n# Registered users"); - for entry in ledger.iter(Some(LABEL_USER_REGISTER)) { - let dcc_id = DccIdentity::new_verifying_from_bytes(entry.key()).unwrap(); - println_identity(&dcc_id, show_balances); - } - } - Ok(()) -} - -fn println_identity(dcc_id: &DccIdentity, show_balance: bool) { - if show_balance { - println!( - "{}, reputation {}, balance {}", - dcc_id.display_as_ic_and_pem_one_line(), - reputation_get(dcc_id.to_bytes_verifying()), - account_balance_get_as_string(&dcc_id.as_icrc_compatible_account()) - ); - } else { - println!( - "{} reputation {}", - dcc_id.display_as_ic_and_pem_one_line(), - reputation_get(dcc_id.to_bytes_verifying()) - ); - } -} - -fn dcc_to_ic_auth(dcc_identity: &DccIdentity) -> Option { - dcc_identity - .signing_key_as_ic_agent_pem_string() - .map(|pem_key| { - let cursor = std::io::Cursor::new(pem_key.as_bytes()); - BasicIdentity::from_pem(cursor).expect("failed to parse pem key") - }) -} - -pub async fn handle_funds_transfer( - network_url: &str, - ledger_canister_id: IcPrincipal, - from_dcc_id: &DccIdentity, - to_icrc1_account: &IcrcCompatibleAccount, - transfer_amount_e9s: TokenAmountE9s, -) -> Result> { - let from_icrc1_account = from_dcc_id.as_icrc_compatible_account(); - let from_ic_auth = dcc_to_ic_auth(from_dcc_id); - - println!( - "Transferring {} tokens from {} \t to account {}", - amount_as_string(transfer_amount_e9s), - from_icrc1_account, - to_icrc1_account, - ); - - let canister = - LedgerCanister::new(ledger_canister_id, from_ic_auth, network_url.to_string()).await?; - let transfer_args = TransferArg { - amount: transfer_amount_e9s.into(), - fee: Some(DC_TOKEN_TRANSFER_FEE_E9S.into()), - from_subaccount: None, - to: to_icrc1_account.into(), - created_at_time: None, - memo: None, - }; - let args = Encode!(&transfer_args).map_err(|e| e.to_string())?; - let result = canister.call_update("icrc1_transfer", &args).await?; - let response = Decode!(&result, Result).map_err(|e| e.to_string())?; - - match response { - Ok(block_num) => Ok(format!( - "Transfer request successful, will be included in block: {}", - block_num - )), - Err(e) => Err(Box::::from(format!( - "Transfer error: {}", - e - ))), - } -} - -async fn ledger_data_fetch( - ledger_canister: &LedgerCanister, - local_ledger_path: PathBuf, -) -> Result<(), Box> { - let mut ledger_file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(&local_ledger_path) - .expect("failed to open the local ledger path"); - - let cursor_local = { - let ledger = LedgerMap::new_with_path(None, Some(local_ledger_path.clone())) - .expect("Failed to create LedgerMap"); - cursor_from_data( - ledger_map::partition_table::get_data_partition().start_lba, - ledger_map::platform_specific::persistent_storage_size_bytes(), - ledger.get_next_block_start_pos(), - ledger.get_next_block_start_pos(), - ) - }; - - let bytes_before = if cursor_local.position > DATA_PULL_BYTES_BEFORE_LEN as u64 { - let mut buf = vec![0u8; DATA_PULL_BYTES_BEFORE_LEN as usize]; - persistent_storage_read( - cursor_local.position - DATA_PULL_BYTES_BEFORE_LEN as u64, - &mut buf, - )?; - Some(buf) - } else { - None - }; - - info!( - "Fetching data from the Ledger canister {}, with local cursor: {} and bytes before: {:?}", - ledger_canister.canister_id(), - cursor_local, - hex::encode(bytes_before.as_ref().unwrap_or(&vec![])), - ); - let (cursor_remote, data) = ledger_canister - .data_fetch(Some(cursor_local.to_request_string()), bytes_before) - .await?; - let cursor_remote = LedgerCursor::new_from_string(cursor_remote); - let offset_remote = cursor_remote.position; - info!( - "Ledger canister returned position {:0x}, full cursor: {}", - offset_remote, cursor_remote - ); - if offset_remote < cursor_local.position { - return Err(format!( - "Ledger canister has less data than available locally {} < {} bytes", - offset_remote, cursor_local.position - ) - .into()); - } - if data.len() <= 64 { - info!("Data: {} bytes ==> {:?}", data.len(), data); - } else { - info!( - "Data: {} bytes ==> {:?}...", - data.len(), - &data[..64.min(data.len())] - ); - } - let file_size_bytes = ledger_file.metadata().unwrap().len(); - let file_size_bytes_target = offset_remote + data.len() as u64 + 1024 * 1024; - if file_size_bytes < file_size_bytes_target { - ledger_file.set_len(file_size_bytes_target).unwrap(); - ledger_file - .seek(std::io::SeekFrom::Start(offset_remote)) - .unwrap(); - } - if offset_remote + cursor_remote.response_bytes > cursor_local.position { - ledger_file.write_all(&data).unwrap(); - info!( - "Wrote {} bytes at offset 0x{:0x} of file {}", - data.len(), - offset_remote, - local_ledger_path.display() - ); - } - // Set the modified time to the current time, to mark that the data is up-to-date - filetime::set_file_mtime(local_ledger_path, std::time::SystemTime::now().into())?; - - Ok(()) -} - -async fn get_ledger_metadata(ledger_canister: &LedgerCanister) -> HashMap { - let no_args = candid::encode_one(()).expect("Failed to encode empty tuple"); - let response = ledger_canister - .call_query("metadata", &no_args) - .await - .expect("Failed to call ledger canister"); - candid::decode_one::>(&response) - .expect("Failed to decode metadata") - .into_iter() - .collect() -} - -pub async fn ledger_data_push( - ledger_canister: &LedgerCanister, - local_ledger_path: PathBuf, -) -> Result<(), Box> { - let ledger_local = LedgerMap::new_with_path(Some(vec![]), Some(local_ledger_path)) - .expect("Failed to create LedgerMap"); - let cursor_local = cursor_from_data( - ledger_map::partition_table::get_data_partition().start_lba, - ledger_map::platform_specific::persistent_storage_size_bytes(), - ledger_local.get_next_block_start_pos(), - ledger_local.get_next_block_start_pos(), - ); - - let remote_metadata = get_ledger_metadata(ledger_canister).await; - let cursor_remote: LedgerCursor = remote_metadata.into(); - - if cursor_local.data_end_position <= cursor_remote.data_end_position { - info!("Nothing to push"); - return Ok(()); - } - - info!( - "Data end position local {} remote {} ==> {} bytes to push", - cursor_local.data_end_position, - cursor_remote.data_end_position, - cursor_local.data_end_position - cursor_remote.data_end_position - ); - - let last_i = (cursor_local - .data_end_position - .saturating_sub(cursor_local.data_begin_position)) - / PUSH_BLOCK_SIZE - + 1; - for i in 0..last_i { - let position = (i * PUSH_BLOCK_SIZE).max(cursor_local.data_begin_position); - - let cursor_push = LedgerCursor::new( - cursor_local.data_begin_position, - position, - cursor_local.data_end_position, - CursorDirection::Forward, - i + 1 < last_i, - ); - - let buf_size = - PUSH_BLOCK_SIZE.min(cursor_local.data_end_position.saturating_sub(position)) as usize; - let mut buf = vec![0u8; buf_size]; - persistent_storage_read(position, &mut buf).map_err(|e| e.to_string())?; - info!( - "Pushing block of {} bytes at position {}", - buf_size, position, - ); - let args = Encode!(&cursor_push.to_urlenc_string(), &buf).map_err(|e| e.to_string())?; - let result = ledger_canister.call_update("data_push", &args).await?; - let result = Decode!(&result, Result).map_err(|e| e.to_string())??; - info!("Response from pushing at position {}: {}", position, result); - } - - Ok(()) -} - -pub fn init_logger(verbose: bool) { - if std::env::var("RUST_LOG").is_err() { - if verbose { - std::env::set_var("RUST_LOG", "debug"); - } else { - std::env::set_var("RUST_LOG", "info"); - } - } - pretty_env_logger::init(); -} diff --git a/cli/src/utils/logger.rs b/cli/src/utils/logger.rs new file mode 100644 index 0000000..c41fb0b --- /dev/null +++ b/cli/src/utils/logger.rs @@ -0,0 +1,10 @@ +pub fn init_logger(verbose: bool) { + if std::env::var("RUST_LOG").is_err() { + if verbose { + std::env::set_var("RUST_LOG", "debug"); + } else { + std::env::set_var("RUST_LOG", "info"); + } + } + pretty_env_logger::init(); +} diff --git a/cli/src/utils/mod.rs b/cli/src/utils/mod.rs new file mode 100644 index 0000000..00b0c77 --- /dev/null +++ b/cli/src/utils/mod.rs @@ -0,0 +1,4 @@ +mod logger; +pub mod prompts; + +pub use logger::init_logger; diff --git a/cli/src/utils/prompts.rs b/cli/src/utils/prompts.rs new file mode 100644 index 0000000..111ed6d --- /dev/null +++ b/cli/src/utils/prompts.rs @@ -0,0 +1,58 @@ +pub fn prompt_input( + prompt_message: &str, + cli_arg_value: &Option, + interactive: bool, + allow_empty: bool, +) -> String { + match cli_arg_value { + Some(value) => value.to_string(), + None => { + if interactive { + dialoguer::Input::::new() + .with_prompt(prompt_message) + .allow_empty(allow_empty) + .show_default(false) + .interact() + .unwrap_or_default() + } else { + panic!("CLI argument required: {}", prompt_message) + } + } + } +} + +pub fn prompt_bool(prompt_message: &str, cli_arg_value: Option, interactive: bool) -> bool { + match cli_arg_value { + Some(value) => value, + None => { + if interactive { + dialoguer::Confirm::new() + .with_prompt(prompt_message) + .default(false) + .show_default(true) + .interact() + .unwrap_or_default() + } else { + panic!("CLI argument required: {}", prompt_message) + } + } + } +} + +pub fn prompt_editor(prompt_message: &str, interactive: bool) -> String { + if interactive { + match dialoguer::Editor::new().edit(prompt_message) { + Ok(Some(content)) => content, + Ok(None) => { + println!("No input received."); + String::new() + } + Err(err) => { + eprintln!("Error opening editor: {}", err); + String::new() + } + } + } else { + panic!("CLI argument required: {}", prompt_message); + } +} diff --git a/common/src/ledger_refresh.rs b/common/src/ledger_refresh.rs index 75d2701..938cb1d 100644 --- a/common/src/ledger_refresh.rs +++ b/common/src/ledger_refresh.rs @@ -115,7 +115,7 @@ pub fn refresh_caches_from_ledger(ledger: &LedgerMap) -> anyhow::Result<()> { ); } Err(e) => { - warn!("Failed to deserialize offering payload: {}", e); + debug!("Failed to deserialize offering payload: {}", e); continue; } } diff --git a/np-offering/src/lib.rs b/np-offering/src/lib.rs index 175c303..3bf2897 100644 --- a/np-offering/src/lib.rs +++ b/np-offering/src/lib.rs @@ -11,7 +11,6 @@ pub enum Offering { // Future versions can be added here } -// Main struct for Cloud Provider Offering version 0.1.0 #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CloudProviderOfferingV0_1_0 { pub kind: String,