diff --git a/Cargo.toml b/Cargo.toml index d6d2b40..ebbe7e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["moat-cli", "moat-request-cli", "moat-core", "wallet-accessor", "integration-tests", "license-provider", "macros/code-hasher"] +members = ["moat-cli", "moat-cli-user", "moat-cli-lp", "moat-cli-sp", "moat-cli-request", "moat-core", "wallet-accessor", "integration-tests", "license-provider", "macros/code-hasher"] diff --git a/integration-tests/tests/blockchain/stake_add_owner.rs b/integration-tests/tests/blockchain/stake_add_owner.rs index f1df0de..5fb098c 100644 --- a/integration-tests/tests/blockchain/stake_add_owner.rs +++ b/integration-tests/tests/blockchain/stake_add_owner.rs @@ -14,7 +14,7 @@ use wallet_accessor::Password::PwdHash; const WALLET_PATH: &str = concat!(env!("HOME"), "/.dusk/rusk-wallet"); const PWD_HASH: &str = - "7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787"; + "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"; const GAS_LIMIT: u64 = 5_000_000_000; const GAS_PRICE: u64 = 1; diff --git a/integration-tests/tests/citadel/int_test_user.rs b/integration-tests/tests/citadel/int_test_user.rs index 046c06d..70a6535 100644 --- a/integration-tests/tests/citadel/int_test_user.rs +++ b/integration-tests/tests/citadel/int_test_user.rs @@ -41,7 +41,7 @@ use zk_citadel::license::Request; const WALLET_PATH: &str = concat!(env!("HOME"), "/.dusk/rusk-wallet"); const PWD_HASH: &str = - "7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787"; + "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"; const GAS_LIMIT: u64 = 5_000_000_000; const GAS_PRICE: u64 = 1; diff --git a/integration-tests/tests/citadel/issue_license.rs b/integration-tests/tests/citadel/issue_license.rs index eb4e027..6bc913a 100644 --- a/integration-tests/tests/citadel/issue_license.rs +++ b/integration-tests/tests/citadel/issue_license.rs @@ -16,7 +16,7 @@ use wallet_accessor::Password::PwdHash; const WALLET_PATH: &str = concat!(env!("HOME"), "/.dusk/rusk-wallet"); const PWD_HASH: &str = - "7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787"; + "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"; const GAS_LIMIT: u64 = 5_000_000_000; const GAS_PRICE: u64 = 1; diff --git a/integration-tests/tests/citadel/send_request.rs b/integration-tests/tests/citadel/send_request.rs index e9db9c3..8b2cf38 100644 --- a/integration-tests/tests/citadel/send_request.rs +++ b/integration-tests/tests/citadel/send_request.rs @@ -22,7 +22,7 @@ use zk_citadel::license::Request; const WALLET_PATH: &str = concat!(env!("HOME"), "/.dusk/rusk-wallet"); const PWD_HASH: &str = - "7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787"; + "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"; const GAS_LIMIT: u64 = 5_000_000_000; const GAS_PRICE: u64 = 1; diff --git a/license-provider/src/reference_lp.rs b/license-provider/src/reference_lp.rs index 1aae847..ff23632 100644 --- a/license-provider/src/reference_lp.rs +++ b/license-provider/src/reference_lp.rs @@ -49,8 +49,15 @@ impl ReferenceLP { pub fn create>(lp_config_path: P) -> Result { let lp_config: LPConfig = LPConfig::from_file(lp_config_path)?; - let psk_bytes = hex::decode(lp_config.psk_lp)?; - let ssk_bytes = hex::decode(lp_config.ssk_lp)?; + Self::create_with_ssk_psk(lp_config.ssk_lp, lp_config.psk_lp) + } + + pub fn create_with_ssk_psk(ssk_lp: S, psk_lp: S) -> Result + where + S: AsRef, + { + let psk_bytes = hex::decode(psk_lp.as_ref())?; + let ssk_bytes = hex::decode(ssk_lp.as_ref())?; let psk_lp = PublicSpendKey::from_slice(psk_bytes.as_slice())?; let ssk_lp = SecretSpendKey::from_slice(ssk_bytes.as_slice())?; let vk_lp = ssk_lp.view_key(); diff --git a/moat-cli-lp/Cargo.toml b/moat-cli-lp/Cargo.toml new file mode 100644 index 0000000..0f77e20 --- /dev/null +++ b/moat-cli-lp/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "moat-cli-lp" +version = "0.1.0" +edition = "2021" + +[dependencies] +dusk-wallet = "0.20.0-rc.0" +wallet-accessor = { path = "../wallet-accessor" } +moat-core = { path = "../moat-core" } +license-provider = { path = "../license-provider" } +zk-citadel = "0.5" +rkyv = { version = "=0.7.39" } +toml-base-config = "0.1" +clap = { version = "4.0", features = ["derive", "env"] } +tokio = { version = "1.21", features = ["full"] } +serde = { version = "1", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = "0.3" +rand = "0.8" +requestty = "0.4.1" +hex = "0.4" +dusk-bytes = "0.1" +group = "0.13" +bytes = "1.4" +futures-core = "0.3" +reqwest = "0.11" +bytecheck = "0.6" +sha3 = "0.10" diff --git a/moat-cli-lp/README.md b/moat-cli-lp/README.md new file mode 100644 index 0000000..d7b5386 --- /dev/null +++ b/moat-cli-lp/README.md @@ -0,0 +1,31 @@ +# Moat CLI LP + +Command line interface to Dusk Citadel License Provider + +Available commands: + +- list relevant license requests +- issue license for a given request +- list licenses + +## Retrieve relevant license requests (LP) + +arguments: +- scope: either an entire blockchain, or block range, or N last blocks +- data for Rusk cluster connection +- LP's view key (created from LP's SSK) + +## Issue license for a given request (LP) + +arguments: +- data for wallet connection +- data for Rusk cluster connection +- gas limit +- gas price +- license (created from the relevant request and LP's SSK) + +## List licenses + +arguments: +- scope: block height range +- data for Rusk cluster connection diff --git a/moat-cli-lp/config.toml b/moat-cli-lp/config.toml new file mode 100644 index 0000000..914f8d8 --- /dev/null +++ b/moat-cli-lp/config.toml @@ -0,0 +1,6 @@ +rusk_address = "http://127.0.0.1:8080" +prover_address = "http://127.0.0.1:8080" +#rusk_address = "http://devnet.nodes.dusk.network:8585" +#prover_address = "http://devnet.provers.dusk.network:8686" +psk_lp = "136d747ff489bd06077f937508b9237ac093ff868dc2e232ab3af0ecd038873288560dbd8aa851e055bc408ebeb89509b26eb6e34b4b43214de467e3ef09594e" +ssk_lp = "fd611dc2cfe15488e3cb94b410fadd3a5e77057be64574eb9b6acaf967a37d0514d0ce88727a24d3756a08bb8ae072d8aaaa88f88768c8a9487fb50678ba5204" diff --git a/moat-cli-lp/src/args.rs b/moat-cli-lp/src/args.rs new file mode 100644 index 0000000..551ebbb --- /dev/null +++ b/moat-cli-lp/src/args.rs @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + /// Wallet directory [default: `$HOME/.dusk/rusk-wallet`] + #[clap(short, long)] + pub wallet_path: PathBuf, + + /// Blockchain access config directory + #[clap(short, long)] + pub config_path: PathBuf, + + /// Password for the wallet + #[clap(long, default_value_t = String::from(""), env = "RUSK_WALLET_PWD")] + pub password: String, + + /// Hash of the password for the wallet [default: ``] + #[clap(short, long, default_value_t = String::from(""))] + pub pwd_hash: String, + + /// Gas limit [default: `500000000`] + #[clap(long, default_value_t = 500000000)] + pub gas_limit: u64, + + /// Gas price [default: `1`] + #[clap(long, default_value_t = 1)] + pub gas_price: u64, +} diff --git a/moat-cli-lp/src/command.rs b/moat-cli-lp/src/command.rs new file mode 100644 index 0000000..816c165 --- /dev/null +++ b/moat-cli-lp/src/command.rs @@ -0,0 +1,164 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use crate::run_result::{ + IssueLicenseSummary, LicenseContractSummary, RequestsLPSummary, RunResult, +}; +use crate::{LPCliConfig, SeedableRng}; +use dusk_wallet::{RuskHttpClient, WalletPath}; +use license_provider::{LicenseIssuer, ReferenceLP}; +use moat_core::{BcInquirer, CitadelInquirer, Error}; +use rand::rngs::StdRng; +use wallet_accessor::{BlockchainAccessConfig, Password}; + +/// Commands that can be run against the Moat +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub(crate) enum Command { + /// List requests + ListRequestsLP, + /// Issue license + IssueLicenseLP { request_hash: String }, + /// List licenses (User) + ListLicenses, + /// Show state + ShowState, +} + +impl Command { + #[allow(clippy::too_many_arguments)] + pub async fn run( + self, + wallet_path: &WalletPath, + psw: &Password, + blockchain_access_config: &BlockchainAccessConfig, + config: &LPCliConfig, + gas_limit: u64, + gas_price: u64, + ) -> Result { + let run_result = match self { + Command::ListRequestsLP => { + Self::list_requests_lp(blockchain_access_config, config).await? + } + Command::IssueLicenseLP { request_hash } => { + Self::issue_license_lp( + wallet_path, + psw, + blockchain_access_config, + config, + gas_limit, + gas_price, + request_hash, + ) + .await? + } + Command::ListLicenses => { + Self::list_licenses(blockchain_access_config).await? + } + Command::ShowState => { + Self::show_state(blockchain_access_config).await? + } + }; + Ok(run_result) + } + + /// Command: List Requests LP + async fn list_requests_lp( + blockchain_access_config: &BlockchainAccessConfig, + config: &LPCliConfig, + ) -> Result { + let mut reference_lp = ReferenceLP::create_with_ssk_psk::<&str>( + config.ssk_lp.as_ref(), + config.psk_lp.as_ref(), + )?; + let (found_total, found_owned) = + reference_lp.scan(blockchain_access_config).await?; + let summary = RequestsLPSummary { + found_total, + found_owned, + }; + Ok(RunResult::RequestsLP( + summary, + reference_lp.requests_to_process, + )) + } + + #[allow(clippy::too_many_arguments)] + /// Command: Issue License LP + async fn issue_license_lp( + wallet_path: &WalletPath, + psw: &Password, + blockchain_access_config: &BlockchainAccessConfig, + config: &LPCliConfig, + gas_limit: u64, + gas_price: u64, + request_hash: String, + ) -> Result { + let mut rng = StdRng::from_entropy(); + let mut reference_lp = ReferenceLP::create_with_ssk_psk::<&str>( + config.ssk_lp.as_ref(), + config.psk_lp.as_ref(), + )?; + let (_total_count, _this_lp_count) = + reference_lp.scan(blockchain_access_config).await?; + + let request = reference_lp.get_request(&request_hash); + Ok(match request { + Some(request) => { + let license_issuer = LicenseIssuer::new( + blockchain_access_config.clone(), + wallet_path.clone(), + psw.clone(), + gas_limit, + gas_price, + ); + let (tx_id, license_blob) = license_issuer + .issue_license(&mut rng, &request, &reference_lp.ssk_lp) + .await?; + let summary = IssueLicenseSummary { + request, + tx_id: hex::encode(tx_id.to_bytes()), + license_blob, + }; + RunResult::IssueLicense(Some(summary)) + } + _ => RunResult::IssueLicense(None), + }) + } + + /// Command: List Licenses + async fn list_licenses( + blockchain_access_config: &BlockchainAccessConfig, + ) -> Result { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let end_height = BcInquirer::block_height(&client).await?; + let block_range = 0..(end_height + 1); + + let mut licenses_stream = + CitadelInquirer::get_licenses(&client, block_range.clone()).await?; + + let pairs = CitadelInquirer::find_all_licenses(&mut licenses_stream)?; + Ok(RunResult::ListLicenses( + block_range, + pairs.into_iter().map(|(_, l)| l).collect(), + )) + } + + /// Command: Show State + async fn show_state( + blockchain_access_config: &BlockchainAccessConfig, + ) -> Result { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let (num_licenses, _, num_sessions) = + CitadelInquirer::get_info(&client).await?; + let summary = LicenseContractSummary { + num_licenses, + num_sessions, + }; + Ok(RunResult::ShowState(summary)) + } +} diff --git a/moat-cli-lp/src/config.rs b/moat-cli-lp/src/config.rs new file mode 100644 index 0000000..16cb84e --- /dev/null +++ b/moat-cli-lp/src/config.rs @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use serde::{Deserialize, Serialize}; +use toml_base_config::BaseConfig; + +#[derive(Debug, Default, Deserialize, Serialize, Clone)] +pub struct LPCliConfig { + pub rusk_address: String, + pub prover_address: String, + pub ssk_lp: String, + pub psk_lp: String, +} + +impl BaseConfig for LPCliConfig { + const PACKAGE: &'static str = env!("CARGO_PKG_NAME"); +} diff --git a/moat-cli-lp/src/error.rs b/moat-cli-lp/src/error.rs new file mode 100644 index 0000000..37c1919 --- /dev/null +++ b/moat-cli-lp/src/error.rs @@ -0,0 +1,43 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::sync::Arc; + +#[derive(Debug)] +pub enum CliError { + /// Moat core error + Moat(Arc), + /// Interaction error + Interaction(Arc), + /// Parsing error + Parsing(Arc), + /// IO Error + IO(Arc), +} + +impl From for CliError { + fn from(e: moat_core::Error) -> Self { + CliError::Moat(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: requestty::ErrorKind) -> Self { + CliError::Interaction(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: clap::error::ErrorKind) -> Self { + CliError::Parsing(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: std::io::Error) -> Self { + CliError::IO(Arc::from(e)) + } +} diff --git a/moat-cli-lp/src/interactor.rs b/moat-cli-lp/src/interactor.rs new file mode 100644 index 0000000..1f4c587 --- /dev/null +++ b/moat-cli-lp/src/interactor.rs @@ -0,0 +1,112 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use crate::error::CliError; +use crate::{prompt, LPCliConfig}; +use crate::{Command, Menu}; +use dusk_wallet::WalletPath; +use moat_core::Error; +use requestty::{ErrorKind, Question}; +use wallet_accessor::{BlockchainAccessConfig, Password}; + +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +enum OpSelection { + Run(Box), + Exit, +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +enum CommandMenuItem { + ListRequestsLP, + IssueLicenseLP, + ListLicenses, + ShowState, + Exit, +} + +fn menu_operation() -> Result { + let cmd_menu = Menu::new() + .add(CommandMenuItem::ListRequestsLP, "List Requests") + .add(CommandMenuItem::IssueLicenseLP, "Issue License") + .add(CommandMenuItem::ListLicenses, "List Licenses") + .add(CommandMenuItem::ShowState, "Show state") + .separator() + .add(CommandMenuItem::Exit, "Exit"); + + let q = Question::select("theme") + .message("What would you like to do?") + .choices(cmd_menu.clone()) + .build(); + + let answer = requestty::prompt_one(q)?; + let cmd = cmd_menu.answer(&answer).to_owned(); + Ok(match cmd { + CommandMenuItem::ListRequestsLP => { + OpSelection::Run(Box::from(Command::ListRequestsLP)) + } + CommandMenuItem::IssueLicenseLP => { + OpSelection::Run(Box::from(Command::IssueLicenseLP { + request_hash: prompt::request_request_hash()?, + })) + } + CommandMenuItem::ListLicenses => { + OpSelection::Run(Box::from(Command::ListLicenses)) + } + CommandMenuItem::ShowState => { + OpSelection::Run(Box::from(Command::ShowState)) + } + CommandMenuItem::Exit => OpSelection::Exit, + }) +} + +pub struct Interactor { + pub wallet_path: WalletPath, + pub psw: Password, + pub blockchain_access_config: BlockchainAccessConfig, + pub config: LPCliConfig, + pub gas_limit: u64, + pub gas_price: u64, +} + +impl Interactor { + pub async fn run_loop(&mut self) -> Result<(), CliError> { + loop { + let op = menu_operation()?; + match op { + OpSelection::Exit => return Ok(()), + OpSelection::Run(command) => { + let result = command + .run( + &self.wallet_path, + &self.psw, + &self.blockchain_access_config, + &self.config, + self.gas_limit, + self.gas_price, + ) + .await; + match result { + Ok(run_result) => { + println!("{}", run_result); + } + Err(error) => match error { + Error::IO(arc) => { + println!("{}", arc.as_ref().to_string()); + } + Error::Transaction(bx) => { + println!("{}", bx.as_ref().to_string()); + } + _ => { + println!("{:?}", error); + } + }, + } + continue; + } + } + } + } +} diff --git a/moat-cli-lp/src/main.rs b/moat-cli-lp/src/main.rs new file mode 100644 index 0000000..d2bea81 --- /dev/null +++ b/moat-cli-lp/src/main.rs @@ -0,0 +1,75 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#![feature(stmt_expr_attributes)] + +mod args; +mod command; +mod config; +mod error; +mod interactor; +mod menu; +mod prompt; +mod run_result; + +use crate::args::Args; +use crate::command::Command; +use crate::menu::Menu; + +use clap::Parser; + +use crate::config::LPCliConfig; +use crate::error::CliError; +use crate::interactor::Interactor; +use dusk_wallet::WalletPath; +use rand::SeedableRng; +use toml_base_config::BaseConfig; +use wallet_accessor::BlockchainAccessConfig; +use wallet_accessor::Password::{Pwd, PwdHash}; + +#[tokio::main] +async fn main() -> Result<(), CliError> { + let cli = Args::parse(); + + let config_path = cli.config_path.as_path(); + let wallet_path = cli.wallet_path.as_path(); + let password = cli.password; + let pwd_hash = cli.pwd_hash; + let gas_limit = cli.gas_limit; + let gas_price = cli.gas_price; + + let config = LPCliConfig::load_path(config_path)?; + let blockchain_access_config = BlockchainAccessConfig { + rusk_address: config.rusk_address.clone(), + prover_address: config.prover_address.clone(), + }; + + let wallet_path = WalletPath::from(wallet_path.join("wallet.dat")); + let psw = if pwd_hash.is_empty() { + Pwd(password) + } else { + PwdHash(pwd_hash) + }; + + let mut interactor = Interactor { + wallet_path, + psw, + blockchain_access_config, + config, + gas_limit, + gas_price, + }; + + interactor.run_loop().await?; + + #[rustfmt::skip] + // old wallet.dat file format: + // cargo r --release --bin moat-cli-lp -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-lp/config.toml --pwd-hash 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787 + // new wallet.dat file format: + // cargo r --release --bin moat-cli-lp -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-lp/config.toml --pwd-hash 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 + + Ok(()) +} diff --git a/moat-cli-lp/src/menu.rs b/moat-cli-lp/src/menu.rs new file mode 100644 index 0000000..f34ec03 --- /dev/null +++ b/moat-cli-lp/src/menu.rs @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use core::fmt::Debug; +use std::collections::HashMap; +use std::hash::Hash; + +use requestty::question::Choice; +use requestty::{Answer, DefaultSeparator, Separator}; + +#[derive(Clone, Debug)] +pub struct Menu { + items: Vec>, + keys: HashMap, +} + +impl Default for Menu +where + K: Eq + Hash + Debug, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl Menu +where + K: Eq + Hash + Debug, +{ + pub fn new() -> Self { + Self { + items: vec![], + keys: HashMap::new(), + } + } + + pub fn title(title: T) -> Self + where + T: Into, + { + let title = format!("─ {:─<12}", format!("{} ", title.into())); + let title = Separator(title); + let items = vec![title]; + let keys = HashMap::new(); + + Self { items, keys } + } + + pub fn add(mut self, key: K, item: V) -> Self + where + V: Into>, + { + self.items.push(item.into()); + self.keys.insert(self.items.len() - 1, key); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(DefaultSeparator); + self + } + + pub fn separator_msg(mut self, msg: String) -> Self { + self.items.push(Separator(msg)); + self + } + + pub fn answer(&self, answer: &Answer) -> &K { + let index = answer.as_list_item().unwrap().index; + let key = self.keys.get(&index); + key.unwrap() + } + + pub fn extend(mut self, other: Self) -> Self { + let len = self.items.len(); + + self.items.extend(other.items); + + for (key, val) in other.keys.into_iter() { + self.keys.insert(key + len, val); + } + + self + } +} + +impl IntoIterator for Menu { + type Item = Choice; + type IntoIter = std::vec::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} diff --git a/moat-cli-lp/src/prompt.rs b/moat-cli-lp/src/prompt.rs new file mode 100644 index 0000000..0b42ca1 --- /dev/null +++ b/moat-cli-lp/src/prompt.rs @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use requestty::{ErrorKind, Question}; + +pub(crate) fn request_request_hash() -> Result { + let q = Question::input("request_hash") + .message("Please enter request hash:".to_string()) + .validate_on_key(|_, _| { + true // todo: add some validation of the request hash + }) + .validate(|request_hash, _| { + if request_hash.is_empty() { + Err("Please enter a valid request hash".to_string()) + } else { + Ok(()) + } + }) + .build(); + + let a = requestty::prompt_one(q)?; + let a_str = a.as_string().expect("answer to be a string").to_string(); + Ok(a_str) +} diff --git a/moat-cli-lp/src/run_result.rs b/moat-cli-lp/src/run_result.rs new file mode 100644 index 0000000..f164b94 --- /dev/null +++ b/moat-cli-lp/src/run_result.rs @@ -0,0 +1,132 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use rkyv::ser::serializers::AllocSerializer; +use std::fmt; +use std::ops::Range; +use zk_citadel::license::{License, Request}; +// use rkyv::{check_archived_root, Archive, Deserialize, Infallible, Serialize}; +use sha3::{Digest, Sha3_256}; + +pub struct RequestsLPSummary { + pub found_total: usize, + pub found_owned: usize, +} + +pub struct IssueLicenseSummary { + pub request: Request, + pub tx_id: String, + pub license_blob: Vec, +} + +pub struct LicenseContractSummary { + pub num_licenses: u32, + pub num_sessions: u32, +} + +#[allow(clippy::large_enum_variant)] +/// Possible results of running a command in interactive mode +pub enum RunResult { + RequestsLP(RequestsLPSummary, Vec), + IssueLicense(Option), + ListLicenses(Range, Vec), + ShowState(LicenseContractSummary), +} + +impl fmt::Display for RunResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use RunResult::*; + match self { + RequestsLP(summary, requests) => { + writeln!( + f, + "found {} requests total, {} requests for this LP:", + summary.found_total, summary.found_owned + )?; + for request in requests.iter() { + writeln!( + f, + "request to process by LP: {}", + RunResult::to_hash_hex(request) + )?; + } + Ok(()) + } + IssueLicense(summary) => match summary { + Some(summary) => { + writeln!( + f, + "issuing license for request: {}", + RunResult::to_hash_hex(&summary.request) + )?; + writeln!( + f, + "license issuing transaction {} confirmed", + summary.tx_id + )?; + writeln!( + f, + "issued license: {}", + RunResult::blob_to_hash_hex( + summary.license_blob.as_slice() + ) + )?; + Ok(()) + } + _ => { + writeln!(f, "Request not found")?; + Ok(()) + } + }, + ListLicenses(block_range, licenses) => { + writeln!( + f, + "getting licenses within the block height range {:?}:", + block_range + )?; + if licenses.is_empty() { + writeln!(f, "licenses not found")?; + } else { + for license in licenses.iter() { + writeln!( + f, + "license: {}", + RunResult::to_hash_hex(license), + )?; + } + } + Ok(()) + } + ShowState(summary) => { + writeln!( + f, + "license contract state - licenses: {}, sessions: {}", + summary.num_licenses, summary.num_sessions + )?; + Ok(()) + } + } + } +} + +impl RunResult { + pub fn to_hash_hex(object: &T) -> String + where + T: rkyv::Serialize>, + { + let blob = rkyv::to_bytes::<_, 16386>(object) + .expect("type should serialize correctly") + .to_vec(); + Self::blob_to_hash_hex(blob.as_slice()) + } + + pub fn blob_to_hash_hex(blob: &[u8]) -> String { + let mut hasher = Sha3_256::new(); + hasher.update(blob); + let result = hasher.finalize(); + hex::encode(result) + } +} diff --git a/moat-request-cli/Cargo.toml b/moat-cli-request/Cargo.toml similarity index 90% rename from moat-request-cli/Cargo.toml rename to moat-cli-request/Cargo.toml index ed76172..9041593 100644 --- a/moat-request-cli/Cargo.toml +++ b/moat-cli-request/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "moat-request-cli" +name = "moat-cli-request" version = "0.1.0" edition = "2021" @@ -13,3 +13,4 @@ tokio = { version = "1.21", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" rand = "0.8" +hex = "0.4" diff --git a/moat-request-cli/config.toml b/moat-cli-request/config.toml similarity index 100% rename from moat-request-cli/config.toml rename to moat-cli-request/config.toml diff --git a/moat-request-cli/request.json b/moat-cli-request/request.json similarity index 100% rename from moat-request-cli/request.json rename to moat-cli-request/request.json diff --git a/moat-request-cli/src/args.rs b/moat-cli-request/src/args.rs similarity index 100% rename from moat-request-cli/src/args.rs rename to moat-cli-request/src/args.rs diff --git a/moat-request-cli/src/main.rs b/moat-cli-request/src/main.rs similarity index 73% rename from moat-request-cli/src/main.rs rename to moat-cli-request/src/main.rs index 444592a..06c0201 100644 --- a/moat-request-cli/src/main.rs +++ b/moat-cli-request/src/main.rs @@ -6,19 +6,19 @@ //! Command line utility for submitting requests to the Dusk blockchain. //! -//! In order to use the moat-request-cli utility you need to install Dusk wallet +//! In order to use the moat-cli-request utility you need to install Dusk wallet //! first. Typically, your Dusk wallet is installed in ~/.dusk/rusk-wallet. //! Path to your wallet's directory needs to be provided as `wallet-path` -//! argument when using moat-request-cli. +//! argument when using moat-cli-request. //! Before usage you need to make sure that the default address of your wallet //! holds some Dusk funds, otherwise the utility won't be able to submit your //! request, as it needs funds for gas in order to do so. //! The CLI will also need password to your wallet, as well as a path to //! a configuration file containing blockchain urls. Example configuration file -//! is provided in the moat-request-cli project main directory, under the name +//! is provided in the moat-cli-request project main directory, under the name //! `config.toml`. The last thing you will need is an actual request. You will //! be able to provide it in a form of a json file. An example request file is -//! provided in the moat-request-cli project's main directory as `request.json`. +//! provided in the moat-cli-request project's main directory as `request.json`. //! //! Note that your wallet cannot be active when running this utility. //! @@ -34,22 +34,22 @@ //! - `wallet_path` - a path to wallet's location, e.g.: `--wallet_path //! ~/.dusk/rusk-wallet` //! - `config_path` - a path to configuratin file, e.g.: `--config_path -//! ./moat-request-cli/config.toml` +//! ./moat-cli-request/config.toml` //! - `password` - wallet's password in the clear, e.g: `--password mypass2!` //! - `psw_hash` - wallet's password's blake3 hash, e.g: `--psw_hash //! 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d5aabb` //! - `gas_limit` - gas limit, e.g.: `--gas_limit 500000000` //! - `gas_price` - gas price, e.g.: `--gas_price 1` //! - a full path (with a name) of the request file, e.g.: -//! `./moat-request-cli/request.json` +//! `./moat-cli-request/request.json` //! -//! Example full command line invocation of `moat-request-cli` may look as +//! Example full command line invocation of `moat-cli-request` may look as //! follows: //! -//! `cargo r --release --bin moat-request-cli -- --wallet-path -//! ~/.dusk/rusk-wallet --config-path ./moat-request-cli/config.toml +//! `cargo r --release --bin moat-cli-request -- --wallet-path +//! ~/.dusk/rusk-wallet --config-path ./moat-cli-request/config.toml //! --psw_hash 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d5aabb -//! ./moat-request-cli/request.json` +//! ./moat-cli-request/request.json` //! //! Note that when psw_hash argument is provided, the password argument will be //! ignored. @@ -60,8 +60,10 @@ mod args; use crate::args::Args; use clap::Parser; -use dusk_wallet::WalletPath; -use moat_core::{JsonLoader, RequestCreator, RequestJson, RequestSender}; +use dusk_wallet::{RuskHttpClient, WalletPath}; +use moat_core::{ + JsonLoader, RequestCreator, RequestJson, RequestSender, TxAwaiter, +}; use rand::rngs::StdRng; use rand::SeedableRng; use std::error::Error; @@ -105,7 +107,7 @@ async fn main() -> Result<(), Box> { PwdHash(pwd_hash) }; - RequestSender::send_request( + let tx_id = RequestSender::send_request( request, &blockchain_access_config, &wallet_path, @@ -114,10 +116,17 @@ async fn main() -> Result<(), Box> { gas_price, ) .await?; + let tx_id_str = hex::encode(tx_id.to_bytes()); + println!("transaction sent: {}", tx_id_str); + let client = RuskHttpClient::new(blockchain_access_config.rusk_address); + TxAwaiter::wait_for(&client, tx_id).await?; + println!("transaction confirmed: {}", tx_id_str); #[rustfmt::skip] - // cargo r --release --bin moat-request-cli -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-request-cli/config.toml --pwd-hash 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787 ./moat-request-cli/request.json // blake3 encoding of "password" - // cargo r --release --bin moat-request-cli -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-request-cli/config.toml --pwd-hash 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 ./moat-request-cli/request.json // sha256 encoding of "password" + // old wallet.dat file format: + // cargo r --release --bin moat-cli-request -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-request/config.toml --pwd-hash 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787 ./moat-cli-request/request.json + // new wallet.dat file format: + // cargo r --release --bin moat-cli-request -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-request/config.toml --pwd-hash 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 ./moat-cli-request/request.json Ok(()) } diff --git a/moat-cli-sp/Cargo.toml b/moat-cli-sp/Cargo.toml new file mode 100644 index 0000000..8191025 --- /dev/null +++ b/moat-cli-sp/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "moat-cli-sp" +version = "0.1.0" +edition = "2021" + +[dependencies] +dusk-wallet = "0.20.0-rc.0" +wallet-accessor = { path = "../wallet-accessor" } +moat-core = { path = "../moat-core" } +rkyv = { version = "=0.7.39" } +dusk-bls12_381 = "0.12" +toml-base-config = "0.1" +clap = { version = "4.0", features = ["derive", "env"] } +tokio = { version = "1.21", features = ["full"] } +tracing = "0.1" +tracing-subscriber = "0.3" +rand = "0.8" +requestty = "0.4.1" +hex = "0.4" +dusk-bytes = "0.1" +group = "0.13" +bytes = "1.4" +futures-core = "0.3" +reqwest = "0.11" +bytecheck = "0.6" +sha3 = "0.10" diff --git a/moat-cli-sp/README.md b/moat-cli-sp/README.md new file mode 100644 index 0000000..cc36e7a --- /dev/null +++ b/moat-cli-sp/README.md @@ -0,0 +1,9 @@ +# Moat CLI SP + +Command line interface to Dusk Citadel Service Provider + +Available commands: + +- request and obtain service from SP based one a session cookie +- get session from license contract based on a session id +- show license contract state diff --git a/moat-cli-sp/config.toml b/moat-cli-sp/config.toml new file mode 100644 index 0000000..a413b04 --- /dev/null +++ b/moat-cli-sp/config.toml @@ -0,0 +1,4 @@ +rusk_address = "http://127.0.0.1:8080" +prover_address = "http://127.0.0.1:8080" +#rusk_address = "http://devnet.nodes.dusk.network:8585" +#prover_address = "http://devnet.provers.dusk.network:8686" diff --git a/moat-cli-sp/src/args.rs b/moat-cli-sp/src/args.rs new file mode 100644 index 0000000..551ebbb --- /dev/null +++ b/moat-cli-sp/src/args.rs @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + /// Wallet directory [default: `$HOME/.dusk/rusk-wallet`] + #[clap(short, long)] + pub wallet_path: PathBuf, + + /// Blockchain access config directory + #[clap(short, long)] + pub config_path: PathBuf, + + /// Password for the wallet + #[clap(long, default_value_t = String::from(""), env = "RUSK_WALLET_PWD")] + pub password: String, + + /// Hash of the password for the wallet [default: ``] + #[clap(short, long, default_value_t = String::from(""))] + pub pwd_hash: String, + + /// Gas limit [default: `500000000`] + #[clap(long, default_value_t = 500000000)] + pub gas_limit: u64, + + /// Gas price [default: `1`] + #[clap(long, default_value_t = 1)] + pub gas_price: u64, +} diff --git a/moat-cli-sp/src/command.rs b/moat-cli-sp/src/command.rs new file mode 100644 index 0000000..1fd747b --- /dev/null +++ b/moat-cli-sp/src/command.rs @@ -0,0 +1,87 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use crate::run_result::{LicenseContractSummary, RunResult, SessionSummary}; +use dusk_bls12_381::BlsScalar; +use dusk_bytes::DeserializableSlice; +use dusk_wallet::RuskHttpClient; +use moat_core::{CitadelInquirer, Error, LicenseSessionId}; +use wallet_accessor::BlockchainAccessConfig; + +/// Commands that can be run against the Moat +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub(crate) enum Command { + /// Request Service (User) + RequestService { session_cookie: String }, + /// Get session (SP) + GetSession { session_id: String }, + /// Show state + ShowState, +} + +impl Command { + #[allow(clippy::too_many_arguments)] + pub async fn run( + self, + blockchain_access_config: &BlockchainAccessConfig, + ) -> Result { + let run_result = match self { + Command::RequestService { session_cookie: _ } => { + println!("Off-chain request service to be placed here"); + RunResult::Empty + } + Command::GetSession { session_id } => { + Self::get_session(blockchain_access_config, session_id).await? + } + Command::ShowState => { + Self::show_state(blockchain_access_config).await? + } + }; + Ok(run_result) + } + + /// Command: Get Session + async fn get_session( + blockchain_access_config: &BlockchainAccessConfig, + session_id: String, + ) -> Result { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let id = LicenseSessionId { + id: BlsScalar::from_slice( + hex::decode(session_id.clone())?.as_slice(), + )?, + }; + Ok(match CitadelInquirer::get_session(&client, id).await? { + Some(session) => { + let mut summary = SessionSummary { + session_id, + session: vec![], + }; + for s in session.public_inputs.iter() { + summary.session.push(hex::encode(s.to_bytes())); + } + RunResult::GetSession(Some(summary)) + } + _ => RunResult::GetSession(None), + }) + } + + /// Command: Show State + async fn show_state( + blockchain_access_config: &BlockchainAccessConfig, + ) -> Result { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let (num_licenses, _, num_sessions) = + CitadelInquirer::get_info(&client).await?; + let summary = LicenseContractSummary { + num_licenses, + num_sessions, + }; + Ok(RunResult::ShowState(summary)) + } +} diff --git a/moat-cli-sp/src/error.rs b/moat-cli-sp/src/error.rs new file mode 100644 index 0000000..37c1919 --- /dev/null +++ b/moat-cli-sp/src/error.rs @@ -0,0 +1,43 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::sync::Arc; + +#[derive(Debug)] +pub enum CliError { + /// Moat core error + Moat(Arc), + /// Interaction error + Interaction(Arc), + /// Parsing error + Parsing(Arc), + /// IO Error + IO(Arc), +} + +impl From for CliError { + fn from(e: moat_core::Error) -> Self { + CliError::Moat(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: requestty::ErrorKind) -> Self { + CliError::Interaction(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: clap::error::ErrorKind) -> Self { + CliError::Parsing(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: std::io::Error) -> Self { + CliError::IO(Arc::from(e)) + } +} diff --git a/moat-cli-sp/src/interactor.rs b/moat-cli-sp/src/interactor.rs new file mode 100644 index 0000000..a8e6811 --- /dev/null +++ b/moat-cli-sp/src/interactor.rs @@ -0,0 +1,103 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use crate::error::CliError; +use crate::prompt; +use crate::{Command, Menu}; +use dusk_wallet::WalletPath; +use moat_core::Error; +use requestty::{ErrorKind, Question}; +use wallet_accessor::{BlockchainAccessConfig, Password}; + +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +enum OpSelection { + Run(Box), + Exit, +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +enum CommandMenuItem { + RequestService, + GetSession, + ShowState, + Exit, +} + +fn menu_operation() -> Result { + let cmd_menu = Menu::new() + .add( + CommandMenuItem::RequestService, + "Request Service (Off-Chain)", + ) + .add(CommandMenuItem::GetSession, "Get Session (SP)") + .add(CommandMenuItem::ShowState, "Show state") + .separator() + .add(CommandMenuItem::Exit, "Exit"); + + let q = Question::select("theme") + .message("What would you like to do?") + .choices(cmd_menu.clone()) + .build(); + + let answer = requestty::prompt_one(q)?; + let cmd = cmd_menu.answer(&answer).to_owned(); + Ok(match cmd { + CommandMenuItem::RequestService => { + OpSelection::Run(Box::from(Command::RequestService { + session_cookie: prompt::request_session_cookie()?, + })) + } + CommandMenuItem::GetSession => { + OpSelection::Run(Box::from(Command::GetSession { + session_id: prompt::request_session_id()?, + })) + } + CommandMenuItem::ShowState => { + OpSelection::Run(Box::from(Command::ShowState)) + } + CommandMenuItem::Exit => OpSelection::Exit, + }) +} + +pub struct Interactor { + pub wallet_path: WalletPath, + pub psw: Password, + pub blockchain_access_config: BlockchainAccessConfig, + pub gas_limit: u64, + pub gas_price: u64, +} + +impl Interactor { + pub async fn run_loop(&mut self) -> Result<(), CliError> { + loop { + let op = menu_operation()?; + match op { + OpSelection::Exit => return Ok(()), + OpSelection::Run(command) => { + let result = + command.run(&self.blockchain_access_config).await; + match result { + Ok(run_result) => { + println!("{}", run_result); + } + Err(error) => match error { + Error::IO(arc) => { + println!("{}", arc.as_ref().to_string()); + } + Error::Transaction(bx) => { + println!("{}", bx.as_ref().to_string()); + } + _ => { + println!("{:?}", error); + } + }, + } + continue; + } + } + } + } +} diff --git a/moat-cli-sp/src/main.rs b/moat-cli-sp/src/main.rs new file mode 100644 index 0000000..f26dbb7 --- /dev/null +++ b/moat-cli-sp/src/main.rs @@ -0,0 +1,67 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#![feature(stmt_expr_attributes)] + +mod args; +mod command; +mod error; +mod interactor; +mod menu; +mod prompt; +mod run_result; + +use crate::args::Args; +use crate::command::Command; +use crate::menu::Menu; + +use clap::Parser; + +use crate::error::CliError; +use crate::interactor::Interactor; +use dusk_wallet::WalletPath; +use toml_base_config::BaseConfig; +use wallet_accessor::BlockchainAccessConfig; +use wallet_accessor::Password::{Pwd, PwdHash}; + +#[tokio::main] +async fn main() -> Result<(), CliError> { + let cli = Args::parse(); + + let config_path = cli.config_path.as_path(); + let wallet_path = cli.wallet_path.as_path(); + let password = cli.password; + let pwd_hash = cli.pwd_hash; + let gas_limit = cli.gas_limit; + let gas_price = cli.gas_price; + + let wallet_path = WalletPath::from(wallet_path.join("wallet.dat")); + let blockchain_access_config = + BlockchainAccessConfig::load_path(config_path)?; + let psw = if pwd_hash.is_empty() { + Pwd(password) + } else { + PwdHash(pwd_hash) + }; + + let mut interactor = Interactor { + wallet_path, + psw, + blockchain_access_config, + gas_limit, + gas_price, + }; + + interactor.run_loop().await?; + + #[rustfmt::skip] + // old wallet.dat file format: + // cargo r --release --bin moat-cli-sp -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-sp/config.toml --pwd-hash 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787 + // new wallet.dat file format: + // cargo r --release --bin moat-cli-sp -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-sp/config.toml --pwd-hash 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 + + Ok(()) +} diff --git a/moat-cli-sp/src/menu.rs b/moat-cli-sp/src/menu.rs new file mode 100644 index 0000000..f34ec03 --- /dev/null +++ b/moat-cli-sp/src/menu.rs @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use core::fmt::Debug; +use std::collections::HashMap; +use std::hash::Hash; + +use requestty::question::Choice; +use requestty::{Answer, DefaultSeparator, Separator}; + +#[derive(Clone, Debug)] +pub struct Menu { + items: Vec>, + keys: HashMap, +} + +impl Default for Menu +where + K: Eq + Hash + Debug, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl Menu +where + K: Eq + Hash + Debug, +{ + pub fn new() -> Self { + Self { + items: vec![], + keys: HashMap::new(), + } + } + + pub fn title(title: T) -> Self + where + T: Into, + { + let title = format!("─ {:─<12}", format!("{} ", title.into())); + let title = Separator(title); + let items = vec![title]; + let keys = HashMap::new(); + + Self { items, keys } + } + + pub fn add(mut self, key: K, item: V) -> Self + where + V: Into>, + { + self.items.push(item.into()); + self.keys.insert(self.items.len() - 1, key); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(DefaultSeparator); + self + } + + pub fn separator_msg(mut self, msg: String) -> Self { + self.items.push(Separator(msg)); + self + } + + pub fn answer(&self, answer: &Answer) -> &K { + let index = answer.as_list_item().unwrap().index; + let key = self.keys.get(&index); + key.unwrap() + } + + pub fn extend(mut self, other: Self) -> Self { + let len = self.items.len(); + + self.items.extend(other.items); + + for (key, val) in other.keys.into_iter() { + self.keys.insert(key + len, val); + } + + self + } +} + +impl IntoIterator for Menu { + type Item = Choice; + type IntoIter = std::vec::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} diff --git a/moat-cli-sp/src/prompt.rs b/moat-cli-sp/src/prompt.rs new file mode 100644 index 0000000..35e791e --- /dev/null +++ b/moat-cli-sp/src/prompt.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use requestty::{ErrorKind, Question}; + +pub(crate) fn request_session_id() -> Result { + let q = Question::input("session_id") + .message("Please enter session id:".to_string()) + .validate_on_key(|_, _| { + true // todo: add some validation of the session id + }) + .validate(|id, _| { + if id.is_empty() { + Err("Please enter a valid session id".to_string()) + } else { + Ok(()) + } + }) + .build(); + + let a = requestty::prompt_one(q)?; + let a_str = a.as_string().expect("answer to be a string").to_string(); + Ok(a_str) +} + +pub(crate) fn request_session_cookie() -> Result { + let q = Question::input("session_cookie") + .message("Please enter session cookie:".to_string()) + .validate_on_key(|_, _| { + true // todo: add some validation of the session id + }) + .validate(|id, _| { + if id.is_empty() { + Err("Please enter a valid session cookie".to_string()) + } else { + Ok(()) + } + }) + .build(); + + let a = requestty::prompt_one(q)?; + let a_str = a.as_string().expect("answer to be a string").to_string(); + Ok(a_str) +} diff --git a/moat-cli-sp/src/run_result.rs b/moat-cli-sp/src/run_result.rs new file mode 100644 index 0000000..588e16c --- /dev/null +++ b/moat-cli-sp/src/run_result.rs @@ -0,0 +1,60 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::fmt; + +pub struct SessionSummary { + pub session_id: String, + pub session: Vec, +} + +pub struct LicenseContractSummary { + pub num_licenses: u32, + pub num_sessions: u32, +} + +#[allow(clippy::large_enum_variant)] +/// Possible results of running a command in interactive mode +pub enum RunResult { + GetSession(Option), + ShowState(LicenseContractSummary), + Empty, +} + +impl fmt::Display for RunResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use RunResult::*; + match self { + GetSession(summary) => { + match summary { + Some(summary) => { + writeln!( + f, + "obtained session with id={}:", + summary.session_id + )?; + for s in summary.session.iter() { + writeln!(f, "{}", s)?; + } + } + _ => { + writeln!(f, "session not found")?; + } + } + Ok(()) + } + ShowState(summary) => { + writeln!( + f, + "license contract state - licenses: {}, sessions: {}", + summary.num_licenses, summary.num_sessions + )?; + Ok(()) + } + Empty => Ok(()), + } + } +} diff --git a/moat-cli-user/Cargo.toml b/moat-cli-user/Cargo.toml new file mode 100644 index 0000000..aa31482 --- /dev/null +++ b/moat-cli-user/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "moat-cli-user" +version = "0.1.0" +edition = "2021" + +[dependencies] +dusk-wallet = "0.20.0-rc.0" +wallet-accessor = { path = "../wallet-accessor" } +moat-core = { path = "../moat-core" } +zk-citadel = "0.5" +dusk-plonk = { version = "0.16", default-features = false, features = ["rkyv-impl", "alloc"] } +rkyv = { version = "=0.7.39" } +dusk-pki = "0.13" +dusk-bls12_381 = "0.12" +toml-base-config = "0.1" +clap = { version = "4.0", features = ["derive", "env"] } +tokio = { version = "1.21", features = ["full"] } +tracing = "0.1" +tracing-subscriber = "0.3" +rand = "0.8" +requestty = "0.4.1" +hex = "0.4" +dusk-bytes = "0.1" +group = "0.13" +bytes = "1.4" +futures-core = "0.3" +reqwest = "0.11" +bytecheck = "0.6" +sha3 = "0.10" diff --git a/moat-cli-user/README.md b/moat-cli-user/README.md new file mode 100644 index 0000000..d3a8ea8 --- /dev/null +++ b/moat-cli-user/README.md @@ -0,0 +1,49 @@ +# Moat CLI User + +Command line interface to Dusk Citadel User + +Available commands: + +- submit a license request to blockchain (USER) +- list requests present on blockchain (USER) +- list user's licenses (USER) +- compute proof and use license (USER) +- obtain service from SP (USER & SP) + +## Submit a license request to blockchain (User) + +arguments: +- data for wallet connection +- data for Rusk cluster connection +- user SSK (Secret Spend Key) +- provider PSK (Public Spend Key) +- gas limit +- gas price + +## Retrieve from blockchain the requests which were sent by the user (User) + +arguments: +- scope: either an entire blockchain, or block range, or N last blocks +- data for Rusk cluster connection +- user's view key (created from user's SSK) + +## List user's licenses (User) + +arguments: +- scope: block height range +- data for Rusk cluster connection +- user's view key (created from user's SSK) + +## Use license (User) + +arguments: +- data for wallet connection +- data for Rusk cluster connection +- license +- more - TBD + +## Obtains service from SP (User and SP) + +arguments: +TBD as we need to mock SP + diff --git a/moat-cli-user/config.toml b/moat-cli-user/config.toml new file mode 100644 index 0000000..a413b04 --- /dev/null +++ b/moat-cli-user/config.toml @@ -0,0 +1,4 @@ +rusk_address = "http://127.0.0.1:8080" +prover_address = "http://127.0.0.1:8080" +#rusk_address = "http://devnet.nodes.dusk.network:8585" +#prover_address = "http://devnet.provers.dusk.network:8686" diff --git a/moat-cli-user/request.json b/moat-cli-user/request.json new file mode 100644 index 0000000..f678c8b --- /dev/null +++ b/moat-cli-user/request.json @@ -0,0 +1,4 @@ +{ + "user_ssk": "c6afd78c8b3902b474d4c0972b62888e4b880dccf8da68e86266fefa45ee7505926f06ab82ac200995f1239d518fdb74903f225f4460d8db62f2449f6d4dc402", + "provider_psk": "136d747ff489bd06077f937508b9237ac093ff868dc2e232ab3af0ecd038873288560dbd8aa851e055bc408ebeb89509b26eb6e34b4b43214de467e3ef09594e" +} diff --git a/moat-cli-user/request2.json b/moat-cli-user/request2.json new file mode 100644 index 0000000..bb65dbf --- /dev/null +++ b/moat-cli-user/request2.json @@ -0,0 +1,4 @@ +{ + "user_ssk": "45654c72b065e143645ae5877524b96126c222005a8d6a1eca24c99627a45803a48481395dabdbe33cec4f89b36878b3c2f638c9796e34cffac0a02f27c21702", + "provider_psk": "29c4336ef24e585f4506e32e269c5363a71f7dcd74586b210c56e569ad2644e832c785f102dd3c985c705008ec188be819bac85b65c9f70decb9adcf4a72cc43" +} diff --git a/moat-cli-user/src/args.rs b/moat-cli-user/src/args.rs new file mode 100644 index 0000000..7ed2a27 --- /dev/null +++ b/moat-cli-user/src/args.rs @@ -0,0 +1,41 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + /// Path of the JSON file to be processed + pub json_path: PathBuf, + + /// Wallet directory [default: `$HOME/.dusk/rusk-wallet`] + #[clap(short, long)] + pub wallet_path: PathBuf, + + /// Blockchain access config directory + #[clap(short, long)] + pub config_path: PathBuf, + + /// Password for the wallet + #[clap(long, default_value_t = String::from(""), env = "RUSK_WALLET_PWD")] + pub password: String, + + /// Hash of the password for the wallet [default: ``] + #[clap(short, long, default_value_t = String::from(""))] + pub pwd_hash: String, + + /// Gas limit [default: `500000000`] + #[clap(long, default_value_t = 500000000)] + pub gas_limit: u64, + + /// Gas price [default: `1`] + #[clap(long, default_value_t = 1)] + pub gas_price: u64, +} diff --git a/moat-cli-user/src/command.rs b/moat-cli-user/src/command.rs new file mode 100644 index 0000000..d45e8fa --- /dev/null +++ b/moat-cli-user/src/command.rs @@ -0,0 +1,386 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use crate::interactor::SetupHolder; +use crate::run_result::{ + LicenseContractSummary, RequestsSummary, RunResult, SubmitRequestSummary, + UseLicenseSummary, +}; +use crate::SeedableRng; +use dusk_bls12_381::BlsScalar; +use dusk_bytes::DeserializableSlice; +use dusk_pki::{PublicSpendKey, SecretSpendKey}; +use dusk_plonk::prelude::*; +use dusk_wallet::{RuskHttpClient, WalletPath}; +use moat_core::{ + BcInquirer, CitadelInquirer, Error, LicenseCircuit, LicenseUser, + RequestCreator, RequestJson, RequestScanner, RequestSender, TxAwaiter, +}; +use rand::rngs::StdRng; +use wallet_accessor::{BlockchainAccessConfig, Password, WalletAccessor}; +use zk_citadel::license::{License, SessionCookie}; + +/// Commands that can be run against the Moat +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub(crate) enum Command { + /// Submit request (User) + SubmitRequest, + /// List requests (User) + ListRequestsUser, + /// List licenses (User) + ListLicenses, + /// Use license (User) + UseLicense { license_hash: String }, + /// Request Service (User) + RequestService { session_cookie: String }, + /// Show state + ShowState, +} + +static LABEL: &[u8] = b"dusk-network"; +const CAPACITY: usize = 17; // capacity required for the setup + +impl Command { + #[allow(clippy::too_many_arguments)] + pub async fn run( + self, + wallet_path: &WalletPath, + psw: &Password, + blockchain_access_config: &BlockchainAccessConfig, + gas_limit: u64, + gas_price: u64, + request_json: RequestJson, + setup_holder: &mut Option, + ) -> Result { + let run_result = match self { + Command::SubmitRequest => { + Self::submit_request( + wallet_path, + psw, + blockchain_access_config, + gas_limit, + gas_price, + request_json, + ) + .await? + } + Command::ListRequestsUser => { + Self::list_requests(wallet_path, psw, blockchain_access_config) + .await? + } + Command::ListLicenses => { + Self::list_licenses(blockchain_access_config, request_json) + .await? + } + Command::UseLicense { license_hash } => { + Self::use_license( + wallet_path, + psw, + blockchain_access_config, + gas_limit, + gas_price, + request_json, + setup_holder, + license_hash, + ) + .await? + } + Command::RequestService { session_cookie: _ } => { + println!("Off-chain request service to be placed here"); + RunResult::Empty + } + Command::ShowState => { + Self::show_state(blockchain_access_config).await? + } + }; + Ok(run_result) + } + + /// Command: Submit Request + async fn submit_request( + wallet_path: &WalletPath, + psw: &Password, + blockchain_access_config: &BlockchainAccessConfig, + gas_limit: u64, + gas_price: u64, + request_json: RequestJson, + ) -> Result { + let rng = &mut StdRng::from_entropy(); // seed_from_u64(0xcafe); + let request = RequestCreator::create_from_hex_args( + request_json.user_ssk, + request_json.provider_psk.clone(), + rng, + )?; + let request_hash = RunResult::to_hash_hex(&request); + let tx_id = RequestSender::send_request( + request, + blockchain_access_config, + wallet_path, + psw, + gas_limit, + gas_price, + ) + .await?; + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + TxAwaiter::wait_for(&client, tx_id).await?; + let summary = SubmitRequestSummary { + psk_lp: request_json.provider_psk, + tx_id: hex::encode(tx_id.to_bytes()), + request_hash, + }; + Ok(RunResult::SubmitRequest(summary)) + } + + /// Command: List Requests + async fn list_requests( + wallet_path: &WalletPath, + psw: &Password, + blockchain_access_config: &BlockchainAccessConfig, + ) -> Result { + let wallet_accessor = + WalletAccessor::create(wallet_path.clone(), psw.clone())?; + let note_hashes: Vec = wallet_accessor + .get_notes(blockchain_access_config) + .await? + .iter() + .flat_map(|n| n.nullified_by) + .collect(); + + let mut found_requests = vec![]; + let mut height = 0; + let mut found_total = 0usize; + loop { + let height_end = height + 10000; + let (requests, top, total) = + RequestScanner::scan_related_to_notes_in_block_range( + height, + height_end, + blockchain_access_config, + ¬e_hashes, + ) + .await?; + found_requests.extend(requests); + found_total += total; + if top <= height_end { + height = top; + break; + } + height = height_end; + } + let found_owned = found_requests.len(); + let summary = RequestsSummary { + height, + found_total, + found_owned, + }; + let run_result = RunResult::Requests(summary, found_requests); + Ok(run_result) + } + + /// Command: List Licenses + async fn list_licenses( + blockchain_access_config: &BlockchainAccessConfig, + request_json: RequestJson, + ) -> Result { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let end_height = BcInquirer::block_height(&client).await?; + let block_range = 0..(end_height + 1); + + let mut licenses_stream = + CitadelInquirer::get_licenses(&client, block_range.clone()).await?; + + let ssk_user = SecretSpendKey::from_slice( + hex::decode(request_json.user_ssk.clone())?.as_slice(), + )?; + + let pairs = CitadelInquirer::find_all_licenses(&mut licenses_stream)?; + let vk = ssk_user.view_key(); + let mut licenses = vec![]; + for (_pos, license) in pairs.into_iter() { + let is_owned = vk.owns(&license.lsa); + licenses.push((license, is_owned)); + } + Ok(RunResult::ListLicenses(block_range, licenses)) + } + + #[allow(clippy::too_many_arguments)] + /// Command: Use License + async fn use_license( + wallet_path: &WalletPath, + psw: &Password, + blockchain_access_config: &BlockchainAccessConfig, + gas_limit: u64, + gas_price: u64, + request_json: RequestJson, + setup_holder: &mut Option, + license_hash: String, + ) -> Result { + let pos_license = Self::get_license_to_use( + blockchain_access_config, + Some(&request_json), + license_hash.clone(), + ) + .await?; + Ok(match pos_license { + Some((pos, license)) => { + println!("using license: {}", RunResult::to_hash_hex(&license)); + let ssk_user = SecretSpendKey::from_slice( + hex::decode(request_json.user_ssk)?.as_slice(), + )?; + let psk_lp = PublicSpendKey::from_slice( + hex::decode(request_json.provider_psk)?.as_slice(), + )?; + let (tx_id, session_cookie) = Self::prove_and_send_use_license( + blockchain_access_config, + wallet_path, + psw, + psk_lp, + ssk_user, + &license, + pos, + gas_limit, + gas_price, + setup_holder, + ) + .await?; + let summary = UseLicenseSummary { + license_blob: RunResult::to_blob(&license), + tx_id: hex::encode(tx_id.to_bytes()), + user_attr: hex::encode(session_cookie.attr_data.to_bytes()), + session_id: hex::encode( + session_cookie.session_id.to_bytes(), + ), + session_cookie: RunResult::to_blob_hex(&session_cookie), + }; + RunResult::UseLicense(Some(summary)) + } + _ => RunResult::UseLicense(None), + }) + } + + /// Command: Show State + async fn show_state( + blockchain_access_config: &BlockchainAccessConfig, + ) -> Result { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let (num_licenses, _, num_sessions) = + CitadelInquirer::get_info(&client).await?; + let summary = LicenseContractSummary { + num_licenses, + num_sessions, + }; + Ok(RunResult::ShowState(summary)) + } + + async fn get_license_to_use( + blockchain_access_config: &BlockchainAccessConfig, + request_json: Option<&RequestJson>, + license_hash: String, + ) -> Result, Error> { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + let end_height = BcInquirer::block_height(&client).await?; + let block_heights = 0..(end_height + 1); + + let mut licenses_stream = + CitadelInquirer::get_licenses(&client, block_heights).await?; + + let ssk_user = SecretSpendKey::from_slice( + hex::decode( + request_json + .expect("request should be provided") + .user_ssk + .clone(), + )? + .as_slice(), + )?; + + let pairs = CitadelInquirer::find_owned_licenses( + ssk_user, + &mut licenses_stream, + )?; + Ok(if pairs.is_empty() { + None + } else { + for (pos, license) in pairs.iter() { + if license_hash == RunResult::to_hash_hex(license) { + return Ok(Some((*pos, license.clone()))); + } + } + None + }) + } + + #[allow(clippy::too_many_arguments)] + async fn prove_and_send_use_license( + blockchain_access_config: &BlockchainAccessConfig, + wallet_path: &WalletPath, + psw: &Password, + psk_lp: PublicSpendKey, + ssk_user: SecretSpendKey, + license: &License, + pos: u64, + gas_limit: u64, + gas_price: u64, + sh_opt: &mut Option, + ) -> Result<(BlsScalar, SessionCookie), Error> { + let client = + RuskHttpClient::new(blockchain_access_config.rusk_address.clone()); + // let (_, _, num_sessions) = CitadelInquirer::get_info(&client).await?; + // let challenge = JubJubScalar::from(num_sessions as u64 + 1); + let challenge = JubJubScalar::from(0xcafebabeu64); + let mut rng = StdRng::seed_from_u64(0xbeef); + + let setup_holder = match sh_opt { + Some(sh) => sh, + _ => { + println!("performing setup"); + let pp = PublicParameters::setup(1 << CAPACITY, &mut rng) + .expect("Initializing public parameters should succeed"); + println!("compiling circuit"); + let (prover, verifier) = + Compiler::compile::(&pp, LABEL) + .expect("Compiling circuit should succeed"); + let sh = SetupHolder { + pp, + prover, + verifier, + }; + *sh_opt = Some(sh); + sh_opt.as_ref().unwrap() + } + }; + + let opening = CitadelInquirer::get_merkle_opening(&client, pos) + .await? + .expect("Opening obtained successfully"); + + println!( + "calculating proof and calling license contract's use_license" + ); + let (tx_id, session_cookie) = LicenseUser::prove_and_use_license( + blockchain_access_config, + wallet_path, + psw, + &ssk_user, + &psk_lp, + &setup_holder.prover, + &setup_holder.verifier, + license, + opening, + &mut rng, + &challenge, + gas_limit, + gas_price, + ) + .await?; + TxAwaiter::wait_for(&client, tx_id).await?; + Ok((tx_id, session_cookie)) + } +} diff --git a/moat-cli-user/src/error.rs b/moat-cli-user/src/error.rs new file mode 100644 index 0000000..37c1919 --- /dev/null +++ b/moat-cli-user/src/error.rs @@ -0,0 +1,43 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::sync::Arc; + +#[derive(Debug)] +pub enum CliError { + /// Moat core error + Moat(Arc), + /// Interaction error + Interaction(Arc), + /// Parsing error + Parsing(Arc), + /// IO Error + IO(Arc), +} + +impl From for CliError { + fn from(e: moat_core::Error) -> Self { + CliError::Moat(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: requestty::ErrorKind) -> Self { + CliError::Interaction(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: clap::error::ErrorKind) -> Self { + CliError::Parsing(Arc::from(e)) + } +} + +impl From for CliError { + fn from(e: std::io::Error) -> Self { + CliError::IO(Arc::from(e)) + } +} diff --git a/moat-cli-user/src/interactor.rs b/moat-cli-user/src/interactor.rs new file mode 100644 index 0000000..15e2b0c --- /dev/null +++ b/moat-cli-user/src/interactor.rs @@ -0,0 +1,139 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use crate::error::CliError; +use crate::prompt; +use crate::{Command, Menu}; +use dusk_plonk::prelude::{Prover, PublicParameters, Verifier}; +use dusk_wallet::WalletPath; +use moat_core::{Error, RequestJson}; +use requestty::{ErrorKind, Question}; +use wallet_accessor::{BlockchainAccessConfig, Password}; + +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +enum OpSelection { + Run(Box), + Exit, +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +enum CommandMenuItem { + SubmitRequest, + ListRequestsUser, + ListLicenses, + UseLicense, + RequestService, + ShowState, + Exit, +} + +fn menu_operation() -> Result { + let cmd_menu = Menu::new() + .add(CommandMenuItem::SubmitRequest, "Submit Request") + .add(CommandMenuItem::ListRequestsUser, "List Requests") + .add(CommandMenuItem::ListLicenses, "List Licenses") + .add(CommandMenuItem::UseLicense, "Use License") + .add( + CommandMenuItem::RequestService, + "Request Service (Off-Chain)", + ) + .add(CommandMenuItem::ShowState, "Show state") + .separator() + .add(CommandMenuItem::Exit, "Exit"); + + let q = Question::select("theme") + .message("What would you like to do?") + .choices(cmd_menu.clone()) + .build(); + + let answer = requestty::prompt_one(q)?; + let cmd = cmd_menu.answer(&answer).to_owned(); + Ok(match cmd { + CommandMenuItem::SubmitRequest => { + OpSelection::Run(Box::from(Command::SubmitRequest)) + } + CommandMenuItem::ListRequestsUser => { + OpSelection::Run(Box::from(Command::ListRequestsUser)) + } + CommandMenuItem::ListLicenses => { + OpSelection::Run(Box::from(Command::ListLicenses)) + } + CommandMenuItem::UseLicense => { + OpSelection::Run(Box::from(Command::UseLicense { + license_hash: prompt::request_license_hash()?, + })) + } + CommandMenuItem::RequestService => { + OpSelection::Run(Box::from(Command::RequestService { + session_cookie: prompt::request_session_cookie()?, + })) + } + CommandMenuItem::ShowState => { + OpSelection::Run(Box::from(Command::ShowState)) + } + CommandMenuItem::Exit => OpSelection::Exit, + }) +} + +pub struct SetupHolder { + pub pp: PublicParameters, + pub prover: Prover, + pub verifier: Verifier, +} + +pub struct Interactor { + pub wallet_path: WalletPath, + pub psw: Password, + pub blockchain_access_config: BlockchainAccessConfig, + pub gas_limit: u64, + pub gas_price: u64, + pub request_json: RequestJson, + pub setup_holder: Option, +} + +impl Interactor { + pub async fn run_loop(&mut self) -> Result<(), CliError> { + loop { + let op = menu_operation()?; + match op { + OpSelection::Exit => return Ok(()), + OpSelection::Run(command) => { + let result = command + .run( + &self.wallet_path, + &self.psw, + &self.blockchain_access_config, + self.gas_limit, + self.gas_price, + self.request_json.clone(), + &mut self.setup_holder, + ) + .await; + match result { + Ok(run_result) => { + println!("{}", run_result); + } + Err(error) => { + match error { + Error::IO(arc) => { + println!("{}", arc.as_ref().to_string()); + } + Error::Transaction(bx) => { + println!("{}", bx.as_ref().to_string()); + } + _ => { + println!("{:?}", error); + } + } + println!(); + } + } + continue; + } + } + } + } +} diff --git a/moat-cli-user/src/main.rs b/moat-cli-user/src/main.rs new file mode 100644 index 0000000..0a73c98 --- /dev/null +++ b/moat-cli-user/src/main.rs @@ -0,0 +1,73 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +#![feature(stmt_expr_attributes)] + +mod args; +mod command; +mod error; +mod interactor; +mod menu; +mod prompt; +mod run_result; + +use crate::args::Args; +use crate::command::Command; +use crate::menu::Menu; + +use clap::Parser; + +use crate::error::CliError; +use crate::interactor::Interactor; +use dusk_wallet::WalletPath; +use moat_core::{JsonLoader, RequestJson}; +use rand::SeedableRng; +use toml_base_config::BaseConfig; +use wallet_accessor::BlockchainAccessConfig; +use wallet_accessor::Password::{Pwd, PwdHash}; + +#[tokio::main] +async fn main() -> Result<(), CliError> { + let cli = Args::parse(); + + let json_path = cli.json_path.as_path(); + let config_path = cli.config_path.as_path(); + let wallet_path = cli.wallet_path.as_path(); + let password = cli.password; + let pwd_hash = cli.pwd_hash; + let gas_limit = cli.gas_limit; + let gas_price = cli.gas_price; + + let request_json: RequestJson = RequestJson::from_file(json_path)?; + let wallet_path = WalletPath::from(wallet_path.join("wallet.dat")); + let blockchain_access_config = + BlockchainAccessConfig::load_path(config_path)?; + let psw = if pwd_hash.is_empty() { + Pwd(password) + } else { + PwdHash(pwd_hash) + }; + + let mut interactor = Interactor { + wallet_path, + psw, + blockchain_access_config, + gas_limit, + gas_price, + request_json, + setup_holder: None, + }; + + interactor.run_loop().await?; + + #[rustfmt::skip] + // old wallet.dat file format: + // cargo r --release --bin moat-cli-user -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-user/config.toml --pwd-hash 7f2611ba158b6dcea4a69c229c303358c5e04493abeadee106a4bfa464d55787 ./moat-cli-user/request.json + // new wallet.dat file format: + // cargo r --release --bin moat-cli-user -- --wallet-path ~/.dusk/rusk-wallet --config-path ./moat-cli-user/config.toml --pwd-hash 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 ./moat-cli-user/request.json + + Ok(()) +} diff --git a/moat-cli-user/src/menu.rs b/moat-cli-user/src/menu.rs new file mode 100644 index 0000000..f34ec03 --- /dev/null +++ b/moat-cli-user/src/menu.rs @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use core::fmt::Debug; +use std::collections::HashMap; +use std::hash::Hash; + +use requestty::question::Choice; +use requestty::{Answer, DefaultSeparator, Separator}; + +#[derive(Clone, Debug)] +pub struct Menu { + items: Vec>, + keys: HashMap, +} + +impl Default for Menu +where + K: Eq + Hash + Debug, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(dead_code)] +impl Menu +where + K: Eq + Hash + Debug, +{ + pub fn new() -> Self { + Self { + items: vec![], + keys: HashMap::new(), + } + } + + pub fn title(title: T) -> Self + where + T: Into, + { + let title = format!("─ {:─<12}", format!("{} ", title.into())); + let title = Separator(title); + let items = vec![title]; + let keys = HashMap::new(); + + Self { items, keys } + } + + pub fn add(mut self, key: K, item: V) -> Self + where + V: Into>, + { + self.items.push(item.into()); + self.keys.insert(self.items.len() - 1, key); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(DefaultSeparator); + self + } + + pub fn separator_msg(mut self, msg: String) -> Self { + self.items.push(Separator(msg)); + self + } + + pub fn answer(&self, answer: &Answer) -> &K { + let index = answer.as_list_item().unwrap().index; + let key = self.keys.get(&index); + key.unwrap() + } + + pub fn extend(mut self, other: Self) -> Self { + let len = self.items.len(); + + self.items.extend(other.items); + + for (key, val) in other.keys.into_iter() { + self.keys.insert(key + len, val); + } + + self + } +} + +impl IntoIterator for Menu { + type Item = Choice; + type IntoIter = std::vec::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} diff --git a/moat-cli-user/src/prompt.rs b/moat-cli-user/src/prompt.rs new file mode 100644 index 0000000..75a290a --- /dev/null +++ b/moat-cli-user/src/prompt.rs @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use requestty::{ErrorKind, Question}; + +pub(crate) fn request_session_cookie() -> Result { + let q = Question::input("session_cookie") + .message("Please enter session cookie:".to_string()) + .validate_on_key(|_, _| { + true // todo: add some validation of the session id + }) + .validate(|id, _| { + if id.is_empty() { + Err("Please enter a valid session cookie".to_string()) + } else { + Ok(()) + } + }) + .build(); + + let a = requestty::prompt_one(q)?; + let a_str = a.as_string().expect("answer to be a string").to_string(); + Ok(a_str) +} + +pub(crate) fn request_license_hash() -> Result { + let q = Question::input("license_hash") + .message("Please enter license hash:".to_string()) + .validate_on_key(|_, _| { + true // todo: add some validation of the license hash + }) + .validate(|license_hash, _| { + if license_hash.is_empty() { + Err("Please enter a valid license hash".to_string()) + } else { + Ok(()) + } + }) + .build(); + + let a = requestty::prompt_one(q)?; + let a_str = a.as_string().expect("answer to be a string").to_string(); + Ok(a_str) +} diff --git a/moat-cli-user/src/run_result.rs b/moat-cli-user/src/run_result.rs new file mode 100644 index 0000000..6fab81d --- /dev/null +++ b/moat-cli-user/src/run_result.rs @@ -0,0 +1,185 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use rkyv::ser::serializers::AllocSerializer; +use std::fmt; +use std::ops::Range; +use zk_citadel::license::{License, Request}; +// use rkyv::{check_archived_root, Archive, Deserialize, Infallible, Serialize}; +use sha3::{Digest, Sha3_256}; + +pub struct SubmitRequestSummary { + pub psk_lp: String, + pub tx_id: String, + pub request_hash: String, +} + +pub struct RequestsSummary { + pub height: u64, + pub found_total: usize, + pub found_owned: usize, +} + +pub struct UseLicenseSummary { + pub license_blob: Vec, + pub tx_id: String, + pub session_cookie: String, + pub user_attr: String, + pub session_id: String, +} + +pub struct LicenseContractSummary { + pub num_licenses: u32, + pub num_sessions: u32, +} + +#[allow(clippy::large_enum_variant)] +/// Possible results of running a command in interactive mode +pub enum RunResult { + SubmitRequest(SubmitRequestSummary), + Requests(RequestsSummary, Vec), + ListLicenses(Range, Vec<(License, bool)>), + UseLicense(Option), + ShowState(LicenseContractSummary), + Empty, +} + +impl fmt::Display for RunResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use RunResult::*; + match self { + SubmitRequest(summary) => { + writeln!( + f, + "submitting request to provider psk: {}", + summary.psk_lp + )?; + writeln!( + f, + "request submitting transaction {} confirmed", + summary.tx_id + )?; + writeln!(f, "request submitted: {}", summary.request_hash)?; + Ok(()) + } + Requests(summary, requests) => { + writeln!( + f, + "scanned {} blocks, found {} requests, {} owned requests:", + summary.height, summary.found_total, summary.found_owned, + )?; + for request in requests.iter() { + writeln!(f, "request: {}", Self::to_hash_hex(request))?; + } + Ok(()) + } + ListLicenses(block_range, licenses) => { + writeln!( + f, + "getting licenses within the block height range {:?}:", + block_range + )?; + if licenses.is_empty() { + writeln!(f, "licenses not found")?; + } else { + for (license, is_owned) in licenses.iter() { + writeln!( + f, + "license: {} {}", + RunResult::to_hash_hex(license), + if *is_owned { "owned" } else { "" } + )?; + } + } + Ok(()) + } + UseLicense(summary) => { + match summary { + Some(summary) => { + writeln!( + f, + "using license: {}", + Self::blob_to_hash_hex( + summary.license_blob.as_slice() + ) + )?; + writeln!( + f, + "use license executing transaction {} confirmed", + summary.tx_id + )?; + writeln!(f)?; + writeln!( + f, + "license {} used", + Self::blob_to_hash_hex( + summary.license_blob.as_slice() + ), + )?; + writeln!(f)?; + writeln!( + f, + "session cookie: {}", + summary.session_cookie + )?; + writeln!(f)?; + writeln!(f, "user attributes: {}", summary.user_attr)?; + writeln!(f, "session id: {}", summary.session_id)?; + } + _ => { + writeln!(f, "Please obtain a license")?; + } + } + Ok(()) + } + ShowState(summary) => { + writeln!( + f, + "license contract state - licenses: {}, sessions: {}", + summary.num_licenses, summary.num_sessions + )?; + Ok(()) + } + Empty => Ok(()), + } + } +} + +impl RunResult { + pub fn to_hash_hex(object: &T) -> String + where + T: rkyv::Serialize>, + { + let blob = rkyv::to_bytes::<_, 16386>(object) + .expect("type should serialize correctly") + .to_vec(); + Self::blob_to_hash_hex(blob.as_slice()) + } + + pub fn blob_to_hash_hex(blob: &[u8]) -> String { + let mut hasher = Sha3_256::new(); + hasher.update(blob); + let result = hasher.finalize(); + hex::encode(result) + } + + pub fn to_blob_hex(object: &T) -> String + where + T: rkyv::Serialize>, + { + let blob = Self::to_blob(object); + hex::encode(blob) + } + + pub fn to_blob(object: &T) -> Vec + where + T: rkyv::Serialize>, + { + rkyv::to_bytes::<_, 16386>(object) + .expect("type should serialize correctly") + .to_vec() + } +}