From 66ee0c54494f2a58b21e3018387d1c44fb221b31 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Fri, 24 Oct 2025 15:26:30 +0300 Subject: [PATCH] fix: account subcommand updated --- wallet/src/cli/account.rs | 237 +++++++++++++++++++++++----------- wallet/src/helperfunctions.rs | 42 +++++- 2 files changed, 202 insertions(+), 77 deletions(-) diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 23758d4..d7dac7d 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,21 +1,81 @@ -use std::str::FromStr; - use anyhow::Result; use base58::ToBase58; use clap::Subcommand; use common::transaction::NSSATransaction; -use nssa::Address; +use nssa::{Address, program::Program}; +use serde::Serialize; use crate::{ - SubcommandReturnValue, WalletCore, cli::WalletSubcommand, helperfunctions::HumanReadableAccount, + SubcommandReturnValue, WalletCore, + cli::WalletSubcommand, + helperfunctions::{AddressPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, }; +const TOKEN_DEFINITION_TYPE: u8 = 0; +const TOKEN_DEFINITION_DATA_SIZE: usize = 23; + +const TOKEN_HOLDING_TYPE: u8 = 1; +const TOKEN_HOLDING_DATA_SIZE: usize = 49; + +struct TokenDefinition { + #[allow(unused)] + account_type: u8, + name: [u8; 6], + total_supply: u128, +} + +struct TokenHolding { + #[allow(unused)] + account_type: u8, + definition_id: Address, + balance: u128, +} + +impl TokenDefinition { + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().unwrap(); + let total_supply = u128::from_le_bytes(data[7..].try_into().unwrap()); + + Some(Self { + account_type, + name, + total_supply, + }) + } + } +} + +impl TokenHolding { + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { + None + } else { + let account_type = data[0]; + let definition_id = Address::new(data[1..33].try_into().unwrap()); + let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); + Some(Self { + definition_id, + balance, + account_type, + }) + } + } +} + ///Represents generic chain CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum AccountSubcommand { ///Get - #[command(subcommand)] - Get(GetSubcommand), + Get { + #[arg(long)] + raw: bool, + #[arg(short, long)] + addr: String, + }, ///Fetch #[command(subcommand)] Fetch(FetchSubcommand), @@ -24,31 +84,6 @@ pub enum AccountSubcommand { New(NewSubcommand), } -///Represents generic getter CLI subcommand -#[derive(Subcommand, Debug, Clone)] -pub enum GetSubcommand { - ///Get account `addr` balance - PublicAccountBalance { - #[arg(short, long)] - addr: String, - }, - ///Get account `addr` nonce - PublicAccountNonce { - #[arg(short, long)] - addr: String, - }, - ///Get account at address `addr` - PublicAccount { - #[arg(short, long)] - addr: String, - }, - ///Get private account with `addr` from storage - PrivateAccount { - #[arg(short, long)] - addr: String, - }, -} - ///Represents generic getter CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum FetchSubcommand { @@ -80,49 +115,6 @@ pub enum NewSubcommand { Private {}, } -impl WalletSubcommand for GetSubcommand { - async fn handle_subcommand( - self, - wallet_core: &mut WalletCore, - ) -> Result { - match self { - GetSubcommand::PublicAccountBalance { addr } => { - let addr = Address::from_str(&addr)?; - - let balance = wallet_core.get_account_balance(addr).await?; - println!("Accounts {addr} balance is {balance}"); - - Ok(SubcommandReturnValue::Empty) - } - GetSubcommand::PublicAccountNonce { addr } => { - let addr = Address::from_str(&addr)?; - - let nonce = wallet_core.get_accounts_nonces(vec![addr]).await?[0]; - println!("Accounts {addr} nonce is {nonce}"); - - Ok(SubcommandReturnValue::Empty) - } - GetSubcommand::PublicAccount { addr } => { - let addr: Address = addr.parse()?; - let account = wallet_core.get_account_public(addr).await?; - let account_hr: HumanReadableAccount = account.clone().into(); - println!("{}", serde_json::to_string(&account_hr).unwrap()); - - Ok(SubcommandReturnValue::Account(account)) - } - GetSubcommand::PrivateAccount { addr } => { - let addr: Address = addr.parse()?; - if let Some(account) = wallet_core.get_account_private(&addr) { - println!("{}", serde_json::to_string(&account).unwrap()); - } else { - println!("Private account not found."); - } - Ok(SubcommandReturnValue::Empty) - } - } - } -} - impl WalletSubcommand for FetchSubcommand { async fn handle_subcommand( self, @@ -237,14 +229,107 @@ impl WalletSubcommand for NewSubcommand { } } +#[derive(Debug, Serialize)] +pub struct AuthenticatedTransferAccountView { + pub balance: u128, +} + +impl From for AuthenticatedTransferAccountView { + fn from(value: nssa::Account) -> Self { + Self { + balance: value.balance, + } + } +} + +#[derive(Debug, Serialize)] +pub struct TokedDefinitionAccountView { + pub account_type: String, + pub name: String, + pub total_supply: u128, +} + +impl From for TokedDefinitionAccountView { + fn from(value: TokenDefinition) -> Self { + Self { + account_type: "Token definition".to_string(), + name: hex::encode(value.name), + total_supply: value.total_supply, + } + } +} + +#[derive(Debug, Serialize)] +pub struct TokedHoldingAccountView { + pub account_type: String, + pub definition_id: String, + pub balance: u128, +} + +impl From for TokedHoldingAccountView { + fn from(value: TokenHolding) -> Self { + Self { + account_type: "Token holding".to_string(), + definition_id: value.definition_id.to_string(), + balance: value.balance, + } + } +} + impl WalletSubcommand for AccountSubcommand { async fn handle_subcommand( self, wallet_core: &mut WalletCore, ) -> Result { match self { - AccountSubcommand::Get(get_subcommand) => { - get_subcommand.handle_subcommand(wallet_core).await + AccountSubcommand::Get { raw, addr } => { + let (addr, addr_kind) = parse_addr_with_privacy_prefix(&addr)?; + + let account = match addr_kind { + AddressPrivacyKind::Public => wallet_core.get_account_public(addr).await?, + AddressPrivacyKind::Private => wallet_core + .get_account_private(&addr) + .ok_or(anyhow::anyhow!("Private account not found in storage"))?, + }; + + if raw { + let account_hr: HumanReadableAccount = account.clone().into(); + println!("{}", serde_json::to_string(&account_hr).unwrap()); + + return Ok(SubcommandReturnValue::Empty); + } + + let auth_tr_prog_id = Program::authenticated_transfer_program().id(); + let token_prog_id = Program::token().id(); + + let acc_view = match &account.program_owner { + _ if &account.program_owner == &auth_tr_prog_id => { + let acc_view: AuthenticatedTransferAccountView = account.into(); + + serde_json::to_string(&acc_view)? + } + _ if &account.program_owner == &token_prog_id => { + if let Some(token_def) = TokenDefinition::parse(&account.data) { + let acc_view: TokedDefinitionAccountView = token_def.into(); + + serde_json::to_string(&acc_view)? + } else if let Some(token_hold) = TokenHolding::parse(&account.data) { + let acc_view: TokedHoldingAccountView = token_hold.into(); + + serde_json::to_string(&acc_view)? + } else { + anyhow::bail!("Invalid data for account {addr:#?} with token program"); + } + } + _ => { + let account_hr: HumanReadableAccount = account.clone().into(); + serde_json::to_string(&account_hr).unwrap() + } + }; + + println!("{}", acc_view); + + Ok(SubcommandReturnValue::Empty) } AccountSubcommand::Fetch(fetch_subcommand) => { fetch_subcommand.handle_subcommand(wallet_core).await diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index a67b8ec..fcc838b 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -6,7 +6,7 @@ use tokio::io::AsyncReadExt; use anyhow::Result; use key_protocol::key_protocol_core::NSSAUserData; -use nssa::Account; +use nssa::{Account, Address}; use serde::Serialize; use crate::{ @@ -86,6 +86,30 @@ pub(crate) fn produce_random_nonces(size: usize) -> Vec { result.into_iter().map(Nonce::from_le_bytes).collect() } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressPrivacyKind { + Public, + Private, +} + +pub(crate) fn parse_addr_with_privacy_prefix( + addr_base58: &str, +) -> Result<(Address, AddressPrivacyKind)> { + if addr_base58.starts_with("Public/") { + Ok(( + addr_base58.strip_prefix("Public/").unwrap().parse()?, + AddressPrivacyKind::Public, + )) + } else if addr_base58.starts_with("Private/") { + Ok(( + addr_base58.strip_prefix("Private/").unwrap().parse()?, + AddressPrivacyKind::Private, + )) + } else { + anyhow::bail!("Unsupported privacy kind, available variants is Public/ and Private/"); + } +} + /// Human-readable representation of an account. #[derive(Serialize)] pub(crate) struct HumanReadableAccount { @@ -126,4 +150,20 @@ mod tests { std::env::remove_var(HOME_DIR_ENV_VAR); } } + + #[test] + fn test_addr_parse_with_privacy() { + let addr_base58 = "Public/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy"; + let (_, addr_kind) = parse_addr_with_privacy_prefix(addr_base58).unwrap(); + + assert_eq!(addr_kind, AddressPrivacyKind::Public); + + let addr_base58 = "Private/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy"; + let (_, addr_kind) = parse_addr_with_privacy_prefix(addr_base58).unwrap(); + + assert_eq!(addr_kind, AddressPrivacyKind::Private); + + let addr_base58 = "asdsada/BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy"; + assert!(parse_addr_with_privacy_prefix(addr_base58).is_err()); + } }