From bc6850d0d50b096127fc0ccf1ac2450bc49da6bd Mon Sep 17 00:00:00 2001 From: devwckd Date: Mon, 13 Oct 2025 19:15:26 -0300 Subject: [PATCH] feat: add cli commands --- cli/src/action/agent.rs | 210 ++++++++-- cli/src/action/application.rs | 237 ++++++++++++ cli/src/action/balance.rs | 39 +- cli/src/action/key.rs | 31 +- cli/src/action/mod.rs | 27 +- cli/src/action/namespace.rs | 79 ++-- cli/src/action/network.rs | 347 +++++++++++++++++ cli/src/action/permission.rs | 464 ++++++++++++++++++++++ cli/src/action/proposal.rs | 706 ++++++++++++++++++++++++++++++++++ cli/src/action/stake.rs | 129 +++---- cli/src/cli.rs | 533 ++++++++++++++++++++++--- cli/src/util.rs | 8 + 12 files changed, 2561 insertions(+), 249 deletions(-) create mode 100644 cli/src/action/application.rs create mode 100644 cli/src/action/network.rs create mode 100644 cli/src/action/permission.rs create mode 100644 cli/src/action/proposal.rs diff --git a/cli/src/action/agent.rs b/cli/src/action/agent.rs index cb72ce2..2d6abde 100644 --- a/cli/src/action/agent.rs +++ b/cli/src/action/agent.rs @@ -2,13 +2,13 @@ use std::fmt::Display; use anyhow::anyhow; use tabled::Table; -use torus_client::{client::TorusClient, subxt::utils::AccountId32}; +use torus_client::{client::TorusClient, interfaces, subxt::utils::AccountId32}; use crate::{ - action::{Action, ActionContext}, + action::{Action, ActionContext, Changes}, keypair::Keypair, store::{get_account, get_key}, - util::format_torus, + util::to_percent_u8, }; pub struct AgentInfoAction { @@ -19,12 +19,12 @@ impl Action for AgentInfoAction { type Params = String; type ResponseData = AgentInfoResponse; - async fn create(_ctx: &impl ActionContext, key: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, key: Self::Params) -> anyhow::Result { let account = get_account(&key)?; Ok(Self { account }) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { get_agent_info(ctx, &self.account) .await? .ok_or(anyhow::anyhow!("Not an agent.")) @@ -80,7 +80,7 @@ impl Action for RegisterAgentAction { type ResponseData = AgentInfoResponse; async fn create( - ctx: &impl ActionContext, + ctx: &mut impl ActionContext, (key, name, metadata, url): Self::Params, ) -> anyhow::Result { let key = get_key(&key)?; @@ -94,8 +94,8 @@ impl Action for RegisterAgentAction { }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let fee = if !ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -124,24 +124,19 @@ impl Action for RegisterAgentAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to register {} as an agent? {}\n[y/N]", - self.keypair.ss58_address(), - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![format!( + "register an agent with name {} which is not changeable later", + self.name + )], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { - if !ctx.is_testnet() { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -182,15 +177,15 @@ impl Action for UnregisterAgentAction { type Params = String; type ResponseData = UnregisterAgentActionResponse; - async fn create(ctx: &impl ActionContext, key: Self::Params) -> anyhow::Result { + async fn create(ctx: &mut impl ActionContext, key: Self::Params) -> anyhow::Result { let key = get_key(&key)?; let (_, keypair) = ctx.decrypt(&key)?; Ok(Self { keypair }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let fee = if !ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -209,24 +204,20 @@ impl Action for UnregisterAgentAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to unregister {} from agent? {}\n[y/N]", - self.keypair.ss58_address(), - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + + Ok(Some(Changes { + changes: vec![format!( + "Unregister the agent {}", + self.keypair.ss58_address() + )], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { - if !ctx.is_testnet() { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -255,8 +246,143 @@ impl Display for UnregisterAgentActionResponse { } } +pub struct UpdateAgentAction { + key: Keypair, + url: String, + metadata: Option, + staking_fee: Option, + weight_control_fee: Option, +} + +impl Action for UpdateAgentAction { + type Params = (String, String, Option, Option, Option); + type ResponseData = UpdateAgentActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, url, metadata, staking_fee, weight_control_fee): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + Ok(Self { + key: keypair, + url, + metadata, + staking_fee, + weight_control_fee, + }) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let staking_fee = self.staking_fee.map(to_percent_u8).transpose()?; + let weight_control_fee = self.weight_control_fee.map(to_percent_u8).transpose()?; + + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .torus0() + .calls() + .update_agent_fee( + self.url.as_bytes().to_vec(), + self.metadata.clone().map(|str| str.as_bytes().to_vec()), + staking_fee.map( + interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + weight_control_fee.map( + interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + self.key.clone(), + ) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .torus0() + .calls() + .update_agent_fee( + self.url.as_bytes().to_vec(), + self.metadata.clone().map(|str| str.as_bytes().to_vec()), + staking_fee.map( + interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + weight_control_fee.map( + interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + self.key.clone(), + ) + .await? + }; + + Ok(fee) + } + + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + Ok(Some(Changes { + changes: vec![format!( + "Update agent `{}` information", + self.key.ss58_address() + )], + fee: Some(fee), + })) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let staking_fee = self.staking_fee.map(to_percent_u8).transpose()?; + let weight_control_fee = self.weight_control_fee.map(to_percent_u8).transpose()?; + + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .torus0() + .calls() + .update_agent_wait( + self.url.as_bytes().to_vec(), + self.metadata.clone().map(|str| str.as_bytes().to_vec()), + staking_fee.map( + interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + weight_control_fee.map( + interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + self.key.clone(), + ) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .torus0() + .calls() + .update_agent_wait( + self.url.as_bytes().to_vec(), + self.metadata.clone().map(|str| str.as_bytes().to_vec()), + staking_fee.map( + interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + weight_control_fee.map( + interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent, + ), + self.key.clone(), + ) + .await?; + } + + Ok(UpdateAgentActionResponse) + } +} + +#[derive(serde::Serialize)] +pub struct UpdateAgentActionResponse; + +impl Display for UpdateAgentActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Updated successfully") + } +} + async fn get_agent_info( - ctx: &impl ActionContext, + ctx: &mut impl ActionContext, account: &AccountId32, ) -> anyhow::Result> { let agent_info = if ctx.is_testnet() { diff --git a/cli/src/action/application.rs b/cli/src/action/application.rs new file mode 100644 index 0000000..ded748f --- /dev/null +++ b/cli/src/action/application.rs @@ -0,0 +1,237 @@ +use std::fmt::Display; + +use anyhow::anyhow; +use tabled::{Table, Tabled}; +use torus_client::{client::TorusClient, subxt::ext::futures::StreamExt}; + +use crate::{ + action::{Action, ActionContext, Changes}, + keypair::Keypair, + store::get_key, +}; + +#[derive(Tabled, serde::Serialize)] +pub struct ApplicationEntry { + pub id: u32, + pub payer_key: String, + pub agent_key: String, + pub data: String, + pub cost: u128, + pub expires_at: u64, + pub action: String, + pub status: String, +} + +pub struct ApplicationListAction { + pub page: u32, + pub count: u32, +} + +impl Action for ApplicationListAction { + type Params = (u32, u32); + type ResponseData = ApplicationListActionResponse; + + async fn create( + _ctx: &mut impl ActionContext, + (page, count): Self::Params, + ) -> anyhow::Result { + Ok(Self { page, count }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + ctx.info("Fetching application list..."); + let applications = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .storage() + .agent_applications_iter() + .await? + .skip((self.page * self.count) as usize) + .take(self.count as usize) + .collect::>() + .await + .iter() + .map(|res| match res { + Ok((id, application)) => Ok(ApplicationEntry { + id: *id, + payer_key: application.payer_key.to_string(), + agent_key: application.agent_key.to_string(), + data: String::from_utf8_lossy(&application.data.0).to_string(), + cost: application.cost, + expires_at: application.expires_at, + action: match application.action { + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::application::ApplicationAction::Add => "add".to_string(), + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::application::ApplicationAction::Remove => "remove".to_string(), + }, + status: match application.status { + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::application::ApplicationStatus::Open => "open".to_string(), + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::application::ApplicationStatus::Resolved { accepted } => if accepted { "accepted".to_string() } else { "denied".to_string() }, + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::application::ApplicationStatus::Expired => "expired".to_string(), + }, + }), + Err(err) => Err(anyhow!("{err}")), + }) + .collect::, anyhow::Error>>()? + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .storage() + .agent_applications_iter() + .await? + .skip((self.page * self.count) as usize) + .take(self.count as usize) + .collect::>() + .await + .iter() + .map(|res| match res { + Ok((id, application)) => Ok(ApplicationEntry { + id: *id, + payer_key: application.payer_key.to_string(), + agent_key: application.agent_key.to_string(), + data: String::from_utf8_lossy(&application.data.0).to_string(), + cost: application.cost, + expires_at: application.expires_at, + action: match application.action { + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::application::ApplicationAction::Add => "add".to_string(), + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::application::ApplicationAction::Remove => "remove".to_string(), + }, + status: match application.status { + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::application::ApplicationStatus::Open => "open".to_string(), + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::application::ApplicationStatus::Resolved { accepted } => if accepted { "accepted".to_string() } else { "denied".to_string() }, + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::application::ApplicationStatus::Expired => "expired".to_string(), + }, + }), + Err(err) => Err(anyhow!("{err}")), + }) + .collect::, anyhow::Error>>()? + }; + + Ok(ApplicationListActionResponse { applications }) + } +} + +#[derive(serde::Serialize)] +pub struct ApplicationListActionResponse { + applications: Vec, +} + +impl Display for ApplicationListActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let table = Table::new(&self.applications); + writeln!(f, "{table}") + } +} + +pub struct ApplicationCreateAction { + key: Keypair, + metadata: String, + remove: bool, +} + +impl Action for ApplicationCreateAction { + type Params = (String, String, bool); + type ResponseData = ApplicationCreateActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, metadata, remove): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + Ok(Self { + key: keypair, + metadata, + remove, + }) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .calls() + .submit_application_fee( + self.key.account(), + self.metadata.as_bytes().to_vec(), + self.remove, + self.key.clone(), + ) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .calls() + .submit_application_fee( + self.key.account(), + self.metadata.as_bytes().to_vec(), + self.remove, + self.key.clone(), + ) + .await? + }; + + Ok(fee) + } + + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + Ok(Some(Changes { + changes: if !self.remove { + vec![format!( + "Apply to add `{}` as an agent", + self.key.ss58_address() + )] + } else { + vec![format!( + "Apply to remove `{}` from agents", + self.key.ss58_address() + )] + }, + fee: Some(fee), + })) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .calls() + .submit_application_wait( + self.key.account(), + self.metadata.as_bytes().to_vec(), + self.remove, + self.key.clone(), + ) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .calls() + .submit_application_wait( + self.key.account(), + self.metadata.as_bytes().to_vec(), + self.remove, + self.key.clone(), + ) + .await?; + } + + Ok(ApplicationCreateActionResponse) + } +} + +#[derive(serde::Serialize)] +pub struct ApplicationCreateActionResponse; + +impl Display for ApplicationCreateActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Application created successfully") + } +} diff --git a/cli/src/action/balance.rs b/cli/src/action/balance.rs index 60bc1a8..1a61080 100644 --- a/cli/src/action/balance.rs +++ b/cli/src/action/balance.rs @@ -10,7 +10,7 @@ use torus_client::{ }; use crate::{ - action::{Action, ActionContext}, + action::{Action, ActionContext, Changes}, keypair::Keypair, store::{get_account, get_key}, util::format_torus, @@ -25,12 +25,12 @@ impl Action for CheckBalanceAction { type ResponseData = CheckBalanceActionResponse; - async fn create(_ctx: &impl ActionContext, key: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, key: Self::Params) -> anyhow::Result { let account = get_account(&key)?; Ok(Self { account }) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { let data = if ctx.is_testnet() { let client = TorusClient::for_testnet().await?; client @@ -93,8 +93,8 @@ impl Action for TransferBalanceAction { type ResponseData = TransferBalanceActionResponse; - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let fee = if !ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .balances() @@ -121,26 +121,21 @@ impl Action for TransferBalanceAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to transfer {} to {}? {}\n[y/N]", - format_torus(self.amount), - self.target, - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![format!( + "Transfer {} to {}", + format_torus(self.amount), + self.target, + )], + fee: Some(fee), + })) } async fn create( - ctx: &impl ActionContext, + ctx: &mut impl ActionContext, (key, target, amount): Self::Params, ) -> anyhow::Result { let key = get_key(&key)?; @@ -155,8 +150,8 @@ impl Action for TransferBalanceAction { }) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { - if !ctx.is_testnet() { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .balances() diff --git a/cli/src/action/key.rs b/cli/src/action/key.rs index a2380dd..d9ef658 100644 --- a/cli/src/action/key.rs +++ b/cli/src/action/key.rs @@ -6,7 +6,7 @@ use inquire::Password; use tabled::Table; use crate::{ - action::{Action, ActionContext}, + action::{Action, ActionContext, Changes}, keypair::generate_sr25519_keypair, store::{delete_key, get_all_keys, get_key, key_exists, store_new_key, Key}, }; @@ -17,11 +17,11 @@ impl Action for ListKeysAction { type Params = (); type ResponseData = ListKeysActionResponse; - async fn create(_ctx: &impl ActionContext, _params: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, _params: Self::Params) -> anyhow::Result { Ok(ListKeysAction) } - async fn execute(&self, _ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, _ctx: &mut impl ActionContext) -> anyhow::Result { let keys = get_all_keys()? .into_iter() .map(|key| KeyEntry { @@ -70,7 +70,7 @@ impl Action for CreateKeyAction { type ResponseData = CreateKeyActionResponse; async fn create( - _ctx: &impl ActionContext, + _ctx: &mut impl ActionContext, (name, no_password, mnemonic): Self::Params, ) -> anyhow::Result { Ok(Self { @@ -80,7 +80,7 @@ impl Action for CreateKeyAction { }) } - async fn execute(&self, _ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, _ctx: &mut impl ActionContext) -> anyhow::Result { let password = if self.no_password { None } else { @@ -138,7 +138,7 @@ impl Action for DeleteKeyAction { type Params = String; type ResponseData = DeleteKeyActionResponse; - async fn create(_ctx: &impl ActionContext, key: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, key: Self::Params) -> anyhow::Result { if !key_exists(&key) { bail!("No keys found with this name."); } @@ -146,17 +146,14 @@ impl Action for DeleteKeyAction { Ok(Self { key }) } - async fn confirmation_phrase( - &self, - _ctx: &impl ActionContext, - ) -> anyhow::Result> { - Ok(Some(format!( - "Are you sure you want to delete the key `{}`?\n[y/N]", - self.key, - ))) + async fn get_changes(&self, _ctx: &mut impl ActionContext) -> anyhow::Result> { + Ok(Some(Changes { + changes: vec![format!("Delete the key `{}`", self.key,)], + fee: None, + })) } - async fn execute(&self, _ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, _ctx: &mut impl ActionContext) -> anyhow::Result { delete_key(&self.key)?; Ok(DeleteKeyActionResponse) @@ -180,13 +177,13 @@ impl Action for KeyInfoAction { type Params = String; type ResponseData = KeyInfoActionResponse; - async fn create(ctx: &impl ActionContext, key: Self::Params) -> anyhow::Result { + async fn create(ctx: &mut impl ActionContext, key: Self::Params) -> anyhow::Result { let key = get_key(&key)?; let (key, _) = ctx.decrypt(&key)?; Ok(Self { key }) } - async fn execute(&self, _ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, _ctx: &mut impl ActionContext) -> anyhow::Result { Ok(KeyInfoActionResponse { name: self.key.name(), address: self.key.ss58_address.clone(), diff --git a/cli/src/action/mod.rs b/cli/src/action/mod.rs index dd218ae..45edc32 100644 --- a/cli/src/action/mod.rs +++ b/cli/src/action/mod.rs @@ -3,39 +3,46 @@ use std::fmt::Display; use crate::{keypair::Keypair, store::Key}; pub mod agent; +pub mod application; pub mod balance; pub mod key; pub mod namespace; +pub mod network; +pub mod permission; +pub mod proposal; pub mod stake; pub trait ActionContext { fn is_json(&self) -> bool; fn is_testnet(&self) -> bool; + fn is_mainnet(&self) -> bool; fn is_dry_run(&self) -> bool; - fn confirm(&self, desc: &str) -> anyhow::Result<()>; + fn confirm(&mut self, desc: &str) -> anyhow::Result<()>; - fn decrypt(&self, key: &Key) -> anyhow::Result<(Key, Keypair)>; + fn decrypt(&mut self, key: &Key) -> anyhow::Result<(Key, Keypair)>; - fn info(&self, message: impl AsRef); + fn info(&mut self, message: impl AsRef); } pub trait Action: Sized { type Params; type ResponseData: serde::Serialize + Display; - async fn create(ctx: &impl ActionContext, params: Self::Params) -> anyhow::Result; + async fn create(ctx: &mut impl ActionContext, params: Self::Params) -> anyhow::Result; - async fn estimate_fee(&self, _ctx: &impl ActionContext) -> anyhow::Result { + async fn estimate_fee(&self, _ctx: &mut impl ActionContext) -> anyhow::Result { Ok(0) } - async fn confirmation_phrase( - &self, - _ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, _ctx: &mut impl ActionContext) -> anyhow::Result> { Ok(None) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result; + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result; +} + +pub struct Changes { + pub changes: Vec, + pub fee: Option, } diff --git a/cli/src/action/namespace.rs b/cli/src/action/namespace.rs index 6f07a40..d922a46 100644 --- a/cli/src/action/namespace.rs +++ b/cli/src/action/namespace.rs @@ -10,10 +10,9 @@ use torus_client::{ }; use crate::{ - action::{Action, ActionContext}, + action::{Action, ActionContext, Changes}, keypair::Keypair, store::{get_account, get_key}, - util::format_torus, }; pub struct NamespaceInfoAction { @@ -24,12 +23,12 @@ impl Action for NamespaceInfoAction { type Params = String; type ResponseData = NamespaceInfoActionResponse; - async fn create(_ctx: &impl ActionContext, account: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, account: Self::Params) -> anyhow::Result { let account = get_account(&account)?; Ok(Self { account }) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { ctx.info("Fetching namespace data..."); let entries = if ctx.is_testnet() { @@ -86,34 +85,36 @@ impl Action for RegisterNamespaceAction { type Params = (String, String); type ResponseData = RegisterNamespaceActionResponse; - async fn create(ctx: &impl ActionContext, (key, path): Self::Params) -> anyhow::Result { + async fn create( + ctx: &mut impl ActionContext, + (key, path): Self::Params, + ) -> anyhow::Result { let key = get_key(&key)?; let (_, keypair) = ctx.decrypt(&key)?; Ok(Self { keypair, path }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let (fee, deposit) = if ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_testnet() { let client = TorusClient::for_testnet().await?; client - .rpc() - .namespace_path_creation_cost(self.keypair.account(), self.path.clone()) + .torus0() + .calls() + .create_namespace_fee(torus_client::interfaces::testnet::api::runtime_types::bounded_collections::bounded_vec::BoundedVec(self.path.as_bytes().to_vec()), self.keypair.clone()) .await? } else { let client = TorusClient::for_mainnet().await?; client - .rpc() - .namespace_path_creation_cost(self.keypair.account(), self.path.clone()) + .torus0() + .calls() + .create_namespace_fee(torus_client::interfaces::mainnet::api::runtime_types::bounded_collections::bounded_vec::BoundedVec(self.path.as_bytes().to_vec()), self.keypair.clone()) .await? }; - Ok(fee + deposit) + Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let (fee, deposit) = if ctx.is_testnet() { let client = TorusClient::for_testnet().await?; client @@ -128,19 +129,16 @@ impl Action for RegisterNamespaceAction { .await? }; - Ok(Some(format!( - "Are you sure you want to register namespace {} for {} torus? {}\n[y/N]", - self.path, - format_torus(deposit), - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![ + format!("Register namespace {}", self.path,), + format!("Charge {} torus for it", deposit + fee), + ], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { ctx.info("Registering namespace..."); if ctx.is_testnet() { @@ -181,13 +179,16 @@ impl Action for UnregisterNamespaceAction { type Params = (String, String); type ResponseData = UnregisterNamespaceActionResponse; - async fn create(ctx: &impl ActionContext, (key, path): Self::Params) -> anyhow::Result { + async fn create( + ctx: &mut impl ActionContext, + (key, path): Self::Params, + ) -> anyhow::Result { let key = get_key(&key)?; let (_, keypair) = ctx.decrypt(&key)?; Ok(Self { keypair, path }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { let fee = if ctx.is_testnet() { let client = TorusClient::for_testnet().await?; client @@ -207,24 +208,16 @@ impl Action for UnregisterNamespaceAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to unregister namespace {}? {}\n[y/N]", - self.path, - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![format!("Unregister namespace {}", self.path,)], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { ctx.info("Unregistering namespace..."); if ctx.is_testnet() { diff --git a/cli/src/action/network.rs b/cli/src/action/network.rs new file mode 100644 index 0000000..7c28c08 --- /dev/null +++ b/cli/src/action/network.rs @@ -0,0 +1,347 @@ +use std::fmt::Display; + +use tabled::Table; +use torus_client::{client::TorusClient, subxt::utils::AccountId32}; + +use crate::action::{Action, ActionContext}; + +pub struct PrintNetworkInfoAction; + +impl Action for PrintNetworkInfoAction { + type Params = (); + type ResponseData = PrintNetworkInfoActionResponse; + + async fn create(_ctx: &mut impl ActionContext, _params: Self::Params) -> anyhow::Result { + Ok(Self) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let global_params = fetch_global_params(ctx).await?; + + Ok(PrintNetworkInfoActionResponse { global_params }) + } +} + +#[derive(serde::Serialize, tabled::Tabled)] +pub struct GlobalParams { + pub last_block: u32, + pub min_name_length: u16, + pub max_name_length: u16, + pub min_weight_control_fee: u8, + pub min_staking_fee: u8, + pub dividends_paticipation_weight: u8, + pub deposit_per_byte: u128, + pub base_fee: u128, + pub count_midpoint: u32, + pub fee_steepness: u8, + pub max_fee_multiplier: u32, + pub proposal_cost: u128, + pub proposal_expiration: u64, + pub agent_application_cost: u128, + pub agent_application_expiration: u64, + pub proposal_reward_treasury_allocation: u8, + pub max_proposal_reward_treasury_allocation: u128, + pub proposal_reward_interval: u64, + pub recycling_percentage: u8, +} + +#[derive(serde::Serialize)] +pub struct PrintNetworkInfoActionResponse { + #[serde(flatten)] + pub global_params: GlobalParams, +} + +impl Display for PrintNetworkInfoActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let table = Table::kv(std::iter::once(&self.global_params)); + write!(f, "{table}") + } +} + +pub struct PrintNetworkSupplyAction; + +impl Action for PrintNetworkSupplyAction { + type Params = (); + type ResponseData = PrintNetworkSupplyActionResponse; + + async fn create(_ctx: &mut impl ActionContext, _params: Self::Params) -> anyhow::Result { + Ok(Self) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let (total_issuance, total_stake) = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + ( + client + .balances() + .storage() + .total_issuance() + .await? + .unwrap_or(0), + client.torus0().storage().total_stake().await?.unwrap_or(0), + ) + } else { + let client = TorusClient::for_testnet().await?; + ( + client + .balances() + .storage() + .total_issuance() + .await? + .unwrap_or(0), + client.torus0().storage().total_stake().await?.unwrap_or(0), + ) + }; + + Ok(PrintNetworkSupplyActionResponse { + total_issuance, + total_stake, + }) + } +} + +#[derive(serde::Serialize, tabled::Tabled)] +pub struct PrintNetworkSupplyActionResponse { + total_issuance: u128, + total_stake: u128, +} + +impl Display for PrintNetworkSupplyActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let table = Table::kv(std::iter::once(&self)); + write!(f, "{table}") + } +} + +pub struct PrintTreasuryAddressAction; + +impl Action for PrintTreasuryAddressAction { + type Params = (); + type ResponseData = PrintTreasuryAddressActionResponse; + + async fn create(_ctx: &mut impl ActionContext, _params: Self::Params) -> anyhow::Result { + Ok(Self) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let address = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client.governance().storage().dao_treasury_address().await? + } else { + let client = TorusClient::for_testnet().await?; + client.governance().storage().dao_treasury_address().await? + } + .ok_or(anyhow::anyhow!("No treasury address found!"))?; + + Ok(PrintTreasuryAddressActionResponse { address }) + } +} + +#[derive(serde::Serialize)] +pub struct PrintTreasuryAddressActionResponse { + address: AccountId32, +} + +impl Display for PrintTreasuryAddressActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "The treasury address is `{}`", self.address) + } +} + +pub async fn fetch_global_params(ctx: &mut impl ActionContext) -> anyhow::Result { + let ( + last_block, + min_name_length, + max_name_length, + dividends_paticipation_weight, + min_weight_control_fee, + min_staking_fee, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + proposal_expiration, + agent_application_cost, + agent_application_expiration, + proposal_reward_treasury_allocation, + max_proposal_reward_treasury_allocation, + proposal_reward_interval, + recycling_percentage, + ) = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + let torus0_storage = client.torus0().storage(); + let fees = torus0_storage.fee_constraints().await?; + let namespace_pricing_config = torus0_storage.namespace_pricing_config().await?; + let governance_storage = client.governance().storage(); + let governance_config = governance_storage.global_governance_config().await?; + ( + client.latest_block().await?.number(), + torus0_storage.min_name_length().await?.unwrap_or_default(), + torus0_storage.max_name_length().await?.unwrap_or_default(), + torus0_storage + .dividends_participation_weight() + .await? + .map(|percent| percent.0) + .unwrap_or_default(), + fees.as_ref() + .map(|fees| fees.min_weight_control_fee.0) + .unwrap_or_default(), + fees.map(|fees| fees.min_staking_fee.0).unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.deposit_per_byte) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.base_fee) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.count_midpoint) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.fee_steepness.0) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.max_fee_multiplier) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_cost) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_expiration) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.agent_application_cost) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.agent_application_expiration) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_reward_treasury_allocation.0) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.max_proposal_reward_treasury_allocation) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_reward_interval) + .unwrap_or_default(), + client + .emission0() + .storage() + .emission_recycling_percentage() + .await? + .map(|perc| perc.0) + .unwrap_or_default(), + ) + } else { + let client = TorusClient::for_testnet().await?; + let torus0_storage = client.torus0().storage(); + let fees = torus0_storage.fee_constraints().await?; + let namespace_pricing_config = torus0_storage.namespace_pricing_config().await?; + let governance_storage = client.governance().storage(); + let governance_config = governance_storage.global_governance_config().await?; + ( + client.latest_block().await?.number(), + torus0_storage.min_name_length().await?.unwrap_or_default(), + torus0_storage.max_name_length().await?.unwrap_or_default(), + torus0_storage + .dividends_participation_weight() + .await? + .map(|percent| percent.0) + .unwrap_or_default(), + fees.as_ref() + .map(|fees| fees.min_weight_control_fee.0) + .unwrap_or_default(), + fees.map(|fees| fees.min_staking_fee.0).unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.deposit_per_byte) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.base_fee) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.count_midpoint) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.fee_steepness.0) + .unwrap_or_default(), + namespace_pricing_config + .as_ref() + .map(|cfg| cfg.max_fee_multiplier) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_cost) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_expiration) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.agent_application_cost) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.agent_application_expiration) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_reward_treasury_allocation.0) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.max_proposal_reward_treasury_allocation) + .unwrap_or_default(), + governance_config + .as_ref() + .map(|governace_config| governace_config.proposal_reward_interval) + .unwrap_or_default(), + client + .emission0() + .storage() + .emission_recycling_percentage() + .await? + .map(|perc| perc.0) + .unwrap_or_default(), + ) + }; + + Ok(GlobalParams { + last_block, + min_name_length, + max_name_length, + dividends_paticipation_weight, + min_weight_control_fee, + min_staking_fee, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + proposal_expiration, + agent_application_cost, + agent_application_expiration, + proposal_reward_treasury_allocation, + max_proposal_reward_treasury_allocation, + proposal_reward_interval, + recycling_percentage, + }) +} diff --git a/cli/src/action/permission.rs b/cli/src/action/permission.rs new file mode 100644 index 0000000..245f8f0 --- /dev/null +++ b/cli/src/action/permission.rs @@ -0,0 +1,464 @@ +use std::{fmt::Display, str::FromStr}; + +use sp_core::H256; +use torus_client::{client::TorusClient, subxt::utils::AccountId32}; + +use crate::{ + action::{self, Action, ActionContext, Changes}, + keypair::Keypair, + store::{get_account, get_key}, +}; +pub struct RevokePermissionAction { + key: Keypair, + permission_id: H256, +} + +impl Action for RevokePermissionAction { + type Params = (String, String); + type ResponseData = RevokePermissionActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, permission_id): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + let permission_id = H256::from_str(&permission_id)?; + + Ok(Self { + key: keypair, + permission_id, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .permission0() + .calls() + .revoke_permission_wait(self.permission_id, self.key.clone()) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .permission0() + .calls() + .revoke_permission_wait(self.permission_id, self.key.clone()) + .await?; + } + + Ok(RevokePermissionActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .permission0() + .calls() + .revoke_permission_fee(self.permission_id, self.key.clone()) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .permission0() + .calls() + .revoke_permission_fee(self.permission_id, self.key.clone()) + .await? + }; + + Ok(fee) + } + + async fn get_changes( + &self, + ctx: &mut impl ActionContext, + ) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + + Ok(Some(super::Changes { + changes: vec![format!( + "Revoke permission {}", + self.permission_id.to_string() + )], + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct RevokePermissionActionResponse; + +impl Display for RevokePermissionActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Permission revoked successfully!") + } +} + +pub struct ExecutePermissionAction { + pub key: Keypair, + pub permission_id: H256, + pub enforce: bool, +} + +impl Action for ExecutePermissionAction { + type Params = (String, String, bool); + type ResponseData = ExecutePermissionActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, permission_id, enforce): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + let permission_id = H256::from_str(&permission_id)?; + + Ok(Self { + key: keypair, + permission_id, + enforce, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + if !self.enforce { + client + .permission0() + .calls() + .execute_permission_wait(self.permission_id, self.key.clone()) + .await?; + } else { + client + .permission0() + .calls() + .enforcement_execute_permission_wait(self.permission_id, self.key.clone()) + .await?; + } + } else { + let client = TorusClient::for_testnet().await?; + if !self.enforce { + client + .permission0() + .calls() + .execute_permission_wait(self.permission_id, self.key.clone()) + .await?; + } else { + client + .permission0() + .calls() + .enforcement_execute_permission_wait(self.permission_id, self.key.clone()) + .await?; + } + } + + Ok(ExecutePermissionActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + if !self.enforce { + client + .permission0() + .calls() + .execute_permission_fee(self.permission_id, self.key.clone()) + .await? + } else { + client + .permission0() + .calls() + .enforcement_execute_permission_fee(self.permission_id, self.key.clone()) + .await? + } + } else { + let client = TorusClient::for_testnet().await?; + if !self.enforce { + client + .permission0() + .calls() + .execute_permission_fee(self.permission_id, self.key.clone()) + .await? + } else { + client + .permission0() + .calls() + .enforcement_execute_permission_fee(self.permission_id, self.key.clone()) + .await? + } + }; + + Ok(fee) + } + + async fn get_changes( + &self, + ctx: &mut impl ActionContext, + ) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + + Ok(Some(Changes { + changes: vec![format!( + "Execute the permission {}", + self.permission_id.to_string() + )], + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct ExecutePermissionActionResponse; + +impl Display for ExecutePermissionActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Permission executed successfully!") + } +} + +pub struct SetPermissionAccumulationAction { + pub key: Keypair, + pub permission_id: H256, + pub accumulating: bool, +} + +impl Action for SetPermissionAccumulationAction { + type Params = (String, String, bool); + type ResponseData = SetPermissionAccumulationActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, permission_id, accumulating): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + let permission_id = H256::from_str(&permission_id)?; + + Ok(Self { + key: keypair, + permission_id, + accumulating, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .permission0() + .calls() + .toggle_permission_accumulation_wait( + self.permission_id, + self.accumulating, + self.key.clone(), + ) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .permission0() + .calls() + .toggle_permission_accumulation_wait( + self.permission_id, + self.accumulating, + self.key.clone(), + ) + .await?; + } + + Ok(SetPermissionAccumulationActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .permission0() + .calls() + .toggle_permission_accumulation_fee( + self.permission_id, + self.accumulating, + self.key.clone(), + ) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .permission0() + .calls() + .toggle_permission_accumulation_fee( + self.permission_id, + self.accumulating, + self.key.clone(), + ) + .await? + }; + + Ok(fee) + } + + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + + Ok(Some(Changes { + changes: vec![format!( + "{} accumulating on the permission {}", + if self.accumulating { "Start" } else { "Stop" }, + self.permission_id.to_string() + )], + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct SetPermissionAccumulationActionResponse; + +impl Display for SetPermissionAccumulationActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Permission accumulation set successfully!") + } +} + +pub struct SetPermissionEnforcementAuthorityAction { + pub key: Keypair, + pub permission_id: H256, + pub enforcement_authority: Option<(Vec, u32)>, +} + +impl Action for SetPermissionEnforcementAuthorityAction { + type Params = (String, String, Option<(Vec, u32)>); + type ResponseData = SetPermissionEnforcementAuthorityActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, permission_id, enforcement_authority): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + let permission_id = H256::from_str(&permission_id)?; + + let enforcement_authority = match enforcement_authority { + Some((vec, votes)) => Some(( + vec.iter() + .map(|acc| get_account(acc)) + .collect::>>()?, + votes, + )), + None => None, + }; + + Ok(Self { + key: keypair, + permission_id, + enforcement_authority, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .permission0() + .calls() + .set_enforcement_authority_wait( + self.permission_id, + match &self.enforcement_authority { + Some((accounts, votes)) => torus_client::interfaces::mainnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::ControlledBy { controllers: torus_client::interfaces::mainnet::api::runtime_types::bounded_collections::bounded_vec::BoundedVec(accounts.clone()), required_votes: *votes }, + None => torus_client::interfaces::mainnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::None, + }, + self.key.clone(), + ) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .permission0() + .calls() + .set_enforcement_authority_wait( + self.permission_id, + match &self.enforcement_authority { + Some((accounts, votes)) => torus_client::interfaces::testnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::ControlledBy { controllers: torus_client::interfaces::testnet::api::runtime_types::bounded_collections::bounded_vec::BoundedVec(accounts.clone()), required_votes: *votes }, + None => torus_client::interfaces::testnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::None, + }, + self.key.clone(), + ) + .await?; + } + + Ok(SetPermissionEnforcementAuthorityActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .permission0() + .calls() + .set_enforcement_authority_fee( + self.permission_id, + match &self.enforcement_authority { + Some((accounts, votes)) => torus_client::interfaces::mainnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::ControlledBy { controllers: torus_client::interfaces::mainnet::api::runtime_types::bounded_collections::bounded_vec::BoundedVec(accounts.clone()), required_votes: *votes }, + None => torus_client::interfaces::mainnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::None, + }, + self.key.clone(), + ) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .permission0() + .calls() + .set_enforcement_authority_fee( + self.permission_id, + match &self.enforcement_authority { + Some((accounts, votes)) => torus_client::interfaces::testnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::ControlledBy { controllers: torus_client::interfaces::testnet::api::runtime_types::bounded_collections::bounded_vec::BoundedVec(accounts.clone()), required_votes: *votes }, + None => torus_client::interfaces::testnet::api::runtime_types::pallet_permission0::permission::EnforcementAuthority::None, + }, + self.key.clone(), + ) + .await? + }; + + Ok(fee) + } + + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + + let changes = match &self.enforcement_authority { + Some((accounts, votes)) => vec![ + format!( + "Add {} accounts as controllers on permission {}", + accounts.len(), + self.permission_id.to_string() + ), + format!("Set the required votes to {}", votes), + ], + None => vec![format!( + "Remove enforcement authority from permission {}", + self.permission_id.to_string() + )], + }; + + Ok(Some(Changes { + changes, + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct SetPermissionEnforcementAuthorityActionResponse; + +impl Display for SetPermissionEnforcementAuthorityActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Enforce authority set successfully!") + } +} diff --git a/cli/src/action/proposal.rs b/cli/src/action/proposal.rs new file mode 100644 index 0000000..6b4a5d7 --- /dev/null +++ b/cli/src/action/proposal.rs @@ -0,0 +1,706 @@ +use std::fmt::Display; + +use torus_client::client::TorusClient; + +use crate::{ + action::{network::fetch_global_params, Action, ActionContext}, + keypair::Keypair, + store::{get_account, get_key}, + util::to_percent_u8, +}; + +pub enum Proposal { + Emission { + data: String, + recycling_percentage: Option, + treasury_percentage: Option, + incentives_ratio: Option, + }, + GlobalParams { + data: String, + min_name_length: Option, + max_name_length: Option, + min_weight_control_fee: Option, + min_staking_fee: Option, + dividends_participation_weight: Option, + deposit_per_byte: Option, + base_fee: Option, + count_midpoint: Option, + fee_steepness: Option, + max_fee_multiplier: Option, + proposal_cost: Option, + }, + GlobalCustom { + data: String, + }, + TreasuryTransfer { + value: u128, + destination: String, + data: String, + }, +} + +pub struct CreateProposalAction { + key: Keypair, + proposal: Proposal, +} + +impl Action for CreateProposalAction { + type Params = (String, Proposal); + type ResponseData = CreateProposalActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, proposal): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + Ok(Self { + key: keypair, + proposal, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + match &self.proposal { + Proposal::Emission { + recycling_percentage, + treasury_percentage, + incentives_ratio, + data, + } => { + let current_recycling_percentage = client + .emission0() + .storage() + .emission_recycling_percentage() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_treasury_percentage = client + .governance() + .storage() + .treasury_emission_fee() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_incentives_ratio = client + .emission0() + .storage() + .incentives_ratio() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + + client + .governance() + .calls() + .add_emission_proposal_wait( + torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(recycling_percentage.unwrap_or(current_recycling_percentage) as u32)?), + torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(treasury_percentage.unwrap_or(current_treasury_percentage) as u32)?), + torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(incentives_ratio.unwrap_or(current_incentives_ratio) as u32)?), + data.clone().as_bytes().to_vec(), + self.key.clone(), + ) + .await?; + } + Proposal::GlobalParams { + data, + min_name_length, + max_name_length, + min_weight_control_fee, + min_staking_fee, + dividends_participation_weight, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + } => { + let params = fetch_global_params(ctx).await?; + client + .governance() + .calls() + .add_global_params_proposal_wait( + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::proposal::GlobalParamsData { + min_name_length: min_name_length.unwrap_or(params.min_name_length), + max_name_length: max_name_length.unwrap_or(params.max_name_length), + min_weight_control_fee: min_weight_control_fee.unwrap_or(params.min_weight_control_fee), + min_staking_fee: min_staking_fee.unwrap_or(params.min_staking_fee), + dividends_participation_weight: torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(dividends_participation_weight.unwrap_or(params.dividends_paticipation_weight) as u32)?), + namespace_pricing_config: torus_client::interfaces::mainnet::api::runtime_types::pallet_torus0::namespace::NamespacePricingConfig { + deposit_per_byte: deposit_per_byte.unwrap_or(params.deposit_per_byte), + base_fee: base_fee.unwrap_or(params.base_fee), + count_midpoint: count_midpoint.unwrap_or(params.count_midpoint), + fee_steepness: torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(fee_steepness.unwrap_or(params.fee_steepness) as u32)?), + max_fee_multiplier: max_fee_multiplier.unwrap_or(params.max_fee_multiplier), + }, + proposal_cost: proposal_cost.unwrap_or(params.proposal_cost) + }, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await?; + } + Proposal::GlobalCustom { data } => { + client + .governance() + .calls() + .add_global_custom_proposal_wait(data.as_bytes().to_vec(), self.key.clone()) + .await? + } + Proposal::TreasuryTransfer { + value, + destination, + data, + } => { + let destination_key = get_account(destination)?; + + client + .governance() + .calls() + .add_dao_treasury_transfer_proposal_wait( + *value, + destination_key, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await?; + } + } + } else { + let client = TorusClient::for_testnet().await?; + match &self.proposal { + Proposal::Emission { + recycling_percentage, + treasury_percentage, + incentives_ratio, + data, + } => { + let current_recycling_percentage = client + .emission0() + .storage() + .emission_recycling_percentage() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_treasury_percentage = client + .governance() + .storage() + .treasury_emission_fee() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_incentives_ratio = client + .emission0() + .storage() + .incentives_ratio() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + + client + .governance() + .calls() + .add_emission_proposal_wait( + torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(recycling_percentage.unwrap_or(current_recycling_percentage) as u32)?), + torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(treasury_percentage.unwrap_or(current_treasury_percentage) as u32)?), + torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(incentives_ratio.unwrap_or(current_incentives_ratio) as u32)?), + data.clone().as_bytes().to_vec(), + self.key.clone(), + ) + .await?; + } + Proposal::GlobalParams { + data, + min_name_length, + max_name_length, + min_weight_control_fee, + min_staking_fee, + dividends_participation_weight, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + } => { + let params = fetch_global_params(ctx).await?; + client + .governance() + .calls() + .add_global_params_proposal_wait( + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::proposal::GlobalParamsData { + min_name_length: min_name_length.unwrap_or(params.min_name_length), + max_name_length: max_name_length.unwrap_or(params.max_name_length), + min_weight_control_fee: min_weight_control_fee.unwrap_or(params.min_weight_control_fee), + min_staking_fee: min_staking_fee.unwrap_or(params.min_staking_fee), + dividends_participation_weight: torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(dividends_participation_weight.unwrap_or(params.dividends_paticipation_weight) as u32)?), + namespace_pricing_config: torus_client::interfaces::testnet::api::runtime_types::pallet_torus0::namespace::NamespacePricingConfig { + deposit_per_byte: deposit_per_byte.unwrap_or(params.deposit_per_byte), + base_fee: base_fee.unwrap_or(params.base_fee), + count_midpoint: count_midpoint.unwrap_or(params.count_midpoint), + fee_steepness: torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(fee_steepness.unwrap_or(params.fee_steepness) as u32)?), + max_fee_multiplier: max_fee_multiplier.unwrap_or(params.max_fee_multiplier), + }, + proposal_cost: proposal_cost.unwrap_or(params.proposal_cost) + }, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + Proposal::GlobalCustom { data } => { + client + .governance() + .calls() + .add_global_custom_proposal_wait(data.as_bytes().to_vec(), self.key.clone()) + .await?; + } + Proposal::TreasuryTransfer { + value, + destination, + data, + } => { + let destination_key = get_account(destination)?; + + client + .governance() + .calls() + .add_dao_treasury_transfer_proposal_wait( + *value, + destination_key, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await?; + } + } + } + + Ok(CreateProposalActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + match &self.proposal { + Proposal::Emission { + recycling_percentage, + treasury_percentage, + incentives_ratio, + data, + } => { + let current_recycling_percentage = client + .emission0() + .storage() + .emission_recycling_percentage() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_treasury_percentage = client + .governance() + .storage() + .treasury_emission_fee() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_incentives_ratio = client + .emission0() + .storage() + .incentives_ratio() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + + client + .governance() + .calls() + .add_emission_proposal_fee( + torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(recycling_percentage.unwrap_or(current_recycling_percentage) as u32)?), + torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(treasury_percentage.unwrap_or(current_treasury_percentage) as u32)?), + torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(incentives_ratio.unwrap_or(current_incentives_ratio) as u32)?), + data.clone().as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + Proposal::GlobalParams { + data, + min_name_length, + max_name_length, + min_weight_control_fee, + min_staking_fee, + dividends_participation_weight, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + } => { + let params = fetch_global_params(ctx).await?; + client + .governance() + .calls() + .add_global_params_proposal_fee( + torus_client::interfaces::mainnet::api::runtime_types::pallet_governance::proposal::GlobalParamsData { + min_name_length: min_name_length.unwrap_or(params.min_name_length), + max_name_length: max_name_length.unwrap_or(params.max_name_length), + min_weight_control_fee: min_weight_control_fee.unwrap_or(params.min_weight_control_fee), + min_staking_fee: min_staking_fee.unwrap_or(params.min_staking_fee), + dividends_participation_weight: torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(dividends_participation_weight.unwrap_or(params.dividends_paticipation_weight) as u32)?), + namespace_pricing_config: torus_client::interfaces::mainnet::api::runtime_types::pallet_torus0::namespace::NamespacePricingConfig { + deposit_per_byte: deposit_per_byte.unwrap_or(params.deposit_per_byte), + base_fee: base_fee.unwrap_or(params.base_fee), + count_midpoint: count_midpoint.unwrap_or(params.count_midpoint), + fee_steepness: torus_client::interfaces::mainnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(fee_steepness.unwrap_or(params.fee_steepness) as u32)?), + max_fee_multiplier: max_fee_multiplier.unwrap_or(params.max_fee_multiplier), + }, + proposal_cost: proposal_cost.unwrap_or(params.proposal_cost) + }, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + Proposal::GlobalCustom { data } => { + client + .governance() + .calls() + .add_global_custom_proposal_fee(data.as_bytes().to_vec(), self.key.clone()) + .await? + } + Proposal::TreasuryTransfer { + value, + destination, + data, + } => { + let destination_key = get_account(destination)?; + + client + .governance() + .calls() + .add_dao_treasury_transfer_proposal_fee( + *value, + destination_key, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + } + } else { + let client = TorusClient::for_testnet().await?; + match &self.proposal { + Proposal::Emission { + recycling_percentage, + treasury_percentage, + incentives_ratio, + data, + } => { + let current_recycling_percentage = client + .emission0() + .storage() + .emission_recycling_percentage() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_treasury_percentage = client + .governance() + .storage() + .treasury_emission_fee() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + let current_incentives_ratio = client + .emission0() + .storage() + .incentives_ratio() + .await? + .map(|perc| perc.0) + .unwrap_or_default(); + + client + .governance() + .calls() + .add_emission_proposal_fee( + torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(recycling_percentage.unwrap_or(current_recycling_percentage) as u32)?), + torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(treasury_percentage.unwrap_or(current_treasury_percentage) as u32)?), + torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(incentives_ratio.unwrap_or(current_incentives_ratio) as u32)?), + data.clone().as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + Proposal::GlobalParams { + data, + min_name_length, + max_name_length, + min_weight_control_fee, + min_staking_fee, + dividends_participation_weight, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + } => { + let params = fetch_global_params(ctx).await?; + client + .governance() + .calls() + .add_global_params_proposal_fee( + torus_client::interfaces::testnet::api::runtime_types::pallet_governance::proposal::GlobalParamsData { + min_name_length: min_name_length.unwrap_or(params.min_name_length), + max_name_length: max_name_length.unwrap_or(params.max_name_length), + min_weight_control_fee: min_weight_control_fee.unwrap_or(params.min_weight_control_fee), + min_staking_fee: min_staking_fee.unwrap_or(params.min_staking_fee), + dividends_participation_weight: torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(dividends_participation_weight.unwrap_or(params.dividends_paticipation_weight) as u32)?), + namespace_pricing_config: torus_client::interfaces::testnet::api::runtime_types::pallet_torus0::namespace::NamespacePricingConfig { + deposit_per_byte: deposit_per_byte.unwrap_or(params.deposit_per_byte), + base_fee: base_fee.unwrap_or(params.base_fee), + count_midpoint: count_midpoint.unwrap_or(params.count_midpoint), + fee_steepness: torus_client::interfaces::testnet::api::runtime_types::sp_arithmetic::per_things::Percent(to_percent_u8(fee_steepness.unwrap_or(params.fee_steepness) as u32)?), + max_fee_multiplier: max_fee_multiplier.unwrap_or(params.max_fee_multiplier), + }, + proposal_cost: proposal_cost.unwrap_or(params.proposal_cost) + }, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + Proposal::GlobalCustom { data } => { + client + .governance() + .calls() + .add_global_custom_proposal_fee(data.as_bytes().to_vec(), self.key.clone()) + .await? + } + Proposal::TreasuryTransfer { + value, + destination, + data, + } => { + let destination_key = get_account(destination)?; + + client + .governance() + .calls() + .add_dao_treasury_transfer_proposal_fee( + *value, + destination_key, + data.as_bytes().to_vec(), + self.key.clone(), + ) + .await? + } + } + }; + + Ok(fee) + } + + async fn get_changes( + &self, + ctx: &mut impl ActionContext, + ) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + + let proposal_str = match self.proposal { + Proposal::Emission { .. } => "Emission", + Proposal::GlobalParams { .. } => "Global Parameters", + Proposal::GlobalCustom { .. } => "Global Custom", + Proposal::TreasuryTransfer { .. } => "Treasury Transfer", + }; + Ok(Some(super::Changes { + changes: vec![format!("Create a {proposal_str} proposal")], + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct CreateProposalActionResponse; + +impl Display for CreateProposalActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Proposal created successfully!") + } +} + +pub struct AddVoteAction { + key: Keypair, + proposal_id: u64, + agree: bool, +} + +impl Action for AddVoteAction { + type Params = (String, u64, bool); + type ResponseData = AddVoteActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, proposal_id, agree): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + Ok(Self { + key: keypair, + proposal_id, + agree, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .calls() + .vote_proposal_wait(self.proposal_id, self.agree, self.key.clone()) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .calls() + .vote_proposal_wait(self.proposal_id, self.agree, self.key.clone()) + .await?; + } + + Ok(AddVoteActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .calls() + .vote_proposal_fee(self.proposal_id, self.agree, self.key.clone()) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .calls() + .vote_proposal_fee(self.proposal_id, self.agree, self.key.clone()) + .await? + }; + + Ok(fee) + } + + async fn get_changes( + &self, + ctx: &mut impl ActionContext, + ) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + Ok(Some(super::Changes { + changes: vec![format!( + "Vote {} proposal {}", + if self.agree { "for" } else { "against" }, + self.proposal_id + )], + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct AddVoteActionResponse; + +impl Display for AddVoteActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Voted successfully") + } +} + +pub struct RemoveVoteAction { + key: Keypair, + proposal_id: u64, +} + +impl Action for RemoveVoteAction { + type Params = (String, u64); + type ResponseData = RemoveVoteActionResponse; + + async fn create( + ctx: &mut impl ActionContext, + (key, proposal_id): Self::Params, + ) -> anyhow::Result { + let key = get_key(&key)?; + let (_, keypair) = ctx.decrypt(&key)?; + + Ok(Self { + key: keypair, + proposal_id, + }) + } + + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .calls() + .remove_vote_proposal_wait(self.proposal_id, self.key.clone()) + .await?; + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .calls() + .remove_vote_proposal_wait(self.proposal_id, self.key.clone()) + .await?; + } + + Ok(RemoveVoteActionResponse) + } + + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { + let client = TorusClient::for_mainnet().await?; + client + .governance() + .calls() + .remove_vote_proposal_fee(self.proposal_id, self.key.clone()) + .await? + } else { + let client = TorusClient::for_testnet().await?; + client + .governance() + .calls() + .remove_vote_proposal_fee(self.proposal_id, self.key.clone()) + .await? + }; + + Ok(fee) + } + + async fn get_changes( + &self, + ctx: &mut impl ActionContext, + ) -> anyhow::Result> { + let fee = self.estimate_fee(ctx).await?; + Ok(Some(super::Changes { + changes: vec![format!("Remove vote on proposal {}", self.proposal_id)], + fee: Some(fee), + })) + } +} + +#[derive(serde::Serialize)] +pub struct RemoveVoteActionResponse; + +impl Display for RemoveVoteActionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Removed vote successfully") + } +} diff --git a/cli/src/action/stake.rs b/cli/src/action/stake.rs index 7c05239..5e5a717 100644 --- a/cli/src/action/stake.rs +++ b/cli/src/action/stake.rs @@ -7,7 +7,7 @@ use torus_client::{ }; use crate::{ - action::{Action, ActionContext}, + action::{Action, ActionContext, Changes}, keypair::Keypair, store::{get_account, get_key}, util::format_torus, @@ -21,12 +21,12 @@ impl Action for GivenStakeAction { type Params = String; type ResponseData = GivenStakeActionResponse; - async fn create(_ctx: &impl ActionContext, account: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, account: Self::Params) -> anyhow::Result { let account = get_account(&account)?; Ok(Self { account }) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { ctx.info("Fetching given stake..."); let staking = if ctx.is_testnet() { @@ -94,12 +94,12 @@ impl Action for ReceivedStakeAction { type Params = String; type ResponseData = ReceivedStakeActionResponse; - async fn create(_ctx: &impl ActionContext, account: Self::Params) -> anyhow::Result { + async fn create(_ctx: &mut impl ActionContext, account: Self::Params) -> anyhow::Result { let account = get_account(&account)?; Ok(Self { account }) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { ctx.info("Fetching given stake..."); let staking = if ctx.is_testnet() { @@ -170,7 +170,7 @@ impl Action for AddStakeAction { type ResponseData = AddStakeActionResponse; async fn create( - ctx: &impl ActionContext, + ctx: &mut impl ActionContext, (key, target, amount): Self::Params, ) -> anyhow::Result { let key = get_key(&key)?; @@ -185,8 +185,8 @@ impl Action for AddStakeAction { }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let fee = if !ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -205,38 +205,33 @@ impl Action for AddStakeAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to add {} stake to `{}`? {}\n[y/N]", - format_torus(self.amount), - self.target, - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![format!( + "Add {} stake to `{}`", + format_torus(self.amount), + self.target, + )], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { - if !ctx.is_testnet() { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() .calls() - .add_stake(self.target.clone(), self.amount, self.keypair.clone()) + .add_stake_wait(self.target.clone(), self.amount, self.keypair.clone()) .await? } else { let client = TorusClient::for_testnet().await?; client .torus0() .calls() - .add_stake(self.target.clone(), self.amount, self.keypair.clone()) + .add_stake_wait(self.target.clone(), self.amount, self.keypair.clone()) .await? }; @@ -264,7 +259,7 @@ impl Action for RemoveStakeAction { type ResponseData = RemoveStakeActionResponse; async fn create( - ctx: &impl ActionContext, + ctx: &mut impl ActionContext, (key, target, amount): Self::Params, ) -> anyhow::Result { let key = get_key(&key)?; @@ -279,8 +274,8 @@ impl Action for RemoveStakeAction { }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let fee = if !ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -299,37 +294,32 @@ impl Action for RemoveStakeAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to remove {} stake from `{}`? {}\n[y/N]", - format_torus(self.amount), - self.target, - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![format!( + "Remove {} stake from `{}`?", + format_torus(self.amount), + self.target, + )], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { - if !ctx.is_testnet() { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() .calls() - .add_stake(self.target.clone(), self.amount, self.keypair.clone()) + .remove_stake_wait(self.target.clone(), self.amount, self.keypair.clone()) .await? } else { let client = TorusClient::for_testnet().await?; client .torus0() .calls() - .add_stake(self.target.clone(), self.amount, self.keypair.clone()) + .remove_stake_wait(self.target.clone(), self.amount, self.keypair.clone()) .await? }; @@ -358,7 +348,7 @@ impl Action for TransferStakeAction { type ResponseData = TransferStakeActionResponse; async fn create( - ctx: &impl ActionContext, + ctx: &mut impl ActionContext, (key, source, target, amount): Self::Params, ) -> anyhow::Result { let key = get_key(&key)?; @@ -375,8 +365,8 @@ impl Action for TransferStakeAction { }) } - async fn estimate_fee(&self, ctx: &impl ActionContext) -> anyhow::Result { - let fee = if !ctx.is_testnet() { + async fn estimate_fee(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + let fee = if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() @@ -405,51 +395,46 @@ impl Action for TransferStakeAction { Ok(fee) } - async fn confirmation_phrase( - &self, - ctx: &impl ActionContext, - ) -> anyhow::Result> { + async fn get_changes(&self, ctx: &mut impl ActionContext) -> anyhow::Result> { let fee = self.estimate_fee(ctx).await?; - Ok(Some(format!( - "Are you sure you want to transfer {} stake from `{}` to `{}`? {}\n[y/N]", - format_torus(self.amount), - self.source, - self.target, - if fee != 0 { - format!("(there will be a {} torus fee)", format_torus(fee)) - } else { - "".to_string() - } - ))) + Ok(Some(Changes { + changes: vec![format!( + "Transfer {} stake from `{}` to `{}`?", + format_torus(self.amount), + self.source, + self.target, + )], + fee: Some(fee), + })) } - async fn execute(&self, ctx: &impl ActionContext) -> anyhow::Result { - if !ctx.is_testnet() { + async fn execute(&self, ctx: &mut impl ActionContext) -> anyhow::Result { + if ctx.is_mainnet() { let client = TorusClient::for_mainnet().await?; client .torus0() .calls() - .transfer_stake_fee( + .transfer_stake_wait( self.source.clone(), self.target.clone(), self.amount, self.keypair.clone(), ) - .await? + .await?; } else { let client = TorusClient::for_testnet().await?; client .torus0() .calls() - .transfer_stake_fee( + .transfer_stake_wait( self.source.clone(), self.target.clone(), self.amount, self.keypair.clone(), ) - .await? - }; + .await?; + } Ok(TransferStakeActionResponse) } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 6e82422..e0a2384 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,18 +1,22 @@ -// mod agent; -// mod balance; -// mod key; -// mod namespace; -// mod stake; - -use clap::Parser; -use inquire::{Password, PasswordDisplayMode}; +use clap::{ArgGroup, Parser}; +use inquire::{ + ui::{RenderConfig, Styled}, + Password, PasswordDisplayMode, +}; use crate::{ action::{ - agent::{AgentInfoAction, RegisterAgentAction, UnregisterAgentAction}, + agent::{AgentInfoAction, RegisterAgentAction, UnregisterAgentAction, UpdateAgentAction}, + application::{ApplicationCreateAction, ApplicationListAction}, balance::*, key::{CreateKeyAction, DeleteKeyAction, KeyInfoAction, ListKeysAction}, namespace::{NamespaceInfoAction, RegisterNamespaceAction, UnregisterNamespaceAction}, + network::{PrintNetworkInfoAction, PrintNetworkSupplyAction, PrintTreasuryAddressAction}, + permission::{ + ExecutePermissionAction, RevokePermissionAction, SetPermissionAccumulationAction, + SetPermissionEnforcementAuthorityAction, + }, + proposal::{AddVoteAction, CreateProposalAction, Proposal, RemoveVoteAction}, stake::{ AddStakeAction, GivenStakeAction, ReceivedStakeAction, RemoveStakeAction, TransferStakeAction, @@ -26,108 +30,311 @@ use crate::{ pub(super) async fn run() -> anyhow::Result<()> { let cli = Cli::parse(); - let ctx = CliActionContext::new(&cli); + let mut ctx = CliActionContext::new(&cli); match cli.command { CliSubCommand::Agent(command) => match command.sub_command { Some(AgentCliSubCommand::Info { account }) => { - execute::(&ctx, account).await? + execute::(&mut ctx, account).await? } Some(AgentCliSubCommand::Register { key, name, metadata, url, - }) => execute::(&ctx, (key, name, metadata, url)).await?, + }) => execute::(&mut ctx, (key, name, metadata, url)).await?, Some(AgentCliSubCommand::Unregister { key }) => { - execute::(&ctx, key).await? + execute::(&mut ctx, key).await? + } + Some(AgentCliSubCommand::Update { + key, + url, + metadata, + staking_fee, + weight_control_fee, + }) => { + execute::( + &mut ctx, + (key, url, metadata, staking_fee, weight_control_fee), + ) + .await? } + None if command.account.is_some() => { - execute::(&ctx, command.account.unwrap()).await? + execute::(&mut ctx, command.account.unwrap()).await? } _ => unreachable!(), }, CliSubCommand::Balance(command) => match command.sub_command { Some(BalanceCliSubCommand::Check { key }) => { - execute::(&ctx, key).await? + execute::(&mut ctx, key).await? } Some(BalanceCliSubCommand::Transfer { key, target, amount, - }) => execute::(&ctx, (key, target, amount)).await?, + }) => execute::(&mut ctx, (key, target, amount)).await?, None if command.key.is_some() => { - execute::(&ctx, command.key.unwrap()).await? + execute::(&mut ctx, command.key.unwrap()).await? } _ => unreachable!(), }, CliSubCommand::Key(command) => match command.sub_command { - Some(KeyCliSubCommand::List) => execute::(&ctx, ()).await?, + Some(KeyCliSubCommand::List) => execute::(&mut ctx, ()).await?, Some(KeyCliSubCommand::Create { name, no_password, mnemonic, - }) => execute::(&ctx, (name, no_password, mnemonic)).await?, + }) => execute::(&mut ctx, (name, no_password, mnemonic)).await?, Some(KeyCliSubCommand::Delete { name }) => { - execute::(&ctx, name).await? + execute::(&mut ctx, name).await? + } + Some(KeyCliSubCommand::Info { name }) => { + execute::(&mut ctx, name).await? } - Some(KeyCliSubCommand::Info { name }) => execute::(&ctx, name).await?, None if command.key.is_some() => { - execute::(&ctx, command.key.unwrap()).await? + execute::(&mut ctx, command.key.unwrap()).await? } _ => unreachable!(), }, CliSubCommand::Namespace(command) => match command.sub_command { Some(NamespaceCliSubCommand::Info { account }) => { - execute::(&ctx, account).await?; + execute::(&mut ctx, account).await?; } Some(NamespaceCliSubCommand::Register { key, path }) => { - execute::(&ctx, (key, path)).await? + execute::(&mut ctx, (key, path)).await? } Some(NamespaceCliSubCommand::Unregister { key, path }) => { - execute::(&ctx, (key, path)).await? + execute::(&mut ctx, (key, path)).await? } None if command.account.is_some() => { - execute::(&ctx, command.account.unwrap()).await? + execute::(&mut ctx, command.account.unwrap()).await? } _ => unreachable!(), }, CliSubCommand::Stake(command) => match command.sub_command { Some(StakeCliSubCommand::Given { key }) => { - execute::(&ctx, key).await? + execute::(&mut ctx, key).await? } Some(StakeCliSubCommand::Received { key }) => { - execute::(&ctx, key).await? + execute::(&mut ctx, key).await? } Some(StakeCliSubCommand::Add { key, target, amount, - }) => execute::(&ctx, (key, target, amount)).await?, + }) => execute::(&mut ctx, (key, target, amount)).await?, Some(StakeCliSubCommand::Remove { key, target, amount, - }) => execute::(&ctx, (key, target, amount)).await?, + }) => execute::(&mut ctx, (key, target, amount)).await?, Some(StakeCliSubCommand::Transfer { key, source, target, amount, - }) => execute::(&ctx, (key, source, target, amount)).await?, - None => todo!(), + }) => execute::(&mut ctx, (key, source, target, amount)).await?, + None if command.key.is_some() => { + execute::(&mut ctx, command.key.unwrap()).await? + } + _ => unreachable!(), + }, + CliSubCommand::Application(application_cli_command) => { + match application_cli_command.sub_command { + ApplicationCliSubCommand::List { page, elements } => { + execute::( + &mut ctx, + (page.unwrap_or(0), elements.unwrap_or(10)), + ) + .await? + } + ApplicationCliSubCommand::Create { + key, + metadata, + remove, + } => execute::(&mut ctx, (key, metadata, remove)).await?, + } + } + CliSubCommand::Network(network_cli_command) => match network_cli_command.sub_command { + NetworkCliSubCommand::Info => execute::(&mut ctx, ()).await?, + NetworkCliSubCommand::Supply => { + execute::(&mut ctx, ()).await? + } + NetworkCliSubCommand::Treasury => { + execute::(&mut ctx, ()).await? + } + }, + CliSubCommand::Permission(permission_cli_command) => { + match permission_cli_command.sub_command { + PermissionCliSubCommand::Revoke { key, permission_id } => { + execute::(&mut ctx, (key, permission_id)).await? + } + PermissionCliSubCommand::Execute { + key, + permission_id, + enforce, + } => { + execute::(&mut ctx, (key, permission_id, enforce)) + .await? + } + PermissionCliSubCommand::Accumulation { + key, + permission_id, + accumulating, + } => { + execute::( + &mut ctx, + (key, permission_id, accumulating), + ) + .await? + } + PermissionCliSubCommand::EnforcementAuthority { sub_command } => { + match sub_command { + PermissionEnforcementAuthorityCliSubCommand::Remove { + key, + permission_id, + } => { + execute::( + &mut ctx, + (key, permission_id, None), + ) + .await? + } + PermissionEnforcementAuthorityCliSubCommand::Add { + key, + permission_id, + votes, + members, + } => { + execute::( + &mut ctx, + (key, permission_id, Some((members, votes))), + ) + .await? + } + } + } + } + } + CliSubCommand::Proposal(proposal_cli_command) => match proposal_cli_command.sub_command { + ProposalCliSubCommand::Add { sub_command } => match sub_command { + ProposalAddCliSubCommand::Emission { + key, + data, + params: + EmissionParams { + recycling_percentage, + treasury_percentage, + incentives_ratio, + }, + } => { + execute::( + &mut ctx, + ( + key, + Proposal::Emission { + data, + recycling_percentage, + treasury_percentage, + incentives_ratio, + }, + ), + ) + .await? + } + ProposalAddCliSubCommand::GlobalParams { + key, + data, + params: + GlobalParamsParams { + min_name_length, + max_name_length, + min_weight_control_fee, + min_staking_fee, + dividends_participation_weight, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + }, + } => { + execute::( + &mut ctx, + ( + key, + Proposal::GlobalParams { + data, + min_name_length, + max_name_length, + min_weight_control_fee, + min_staking_fee, + dividends_participation_weight, + deposit_per_byte, + base_fee, + count_midpoint, + fee_steepness, + max_fee_multiplier, + proposal_cost, + }, + ), + ) + .await? + } + ProposalAddCliSubCommand::GlobalCustom { key, data } => { + execute::( + &mut ctx, + (key, Proposal::GlobalCustom { data }), + ) + .await? + } + ProposalAddCliSubCommand::TreasuryTransfer { + key, + data, + destination, + value, + } => { + execute::( + &mut ctx, + ( + key, + Proposal::TreasuryTransfer { + value, + destination, + data, + }, + ), + ) + .await? + } + }, + ProposalCliSubCommand::Vote { sub_command } => match sub_command { + ProposalVoteCliSubCommand::Add { + key, + proposal_id, + agree, + } => execute::(&mut ctx, (key, proposal_id, agree)).await?, + ProposalVoteCliSubCommand::Remove { key, proposal_id } => { + execute::(&mut ctx, (key, proposal_id)).await? + } + }, }, }; Ok(()) } -async fn execute(ctx: &CliActionContext, params: A::Params) -> anyhow::Result<()> { +async fn execute( + mut ctx: &mut CliActionContext, + params: A::Params, +) -> anyhow::Result<()> { let result: anyhow::Result<()> = async { - let action = A::create(&ctx, params).await?; + let action = A::create(&mut ctx, params).await?; if ctx.is_dry_run() { - let fee = action.estimate_fee(&ctx).await?; + let fee = action.estimate_fee(&mut ctx).await?; if ctx.is_json() { let serialized = serde_json::to_string(&DryRunResponse { fee })?; @@ -140,12 +347,26 @@ async fn execute(ctx: &CliActionContext, params: A::Params) -> anyhow } if !ctx.is_json() { - if let Some(desc) = action.confirmation_phrase(&ctx).await? { - ctx.confirm(&desc)?; + if let Some(changes) = action.get_changes(&mut ctx).await? { + let desc = changes + .changes + .iter() + .map(|desc| format!(" - {desc}")) + .collect::>() + .join("\n"); + let fee = if let Some(fee) = changes.fee { + format!(" (pays {} fee)\n", format_torus(fee)) + } else { + "".to_string() + }; + let str = + format!("This command will: \n{desc}\n{fee} Do you want to continue? [y/N]"); + + ctx.confirm(&str)?; } } - let res = action.execute(&ctx).await?; + let res = action.execute(&mut ctx).await?; if ctx.is_json() { let serialized = serde_json::to_string(&JsonResponse::Success { data: res })?; @@ -187,7 +408,6 @@ impl CliActionContext { let is_json = cli.json || std::env::var("TORURS_JSON").is_ok(); let is_testnet = cli.testnet || std::env::var("TORURS_TESTNET").is_ok(); let skip_confirmation = cli.yes || std::env::var("TORURS_SKIP_CONFIRMATION").is_ok(); - Self { is_dry_run, is_json, @@ -197,7 +417,7 @@ impl CliActionContext { } } -impl ActionContext for &CliActionContext { +impl ActionContext for &mut CliActionContext { fn is_json(&self) -> bool { self.is_json } @@ -210,7 +430,7 @@ impl ActionContext for &CliActionContext { self.is_dry_run } - fn confirm(&self, desc: &str) -> anyhow::Result<()> { + fn confirm(&mut self, desc: &str) -> anyhow::Result<()> { if self.skip_confirmation { return Ok(()); } @@ -218,6 +438,14 @@ impl ActionContext for &CliActionContext { let res = Password::new(desc) .without_confirmation() .with_display_mode(PasswordDisplayMode::Full) + .with_render_config( + RenderConfig::empty() + .with_scroll_up_prefix(Styled::default()) + .with_scroll_down_prefix(Styled::default()) + .with_answered_prompt_prefix(Styled::default()) + .with_highlighted_option_prefix(Styled::default()) + .with_prompt_prefix(Styled::default()), + ) .prompt()?; if !res.to_lowercase().starts_with("y") { @@ -228,7 +456,7 @@ impl ActionContext for &CliActionContext { } fn decrypt( - &self, + &mut self, key: &crate::store::Key, ) -> anyhow::Result<(crate::store::Key, crate::keypair::Keypair)> { if !key.encrypted { @@ -246,13 +474,17 @@ impl ActionContext for &CliActionContext { Ok((key.clone(), Keypair::from_key(key)?)) } - fn info(&self, message: impl AsRef) { + fn info(&mut self, message: impl AsRef) { if self.is_json { return; } println!("{}", message.as_ref()); } + + fn is_mainnet(&self) -> bool { + !self.is_testnet + } } #[derive(clap::Parser)] @@ -276,6 +508,8 @@ pub struct Cli { #[derive(clap::Subcommand)] pub enum CliSubCommand { + /// Commands related to agent applications. + Application(ApplicationCliCommand), /// Commands related to agents. Agent(AgentCliCommand), /// Commands related to balance. @@ -284,10 +518,45 @@ pub enum CliSubCommand { Key(KeyCliCommand), /// Commands related to namespaces. Namespace(NamespaceCliCommand), + /// Commands related to the network. + Network(NetworkCliCommand), + /// Commands Related to permissions. + Permission(PermissionCliCommand), + /// Commands Related to proposals. + Proposal(ProposalCliCommand), /// Commands related to stake. Stake(StakeCliCommand), } +#[derive(clap::Args)] +#[command(arg_required_else_help = true)] +pub struct ApplicationCliCommand { + #[command(subcommand)] + pub sub_command: ApplicationCliSubCommand, +} + +#[derive(clap::Subcommand, Clone)] +pub enum ApplicationCliSubCommand { + /// Prints the list of applications. + List { + #[arg(short, long)] + page: Option, + + #[arg(short, long)] + elements: Option, + }, + /// Creates an agent application. + Create { + /// The saved key name that will become an agent. + key: String, + /// The metadata of the application. + metadata: String, + /// If it is an application to remove an agent. + #[arg(short, long)] + remove: bool, + }, +} + #[derive(clap::Args)] #[command(arg_required_else_help = true)] pub struct AgentCliCommand { @@ -320,6 +589,22 @@ pub enum AgentCliSubCommand { /// The saved key name that will be unregistered as an agent. key: String, }, + /// Updates agent information. + Update { + /// The saved key name that will be updated. + key: String, + /// The new url. + url: String, + #[arg(long)] + /// The new metadata. + metadata: Option, + #[arg(long)] + /// The new staking fee. + staking_fee: Option, + #[arg(long)] + /// The new weight control fee. + weight_control_fee: Option, + }, } #[derive(clap::Args)] @@ -416,6 +701,168 @@ pub enum NamespaceCliSubCommand { }, } +#[derive(clap::Args)] +#[command(arg_required_else_help = true)] +pub struct NetworkCliCommand { + #[command(subcommand)] + pub sub_command: NetworkCliSubCommand, +} + +#[derive(clap::Subcommand, Clone)] +pub enum NetworkCliSubCommand { + /// Prints information about the network. + Info, + /// Prints the current supply of the network. + Supply, + /// Prints the treasury address of the network. + Treasury, +} + +#[derive(clap::Args)] +pub struct PermissionCliCommand { + #[command(subcommand)] + pub sub_command: PermissionCliSubCommand, +} + +#[derive(clap::Subcommand, Clone)] +pub enum PermissionCliSubCommand { + /// Revokes a permission. + Revoke { key: String, permission_id: String }, + /// Executes a permission. + Execute { + key: String, + permission_id: String, + #[arg(short, long)] + enforce: bool, + }, + /// Enables or disables the accumulation for the given permission. + Accumulation { + key: String, + permission_id: String, + accumulating: bool, + }, + /// Modifies the enforcement authority of the given permission. + EnforcementAuthority { + #[command(subcommand)] + sub_command: PermissionEnforcementAuthorityCliSubCommand, + }, +} + +#[derive(clap::Subcommand, Clone)] +pub enum PermissionEnforcementAuthorityCliSubCommand { + /// Removes the current enforcement authority from the given permission. + Remove { key: String, permission_id: String }, + /// Adds an enforcement authority to the given permission. + Add { + key: String, + permission_id: String, + #[arg(value_delimiter = ',')] + /// The controllers of the enforcement authority. + /// (Comma-separated addresses or key names). + members: Vec, + /// The votes needed. + votes: u32, + }, +} + +#[derive(clap::Args)] +pub struct ProposalCliCommand { + #[command(subcommand)] + pub sub_command: ProposalCliSubCommand, +} + +#[derive(clap::Subcommand, Clone)] +pub enum ProposalCliSubCommand { + /// Adds a proposal. + Add { + #[command(subcommand)] + sub_command: ProposalAddCliSubCommand, + }, + /// Votes on a proposal. + Vote { + #[command(subcommand)] + sub_command: ProposalVoteCliSubCommand, + }, +} + +#[derive(clap::Args, Clone)] +#[command(group = ArgGroup::default().id("emission-params").required(true).multiple(true).args(&["recycling_percentage", "treasury_percentage", "incentives_ratio"]))] +pub struct EmissionParams { + #[arg(long)] + recycling_percentage: Option, + #[arg(long)] + treasury_percentage: Option, + #[arg(long)] + incentives_ratio: Option, +} + +#[derive(clap::Args, Clone)] +#[command(group = ArgGroup::default().id("global-params").required(true).multiple(true) +.args(&["min_name_length", "max_name_length", "min_weight_control_fee", "min_staking_fee", "dividends_participation_weight", "deposit_per_byte", "base_fee", "count_midpoint", "fee_steepness", "max_fee_multiplier", "proposal_cost"]))] +pub struct GlobalParamsParams { + #[arg(long)] + min_name_length: Option, + #[arg(long)] + max_name_length: Option, + #[arg(long)] + min_weight_control_fee: Option, + #[arg(long)] + min_staking_fee: Option, + #[arg(long)] + dividends_participation_weight: Option, + #[arg(long)] + deposit_per_byte: Option, + #[arg(long)] + base_fee: Option, + #[arg(long)] + count_midpoint: Option, + #[arg(long)] + fee_steepness: Option, + #[arg(long)] + max_fee_multiplier: Option, + #[arg(long)] + proposal_cost: Option, +} + +#[derive(clap::Subcommand, Clone)] +pub enum ProposalAddCliSubCommand { + /// Creates a new emission proposal. + Emission { + key: String, + data: String, + #[command(flatten)] + params: EmissionParams, + }, + /// Creates a new global params proposal. + GlobalParams { + key: String, + data: String, + #[command(flatten)] + params: GlobalParamsParams, + }, + /// Creates a new global custom proposal. + GlobalCustom { key: String, data: String }, + /// Creates a new treasury transfer proposal. + TreasuryTransfer { + key: String, + data: String, + destination: String, + value: u128, + }, +} + +#[derive(clap::Subcommand, Clone)] +pub enum ProposalVoteCliSubCommand { + /// Votes on a proposal. + Add { + key: String, + proposal_id: u64, + agree: bool, + }, + /// Removes a vote from a proposal. + Remove { key: String, proposal_id: u64 }, +} + #[derive(clap::Parser)] #[command(arg_required_else_help = true)] pub struct StakeCliCommand { diff --git a/cli/src/util.rs b/cli/src/util.rs index e02c9c0..6b6a3e0 100644 --- a/cli/src/util.rs +++ b/cli/src/util.rs @@ -1,3 +1,11 @@ pub fn format_torus(amount: u128) -> String { format!("{:.5}", amount as f64 / 10f64.powf(18.0)) } + +pub fn to_percent_u8(amount: u32) -> anyhow::Result { + if amount > 100 { + anyhow::bail!("Invalid percent: {amount}. Must be between 0-100."); + } + + Ok(amount.try_into()?) +}