From 4e36ae46792750816f6a3ae6d921320f20e6dc3c Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Thu, 23 Oct 2025 17:33:25 +0300 Subject: [PATCH 01/15] fix; first refactor --- Cargo.toml | 1 + common/Cargo.toml | 3 +- common/src/transaction.rs | 9 +- integration_tests/src/test_suite_map.rs | 128 ++++++++++++------------ key_protocol/Cargo.toml | 3 +- key_protocol/src/key_management/mod.rs | 3 +- nssa/core/Cargo.toml | 5 +- nssa/core/src/address.rs | 15 ++- sequencer_core/Cargo.toml | 2 +- sequencer_core/src/lib.rs | 97 +++++++++++++----- sequencer_rpc/Cargo.toml | 3 +- sequencer_rpc/src/process.rs | 16 +-- wallet/Cargo.toml | 3 +- wallet/src/cli/account.rs | 21 ++-- wallet/src/cli/chain.rs | 12 +-- wallet/src/lib.rs | 16 +-- 16 files changed, 202 insertions(+), 135 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2019628..9ceb790 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ bip39 = "2.2.0" hmac-sha512 = "1.1.7" chrono = "0.4.41" borsh = "1.5.7" +base58 = "0.2.0" rocksdb = { version = "0.21.0", default-features = false, features = [ "snappy", diff --git a/common/Cargo.toml b/common/Cargo.toml index d0d145f..95d1c02 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -15,7 +15,8 @@ rs_merkle.workspace = true sha2.workspace = true log.workspace = true elliptic-curve.workspace = true -hex.workspace = true +base58.workspace = true +hex = "0.4.3" nssa-core = { path = "../nssa/core", features = ["host"] } borsh.workspace = true diff --git a/common/src/transaction.rs b/common/src/transaction.rs index c99cf31..65c55c9 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -1,3 +1,4 @@ +use base58::ToBase58; use borsh::{BorshDeserialize, BorshSerialize}; use k256::ecdsa::{Signature, SigningKey, VerifyingKey}; use log::info; @@ -125,7 +126,7 @@ impl From for OwnedUTXOForPublication { fn from(value: OwnedUTXO) -> Self { Self { hash: hex::encode(value.hash), - owner: hex::encode(value.owner), + owner: value.owner.to_base58(), amount: value.amount, } } @@ -150,7 +151,7 @@ impl ActionData { ActionData::MintMoneyPublicTx(action) => { format!( "Account {:?} minted {:?} balance", - hex::encode(action.acc), + action.acc.to_base58(), action.amount ) } @@ -160,14 +161,14 @@ impl ActionData { action .receiver_data .into_iter() - .map(|(amount, rec)| (amount, hex::encode(rec))) + .map(|(amount, rec)| (amount, rec.to_base58())) .collect::>() ) } ActionData::SendMoneyShieldedTx(action) => { format!( "Shielded send from {:?} for {:?} balance", - hex::encode(action.acc_sender), + action.acc_sender.to_base58(), action.amount ) } diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 99a1371..80bf894 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -7,7 +7,7 @@ use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1 use wallet::{ Command, SubcommandReturnValue, WalletCore, cli::{ - account::{AccountSubcommand, FetchSubcommand, RegisterSubcommand}, + account::{AccountSubcommand, FetchSubcommand, NewSubcommand}, native_token_transfer_program::{ NativeTokenTransferProgramSubcommand, NativeTokenTransferProgramSubcommandPrivate, NativeTokenTransferProgramSubcommandShielded, @@ -40,7 +40,7 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_success() { info!("test_success"); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Public { + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 100, @@ -77,7 +77,7 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_success_move_to_another_account() { info!("test_success_move_to_another_account"); - let command = Command::Account(AccountSubcommand::Register(RegisterSubcommand::Public {})); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {})); let wallet_config = fetch_config().await.unwrap(); @@ -101,7 +101,7 @@ pub fn prepare_function_map() -> HashMap { panic!("Failed to produce new account, not present in persistent accounts"); } - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Public { + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { from: ACC_SENDER.to_string(), to: new_persistent_account_addr.clone(), amount: 100, @@ -134,7 +134,7 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_failure() { info!("test_failure"); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Public { + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 1000000, @@ -173,7 +173,7 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_success_two_transactions() { info!("test_success_two_transactions"); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Public { + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 100, @@ -206,7 +206,7 @@ pub fn prepare_function_map() -> HashMap { info!("First TX Success!"); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Public { + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { from: ACC_SENDER.to_string(), to: ACC_RECEIVER.to_string(), amount: 100, @@ -264,20 +264,20 @@ pub fn prepare_function_map() -> HashMap { let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap(); // Create new account for the token supply holder - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap(); // Create new account for receiving a token transaction - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap(); @@ -311,7 +311,7 @@ pub fn prepare_function_map() -> HashMap { name: "A NAME".to_string(), total_supply: 37, }); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); info!("Waiting for next block creation"); @@ -365,7 +365,7 @@ pub fn prepare_function_map() -> HashMap { recipient_addr: recipient_addr.to_string(), balance_to_move: 7, }); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); info!("Waiting for next block creation"); @@ -416,8 +416,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { addr: definition_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap() @@ -426,8 +426,8 @@ pub fn prepare_function_map() -> HashMap { }; // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -437,8 +437,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -456,7 +456,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -501,7 +501,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -532,7 +532,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -564,8 +564,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { addr: definition_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap() @@ -574,8 +574,8 @@ pub fn prepare_function_map() -> HashMap { }; // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -585,8 +585,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -604,7 +604,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -657,7 +657,7 @@ pub fn prepare_function_map() -> HashMap { ); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap() else { @@ -700,8 +700,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { addr: definition_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap() @@ -710,8 +710,8 @@ pub fn prepare_function_map() -> HashMap { }; // Create new account for the token supply holder (public) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap() @@ -721,8 +721,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -739,7 +739,7 @@ pub fn prepare_function_map() -> HashMap { total_supply: 37, }); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -774,7 +774,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -800,7 +800,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -827,8 +827,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token definition (public) let SubcommandReturnValue::RegisterAccount { addr: definition_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap() @@ -837,8 +837,8 @@ pub fn prepare_function_map() -> HashMap { }; // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -848,8 +848,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for receiving a token transaction let SubcommandReturnValue::RegisterAccount { addr: recipient_addr, - } = wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Public {}, + } = wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public {}, ))) .await .unwrap() @@ -867,7 +867,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -912,7 +912,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -938,7 +938,7 @@ pub fn prepare_function_map() -> HashMap { }, ); - wallet::execute_subcommand(Command::TokenProgram(subcommand)) + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -962,7 +962,7 @@ pub fn prepare_function_map() -> HashMap { let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to: Address = ACC_RECEIVER_PRIVATE.parse().unwrap(); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Private( + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( NativeTokenTransferProgramSubcommandPrivate::PrivateOwned { from: from.to_string(), to: to.to_string(), @@ -1000,7 +1000,7 @@ pub fn prepare_function_map() -> HashMap { let to_npk_string = hex::encode(to_npk.0); let to_ipk = Secp256k1Point::from_scalar(to_npk.0); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Private( + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { from: from.to_string(), to_npk: to_npk_string, @@ -1044,7 +1044,7 @@ pub fn prepare_function_map() -> HashMap { info!("test_success_private_transfer_to_another_owned_account_claiming_path"); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); - let command = Command::Account(AccountSubcommand::Register(RegisterSubcommand::Private {})); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); let sub_ret = wallet::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::RegisterAccount { addr: to_addr } = sub_ret else { @@ -1065,7 +1065,7 @@ pub fn prepare_function_map() -> HashMap { .cloned() .unwrap(); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Private( + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { from: from.to_string(), to_npk: hex::encode(to_keys.nullifer_public_key.0), @@ -1115,7 +1115,7 @@ pub fn prepare_function_map() -> HashMap { let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); - let command = Command::Account(AccountSubcommand::Register(RegisterSubcommand::Private {})); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); let sub_ret = wallet::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::RegisterAccount { addr: to_addr } = sub_ret else { @@ -1136,7 +1136,7 @@ pub fn prepare_function_map() -> HashMap { .cloned() .unwrap(); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Private( + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { from: from.to_string(), to_npk: hex::encode(to_keys.nullifer_public_key.0), @@ -1184,7 +1184,7 @@ pub fn prepare_function_map() -> HashMap { info!("test_success_deshielded_transfer_to_another_account"); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to: Address = ACC_RECEIVER.parse().unwrap(); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Deshielded { + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Deshielded { from: from.to_string(), to: to.to_string(), amount: 100, @@ -1230,7 +1230,7 @@ pub fn prepare_function_map() -> HashMap { info!("test_success_shielded_transfer_to_another_owned_account"); let from: Address = ACC_SENDER.parse().unwrap(); let to: Address = ACC_RECEIVER_PRIVATE.parse().unwrap(); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Shielded( + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Shielded( NativeTokenTransferProgramSubcommandShielded::ShieldedOwned { from: from.to_string(), to: to.to_string(), @@ -1274,7 +1274,7 @@ pub fn prepare_function_map() -> HashMap { let to_ipk = Secp256k1Point::from_scalar(to_npk.0); let from: Address = ACC_SENDER.parse().unwrap(); - let command = Command::Transfer(NativeTokenTransferProgramSubcommand::Shielded( + let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Shielded( NativeTokenTransferProgramSubcommandShielded::ShieldedForeign { from: from.to_string(), to_npk: to_npk_string, @@ -1318,7 +1318,7 @@ pub fn prepare_function_map() -> HashMap { let pinata_addr = "cafe".repeat(16); let pinata_prize = 150; let solution = 989106; - let command = Command::PinataProgram(PinataProgramSubcommand::Public( + let command = Command::Pinata(PinataProgramSubcommand::Public( PinataProgramSubcommandPublic::Claim { pinata_addr: pinata_addr.clone(), winner_addr: ACC_SENDER.to_string(), @@ -1446,7 +1446,7 @@ pub fn prepare_function_map() -> HashMap { let pinata_prize = 150; let solution = 989106; - let command = Command::PinataProgram(PinataProgramSubcommand::Private( + let command = Command::Pinata(PinataProgramSubcommand::Private( PinataProgramSubcommandPrivate::ClaimPrivateOwned { pinata_addr: pinata_addr.clone(), winner_addr: ACC_SENDER_PRIVATE.to_string(), @@ -1512,8 +1512,8 @@ pub fn prepare_function_map() -> HashMap { // Create new account for the token supply holder (private) let SubcommandReturnValue::RegisterAccount { addr: winner_addr } = - wallet::execute_subcommand(Command::Account(AccountSubcommand::Register( - RegisterSubcommand::Private {}, + wallet::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Private {}, ))) .await .unwrap() @@ -1521,7 +1521,7 @@ pub fn prepare_function_map() -> HashMap { panic!("invalid subcommand return value"); }; - let command = Command::PinataProgram(PinataProgramSubcommand::Private( + let command = Command::Pinata(PinataProgramSubcommand::Private( PinataProgramSubcommandPrivate::ClaimPrivateOwned { pinata_addr: pinata_addr.clone(), winner_addr: winner_addr.to_string(), diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 544a2f8..b0708b4 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -9,7 +9,8 @@ serde.workspace = true k256.workspace = true sha2.workspace = true rand.workspace = true -hex.workspace = true +base58.workspace = true +hex = "0.4.3" aes-gcm.workspace = true bip39.workspace = true hmac-sha512.workspace = true diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index 5650fd5..f22a99f 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -55,6 +55,7 @@ impl KeyChain { #[cfg(test)] mod tests { use aes_gcm::aead::OsRng; + use base58::ToBase58; use k256::AffinePoint; use k256::elliptic_curve::group::GroupEncoding; use rand::RngCore; @@ -119,7 +120,7 @@ mod tests { println!("======Public data======"); println!(); - println!("Address{:?}", hex::encode(address.value())); + println!("Address{:?}", address.value().to_base58()); println!( "Nulifier public key {:?}", hex::encode(nullifer_public_key.to_byte_array()) diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index e1951c4..5712eaf 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -10,8 +10,9 @@ thiserror = { version = "2.0.12", optional = true } bytemuck = { version = "1.13", optional = true } chacha20 = { version = "0.9", default-features = false } k256 = { version = "0.13.3", optional = true } -hex = { version = "0.4.3", optional = true } +base58 = { version = "0.2.0", optional = true } +anyhow = { version = "1.0.98", optional = true } [features] default = [] -host = ["thiserror", "bytemuck", "k256", "hex"] +host = ["thiserror", "bytemuck", "k256", "base58", "anyhow"] diff --git a/nssa/core/src/address.rs b/nssa/core/src/address.rs index 2627368..774145e 100644 --- a/nssa/core/src/address.rs +++ b/nssa/core/src/address.rs @@ -3,6 +3,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] use std::{fmt::Display, str::FromStr}; +#[cfg(feature = "host")] +use base58::{FromBase58, ToBase58}; + #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr( any(feature = "host", test), @@ -31,8 +34,8 @@ impl AsRef<[u8]> for Address { #[cfg(feature = "host")] #[derive(Debug, thiserror::Error)] pub enum AddressError { - #[error("invalid hex")] - InvalidHex(#[from] hex::FromHexError), + #[error("invalid base58")] + InvalidBase58(#[from] anyhow::Error), #[error("invalid length: expected 32 bytes, got {0}")] InvalidLength(usize), } @@ -41,7 +44,9 @@ pub enum AddressError { impl FromStr for Address { type Err = AddressError; fn from_str(s: &str) -> Result { - let bytes = hex::decode(s)?; + let bytes = s + .from_base58() + .map_err(|err| anyhow::anyhow!("Invalid base58 err {err:?}"))?; if bytes.len() != 32 { return Err(AddressError::InvalidLength(bytes.len())); } @@ -54,7 +59,7 @@ impl FromStr for Address { #[cfg(feature = "host")] impl Display for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.value)) + write!(f, "{}", self.value.to_base58()) } } @@ -74,7 +79,7 @@ mod tests { fn parse_invalid_hex() { let hex_str = "zz".repeat(32); // invalid hex chars let result = hex_str.parse::
().unwrap_err(); - assert!(matches!(result, AddressError::InvalidHex(_))); + assert!(matches!(result, AddressError::InvalidBase58(_))); } #[test] diff --git a/sequencer_core/Cargo.toml b/sequencer_core/Cargo.toml index 72a8cc4..6e9979c 100644 --- a/sequencer_core/Cargo.toml +++ b/sequencer_core/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -hex.workspace = true +base58.workspace = true anyhow.workspace = true serde.workspace = true rand.workspace = true diff --git a/sequencer_core/src/lib.rs b/sequencer_core/src/lib.rs index 92d53e7..98ff16d 100644 --- a/sequencer_core/src/lib.rs +++ b/sequencer_core/src/lib.rs @@ -204,6 +204,7 @@ impl SequencerCore { #[cfg(test)] mod tests { + use base58::{FromBase58, ToBase58}; use common::test_utils::sequencer_sign_key_for_testing; use crate::config::AccountInitialData; @@ -237,23 +238,23 @@ mod tests { } fn setup_sequencer_config() -> SequencerConfig { - let acc1_addr = vec![ + let acc1_addr: Vec = vec![ 208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9, 115, 84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68, ]; - let acc2_addr = vec![ + let acc2_addr: Vec = vec![ 231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57, 141, 98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188, ]; let initial_acc1 = AccountInitialData { - addr: hex::encode(acc1_addr), + addr: acc1_addr.to_base58(), balance: 10000, }; let initial_acc2 = AccountInitialData { - addr: hex::encode(acc2_addr), + addr: acc2_addr.to_base58(), balance: 20000, }; @@ -288,11 +289,17 @@ mod tests { assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); assert_eq!(sequencer.sequencer_config.port, 8080); - let acc1_addr = hex::decode(config.initial_accounts[0].addr.clone()) + let acc1_addr = config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2_addr = hex::decode(config.initial_accounts[1].addr.clone()) + let acc2_addr = config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -314,23 +321,23 @@ mod tests { #[test] fn test_start_different_intial_accounts_balances() { - let acc1_addr = vec![ + let acc1_addr: Vec = vec![ 27, 132, 197, 86, 123, 18, 100, 64, 153, 93, 62, 213, 170, 186, 5, 101, 215, 30, 24, 52, 96, 72, 25, 255, 156, 23, 245, 233, 213, 221, 7, 143, ]; - let acc2_addr = vec![ + let acc2_addr: Vec = vec![ 77, 75, 108, 209, 54, 16, 50, 202, 155, 210, 174, 185, 217, 0, 170, 77, 69, 217, 234, 216, 10, 201, 66, 51, 116, 196, 81, 167, 37, 77, 7, 102, ]; let initial_acc1 = AccountInitialData { - addr: hex::encode(acc1_addr), + addr: acc1_addr.to_base58(), balance: 10000, }; let initial_acc2 = AccountInitialData { - addr: hex::encode(acc2_addr), + addr: acc2_addr.to_base58(), balance: 20000, }; @@ -339,11 +346,17 @@ mod tests { let config = setup_sequencer_config_variable_initial_accounts(initial_accounts); let sequencer = SequencerCore::start_from_config(config.clone()); - let acc1_addr = hex::decode(config.initial_accounts[0].addr.clone()) + let acc1_addr = config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2_addr = hex::decode(config.initial_accounts[1].addr.clone()) + let acc2_addr = config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -386,11 +399,17 @@ mod tests { common_setup(&mut sequencer); - let acc1 = hex::decode(sequencer.sequencer_config.initial_accounts[0].addr.clone()) + let acc1 = sequencer.sequencer_config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2 = hex::decode(sequencer.sequencer_config.initial_accounts[1].addr.clone()) + let acc2 = sequencer.sequencer_config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -412,11 +431,17 @@ mod tests { common_setup(&mut sequencer); - let acc1 = hex::decode(sequencer.sequencer_config.initial_accounts[0].addr.clone()) + let acc1 = sequencer.sequencer_config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2 = hex::decode(sequencer.sequencer_config.initial_accounts[1].addr.clone()) + let acc2 = sequencer.sequencer_config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -448,11 +473,17 @@ mod tests { common_setup(&mut sequencer); - let acc1 = hex::decode(sequencer.sequencer_config.initial_accounts[0].addr.clone()) + let acc1 = sequencer.sequencer_config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2 = hex::decode(sequencer.sequencer_config.initial_accounts[1].addr.clone()) + let acc2 = sequencer.sequencer_config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -484,11 +515,17 @@ mod tests { common_setup(&mut sequencer); - let acc1 = hex::decode(sequencer.sequencer_config.initial_accounts[0].addr.clone()) + let acc1 = sequencer.sequencer_config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2 = hex::decode(sequencer.sequencer_config.initial_accounts[1].addr.clone()) + let acc2 = sequencer.sequencer_config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -576,11 +613,17 @@ mod tests { common_setup(&mut sequencer); - let acc1 = hex::decode(sequencer.sequencer_config.initial_accounts[0].addr.clone()) + let acc1 = sequencer.sequencer_config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2 = hex::decode(sequencer.sequencer_config.initial_accounts[1].addr.clone()) + let acc2 = sequencer.sequencer_config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); @@ -618,11 +661,17 @@ mod tests { common_setup(&mut sequencer); - let acc1 = hex::decode(sequencer.sequencer_config.initial_accounts[0].addr.clone()) + let acc1 = sequencer.sequencer_config.initial_accounts[0] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); - let acc2 = hex::decode(sequencer.sequencer_config.initial_accounts[1].addr.clone()) + let acc2 = sequencer.sequencer_config.initial_accounts[1] + .addr + .clone() + .from_base58() .unwrap() .try_into() .unwrap(); diff --git a/sequencer_rpc/Cargo.toml b/sequencer_rpc/Cargo.toml index af7e011..557ce6a 100644 --- a/sequencer_rpc/Cargo.toml +++ b/sequencer_rpc/Cargo.toml @@ -10,7 +10,8 @@ log.workspace = true serde.workspace = true actix-cors.workspace = true futures.workspace = true -hex.workspace = true +base58.workspace = true +hex = "0.4.3" tempfile.workspace = true base64.workspace = true diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index cb49d58..faf71ef 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use actix_web::Error as HttpError; +use base58::FromBase58; use base64::{Engine, engine::general_purpose}; use nssa::{self, program::Program}; use sequencer_core::config::AccountInitialData; @@ -163,8 +164,10 @@ impl JsonHandler { /// The address must be a valid hex string of the correct length. async fn process_get_account_balance(&self, request: Request) -> Result { let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?; - let address_bytes = hex::decode(get_account_req.address) - .map_err(|_| RpcError::invalid_params("invalid hex".to_string()))?; + let address_bytes = get_account_req + .address + .from_base58() + .map_err(|_| RpcError::invalid_params("invalid base58".to_string()))?; let address = nssa::Address::new( address_bytes .try_into() @@ -312,6 +315,7 @@ mod tests { use std::sync::Arc; use crate::{JsonHandler, rpc_handler}; + use base58::ToBase58; use base64::{Engine, engine::general_purpose}; use common::{ rpc_primitives::RpcPollingConfig, test_utils::sequencer_sign_key_for_testing, @@ -329,23 +333,23 @@ mod tests { fn sequencer_config_for_tests() -> SequencerConfig { let tempdir = tempdir().unwrap(); let home = tempdir.path().to_path_buf(); - let acc1_addr = vec![ + let acc1_addr: Vec = vec![ 208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9, 115, 84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68, ]; - let acc2_addr = vec![ + let acc2_addr: Vec = vec![ 231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57, 141, 98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188, ]; let initial_acc1 = AccountInitialData { - addr: hex::encode(acc1_addr), + addr: acc1_addr.to_base58(), balance: 10000, }; let initial_acc2 = AccountInitialData { - addr: hex::encode(acc2_addr), + addr: acc2_addr.to_base58(), balance: 20000, }; diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 48d79e2..b04d67e 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -16,7 +16,8 @@ nssa-core = { path = "../nssa/core" } base64.workspace = true bytemuck = "1.23.2" borsh.workspace = true -hex.workspace = true +base58.workspace = true +hex = "0.4.3" rand.workspace = true [dependencies.key_protocol] diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 9ec4b20..f801e30 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use anyhow::Result; +use base58::ToBase58; use clap::Subcommand; use common::transaction::NSSATransaction; use nssa::Address; @@ -18,9 +19,9 @@ pub enum AccountSubcommand { ///Fetch #[command(subcommand)] Fetch(FetchSubcommand), - ///Register + ///New #[command(subcommand)] - Register(RegisterSubcommand), + New(NewSubcommand), } ///Represents generic getter CLI subcommand @@ -72,7 +73,7 @@ pub enum FetchSubcommand { ///Represents generic register CLI subcommand #[derive(Subcommand, Debug, Clone)] -pub enum RegisterSubcommand { +pub enum NewSubcommand { ///Register new public account Public {}, ///Register new private account @@ -190,13 +191,13 @@ impl WalletSubcommand for FetchSubcommand { } } -impl WalletSubcommand for RegisterSubcommand { +impl WalletSubcommand for NewSubcommand { async fn handle_subcommand( self, wallet_core: &mut WalletCore, ) -> Result { match self { - RegisterSubcommand::Public {} => { + NewSubcommand::Public {} => { let addr = wallet_core.create_new_account_public(); println!("Generated new account with addr {addr}"); @@ -207,7 +208,7 @@ impl WalletSubcommand for RegisterSubcommand { Ok(SubcommandReturnValue::RegisterAccount { addr }) } - RegisterSubcommand::Private {} => { + NewSubcommand::Private {} => { let addr = wallet_core.create_new_account_private(); let (key, _) = wallet_core @@ -216,8 +217,8 @@ impl WalletSubcommand for RegisterSubcommand { .get_private_account(&addr) .unwrap(); - println!("Generated new account with addr {addr}"); - println!("With npk {}", hex::encode(&key.nullifer_public_key)); + println!("Generated new account with addr {}", addr.to_bytes().to_base58()); + println!("With npk {}", hex::encode(&key.nullifer_public_key.0)); println!( "With ipk {}", hex::encode(key.incoming_viewing_public_key.to_bytes()) @@ -245,8 +246,8 @@ impl WalletSubcommand for AccountSubcommand { AccountSubcommand::Fetch(fetch_subcommand) => { fetch_subcommand.handle_subcommand(wallet_core).await } - AccountSubcommand::Register(register_subcommand) => { - register_subcommand.handle_subcommand(wallet_core).await + AccountSubcommand::New(new_subcommand) => { + new_subcommand.handle_subcommand(wallet_core).await } } } diff --git a/wallet/src/cli/chain.rs b/wallet/src/cli/chain.rs index 4db18fc..aec2c9a 100644 --- a/wallet/src/cli/chain.rs +++ b/wallet/src/cli/chain.rs @@ -6,12 +6,12 @@ use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; ///Represents generic chain CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum ChainSubcommand { - GetLatestBlockId {}, - GetBlockAtId { + CurrentBlockId {}, + Block { #[arg(short, long)] id: u64, }, - GetTransactionAtHash { + Transaction { #[arg(short, long)] hash: String, }, @@ -23,17 +23,17 @@ impl WalletSubcommand for ChainSubcommand { wallet_core: &mut WalletCore, ) -> Result { match self { - ChainSubcommand::GetLatestBlockId {} => { + ChainSubcommand::CurrentBlockId {} => { let latest_block_res = wallet_core.sequencer_client.get_last_block().await?; println!("Last block id is {}", latest_block_res.last_block); } - ChainSubcommand::GetBlockAtId { id } => { + ChainSubcommand::Block { id } => { let block_res = wallet_core.sequencer_client.get_block(id).await?; println!("Last block id is {:#?}", block_res.block); } - ChainSubcommand::GetTransactionAtHash { hash } => { + ChainSubcommand::Transaction { hash } => { let tx_res = wallet_core .sequencer_client .get_transaction_by_hash(hash) diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 833422f..5de7f5d 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -193,19 +193,19 @@ impl WalletCore { pub enum Command { ///Transfer command #[command(subcommand)] - Transfer(NativeTokenTransferProgramSubcommand), + AuthTransfer(NativeTokenTransferProgramSubcommand), ///Chain command #[command(subcommand)] - Chain(ChainSubcommand), + ChainInfo(ChainSubcommand), ///Chain command #[command(subcommand)] Account(AccountSubcommand), ///Pinata command #[command(subcommand)] - PinataProgram(PinataProgramSubcommand), + Pinata(PinataProgramSubcommand), ///Token command #[command(subcommand)] - TokenProgram(TokenProgramSubcommand), + Token(TokenProgramSubcommand), AuthenticatedTransferInitializePublicAccount {}, // Check the wallet can connect to the node and builtin local programs // match the remote versions @@ -237,12 +237,12 @@ pub async fn execute_subcommand(command: Command) -> Result { + Command::AuthTransfer(transfer_subcommand) => { transfer_subcommand .handle_subcommand(&mut wallet_core) .await? } - Command::Chain(chain_subcommand) => { + Command::ChainInfo(chain_subcommand) => { chain_subcommand.handle_subcommand(&mut wallet_core).await? } Command::Account(account_subcommand) => { @@ -250,7 +250,7 @@ pub async fn execute_subcommand(command: Command) -> Result { + Command::Pinata(pinata_subcommand) => { pinata_subcommand .handle_subcommand(&mut wallet_core) .await? @@ -304,7 +304,7 @@ pub async fn execute_subcommand(command: Command) -> Result { + Command::Token(token_subcommand) => { token_subcommand.handle_subcommand(&mut wallet_core).await? } }; From 0384efc38f9296e0a7af4406a0a392a5976e79d3 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Fri, 24 Oct 2025 11:12:32 +0300 Subject: [PATCH 02/15] fix: base58 adoption fixes --- .../debug/sequencer/sequencer_config.json | 126 +-- .../configs/debug/wallet/wallet_config.json | 858 +++++++++--------- integration_tests/src/lib.rs | 10 +- integration_tests/src/test_suite_map.rs | 28 +- key_protocol/src/key_protocol_core/mod.rs | 5 + nssa/core/src/address.rs | 18 +- sequencer_core/src/sequencer_store/mod.rs | 4 +- sequencer_rpc/src/process.rs | 12 +- wallet/src/chain_storage/mod.rs | 88 +- wallet/src/cli/account.rs | 5 +- 10 files changed, 618 insertions(+), 536 deletions(-) diff --git a/integration_tests/configs/debug/sequencer/sequencer_config.json b/integration_tests/configs/debug/sequencer/sequencer_config.json index 2a2037d..beb39cb 100644 --- a/integration_tests/configs/debug/sequencer/sequencer_config.json +++ b/integration_tests/configs/debug/sequencer/sequencer_config.json @@ -8,49 +8,49 @@ "port": 3040, "initial_accounts": [ { - "addr": "d07ad2e84b27fa00c262f0a1eea0ff35ca0973547e6a106f72f193c2dc838b44", + "addr": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", "balance": 10000 }, { - "addr": "e7ae77c5ef1a05999344af499fc78a1705398d62ed06cf2e1479f6def89a39bc", + "addr": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", "balance": 20000 } ], "initial_commitments": [ { "npk": [ - 193, - 209, - 150, - 113, - 47, - 241, - 48, - 145, - 250, - 79, - 235, - 51, - 119, - 40, - 184, - 232, - 5, + 63, + 202, + 178, + 231, + 183, + 82, + 237, + 212, + 216, 221, - 36, - 21, - 201, - 106, - 90, - 210, - 129, - 106, - 71, - 99, - 208, + 215, + 255, 153, - 75, - 215 + 101, + 177, + 161, + 254, + 210, + 128, + 122, + 54, + 190, + 230, + 151, + 183, + 64, + 225, + 229, + 113, + 1, + 228, + 97 ], "account": { "program_owner": [ @@ -70,38 +70,38 @@ }, { "npk": [ - 27, - 250, + 192, + 251, + 166, + 243, + 167, + 236, + 84, + 249, + 35, 136, - 142, - 88, - 128, - 138, - 21, - 49, - 183, - 118, - 160, - 117, - 114, - 110, - 47, - 136, - 87, - 60, - 70, - 59, - 60, - 18, - 223, - 23, - 147, - 241, - 5, - 184, - 103, + 130, + 172, + 219, 225, - 105 + 161, + 139, + 229, + 89, + 243, + 125, + 194, + 213, + 209, + 30, + 23, + 174, + 100, + 244, + 124, + 74, + 140, + 47 ], "account": { "program_owner": [ @@ -154,4 +154,4 @@ 37, 37 ] -} +} \ No newline at end of file diff --git a/integration_tests/configs/debug/wallet/wallet_config.json b/integration_tests/configs/debug/wallet/wallet_config.json index 0081da6..95c95e9 100644 --- a/integration_tests/configs/debug/wallet/wallet_config.json +++ b/integration_tests/configs/debug/wallet/wallet_config.json @@ -9,85 +9,85 @@ "initial_accounts": [ { "Public": { - "address": "d07ad2e84b27fa00c262f0a1eea0ff35ca0973547e6a106f72f193c2dc838b44", + "address": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", "pub_sign_key": [ - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1 + 16, + 162, + 106, + 154, + 236, + 125, + 52, + 184, + 35, + 100, + 238, + 174, + 69, + 197, + 41, + 77, + 187, + 10, + 118, + 75, + 0, + 11, + 148, + 238, + 185, + 181, + 133, + 17, + 220, + 72, + 124, + 77 ] } }, { "Public": { - "address": "e7ae77c5ef1a05999344af499fc78a1705398d62ed06cf2e1479f6def89a39bc", + "address": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", "pub_sign_key": [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 113, + 121, + 64, + 177, + 204, + 85, + 229, + 214, + 178, + 6, + 109, + 191, + 29, + 154, + 63, + 38, + 242, + 18, + 244, + 219, + 8, + 208, + 35, + 136, + 23, + 127, + 207, + 237, + 216, + 169, + 190, + 27 ] } }, { "Private": { - "address": "d360d6b5763f71ac6af56253687fd7d556d5c6c64312e53c0b92ef039a4375df", + "address": "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw", "account": { "program_owner": [ 0, @@ -105,218 +105,218 @@ }, "key_chain": { "secret_spending_key": [ - 10, - 125, - 171, - 38, - 201, - 35, - 164, - 43, - 7, - 80, - 7, - 215, - 97, - 42, - 48, - 229, - 101, - 216, - 140, - 21, - 170, - 214, + 251, 82, - 53, + 235, + 1, + 146, + 96, + 30, + 81, + 162, + 234, + 33, + 15, + 123, + 129, 116, - 22, - 62, - 79, - 61, - 76, - 71, - 79 + 0, + 84, + 136, + 176, + 70, + 190, + 224, + 161, + 54, + 134, + 142, + 154, + 1, + 18, + 251, + 242, + 189 ], "private_key_holder": { "nullifier_secret_key": [ - 228, - 136, - 4, + 29, + 250, + 10, + 187, + 35, + 123, + 180, + 250, + 246, + 97, + 216, + 153, + 44, 156, - 33, - 40, - 194, - 172, - 95, - 168, - 201, - 33, - 24, - 30, - 126, - 197, - 156, - 113, - 64, - 162, - 131, - 210, - 110, - 60, - 24, - 154, - 86, - 59, - 184, - 95, - 245, - 176 + 16, + 93, + 241, + 26, + 174, + 219, + 72, + 84, + 34, + 247, + 112, + 101, + 217, + 243, + 189, + 173, + 75, + 20 ], "incoming_viewing_secret_key": [ - 197, - 33, - 51, - 200, - 1, - 121, - 60, - 52, - 233, - 234, - 12, - 166, - 196, - 227, - 187, - 1, - 10, - 101, - 183, - 105, - 140, - 28, - 152, + 251, + 201, + 22, + 154, + 100, + 165, + 218, + 108, + 163, + 190, + 135, + 91, + 145, + 84, + 69, + 241, + 46, + 117, 217, - 109, - 220, - 112, - 103, - 253, 110, - 98, - 6 + 197, + 248, + 91, + 193, + 14, + 104, + 88, + 103, + 67, + 153, + 182, + 158 ], "outgoing_viewing_secret_key": [ - 147, - 34, - 193, - 29, - 39, - 173, - 222, + 25, + 67, + 121, + 76, + 175, + 100, 30, - 118, - 199, - 44, - 204, - 43, - 232, - 107, - 223, - 249, - 207, - 245, - 183, - 63, - 209, - 129, - 48, - 254, - 66, - 22, - 199, - 81, - 145, - 126, - 92 + 198, + 105, + 123, + 49, + 169, + 75, + 178, + 75, + 210, + 100, + 143, + 210, + 243, + 228, + 243, + 21, + 18, + 36, + 84, + 164, + 186, + 139, + 113, + 214, + 12 ] }, "nullifer_public_key": [ - 193, - 209, - 150, - 113, - 47, - 241, - 48, - 145, - 250, - 79, - 235, - 51, - 119, - 40, - 184, - 232, - 5, + 63, + 202, + 178, + 231, + 183, + 82, + 237, + 212, + 216, 221, - 36, - 21, - 201, - 106, - 90, - 210, - 129, - 106, - 71, - 99, - 208, + 215, + 255, 153, - 75, - 215 + 101, + 177, + 161, + 254, + 210, + 128, + 122, + 54, + 190, + 230, + 151, + 183, + 64, + 225, + 229, + 113, + 1, + 228, + 97 ], "incoming_viewing_public_key": [ 3, - 78, + 235, + 139, + 131, + 237, 177, - 87, - 193, - 219, - 230, - 160, - 222, - 38, - 182, - 100, - 101, - 223, - 204, - 223, - 198, - 140, - 253, - 94, - 16, - 98, - 77, - 79, - 114, - 30, - 158, - 104, - 34, - 152, + 122, 189, - 31, - 95 + 6, + 177, + 167, + 178, + 202, + 117, + 246, + 58, + 28, + 65, + 132, + 79, + 220, + 139, + 119, + 243, + 187, + 160, + 212, + 121, + 61, + 247, + 116, + 72, + 205 ] } } }, { "Private": { - "address": "f27087ffc29b99035303697dcf6c8e323b1847d4261e6afd49e0d71c6dfa31ea", + "address": "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX", "account": { "program_owner": [ 0, @@ -334,214 +334,214 @@ }, "key_chain": { "secret_spending_key": [ - 153, - 109, - 202, - 226, - 97, - 212, - 77, - 147, - 75, - 107, - 153, - 106, - 89, - 167, - 49, - 230, - 122, + 238, + 171, + 241, + 69, + 111, + 217, + 85, + 64, + 19, + 82, + 18, + 189, + 32, + 91, 78, - 167, - 146, - 14, - 180, - 206, + 175, 107, - 96, - 193, - 255, - 122, - 207, - 30, - 142, - 99 + 7, + 109, + 60, + 52, + 44, + 243, + 230, + 72, + 244, + 192, + 92, + 137, + 33, + 118, + 254 ], "private_key_holder": { "nullifier_secret_key": [ - 128, + 25, + 211, 215, - 147, - 175, 119, - 16, - 140, - 219, - 155, - 134, - 27, - 81, - 64, - 40, - 196, - 240, - 61, - 144, - 232, - 164, - 181, 57, - 139, - 96, - 137, - 121, - 140, + 223, + 247, + 37, + 245, + 144, + 122, 29, - 169, - 68, - 187, - 65 + 118, + 245, + 83, + 228, + 23, + 9, + 101, + 120, + 88, + 33, + 238, + 207, + 128, + 61, + 110, + 2, + 89, + 62, + 164, + 13 ], "incoming_viewing_secret_key": [ - 185, - 121, - 146, - 213, - 13, - 3, - 93, - 206, - 25, - 127, - 155, - 21, - 155, - 115, + 193, + 181, + 14, + 196, + 142, + 84, + 15, + 65, + 128, + 101, + 70, + 196, + 241, + 47, 130, - 27, - 57, - 5, - 116, - 80, - 62, - 214, - 67, - 228, - 147, - 189, - 28, - 200, - 62, - 152, - 178, - 103 + 221, + 23, + 146, + 161, + 237, + 221, + 40, + 19, + 126, + 59, + 15, + 169, + 236, + 25, + 105, + 104, + 231 ], "outgoing_viewing_secret_key": [ - 163, - 58, - 118, - 160, + 20, + 170, + 220, + 108, + 41, + 23, + 155, + 217, + 247, + 190, 175, - 86, - 72, + 168, + 247, + 34, + 105, + 134, + 114, + 74, + 104, 91, - 81, - 69, - 150, - 154, - 113, 211, - 118, - 110, - 25, - 156, - 250, - 67, - 212, - 198, - 147, - 231, - 213, - 136, - 212, - 198, - 192, - 255, + 62, 126, - 122 + 13, + 130, + 100, + 241, + 214, + 250, + 236, + 38, + 150 ] }, "nullifer_public_key": [ - 27, - 250, + 192, + 251, + 166, + 243, + 167, + 236, + 84, + 249, + 35, 136, - 142, - 88, - 128, - 138, - 21, - 49, - 183, - 118, - 160, - 117, - 114, - 110, - 47, - 136, - 87, - 60, - 70, - 59, - 60, - 18, - 223, - 23, - 147, - 241, - 5, - 184, - 103, + 130, + 172, + 219, 225, - 105 + 161, + 139, + 229, + 89, + 243, + 125, + 194, + 213, + 209, + 30, + 23, + 174, + 100, + 244, + 124, + 74, + 140, + 47 ], "incoming_viewing_public_key": [ 2, - 56, - 160, - 1, - 22, - 197, - 187, - 214, - 204, - 221, - 84, - 87, - 12, - 204, - 0, - 119, - 116, - 176, - 6, - 149, - 145, - 100, - 211, - 162, - 19, - 158, - 197, - 112, - 142, - 172, - 1, + 181, 98, - 226 + 93, + 216, + 241, + 241, + 110, + 58, + 198, + 119, + 174, + 250, + 184, + 1, + 204, + 200, + 173, + 44, + 238, + 37, + 247, + 170, + 156, + 100, + 254, + 116, + 242, + 28, + 183, + 187, + 77, + 255 ] } } } ] -} +} \ No newline at end of file diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index bbd5066..5b21057 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -32,13 +32,11 @@ struct Args { test_name: String, } -pub const ACC_SENDER: &str = "d07ad2e84b27fa00c262f0a1eea0ff35ca0973547e6a106f72f193c2dc838b44"; -pub const ACC_RECEIVER: &str = "e7ae77c5ef1a05999344af499fc78a1705398d62ed06cf2e1479f6def89a39bc"; +pub const ACC_SENDER: &str = "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy"; +pub const ACC_RECEIVER: &str = "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw"; -pub const ACC_SENDER_PRIVATE: &str = - "d360d6b5763f71ac6af56253687fd7d556d5c6c64312e53c0b92ef039a4375df"; -pub const ACC_RECEIVER_PRIVATE: &str = - "f27087ffc29b99035303697dcf6c8e323b1847d4261e6afd49e0d71c6dfa31ea"; +pub const ACC_SENDER_PRIVATE: &str = "3oCG8gqdKLMegw4rRfyaMQvuPHpcASt7xwttsmnZLSkw"; +pub const ACC_RECEIVER_PRIVATE: &str = "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9er6xX"; pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 80bf894..67d78e8 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -25,6 +25,8 @@ use wallet::{ helperfunctions::{fetch_config, fetch_persistent_accounts}, }; +use sequencer_core::sequencer_store::PINATA_BASE58; + use crate::{ ACC_RECEIVER, ACC_RECEIVER_PRIVATE, ACC_SENDER, ACC_SENDER_PRIVATE, NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, @@ -1315,12 +1317,12 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_pinata() { info!("test_pinata"); - let pinata_addr = "cafe".repeat(16); + let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; let command = Command::Pinata(PinataProgramSubcommand::Public( PinataProgramSubcommandPublic::Claim { - pinata_addr: pinata_addr.clone(), + pinata_addr: pinata_addr.to_string(), winner_addr: ACC_SENDER.to_string(), solution, }, @@ -1331,7 +1333,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); let pinata_balance_pre = seq_client - .get_account_balance(pinata_addr.clone()) + .get_account_balance(pinata_addr.to_string()) .await .unwrap() .balance; @@ -1343,7 +1345,7 @@ pub fn prepare_function_map() -> HashMap { info!("Checking correct balance move"); let pinata_balance_post = seq_client - .get_account_balance(pinata_addr.clone()) + .get_account_balance(pinata_addr.to_string()) .await .unwrap() .balance; @@ -1379,7 +1381,7 @@ pub fn prepare_function_map() -> HashMap { // We pass an uninitialized account and we expect after execution to be owned by the data // changer program (NSSA account claiming mechanism) with data equal to [0] (due to program logic) let data_changer = Program::new(bytecode).unwrap(); - let address: Address = "deadbeef".repeat(8).parse().unwrap(); + let address: Address = "11".repeat(16).parse().unwrap(); let message = nssa::public_transaction::Message::try_new( data_changer.id(), vec![address], @@ -1442,13 +1444,13 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_pinata_private_receiver() { info!("test_pinata_private_receiver"); - let pinata_addr = "cafe".repeat(16); + let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; let command = Command::Pinata(PinataProgramSubcommand::Private( PinataProgramSubcommandPrivate::ClaimPrivateOwned { - pinata_addr: pinata_addr.clone(), + pinata_addr: pinata_addr.to_string(), winner_addr: ACC_SENDER_PRIVATE.to_string(), solution, }, @@ -1459,7 +1461,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); let pinata_balance_pre = seq_client - .get_account_balance(pinata_addr.clone()) + .get_account_balance(pinata_addr.to_string()) .await .unwrap() .balance; @@ -1475,7 +1477,7 @@ pub fn prepare_function_map() -> HashMap { info!("Checking correct balance move"); let pinata_balance_post = seq_client - .get_account_balance(pinata_addr.clone()) + .get_account_balance(pinata_addr.to_string()) .await .unwrap() .balance; @@ -1506,7 +1508,7 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_pinata_private_receiver_new_account() { info!("test_pinata_private_receiver"); - let pinata_addr = "cafe".repeat(16); + let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; @@ -1523,7 +1525,7 @@ pub fn prepare_function_map() -> HashMap { let command = Command::Pinata(PinataProgramSubcommand::Private( PinataProgramSubcommandPrivate::ClaimPrivateOwned { - pinata_addr: pinata_addr.clone(), + pinata_addr: pinata_addr.to_string(), winner_addr: winner_addr.to_string(), solution, }, @@ -1534,7 +1536,7 @@ pub fn prepare_function_map() -> HashMap { let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); let pinata_balance_pre = seq_client - .get_account_balance(pinata_addr.clone()) + .get_account_balance(pinata_addr.to_string()) .await .unwrap() .balance; @@ -1546,7 +1548,7 @@ pub fn prepare_function_map() -> HashMap { info!("Checking correct balance move"); let pinata_balance_post = seq_client - .get_account_balance(pinata_addr.clone()) + .get_account_balance(pinata_addr.to_string()) .await .unwrap() .balance; diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index df5502e..b1ebe71 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -142,5 +142,10 @@ mod tests { let is_key_chain_generated = user_data.get_private_account(&addr_private).is_some(); assert!(is_key_chain_generated); + + let addr_private_str = addr_private.to_string(); + println!("{addr_private_str:#?}"); + let key_chain = &user_data.get_private_account(&addr_private).unwrap().0; + println!("{key_chain:#?}"); } } diff --git a/nssa/core/src/address.rs b/nssa/core/src/address.rs index 774145e..6355351 100644 --- a/nssa/core/src/address.rs +++ b/nssa/core/src/address.rs @@ -70,29 +70,29 @@ mod tests { #[test] fn parse_valid_address() { - let hex_str = "00".repeat(32); // 64 hex chars = 32 bytes - let addr: Address = hex_str.parse().unwrap(); + let base58_str = "11111111111111111111111111111111"; + let addr: Address = base58_str.parse().unwrap(); assert_eq!(addr.value, [0u8; 32]); } #[test] - fn parse_invalid_hex() { - let hex_str = "zz".repeat(32); // invalid hex chars - let result = hex_str.parse::
().unwrap_err(); + fn parse_invalid_base58() { + let base58_str = "00".repeat(32); // invalid base58 chars + let result = base58_str.parse::
().unwrap_err(); assert!(matches!(result, AddressError::InvalidBase58(_))); } #[test] fn parse_wrong_length_short() { - let hex_str = "00".repeat(31); // 62 chars = 31 bytes - let result = hex_str.parse::
().unwrap_err(); + let base58_str = "11".repeat(31); // 62 chars = 31 bytes + let result = base58_str.parse::
().unwrap_err(); assert!(matches!(result, AddressError::InvalidLength(_))); } #[test] fn parse_wrong_length_long() { - let hex_str = "00".repeat(33); // 66 chars = 33 bytes - let result = hex_str.parse::
().unwrap_err(); + let base58_str = "11".repeat(33); // 66 chars = 33 bytes + let result = base58_str.parse::
().unwrap_err(); assert!(matches!(result, AddressError::InvalidLength(_))); } } diff --git a/sequencer_core/src/sequencer_store/mod.rs b/sequencer_core/src/sequencer_store/mod.rs index 4f18405..186b266 100644 --- a/sequencer_core/src/sequencer_store/mod.rs +++ b/sequencer_core/src/sequencer_store/mod.rs @@ -9,6 +9,8 @@ use crate::config::AccountInitialData; pub mod block_store; +pub const PINATA_BASE58: &str = "EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7"; + pub struct SequecerChainStore { pub state: nssa::V02State, pub block_store: SequecerBlockStore, @@ -35,7 +37,7 @@ impl SequecerChainStore { let state = { let mut this = nssa::V02State::new_with_genesis_accounts(&init_accs, initial_commitments); - this.add_pinata_program("cafe".repeat(16).parse().unwrap()); + this.add_pinata_program(PINATA_BASE58.parse().unwrap()); this }; diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 0ff1b00..4147e5c 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -434,7 +434,7 @@ mod tests { let request = serde_json::json!({ "jsonrpc": "2.0", "method": "get_account_balance", - "params": { "address": "efac".repeat(16) }, + "params": { "address": "11".repeat(16) }, "id": 1 }); let expected_response = serde_json::json!({ @@ -451,12 +451,12 @@ mod tests { } #[actix_web::test] - async fn test_get_account_balance_for_invalid_hex() { + async fn test_get_account_balance_for_invalid_base58() { let (json_handler, _, _) = components_for_tests(); let request = serde_json::json!({ "jsonrpc": "2.0", "method": "get_account_balance", - "params": { "address": "not_a_valid_hex" }, + "params": { "address": "not_a_valid_base58" }, "id": 1 }); let expected_response = serde_json::json!({ @@ -465,7 +465,7 @@ mod tests { "error": { "code": -32602, "message": "Invalid params", - "data": "invalid hex" + "data": "invalid base58" } }); let response = call_rpc_handler_with_json(json_handler, request).await; @@ -527,7 +527,7 @@ mod tests { let request = serde_json::json!({ "jsonrpc": "2.0", "method": "get_accounts_nonces", - "params": { "addresses": ["efac".repeat(16)] }, + "params": { "addresses": ["11".repeat(16)] }, "id": 1 }); let expected_response = serde_json::json!({ @@ -575,7 +575,7 @@ mod tests { let request = serde_json::json!({ "jsonrpc": "2.0", "method": "get_account", - "params": { "address": "efac".repeat(16) }, + "params": { "address": "11".repeat(16) }, "id": 1 }); let expected_response = serde_json::json!({ diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index e07ba8e..8fc8805 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -75,19 +75,91 @@ mod tests { use tempfile::tempdir; fn create_initial_accounts() -> Vec { - let initial_acc1 = serde_json::from_str(r#"{ + let initial_acc1 = serde_json::from_str( + r#"{ "Public": { - "address": "d07ad2e84b27fa00c262f0a1eea0ff35ca0973547e6a106f72f193c2dc838b44", - "pub_sign_key": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + "address": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", + "pub_sign_key": [ + 16, + 162, + 106, + 154, + 236, + 125, + 52, + 184, + 35, + 100, + 238, + 174, + 69, + 197, + 41, + 77, + 187, + 10, + 118, + 75, + 0, + 11, + 148, + 238, + 185, + 181, + 133, + 17, + 220, + 72, + 124, + 77 + ] } - }"#).unwrap(); + }"#, + ) + .unwrap(); - let initial_acc2 = serde_json::from_str(r#"{ + let initial_acc2 = serde_json::from_str( + r#"{ "Public": { - "address": "e7ae77c5ef1a05999344af499fc78a1705398d62ed06cf2e1479f6def89a39bc", - "pub_sign_key": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + "address": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", + "pub_sign_key": [ + 113, + 121, + 64, + 177, + 204, + 85, + 229, + 214, + 178, + 6, + 109, + 191, + 29, + 154, + 63, + 38, + 242, + 18, + 244, + 219, + 8, + 208, + 35, + 136, + 23, + 127, + 207, + 237, + 216, + 169, + 190, + 27 + ] } - }"#).unwrap(); + }"#, + ) + .unwrap(); let initial_accounts = vec![initial_acc1, initial_acc2]; diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index f801e30..23758d4 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -217,7 +217,10 @@ impl WalletSubcommand for NewSubcommand { .get_private_account(&addr) .unwrap(); - println!("Generated new account with addr {}", addr.to_bytes().to_base58()); + println!( + "Generated new account with addr {}", + addr.to_bytes().to_base58() + ); println!("With npk {}", hex::encode(&key.nullifer_public_key.0)); println!( "With ipk {}", From 66ee0c54494f2a58b21e3018387d1c44fb221b31 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Fri, 24 Oct 2025 15:26:30 +0300 Subject: [PATCH 03/15] 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()); + } } From 62668161b223129f047de58267be2606d7eff17a Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Mon, 27 Oct 2025 14:32:28 +0200 Subject: [PATCH 04/15] feat: transfers changes --- integration_tests/src/lib.rs | 8 + integration_tests/src/test_suite_map.rs | 351 +++++++++--------- wallet/src/cli/account.rs | 11 +- .../src/cli/native_token_transfer_program.rs | 182 ++++++++- wallet/src/cli/token_program.rs | 182 ++++++++- wallet/src/lib.rs | 9 +- wallet/src/transaction_utils.rs | 49 +++ 7 files changed, 614 insertions(+), 178 deletions(-) diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 5b21057..f09d808 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -42,6 +42,14 @@ pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &[u8] = include_bytes!("data_changer.bin"); +fn make_public_account_input_from_str(addr: &str) -> String { + format!("Public/{addr:?}") +} + +fn make_private_account_input_from_str(addr: &str) -> String { + format!("Private/{addr:?}") +} + #[allow(clippy::type_complexity)] pub async fn pre_test( home_dir: PathBuf, diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 67d78e8..7874b5c 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -8,18 +8,11 @@ use wallet::{ Command, SubcommandReturnValue, WalletCore, cli::{ account::{AccountSubcommand, FetchSubcommand, NewSubcommand}, - native_token_transfer_program::{ - NativeTokenTransferProgramSubcommand, NativeTokenTransferProgramSubcommandPrivate, - NativeTokenTransferProgramSubcommandShielded, - }, + native_token_transfer_program::AuthTransferSubcommand, pinata_program::{ PinataProgramSubcommand, PinataProgramSubcommandPrivate, PinataProgramSubcommandPublic, }, - token_program::{ - TokenProgramSubcommand, TokenProgramSubcommandDeshielded, - TokenProgramSubcommandPrivate, TokenProgramSubcommandPublic, - TokenProgramSubcommandShielded, - }, + token_program::TokenProgramAgnosticSubcommand, }, config::PersistentAccountData, helperfunctions::{fetch_config, fetch_persistent_accounts}, @@ -30,7 +23,8 @@ use sequencer_core::sequencer_store::PINATA_BASE58; use crate::{ ACC_RECEIVER, ACC_RECEIVER_PRIVATE, ACC_SENDER, ACC_SENDER_PRIVATE, NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, - fetch_privacy_preserving_tx, + fetch_privacy_preserving_tx, make_private_account_input_from_str, + make_public_account_input_from_str, }; use crate::{post_test, pre_test, verify_commitment_is_in_state}; @@ -42,9 +36,11 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_success() { info!("test_success"); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { - from: ACC_SENDER.to_string(), - to: ACC_RECEIVER.to_string(), + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(ACC_SENDER), + to: Some(make_public_account_input_from_str(ACC_RECEIVER)), + to_npk: None, + to_ipk: None, amount: 100, }); @@ -103,9 +99,13 @@ pub fn prepare_function_map() -> HashMap { panic!("Failed to produce new account, not present in persistent accounts"); } - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { - from: ACC_SENDER.to_string(), - to: new_persistent_account_addr.clone(), + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(ACC_SENDER), + to: Some(make_public_account_input_from_str( + &new_persistent_account_addr, + )), + to_npk: None, + to_ipk: None, amount: 100, }); @@ -136,9 +136,11 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_failure() { info!("test_failure"); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { - from: ACC_SENDER.to_string(), - to: ACC_RECEIVER.to_string(), + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(ACC_SENDER), + to: Some(make_public_account_input_from_str(ACC_RECEIVER)), + to_npk: None, + to_ipk: None, amount: 1000000, }); @@ -175,9 +177,11 @@ pub fn prepare_function_map() -> HashMap { #[test_suite_fn] pub async fn test_success_two_transactions() { info!("test_success_two_transactions"); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { - from: ACC_SENDER.to_string(), - to: ACC_RECEIVER.to_string(), + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(ACC_SENDER), + to: Some(make_public_account_input_from_str(ACC_RECEIVER)), + to_npk: None, + to_ipk: None, amount: 100, }); @@ -208,9 +212,11 @@ pub fn prepare_function_map() -> HashMap { info!("First TX Success!"); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Public { - from: ACC_SENDER.to_string(), - to: ACC_RECEIVER.to_string(), + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(ACC_SENDER), + to: Some(make_public_account_input_from_str(ACC_RECEIVER)), + to_npk: None, + to_ipk: None, amount: 100, }); @@ -306,13 +312,12 @@ pub fn prepare_function_map() -> HashMap { .expect("Failed to produce new account, not present in persistent accounts"); // Create new token - let subcommand = - TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::CreateNewToken { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), - name: "A NAME".to_string(), - total_supply: 37, - }); + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_addr: make_public_account_input_from_str(&definition_addr.to_string()), + supply_addr: make_public_account_input_from_str(&supply_addr.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -361,12 +366,16 @@ pub fn prepare_function_map() -> HashMap { ); // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = - TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::TransferToken { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_public_account_input_from_str(&supply_addr.to_string()), + to: Some(make_public_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; + wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap(); @@ -449,14 +458,12 @@ pub fn prepare_function_map() -> HashMap { }; // Create new token - let subcommand = TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), - name: "A NAME".to_string(), - total_supply: 37, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_addr: make_public_account_input_from_str(&definition_addr.to_string()), + supply_addr: make_private_account_input_from_str(&supply_addr.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -495,13 +502,15 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::TransferTokenPrivateOwned { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_private_account_input_from_str(&supply_addr.to_string()), + to: Some(make_private_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -526,13 +535,15 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); // Transfer additional 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::TransferTokenPrivateOwned { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_private_account_input_from_str(&supply_addr.to_string()), + to: Some(make_private_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -597,14 +608,12 @@ pub fn prepare_function_map() -> HashMap { }; // Create new token - let subcommand = TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), - name: "A NAME".to_string(), - total_supply: 37, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_addr: make_public_account_input_from_str(&definition_addr.to_string()), + supply_addr: make_private_account_input_from_str(&supply_addr.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -649,14 +658,15 @@ pub fn prepare_function_map() -> HashMap { .unwrap(); // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::TransferTokenPrivateForeign { - sender_addr: supply_addr.to_string(), - recipient_npk: hex::encode(recipient_keys.nullifer_public_key.0), - recipient_ipk: hex::encode(recipient_keys.incoming_viewing_public_key.0.clone()), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_private_account_input_from_str(&supply_addr.to_string()), + to: None, + to_npk: Some(hex::encode(recipient_keys.nullifer_public_key.0)), + to_ipk: Some(hex::encode( + recipient_keys.incoming_viewing_public_key.0.clone(), + )), + amount: 7, + }; let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = wallet::execute_subcommand(Command::Token(subcommand)) @@ -733,13 +743,12 @@ pub fn prepare_function_map() -> HashMap { }; // Create new token - let subcommand = - TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::CreateNewToken { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), - name: "A NAME".to_string(), - total_supply: 37, - }); + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_addr: make_public_account_input_from_str(&definition_addr.to_string()), + supply_addr: make_public_account_input_from_str(&supply_addr.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -768,13 +777,15 @@ pub fn prepare_function_map() -> HashMap { ); // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Shielded( - TokenProgramSubcommandShielded::TransferTokenShieldedOwned { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_public_account_input_from_str(&supply_addr.to_string()), + to: Some(make_private_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -794,13 +805,15 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); // Transfer additional 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Shielded( - TokenProgramSubcommandShielded::TransferTokenShieldedOwned { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_public_account_input_from_str(&supply_addr.to_string()), + to: Some(make_private_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -860,14 +873,12 @@ pub fn prepare_function_map() -> HashMap { }; // Create new token - let subcommand = TokenProgramSubcommand::Private( - TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), - name: "A NAME".to_string(), - total_supply: 37, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_addr: make_public_account_input_from_str(&definition_addr.to_string()), + supply_addr: make_private_account_input_from_str(&supply_addr.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -906,13 +917,15 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); // Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Deshielded( - TokenProgramSubcommandDeshielded::TransferTokenDeshielded { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_private_account_input_from_str(&supply_addr.to_string()), + to: Some(make_public_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -932,13 +945,15 @@ pub fn prepare_function_map() -> HashMap { assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); // Transfer additional 7 tokens from `supply_acc` to the account at address `recipient_addr` - let subcommand = TokenProgramSubcommand::Deshielded( - TokenProgramSubcommandDeshielded::TransferTokenDeshielded { - sender_addr: supply_addr.to_string(), - recipient_addr: recipient_addr.to_string(), - balance_to_move: 7, - }, - ); + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_private_account_input_from_str(&supply_addr.to_string()), + to: Some(make_public_account_input_from_str( + &recipient_addr.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; wallet::execute_subcommand(Command::Token(subcommand)) .await @@ -964,13 +979,13 @@ pub fn prepare_function_map() -> HashMap { let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to: Address = ACC_RECEIVER_PRIVATE.parse().unwrap(); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( - NativeTokenTransferProgramSubcommandPrivate::PrivateOwned { - from: from.to_string(), - to: to.to_string(), - amount: 100, - }, - )); + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: Some(make_private_account_input_from_str(&to.to_string())), + to_npk: None, + to_ipk: None, + amount: 100, + }); wallet::execute_subcommand(command).await.unwrap(); @@ -1002,14 +1017,13 @@ pub fn prepare_function_map() -> HashMap { let to_npk_string = hex::encode(to_npk.0); let to_ipk = Secp256k1Point::from_scalar(to_npk.0); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( - NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { - from: from.to_string(), - to_npk: to_npk_string, - to_ipk: hex::encode(to_ipk.0), - amount: 100, - }, - )); + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: None, + to_npk: Some(to_npk_string), + to_ipk: Some(hex::encode(to_ipk.0)), + amount: 100, + }); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = wallet::execute_subcommand(command).await.unwrap() @@ -1067,14 +1081,13 @@ pub fn prepare_function_map() -> HashMap { .cloned() .unwrap(); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( - NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { - from: from.to_string(), - to_npk: hex::encode(to_keys.nullifer_public_key.0), - to_ipk: hex::encode(to_keys.incoming_viewing_public_key.0), - amount: 100, - }, - )); + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: None, + to_npk: Some(hex::encode(to_keys.nullifer_public_key.0)), + to_ipk: Some(hex::encode(to_keys.incoming_viewing_public_key.0)), + amount: 100, + }); let sub_ret = wallet::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { @@ -1138,14 +1151,13 @@ pub fn prepare_function_map() -> HashMap { .cloned() .unwrap(); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Private( - NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { - from: from.to_string(), - to_npk: hex::encode(to_keys.nullifer_public_key.0), - to_ipk: hex::encode(to_keys.incoming_viewing_public_key.0), - amount: 100, - }, - )); + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: None, + to_npk: Some(hex::encode(to_keys.nullifer_public_key.0)), + to_ipk: Some(hex::encode(to_keys.incoming_viewing_public_key.0)), + amount: 100, + }); let sub_ret = wallet::execute_subcommand(command).await.unwrap(); let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else { @@ -1186,9 +1198,12 @@ pub fn prepare_function_map() -> HashMap { info!("test_success_deshielded_transfer_to_another_account"); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to: Address = ACC_RECEIVER.parse().unwrap(); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Deshielded { - from: from.to_string(), - to: to.to_string(), + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_private_account_input_from_str(&from.to_string()), + to: Some(make_public_account_input_from_str(&to.to_string())), + to_npk: None, + to_ipk: None, amount: 100, }); @@ -1232,13 +1247,14 @@ pub fn prepare_function_map() -> HashMap { info!("test_success_shielded_transfer_to_another_owned_account"); let from: Address = ACC_SENDER.parse().unwrap(); let to: Address = ACC_RECEIVER_PRIVATE.parse().unwrap(); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Shielded( - NativeTokenTransferProgramSubcommandShielded::ShieldedOwned { - from: from.to_string(), - to: to.to_string(), - amount: 100, - }, - )); + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(&from.to_string()), + to: Some(make_private_account_input_from_str(&to.to_string())), + to_npk: None, + to_ipk: None, + amount: 100, + }); let wallet_config = fetch_config().await.unwrap(); let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); @@ -1276,14 +1292,13 @@ pub fn prepare_function_map() -> HashMap { let to_ipk = Secp256k1Point::from_scalar(to_npk.0); let from: Address = ACC_SENDER.parse().unwrap(); - let command = Command::AuthTransfer(NativeTokenTransferProgramSubcommand::Shielded( - NativeTokenTransferProgramSubcommandShielded::ShieldedForeign { - from: from.to_string(), - to_npk: to_npk_string, - to_ipk: hex::encode(to_ipk.0), - amount: 100, - }, - )); + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: make_public_account_input_from_str(&from.to_string()), + to: None, + to_npk: Some(to_npk_string), + to_ipk: Some(hex::encode(to_ipk.0)), + amount: 100, + }); let wallet_config = fetch_config().await.unwrap(); diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index d7dac7d..b902346 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -82,6 +82,8 @@ pub enum AccountSubcommand { ///New #[command(subcommand)] New(NewSubcommand), + ///Sync private accounts + SyncPrivate {}, } ///Represents generic getter CLI subcommand @@ -213,7 +215,7 @@ impl WalletSubcommand for NewSubcommand { "Generated new account with addr {}", addr.to_bytes().to_base58() ); - println!("With npk {}", hex::encode(&key.nullifer_public_key.0)); + println!("With npk {}", hex::encode(key.nullifer_public_key.0)); println!( "With ipk {}", hex::encode(key.incoming_viewing_public_key.to_bytes()) @@ -303,12 +305,12 @@ impl WalletSubcommand for AccountSubcommand { let token_prog_id = Program::token().id(); let acc_view = match &account.program_owner { - _ if &account.program_owner == &auth_tr_prog_id => { + _ 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 account.program_owner == token_prog_id => { if let Some(token_def) = TokenDefinition::parse(&account.data) { let acc_view: TokedDefinitionAccountView = token_def.into(); @@ -337,6 +339,9 @@ impl WalletSubcommand for AccountSubcommand { AccountSubcommand::New(new_subcommand) => { new_subcommand.handle_subcommand(wallet_core).await } + AccountSubcommand::SyncPrivate {} => { + todo!(); + } } } } diff --git a/wallet/src/cli/native_token_transfer_program.rs b/wallet/src/cli/native_token_transfer_program.rs index b568931..883e850 100644 --- a/wallet/src/cli/native_token_transfer_program.rs +++ b/wallet/src/cli/native_token_transfer_program.rs @@ -3,7 +3,187 @@ use clap::Subcommand; use common::transaction::NSSATransaction; use nssa::Address; -use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; +use crate::{ + SubcommandReturnValue, WalletCore, + cli::WalletSubcommand, + helperfunctions::{AddressPrivacyKind, parse_addr_with_privacy_prefix}, +}; + +///Represents generic CLI subcommand for a wallet working with native token transfer program +#[derive(Subcommand, Debug, Clone)] +pub enum AuthTransferSubcommand { + Init { + ///addr - valid 32 byte base58 string + #[arg(long)] + addr: String, + }, + Send { + ///from - valid 32 byte base58 string + #[arg(long)] + from: String, + ///to - valid 32 byte base58 string + #[arg(long)] + to: Option, + ///to_npk - valid 32 byte base58 string + #[arg(long)] + to_npk: Option, + ///to_ipk - valid 33 byte base58 string + #[arg(long)] + to_ipk: Option, + ///amount - amount of balance to move + #[arg(long)] + amount: u128, + }, +} + +impl WalletSubcommand for AuthTransferSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + AuthTransferSubcommand::Init { addr } => { + let (addr, addr_privacy) = parse_addr_with_privacy_prefix(&addr)?; + + match addr_privacy { + AddressPrivacyKind::Public => { + let res = wallet_core + .register_account_under_authenticated_transfers_programs(addr) + .await?; + + println!("Results of tx send is {res:#?}"); + + let transfer_tx = + wallet_core.poll_native_token_transfer(res.tx_hash).await?; + + println!("Transaction data is {transfer_tx:?}"); + + let path = wallet_core.store_persistent_accounts().await?; + + println!("Stored persistent accounts at {path:#?}"); + } + AddressPrivacyKind::Private => { + let (res, [secret]) = wallet_core + .register_account_under_authenticated_transfers_programs_private(addr) + .await?; + + println!("Results of tx send is {res:#?}"); + + let tx_hash = res.tx_hash; + let transfer_tx = wallet_core + .poll_native_token_transfer(tx_hash.clone()) + .await?; + + if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { + let acc_decode_data = vec![(secret, addr)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_accounts().await?; + + println!("Stored persistent accounts at {path:#?}"); + } + } + + Ok(SubcommandReturnValue::Empty) + } + AuthTransferSubcommand::Send { + from, + to, + to_npk, + to_ipk, + amount, + } => { + let underlying_subcommand = match (to, to_npk, to_ipk) { + (None, None, None) => { + anyhow::bail!( + "Provide either account address of receiver or their public keys" + ); + } + (Some(_), Some(_), Some(_)) => { + anyhow::bail!( + "Provide only one variant: either account address of receiver or their public keys" + ); + } + (_, Some(_), None) | (_, None, Some(_)) => { + anyhow::bail!("List of public keys is uncomplete"); + } + (Some(to), None, None) => { + let (from, from_privacy) = parse_addr_with_privacy_prefix(&from)?; + let (to, to_privacy) = parse_addr_with_privacy_prefix(&to)?; + + match (from_privacy, to_privacy) { + (AddressPrivacyKind::Public, AddressPrivacyKind::Public) => { + NativeTokenTransferProgramSubcommand::Public { + from: from.to_string(), + to: to.to_string(), + amount, + } + } + (AddressPrivacyKind::Private, AddressPrivacyKind::Private) => { + NativeTokenTransferProgramSubcommand::Private( + NativeTokenTransferProgramSubcommandPrivate::PrivateOwned { + from: from.to_string(), + to: to.to_string(), + amount, + }, + ) + } + (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { + NativeTokenTransferProgramSubcommand::Deshielded { + from: from.to_string(), + to: to.to_string(), + amount, + } + } + (AddressPrivacyKind::Public, AddressPrivacyKind::Private) => { + NativeTokenTransferProgramSubcommand::Shielded( + NativeTokenTransferProgramSubcommandShielded::ShieldedOwned { + from: from.to_string(), + to: to.to_string(), + amount, + }, + ) + } + } + } + (None, Some(to_npk), Some(to_ipk)) => { + let (from, from_privacy) = parse_addr_with_privacy_prefix(&from)?; + + match from_privacy { + AddressPrivacyKind::Private => { + NativeTokenTransferProgramSubcommand::Private( + NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { + from: from.to_string(), + to_npk, + to_ipk, + amount, + }, + ) + } + AddressPrivacyKind::Public => { + NativeTokenTransferProgramSubcommand::Shielded( + NativeTokenTransferProgramSubcommandShielded::ShieldedForeign { + from: from.to_string(), + to_npk, + to_ipk, + amount, + }, + ) + } + } + } + }; + + underlying_subcommand.handle_subcommand(wallet_core).await + } + } + } +} ///Represents generic CLI subcommand for a wallet working with native token transfer program #[derive(Subcommand, Debug, Clone)] diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/token_program.rs index 25de77d..dc269e4 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/token_program.rs @@ -3,7 +3,187 @@ use clap::Subcommand; use common::transaction::NSSATransaction; use nssa::Address; -use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; +use crate::{ + SubcommandReturnValue, WalletCore, + cli::WalletSubcommand, + helperfunctions::{AddressPrivacyKind, parse_addr_with_privacy_prefix}, +}; + +///Represents generic CLI subcommand for a wallet working with token program +#[derive(Subcommand, Debug, Clone)] +pub enum TokenProgramAgnosticSubcommand { + New { + ///addr - valid 32 byte base58 string + #[arg(long)] + definition_addr: String, + ///addr - valid 32 byte base58 string + #[arg(long)] + supply_addr: String, + #[arg(short, long)] + name: String, + #[arg(short, long)] + total_supply: u128, + }, + Send { + ///from - valid 32 byte base58 string + #[arg(long)] + from: String, + ///to - valid 32 byte base58 string + #[arg(long)] + to: Option, + ///to_npk - valid 32 byte hex string + #[arg(long)] + to_npk: Option, + ///to_ipk - valid 33 byte hex string + #[arg(long)] + to_ipk: Option, + ///amount - amount of balance to move + #[arg(long)] + amount: u128, + }, +} + +impl WalletSubcommand for TokenProgramAgnosticSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + TokenProgramAgnosticSubcommand::New { + definition_addr, + supply_addr, + name, + total_supply, + } => { + let (definition_addr, definition_addr_privacy) = + parse_addr_with_privacy_prefix(&definition_addr)?; + let (supply_addr, supply_addr_privacy) = + parse_addr_with_privacy_prefix(&supply_addr)?; + + let underlying_subcommand = match (definition_addr_privacy, supply_addr_privacy) { + (AddressPrivacyKind::Public, AddressPrivacyKind::Public) => { + TokenProgramSubcommand::Public( + TokenProgramSubcommandPublic::CreateNewToken { + definition_addr: definition_addr.to_string(), + supply_addr: supply_addr.to_string(), + name, + total_supply, + }, + ) + } + (AddressPrivacyKind::Public, AddressPrivacyKind::Private) => { + TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { + definition_addr: definition_addr.to_string(), + supply_addr: supply_addr.to_string(), + name, + total_supply, + }, + ) + } + (AddressPrivacyKind::Private, AddressPrivacyKind::Private) => { + todo!(); + } + (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { + todo!(); + } + }; + + underlying_subcommand.handle_subcommand(wallet_core).await + } + TokenProgramAgnosticSubcommand::Send { + from, + to, + to_npk, + to_ipk, + amount, + } => { + let underlying_subcommand = match (to, to_npk, to_ipk) { + (None, None, None) => { + anyhow::bail!( + "Provide either account address of receiver or their public keys" + ); + } + (Some(_), Some(_), Some(_)) => { + anyhow::bail!( + "Provide only one variant: either account address of receiver or their public keys" + ); + } + (_, Some(_), None) | (_, None, Some(_)) => { + anyhow::bail!("List of public keys is uncomplete"); + } + (Some(to), None, None) => { + let (from, from_privacy) = parse_addr_with_privacy_prefix(&from)?; + let (to, to_privacy) = parse_addr_with_privacy_prefix(&to)?; + + match (from_privacy, to_privacy) { + (AddressPrivacyKind::Public, AddressPrivacyKind::Public) => { + TokenProgramSubcommand::Public( + TokenProgramSubcommandPublic::TransferToken { + sender_addr: from.to_string(), + recipient_addr: to.to_string(), + balance_to_move: amount, + }, + ) + } + (AddressPrivacyKind::Private, AddressPrivacyKind::Private) => { + TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::TransferTokenPrivateOwned { + sender_addr: from.to_string(), + recipient_addr: to.to_string(), + balance_to_move: amount, + }, + ) + } + (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { + TokenProgramSubcommand::Deshielded( + TokenProgramSubcommandDeshielded::TransferTokenDeshielded { + sender_addr: from.to_string(), + recipient_addr: to.to_string(), + balance_to_move: amount, + }, + ) + } + (AddressPrivacyKind::Public, AddressPrivacyKind::Private) => { + TokenProgramSubcommand::Shielded( + TokenProgramSubcommandShielded::TransferTokenShieldedOwned { + sender_addr: from.to_string(), + recipient_addr: to.to_string(), + balance_to_move: amount, + }, + ) + } + } + } + (None, Some(to_npk), Some(to_ipk)) => { + let (from, from_privacy) = parse_addr_with_privacy_prefix(&from)?; + + match from_privacy { + AddressPrivacyKind::Private => TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::TransferTokenPrivateForeign { + sender_addr: from.to_string(), + recipient_npk: to_npk, + recipient_ipk: to_ipk, + balance_to_move: amount, + }, + ), + AddressPrivacyKind::Public => TokenProgramSubcommand::Shielded( + TokenProgramSubcommandShielded::TransferTokenShieldedForeign { + sender_addr: from.to_string(), + recipient_npk: to_npk, + recipient_ipk: to_ipk, + balance_to_move: amount, + }, + ), + } + } + }; + + underlying_subcommand.handle_subcommand(wallet_core).await + } + } + } +} ///Represents generic CLI subcommand for a wallet working with token_program #[derive(Subcommand, Debug, Clone)] diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 5de7f5d..897b7d5 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -22,11 +22,10 @@ use tokio::io::AsyncWriteExt; use crate::cli::{ WalletSubcommand, account::AccountSubcommand, chain::ChainSubcommand, - native_token_transfer_program::NativeTokenTransferProgramSubcommand, - pinata_program::PinataProgramSubcommand, + native_token_transfer_program::AuthTransferSubcommand, pinata_program::PinataProgramSubcommand, + token_program::TokenProgramAgnosticSubcommand, }; use crate::{ - cli::token_program::TokenProgramSubcommand, helperfunctions::{ fetch_config, fetch_persistent_accounts, get_home, produce_data_for_storage, }, @@ -193,7 +192,7 @@ impl WalletCore { pub enum Command { ///Transfer command #[command(subcommand)] - AuthTransfer(NativeTokenTransferProgramSubcommand), + AuthTransfer(AuthTransferSubcommand), ///Chain command #[command(subcommand)] ChainInfo(ChainSubcommand), @@ -205,7 +204,7 @@ pub enum Command { Pinata(PinataProgramSubcommand), ///Token command #[command(subcommand)] - Token(TokenProgramSubcommand), + Token(TokenProgramAgnosticSubcommand), AuthenticatedTransferInitializePublicAccount {}, // Check the wallet can connect to the node and builtin local programs // match the remote versions diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs index 0761fa7..2dd69ca 100644 --- a/wallet/src/transaction_utils.rs +++ b/wallet/src/transaction_utils.rs @@ -537,4 +537,53 @@ impl WalletCore { Ok(self.sequencer_client.send_tx_private(tx).await?) } + + pub async fn register_account_under_authenticated_transfers_programs_private( + &self, + from: Address, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: _, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof: _, + } = self.private_acc_preparation(from, false, false).await?; + + let eph_holder_from = EphemeralKeyHolder::new(&from_npk); + let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); + + let instruction: u128 = 0; + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre], + &Program::serialize_instruction(instruction).unwrap(), + &[2], + &produce_random_nonces(1), + &[(from_npk.clone(), shared_secret_from.clone())], + &[], + &Program::authenticated_transfer_program(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![( + from_npk.clone(), + from_ipk.clone(), + eph_holder_from.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_from], + )) + } } From 27bb5bbb0fd09a29816d6da344e4be48f55bec12 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Tue, 28 Oct 2025 14:40:16 +0200 Subject: [PATCH 05/15] temp: temp changes --- wallet/src/cli/native_token_transfer_program.rs | 2 ++ wallet/src/helperfunctions.rs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/wallet/src/cli/native_token_transfer_program.rs b/wallet/src/cli/native_token_transfer_program.rs index 883e850..2f97365 100644 --- a/wallet/src/cli/native_token_transfer_program.rs +++ b/wallet/src/cli/native_token_transfer_program.rs @@ -47,6 +47,8 @@ impl WalletSubcommand for AuthTransferSubcommand { match addr_privacy { AddressPrivacyKind::Public => { + let addr = addr.parse()?; + let res = wallet_core .register_account_under_authenticated_transfers_programs(addr) .await?; diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index fcc838b..83eef43 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, Address}; +use nssa::Account; use serde::Serialize; use crate::{ @@ -94,15 +94,15 @@ pub enum AddressPrivacyKind { pub(crate) fn parse_addr_with_privacy_prefix( addr_base58: &str, -) -> Result<(Address, AddressPrivacyKind)> { +) -> Result<(String, AddressPrivacyKind)> { if addr_base58.starts_with("Public/") { Ok(( - addr_base58.strip_prefix("Public/").unwrap().parse()?, + addr_base58.strip_prefix("Public/").unwrap().to_string(), AddressPrivacyKind::Public, )) } else if addr_base58.starts_with("Private/") { Ok(( - addr_base58.strip_prefix("Private/").unwrap().parse()?, + addr_base58.strip_prefix("Private/").unwrap().to_string(), AddressPrivacyKind::Private, )) } else { From 5840f9b779b937639834e82b1a873680d62739c4 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Tue, 28 Oct 2025 16:02:30 +0200 Subject: [PATCH 06/15] fix: cli full refactor --- common/src/lib.rs | 2 + integration_tests/src/lib.rs | 23 +- integration_tests/src/test_suite_map.rs | 87 +++----- sequencer_core/src/sequencer_store/mod.rs | 4 +- wallet/src/cli/account.rs | 134 +++-------- .../src/cli/native_token_transfer_program.rs | 40 ++-- wallet/src/cli/pinata_program.rs | 56 ++++- wallet/src/cli/token_program.rs | 46 ++-- wallet/src/config.rs | 6 + wallet/src/helperfunctions.rs | 32 ++- wallet/src/lib.rs | 210 +++++++++--------- 11 files changed, 324 insertions(+), 316 deletions(-) diff --git a/common/src/lib.rs b/common/src/lib.rs index c44d03f..7976479 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -8,3 +8,5 @@ pub mod transaction; //TODO: Compile only for tests pub mod test_utils; pub type HashType = [u8; 32]; + +pub const PINATA_BASE58: &str = "EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7"; diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index f09d808..f718f9d 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -43,11 +43,11 @@ pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &[u8] = include_bytes!("data_changer.bin"); fn make_public_account_input_from_str(addr: &str) -> String { - format!("Public/{addr:?}") + format!("Public/{addr}") } fn make_private_account_input_from_str(addr: &str) -> String { - format!("Private/{addr:?}") + format!("Private/{addr}") } #[allow(clippy::type_complexity)] @@ -92,7 +92,7 @@ pub async fn post_test(residual: (ServerHandle, JoinHandle>, TempDir) seq_http_server_handle.stop(true).await; let wallet_home = wallet::helperfunctions::get_home().unwrap(); - let persistent_data_home = wallet_home.join("curr_accounts.json"); + let persistent_data_home = wallet_home.join("storage.json"); //Removing persistent accounts after run to not affect other executions //Not necessary an error, if fails as there is tests for failure scenario @@ -163,3 +163,20 @@ async fn verify_commitment_is_in_state( Ok(Some(_)) ) } + +#[cfg(test)] +mod tests { + use crate::{make_private_account_input_from_str, make_public_account_input_from_str}; + + #[test] + fn correct_addr_from_prefix() { + let addr1 = "cafecafe"; + let addr2 = "deadbeaf"; + + let addr1_pub = make_public_account_input_from_str(addr1); + let addr2_priv = make_private_account_input_from_str(addr2); + + assert_eq!(addr1_pub, "Public/cafecafe".to_string()); + assert_eq!(addr2_priv, "Private/deadbeaf".to_string()); + } +} diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index ec75a32..0bf8f00 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1,25 +1,21 @@ use std::{collections::HashMap, path::PathBuf, pin::Pin, time::Duration}; -use common::sequencer_client::SequencerClient; +use common::{PINATA_BASE58, sequencer_client::SequencerClient}; use log::info; use nssa::{Address, ProgramDeploymentTransaction, program::Program}; use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; use wallet::{ Command, SubcommandReturnValue, WalletCore, cli::{ - account::{AccountSubcommand, FetchSubcommand, NewSubcommand}, + account::{AccountSubcommand, NewSubcommand}, native_token_transfer_program::AuthTransferSubcommand, - pinata_program::{ - PinataProgramSubcommand, PinataProgramSubcommandPrivate, PinataProgramSubcommandPublic, - }, + pinata_program::PinataProgramAgnosticSubcommand, token_program::TokenProgramAgnosticSubcommand, }, - config::PersistentAccountData, - helperfunctions::{fetch_config, fetch_persistent_accounts}, + config::{PersistentAccountData, PersistentStorage}, + helperfunctions::{fetch_config, fetch_persistent_storage}, }; -use sequencer_core::sequencer_store::PINATA_BASE58; - use crate::{ ACC_RECEIVER, ACC_RECEIVER_PRIVATE, ACC_SENDER, ACC_SENDER_PRIVATE, NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, @@ -83,7 +79,10 @@ pub fn prepare_function_map() -> HashMap { wallet::execute_subcommand(command).await.unwrap(); - let persistent_accounts = fetch_persistent_accounts().await.unwrap(); + let PersistentStorage { + accounts: persistent_accounts, + last_synced_block: _, + } = fetch_persistent_storage().await.unwrap(); let mut new_persistent_account_addr = String::new(); @@ -290,7 +289,10 @@ pub fn prepare_function_map() -> HashMap { .await .unwrap(); - let persistent_accounts = fetch_persistent_accounts().await.unwrap(); + let PersistentStorage { + accounts: persistent_accounts, + last_synced_block: _, + } = fetch_persistent_storage().await.unwrap(); let mut new_persistent_accounts_addr = Vec::new(); @@ -668,7 +670,7 @@ pub fn prepare_function_map() -> HashMap { amount: 7, }; - let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = + let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } = wallet::execute_subcommand(Command::Token(subcommand)) .await .unwrap() @@ -679,11 +681,7 @@ pub fn prepare_function_map() -> HashMap { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let command = Command::Account(AccountSubcommand::Fetch(FetchSubcommand::PrivateAccount { - tx_hash, - acc_addr: recipient_addr.to_string(), - output_id: 1, - })); + let command = Command::Account(AccountSubcommand::SyncPrivate {}); wallet::execute_subcommand(command).await.unwrap(); @@ -1096,11 +1094,7 @@ pub fn prepare_function_map() -> HashMap { let tx = fetch_privacy_preserving_tx(&seq_client, tx_hash.clone()).await; - let command = Command::Account(AccountSubcommand::Fetch(FetchSubcommand::PrivateAccount { - tx_hash, - acc_addr: to_addr.to_string(), - output_id: 1, - })); + let command = Command::Account(AccountSubcommand::SyncPrivate {}); wallet::execute_subcommand(command).await.unwrap(); let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config) .await @@ -1335,13 +1329,10 @@ pub fn prepare_function_map() -> HashMap { let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; - let command = Command::Pinata(PinataProgramSubcommand::Public( - PinataProgramSubcommandPublic::Claim { - pinata_addr: pinata_addr.to_string(), - winner_addr: ACC_SENDER.to_string(), - solution, - }, - )); + let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to_addr: make_public_account_input_from_str(ACC_SENDER), + solution, + }); let wallet_config = fetch_config().await.unwrap(); @@ -1427,14 +1418,18 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_authenticated_transfer_initialize_function() { info!("test initialize account for authenticated transfer"); - let command = Command::AuthenticatedTransferInitializePublicAccount {}; - + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {})); let SubcommandReturnValue::RegisterAccount { addr } = wallet::execute_subcommand(command).await.unwrap() else { panic!("Error creating account"); }; + let command = Command::AuthTransfer(AuthTransferSubcommand::Init { + addr: addr.to_string(), + }); + wallet::execute_subcommand(command).await.unwrap(); + info!("Checking correct execution"); let wallet_config = fetch_config().await.unwrap(); let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); @@ -1463,13 +1458,10 @@ pub fn prepare_function_map() -> HashMap { let pinata_prize = 150; let solution = 989106; - let command = Command::Pinata(PinataProgramSubcommand::Private( - PinataProgramSubcommandPrivate::ClaimPrivateOwned { - pinata_addr: pinata_addr.to_string(), - winner_addr: ACC_SENDER_PRIVATE.to_string(), - solution, - }, - )); + let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to_addr: make_private_account_input_from_str(ACC_SENDER_PRIVATE), + solution, + }); let wallet_config = fetch_config().await.unwrap(); @@ -1481,7 +1473,7 @@ pub fn prepare_function_map() -> HashMap { .unwrap() .balance; - let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = + let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } = wallet::execute_subcommand(command).await.unwrap() else { panic!("invalid subcommand return value"); @@ -1497,11 +1489,7 @@ pub fn prepare_function_map() -> HashMap { .unwrap() .balance; - let command = Command::Account(AccountSubcommand::Fetch(FetchSubcommand::PrivateAccount { - tx_hash: tx_hash.clone(), - acc_addr: ACC_SENDER_PRIVATE.to_string(), - output_id: 0, - })); + let command = Command::Account(AccountSubcommand::SyncPrivate {}); wallet::execute_subcommand(command).await.unwrap(); let wallet_config = fetch_config().await.unwrap(); @@ -1538,13 +1526,10 @@ pub fn prepare_function_map() -> HashMap { panic!("invalid subcommand return value"); }; - let command = Command::Pinata(PinataProgramSubcommand::Private( - PinataProgramSubcommandPrivate::ClaimPrivateOwned { - pinata_addr: pinata_addr.to_string(), - winner_addr: winner_addr.to_string(), - solution, - }, - )); + let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to_addr: make_private_account_input_from_str(&winner_addr.to_string()), + solution, + }); let wallet_config = fetch_config().await.unwrap(); diff --git a/sequencer_core/src/sequencer_store/mod.rs b/sequencer_core/src/sequencer_store/mod.rs index 186b266..dd99639 100644 --- a/sequencer_core/src/sequencer_store/mod.rs +++ b/sequencer_core/src/sequencer_store/mod.rs @@ -9,8 +9,6 @@ use crate::config::AccountInitialData; pub mod block_store; -pub const PINATA_BASE58: &str = "EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7"; - pub struct SequecerChainStore { pub state: nssa::V02State, pub block_store: SequecerBlockStore, @@ -35,6 +33,8 @@ impl SequecerChainStore { #[cfg(feature = "testnet")] let state = { + use common::PINATA_BASE58; + let mut this = nssa::V02State::new_with_genesis_accounts(&init_accs, initial_commitments); this.add_pinata_program(PINATA_BASE58.parse().unwrap()); diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index b902346..06218fe 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,7 +1,6 @@ use anyhow::Result; use base58::ToBase58; use clap::Subcommand; -use common::transaction::NSSATransaction; use nssa::{Address, program::Program}; use serde::Serialize; @@ -9,6 +8,7 @@ use crate::{ SubcommandReturnValue, WalletCore, cli::WalletSubcommand, helperfunctions::{AddressPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, + parse_block_range, }; const TOKEN_DEFINITION_TYPE: u8 = 0; @@ -76,9 +76,6 @@ pub enum AccountSubcommand { #[arg(short, long)] addr: String, }, - ///Fetch - #[command(subcommand)] - Fetch(FetchSubcommand), ///New #[command(subcommand)] New(NewSubcommand), @@ -86,28 +83,6 @@ pub enum AccountSubcommand { SyncPrivate {}, } -///Represents generic getter CLI subcommand -#[derive(Subcommand, Debug, Clone)] -pub enum FetchSubcommand { - ///Fetch transaction by `hash` - Tx { - #[arg(short, long)] - tx_hash: String, - }, - ///Claim account `acc_addr` generated in transaction `tx_hash`, using secret `sh_secret` at ciphertext id `ciph_id` - PrivateAccount { - ///tx_hash - valid 32 byte hex string - #[arg(long)] - tx_hash: String, - ///acc_addr - valid 32 byte hex string - #[arg(long)] - acc_addr: String, - ///output_id - id of the output in the transaction - #[arg(long)] - output_id: usize, - }, -} - ///Represents generic register CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum NewSubcommand { @@ -117,74 +92,6 @@ pub enum NewSubcommand { Private {}, } -impl WalletSubcommand for FetchSubcommand { - async fn handle_subcommand( - self, - wallet_core: &mut WalletCore, - ) -> Result { - match self { - FetchSubcommand::Tx { tx_hash } => { - let tx_obj = wallet_core - .sequencer_client - .get_transaction_by_hash(tx_hash) - .await?; - - println!("Transaction object {tx_obj:#?}"); - - Ok(SubcommandReturnValue::Empty) - } - FetchSubcommand::PrivateAccount { - tx_hash, - acc_addr, - output_id: ciph_id, - } => { - let acc_addr: Address = acc_addr.parse().unwrap(); - - let account_key_chain = wallet_core - .storage - .user_data - .user_private_accounts - .get(&acc_addr); - - let Some((account_key_chain, _)) = account_key_chain else { - anyhow::bail!("Account not found"); - }; - - let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; - - if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let to_ebc = tx.message.encrypted_private_post_states[ciph_id].clone(); - let to_comm = tx.message.new_commitments[ciph_id].clone(); - let shared_secret = - account_key_chain.calculate_shared_secret_receiver(to_ebc.epk); - - let res_acc_to = nssa_core::EncryptionScheme::decrypt( - &to_ebc.ciphertext, - &shared_secret, - &to_comm, - ciph_id as u32, - ) - .unwrap(); - - println!("RES acc to {res_acc_to:#?}"); - - println!("Transaction data is {:?}", tx.message); - - wallet_core - .storage - .insert_private_account_data(acc_addr, res_acc_to); - } - - let path = wallet_core.store_persistent_accounts().await?; - - println!("Stored persistent accounts at {path:#?}"); - - Ok(SubcommandReturnValue::Empty) - } - } - } -} - impl WalletSubcommand for NewSubcommand { async fn handle_subcommand( self, @@ -196,7 +103,7 @@ impl WalletSubcommand for NewSubcommand { println!("Generated new account with addr {addr}"); - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -221,7 +128,7 @@ impl WalletSubcommand for NewSubcommand { hex::encode(key.incoming_viewing_public_key.to_bytes()) ); - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -287,6 +194,8 @@ impl WalletSubcommand for AccountSubcommand { AccountSubcommand::Get { raw, addr } => { let (addr, addr_kind) = parse_addr_with_privacy_prefix(&addr)?; + let addr = addr.parse()?; + let account = match addr_kind { AddressPrivacyKind::Public => wallet_core.get_account_public(addr).await?, AddressPrivacyKind::Private => wallet_core @@ -333,14 +242,39 @@ impl WalletSubcommand for AccountSubcommand { Ok(SubcommandReturnValue::Empty) } - AccountSubcommand::Fetch(fetch_subcommand) => { - fetch_subcommand.handle_subcommand(wallet_core).await - } AccountSubcommand::New(new_subcommand) => { new_subcommand.handle_subcommand(wallet_core).await } AccountSubcommand::SyncPrivate {} => { - todo!(); + let last_synced_block = wallet_core.last_synced_block; + let curr_last_block = wallet_core + .sequencer_client + .get_last_block() + .await? + .last_block; + + if !wallet_core + .storage + .user_data + .user_private_accounts + .is_empty() + { + parse_block_range( + last_synced_block, + curr_last_block, + wallet_core.sequencer_client.clone(), + wallet_core, + ) + .await?; + } else { + wallet_core.last_synced_block = curr_last_block; + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent data at {path:#?}"); + } + + Ok(SubcommandReturnValue::SyncedToBlock(curr_last_block)) } } } diff --git a/wallet/src/cli/native_token_transfer_program.rs b/wallet/src/cli/native_token_transfer_program.rs index 2f97365..19e8368 100644 --- a/wallet/src/cli/native_token_transfer_program.rs +++ b/wallet/src/cli/native_token_transfer_program.rs @@ -60,11 +60,13 @@ impl WalletSubcommand for AuthTransferSubcommand { println!("Transaction data is {transfer_tx:?}"); - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); } AddressPrivacyKind::Private => { + let addr = addr.parse()?; + let (res, [secret]) = wallet_core .register_account_under_authenticated_transfers_programs_private(addr) .await?; @@ -85,7 +87,7 @@ impl WalletSubcommand for AuthTransferSubcommand { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); } @@ -120,33 +122,29 @@ impl WalletSubcommand for AuthTransferSubcommand { match (from_privacy, to_privacy) { (AddressPrivacyKind::Public, AddressPrivacyKind::Public) => { - NativeTokenTransferProgramSubcommand::Public { - from: from.to_string(), - to: to.to_string(), - amount, - } + NativeTokenTransferProgramSubcommand::Public { from, to, amount } } (AddressPrivacyKind::Private, AddressPrivacyKind::Private) => { NativeTokenTransferProgramSubcommand::Private( NativeTokenTransferProgramSubcommandPrivate::PrivateOwned { - from: from.to_string(), - to: to.to_string(), + from, + to, amount, }, ) } (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { NativeTokenTransferProgramSubcommand::Deshielded { - from: from.to_string(), - to: to.to_string(), + from, + to, amount, } } (AddressPrivacyKind::Public, AddressPrivacyKind::Private) => { NativeTokenTransferProgramSubcommand::Shielded( NativeTokenTransferProgramSubcommandShielded::ShieldedOwned { - from: from.to_string(), - to: to.to_string(), + from, + to, amount, }, ) @@ -160,7 +158,7 @@ impl WalletSubcommand for AuthTransferSubcommand { AddressPrivacyKind::Private => { NativeTokenTransferProgramSubcommand::Private( NativeTokenTransferProgramSubcommandPrivate::PrivateForeign { - from: from.to_string(), + from, to_npk, to_ipk, amount, @@ -170,7 +168,7 @@ impl WalletSubcommand for AuthTransferSubcommand { AddressPrivacyKind::Public => { NativeTokenTransferProgramSubcommand::Shielded( NativeTokenTransferProgramSubcommandShielded::ShieldedForeign { - from: from.to_string(), + from, to_npk, to_ipk, amount, @@ -340,7 +338,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -384,7 +382,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -434,7 +432,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -467,7 +465,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { let tx_hash = res.tx_hash; - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -513,7 +511,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -533,7 +531,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { println!("Transaction data is {transfer_tx:?}"); - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); diff --git a/wallet/src/cli/pinata_program.rs b/wallet/src/cli/pinata_program.rs index 75d3d6a..6e79362 100644 --- a/wallet/src/cli/pinata_program.rs +++ b/wallet/src/cli/pinata_program.rs @@ -1,9 +1,59 @@ use anyhow::Result; use clap::Subcommand; -use common::transaction::NSSATransaction; +use common::{PINATA_BASE58, transaction::NSSATransaction}; use log::info; -use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; +use crate::{ + SubcommandReturnValue, WalletCore, + cli::WalletSubcommand, + helperfunctions::{AddressPrivacyKind, parse_addr_with_privacy_prefix}, +}; + +///Represents generic CLI subcommand for a wallet working with pinata program +#[derive(Subcommand, Debug, Clone)] +pub enum PinataProgramAgnosticSubcommand { + ///Claim + Claim { + ///to_addr - valid 32 byte base58 string + #[arg(long)] + to_addr: String, + ///solution - solution to pinata challenge + #[arg(long)] + solution: u128, + }, +} + +impl WalletSubcommand for PinataProgramAgnosticSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + let underlying_subcommand = match self { + PinataProgramAgnosticSubcommand::Claim { to_addr, solution } => { + let (to_addr, to_addr_privacy) = parse_addr_with_privacy_prefix(&to_addr)?; + + match to_addr_privacy { + AddressPrivacyKind::Public => { + PinataProgramSubcommand::Public(PinataProgramSubcommandPublic::Claim { + pinata_addr: PINATA_BASE58.to_string(), + winner_addr: to_addr, + solution, + }) + } + AddressPrivacyKind::Private => PinataProgramSubcommand::Private( + PinataProgramSubcommandPrivate::ClaimPrivateOwned { + pinata_addr: PINATA_BASE58.to_string(), + winner_addr: to_addr, + solution, + }, + ), + } + } + }; + + underlying_subcommand.handle_subcommand(wallet_core).await + } +} ///Represents generic CLI subcommand for a wallet working with pinata program #[derive(Subcommand, Debug, Clone)] @@ -131,7 +181,7 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/token_program.rs index dc269e4..99ebecd 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/token_program.rs @@ -64,8 +64,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AddressPrivacyKind::Public, AddressPrivacyKind::Public) => { TokenProgramSubcommand::Public( TokenProgramSubcommandPublic::CreateNewToken { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), + definition_addr, + supply_addr, name, total_supply, }, @@ -74,18 +74,20 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AddressPrivacyKind::Public, AddressPrivacyKind::Private) => { TokenProgramSubcommand::Private( TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned { - definition_addr: definition_addr.to_string(), - supply_addr: supply_addr.to_string(), + definition_addr, + supply_addr, name, total_supply, }, ) } (AddressPrivacyKind::Private, AddressPrivacyKind::Private) => { - todo!(); + //ToDo: maybe implement this one. It is not immediately clear why definition should be private. + anyhow::bail!("Unavailable privacy pairing") } (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { - todo!(); + //Probably valid. If definition is not public, but supply is it is very suspicious. + anyhow::bail!("Unavailable privacy pairing") } }; @@ -120,8 +122,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AddressPrivacyKind::Public, AddressPrivacyKind::Public) => { TokenProgramSubcommand::Public( TokenProgramSubcommandPublic::TransferToken { - sender_addr: from.to_string(), - recipient_addr: to.to_string(), + sender_addr: from, + recipient_addr: to, balance_to_move: amount, }, ) @@ -129,8 +131,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AddressPrivacyKind::Private, AddressPrivacyKind::Private) => { TokenProgramSubcommand::Private( TokenProgramSubcommandPrivate::TransferTokenPrivateOwned { - sender_addr: from.to_string(), - recipient_addr: to.to_string(), + sender_addr: from, + recipient_addr: to, balance_to_move: amount, }, ) @@ -138,8 +140,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { TokenProgramSubcommand::Deshielded( TokenProgramSubcommandDeshielded::TransferTokenDeshielded { - sender_addr: from.to_string(), - recipient_addr: to.to_string(), + sender_addr: from, + recipient_addr: to, balance_to_move: amount, }, ) @@ -147,8 +149,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AddressPrivacyKind::Public, AddressPrivacyKind::Private) => { TokenProgramSubcommand::Shielded( TokenProgramSubcommandShielded::TransferTokenShieldedOwned { - sender_addr: from.to_string(), - recipient_addr: to.to_string(), + sender_addr: from, + recipient_addr: to, balance_to_move: amount, }, ) @@ -161,7 +163,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { match from_privacy { AddressPrivacyKind::Private => TokenProgramSubcommand::Private( TokenProgramSubcommandPrivate::TransferTokenPrivateForeign { - sender_addr: from.to_string(), + sender_addr: from, recipient_npk: to_npk, recipient_ipk: to_ipk, balance_to_move: amount, @@ -169,7 +171,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { ), AddressPrivacyKind::Public => TokenProgramSubcommand::Shielded( TokenProgramSubcommandShielded::TransferTokenShieldedForeign { - sender_addr: from.to_string(), + sender_addr: from, recipient_npk: to_npk, recipient_ipk: to_ipk, balance_to_move: amount, @@ -401,7 +403,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -458,7 +460,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -508,7 +510,7 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -556,7 +558,7 @@ impl WalletSubcommand for TokenProgramSubcommandDeshielded { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -611,7 +613,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { println!("Transaction data is {:?}", tx.message); } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); @@ -665,7 +667,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { )?; } - let path = wallet_core.store_persistent_accounts().await?; + let path = wallet_core.store_persistent_data().await?; println!("Stored persistent accounts at {path:#?}"); diff --git a/wallet/src/config.rs b/wallet/src/config.rs index aa9b5ba..af1f493 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -46,6 +46,12 @@ pub enum PersistentAccountData { Private(PersistentAccountDataPrivate), } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PersistentStorage { + pub accounts: Vec, + pub last_synced_block: u64, +} + impl InitialAccountData { pub fn address(&self) -> nssa::Address { match &self { diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 83eef43..1d35efb 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -12,8 +12,7 @@ use serde::Serialize; use crate::{ HOME_DIR_ENV_VAR, config::{ - PersistentAccountData, PersistentAccountDataPrivate, PersistentAccountDataPublic, - WalletConfig, + PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig, }, }; @@ -30,21 +29,24 @@ pub async fn fetch_config() -> Result { Ok(serde_json::from_slice(&config_contents)?) } -/// Fetch list of accounts stored at `NSSA_WALLET_HOME_DIR/curr_accounts.json` +/// Fetch data stored at `NSSA_WALLET_HOME_DIR/storage.json` /// /// If file not present, it is considered as empty list of persistent accounts -pub async fn fetch_persistent_accounts() -> Result> { +pub async fn fetch_persistent_storage() -> Result { let home = get_home()?; - let accs_path = home.join("curr_accounts.json"); - let mut persistent_accounts_content = vec![]; + let accs_path = home.join("storage.json"); + let mut storage_content = vec![]; match tokio::fs::File::open(accs_path).await { Ok(mut file) => { - file.read_to_end(&mut persistent_accounts_content).await?; - Ok(serde_json::from_slice(&persistent_accounts_content)?) + file.read_to_end(&mut storage_content).await?; + Ok(serde_json::from_slice(&storage_content)?) } Err(err) => match err.kind() { - std::io::ErrorKind::NotFound => Ok(vec![]), + std::io::ErrorKind::NotFound => Ok(PersistentStorage { + accounts: vec![], + last_synced_block: 0, + }), _ => { anyhow::bail!("IO error {err:#?}"); } @@ -52,8 +54,11 @@ pub async fn fetch_persistent_accounts() -> Result> { } } -/// Produces a list of accounts for storage -pub fn produce_data_for_storage(user_data: &NSSAUserData) -> Vec { +/// Produces data for storage +pub fn produce_data_for_storage( + user_data: &NSSAUserData, + last_synced_block: u64, +) -> PersistentStorage { let mut vec_for_storage = vec![]; for (addr, key) in &user_data.pub_account_signing_keys { @@ -77,7 +82,10 @@ pub fn produce_data_for_storage(user_data: &NSSAUserData) -> Vec Vec { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 897b7d5..8b9ec78 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -20,15 +20,18 @@ use clap::{Parser, Subcommand}; use nssa_core::{Commitment, MembershipProof}; use tokio::io::AsyncWriteExt; -use crate::cli::{ - WalletSubcommand, account::AccountSubcommand, chain::ChainSubcommand, - native_token_transfer_program::AuthTransferSubcommand, pinata_program::PinataProgramSubcommand, - token_program::TokenProgramAgnosticSubcommand, +use crate::{ + cli::{ + WalletSubcommand, account::AccountSubcommand, chain::ChainSubcommand, + native_token_transfer_program::AuthTransferSubcommand, + pinata_program::PinataProgramAgnosticSubcommand, + token_program::TokenProgramAgnosticSubcommand, + }, + config::PersistentStorage, + helperfunctions::fetch_persistent_storage, }; use crate::{ - helperfunctions::{ - fetch_config, fetch_persistent_accounts, get_home, produce_data_for_storage, - }, + helperfunctions::{fetch_config, get_home, produce_data_for_storage}, poller::TxPoller, }; @@ -48,6 +51,7 @@ pub struct WalletCore { pub storage: WalletChainStore, pub poller: TxPoller, pub sequencer_client: Arc, + pub last_synced_block: u64, } impl WalletCore { @@ -57,7 +61,10 @@ impl WalletCore { let mut storage = WalletChainStore::new(config)?; - let persistent_accounts = fetch_persistent_accounts().await?; + let PersistentStorage { + accounts: persistent_accounts, + last_synced_block, + } = fetch_persistent_storage().await?; for pers_acc_data in persistent_accounts { storage.insert_account_data(pers_acc_data); } @@ -66,23 +73,24 @@ impl WalletCore { storage, poller: tx_poller, sequencer_client: client.clone(), + last_synced_block, }) } - ///Store persistent accounts at home - pub async fn store_persistent_accounts(&self) -> Result { + ///Store persistent data at home + pub async fn store_persistent_data(&self) -> Result { let home = get_home()?; - let accs_path = home.join("curr_accounts.json"); + let storage_path = home.join("storage.json"); - let data = produce_data_for_storage(&self.storage.user_data); - let accs = serde_json::to_vec_pretty(&data)?; + let data = produce_data_for_storage(&self.storage.user_data, self.last_synced_block); + let storage = serde_json::to_vec_pretty(&data)?; - let mut accs_file = tokio::fs::File::create(accs_path.as_path()).await?; - accs_file.write_all(&accs).await?; + let mut storage_file = tokio::fs::File::create(storage_path.as_path()).await?; + storage_file.write_all(&storage).await?; - info!("Stored accounts data at {accs_path:#?}"); + info!("Stored data at {storage_path:#?}"); - Ok(accs_path) + Ok(storage_path) } pub fn create_new_account_public(&mut self) -> Address { @@ -201,11 +209,10 @@ pub enum Command { Account(AccountSubcommand), ///Pinata command #[command(subcommand)] - Pinata(PinataProgramSubcommand), + Pinata(PinataProgramAgnosticSubcommand), ///Token command #[command(subcommand)] Token(TokenProgramAgnosticSubcommand), - AuthenticatedTransferInitializePublicAccount {}, // Check the wallet can connect to the node and builtin local programs // match the remote versions CheckHealth {}, @@ -229,6 +236,7 @@ pub enum SubcommandReturnValue { RegisterAccount { addr: nssa::Address }, Account(nssa::Account), Empty, + SyncedToBlock(u64), } pub async fn execute_subcommand(command: Command) -> Result { @@ -284,25 +292,6 @@ pub async fn execute_subcommand(command: Command) -> Result { - let addr = wallet_core.create_new_account_public(); - - println!("Generated new account with addr {addr}"); - - let path = wallet_core.store_persistent_accounts().await?; - - println!("Stored persistent accounts at {path:#?}"); - - let res = wallet_core - .register_account_under_authenticated_transfers_programs(addr) - .await?; - - println!("Results of tx send is {res:#?}"); - - let _transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; - - SubcommandReturnValue::RegisterAccount { addr } - } Command::Token(token_subcommand) => { token_subcommand.handle_subcommand(&mut wallet_core).await? } @@ -311,6 +300,80 @@ pub async fn execute_subcommand(command: Command) -> Result, + wallet_core: &mut WalletCore, +) -> Result<()> { + for block_id in start..(stop + 1) { + let block = + borsh::from_slice::(&seq_client.get_block(block_id).await?.block)?; + + for tx in block.transactions { + let nssa_tx = NSSATransaction::try_from(&tx)?; + + if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx { + let mut affected_accounts = vec![]; + + for (acc_addr, (key_chain, _)) in + &wallet_core.storage.user_data.user_private_accounts + { + let view_tag = EncryptedAccountData::compute_view_tag( + key_chain.nullifer_public_key.clone(), + key_chain.incoming_viewing_public_key.clone(), + ); + + for (ciph_id, encrypted_data) in tx + .message() + .encrypted_private_post_states + .iter() + .enumerate() + { + if encrypted_data.view_tag == view_tag { + let ciphertext = &encrypted_data.ciphertext; + let commitment = &tx.message.new_commitments[ciph_id]; + let shared_secret = key_chain + .calculate_shared_secret_receiver(encrypted_data.epk.clone()); + + let res_acc = nssa_core::EncryptionScheme::decrypt( + ciphertext, + &shared_secret, + commitment, + ciph_id as u32, + ); + + if let Some(res_acc) = res_acc { + println!( + "Received new account for addr {acc_addr:#?} with account object {res_acc:#?}" + ); + + affected_accounts.push((*acc_addr, res_acc)); + } + } + } + } + + for (affected_addr, new_acc) in affected_accounts { + wallet_core + .storage + .insert_private_account_data(affected_addr, new_acc); + } + } + } + + wallet_core.last_synced_block = block_id; + wallet_core.store_persistent_data().await?; + + println!( + "Block at id {block_id} with timestamp {} parsed", + block.timestamp + ); + } + + Ok(()) +} + pub async fn execute_continious_run() -> Result<()> { let config = fetch_config().await?; let seq_client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); @@ -320,70 +383,13 @@ pub async fn execute_continious_run() -> Result<()> { let mut curr_last_block = latest_block_num; loop { - for block_id in curr_last_block..(latest_block_num + 1) { - let block = borsh::from_slice::( - &seq_client.get_block(block_id).await?.block, - )?; - - for tx in block.transactions { - let nssa_tx = NSSATransaction::try_from(&tx)?; - - if let NSSATransaction::PrivacyPreserving(tx) = nssa_tx { - let mut affected_accounts = vec![]; - - for (acc_addr, (key_chain, _)) in - &wallet_core.storage.user_data.user_private_accounts - { - let view_tag = EncryptedAccountData::compute_view_tag( - key_chain.nullifer_public_key.clone(), - key_chain.incoming_viewing_public_key.clone(), - ); - - for (ciph_id, encrypted_data) in tx - .message() - .encrypted_private_post_states - .iter() - .enumerate() - { - if encrypted_data.view_tag == view_tag { - let ciphertext = &encrypted_data.ciphertext; - let commitment = &tx.message.new_commitments[ciph_id]; - let shared_secret = key_chain - .calculate_shared_secret_receiver(encrypted_data.epk.clone()); - - let res_acc = nssa_core::EncryptionScheme::decrypt( - ciphertext, - &shared_secret, - commitment, - ciph_id as u32, - ); - - if let Some(res_acc) = res_acc { - println!( - "Received new account for addr {acc_addr:#?} with account object {res_acc:#?}" - ); - - affected_accounts.push((*acc_addr, res_acc)); - } - } - } - } - - for (affected_addr, new_acc) in affected_accounts { - wallet_core - .storage - .insert_private_account_data(affected_addr, new_acc); - } - } - } - - wallet_core.store_persistent_accounts().await?; - - println!( - "Block at id {block_id} with timestamp {} parsed", - block.timestamp - ); - } + parse_block_range( + curr_last_block, + latest_block_num, + seq_client.clone(), + &mut wallet_core, + ) + .await?; curr_last_block = latest_block_num + 1; From 785ed5f1696e54a57a04b58e05d485e3e5f6fd4e Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 28 Oct 2025 16:53:39 +0200 Subject: [PATCH 07/15] doc: cli docs added --- wallet/src/cli/account.rs | 10 ++++++---- wallet/src/cli/chain.rs | 4 ++++ .../src/cli/native_token_transfer_program.rs | 16 +++++++++++----- wallet/src/cli/pinata_program.rs | 4 ++-- wallet/src/cli/token_program.rs | 16 ++++++++++++---- wallet/src/lib.rs | 19 ++++++++++++------- 6 files changed, 47 insertions(+), 22 deletions(-) diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 06218fe..54e383a 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -69,14 +69,16 @@ impl TokenHolding { ///Represents generic chain CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum AccountSubcommand { - ///Get + ///Get account data Get { - #[arg(long)] + ///Flag to get raw account data + #[arg(short, long)] raw: bool, + ///Valid 32 byte base58 string with privacy prefix #[arg(short, long)] addr: String, }, - ///New + ///Produce new public or private account #[command(subcommand)] New(NewSubcommand), ///Sync private accounts @@ -260,7 +262,7 @@ impl WalletSubcommand for AccountSubcommand { .is_empty() { parse_block_range( - last_synced_block, + last_synced_block + 1, curr_last_block, wallet_core.sequencer_client.clone(), wallet_core, diff --git a/wallet/src/cli/chain.rs b/wallet/src/cli/chain.rs index aec2c9a..a6e7999 100644 --- a/wallet/src/cli/chain.rs +++ b/wallet/src/cli/chain.rs @@ -6,12 +6,16 @@ use crate::{SubcommandReturnValue, WalletCore, cli::WalletSubcommand}; ///Represents generic chain CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum ChainSubcommand { + ///Get current block id from sequencer CurrentBlockId {}, + ///Get block at id from sequencer Block { #[arg(short, long)] id: u64, }, + ///Get transaction at hash from sequencer Transaction { + ///hash - valid 32 byte hex string #[arg(short, long)] hash: String, }, diff --git a/wallet/src/cli/native_token_transfer_program.rs b/wallet/src/cli/native_token_transfer_program.rs index 19e8368..73243d2 100644 --- a/wallet/src/cli/native_token_transfer_program.rs +++ b/wallet/src/cli/native_token_transfer_program.rs @@ -12,22 +12,28 @@ use crate::{ ///Represents generic CLI subcommand for a wallet working with native token transfer program #[derive(Subcommand, Debug, Clone)] pub enum AuthTransferSubcommand { + ///Initialize account under authenticated transfer program Init { - ///addr - valid 32 byte base58 string + ///addr - valid 32 byte base58 string with privacy prefix #[arg(long)] addr: String, }, + ///Send native tokens from one account to another with variable privacy + /// + ///If receiver is private, then `to` and (`to_npk` , `to_ipk`) is a mutually exclusive patterns. + /// + ///First is used for owned accounts, second otherwise. Send { - ///from - valid 32 byte base58 string + ///from - valid 32 byte base58 string with privacy prefix #[arg(long)] from: String, - ///to - valid 32 byte base58 string + ///to - valid 32 byte base58 string with privacy prefix #[arg(long)] to: Option, - ///to_npk - valid 32 byte base58 string + ///to_npk - valid 32 byte hex string #[arg(long)] to_npk: Option, - ///to_ipk - valid 33 byte base58 string + ///to_ipk - valid 33 byte hex string #[arg(long)] to_ipk: Option, ///amount - amount of balance to move diff --git a/wallet/src/cli/pinata_program.rs b/wallet/src/cli/pinata_program.rs index 6e79362..fafd5f1 100644 --- a/wallet/src/cli/pinata_program.rs +++ b/wallet/src/cli/pinata_program.rs @@ -12,9 +12,9 @@ use crate::{ ///Represents generic CLI subcommand for a wallet working with pinata program #[derive(Subcommand, Debug, Clone)] pub enum PinataProgramAgnosticSubcommand { - ///Claim + ///Claim pinata Claim { - ///to_addr - valid 32 byte base58 string + ///to_addr - valid 32 byte base58 string with privacy prefix #[arg(long)] to_addr: String, ///solution - solution to pinata challenge diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/token_program.rs index 99ebecd..e077efb 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/token_program.rs @@ -12,11 +12,14 @@ use crate::{ ///Represents generic CLI subcommand for a wallet working with token program #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramAgnosticSubcommand { + ///Produce new ERC-20 token + /// + ///Currently the only supported privacy options is for public definition New { - ///addr - valid 32 byte base58 string + ///definition_addr - valid 32 byte base58 string with privacy prefix #[arg(long)] definition_addr: String, - ///addr - valid 32 byte base58 string + ///supply_addr - valid 32 byte base58 string with privacy prefix #[arg(long)] supply_addr: String, #[arg(short, long)] @@ -24,11 +27,16 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(short, long)] total_supply: u128, }, + ///Send tokens from one account to another with variable privacy + /// + ///If receiver is private, then `to` and (`to_npk` , `to_ipk`) is a mutually exclusive patterns. + /// + ///First is used for owned accounts, second otherwise. Send { - ///from - valid 32 byte base58 string + ///from - valid 32 byte base58 string with privacy prefix #[arg(long)] from: String, - ///to - valid 32 byte base58 string + ///to - valid 32 byte base58 string with privacy prefix #[arg(long)] to: Option, ///to_npk - valid 32 byte hex string diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 8b9ec78..dc17292 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -198,27 +198,32 @@ impl WalletCore { #[derive(Subcommand, Debug, Clone)] #[clap(about)] pub enum Command { - ///Transfer command + ///Authenticated transfer subcommand #[command(subcommand)] AuthTransfer(AuthTransferSubcommand), - ///Chain command + ///Generic chain info subcommand #[command(subcommand)] ChainInfo(ChainSubcommand), - ///Chain command + ///Account view and sync subcommand #[command(subcommand)] Account(AccountSubcommand), - ///Pinata command + ///Pinata program interaction subcommand #[command(subcommand)] Pinata(PinataProgramAgnosticSubcommand), - ///Token command + ///Token program interaction subcommand #[command(subcommand)] Token(TokenProgramAgnosticSubcommand), - // Check the wallet can connect to the node and builtin local programs - // match the remote versions + /// Check the wallet can connect to the node and builtin local programs + /// match the remote versions CheckHealth {}, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config +/// +/// All account adresses must be valid 32 byte base58 strings. +/// +/// All account addresses must be provided as {privacy_prefix}/{addr}, +/// where valid options for `privacy_prefix` is `Public` and `Private` #[derive(Parser, Debug)] #[clap(version, about)] pub struct Args { From 6d9d6b3d280315e191b0f1be60902e050f81c809 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 29 Oct 2025 00:40:56 -0300 Subject: [PATCH 08/15] add chained_call field to program output --- nssa/core/src/program.rs | 10 ++++++++++ .../guest/src/bin/privacy_preserving_circuit.rs | 1 + 2 files changed, 11 insertions(+) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 82023f3..a96d3cf 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -12,11 +12,20 @@ pub struct ProgramInput { pub instruction: T, } +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +pub struct ChainedCall { + pub program_id: ProgramId, + pub instruction_data: InstructionData, + pub account_indices: Vec, +} + #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, + pub chained_call: Option } pub fn read_nssa_inputs() -> ProgramInput { @@ -33,6 +42,7 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec let output = ProgramOutput { pre_states, post_states, + chained_call: None }; env::commit(&output); } diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index a1aa8c9..1baa5a0 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -27,6 +27,7 @@ fn main() { let ProgramOutput { pre_states, post_states, + chained_call: _, } = program_output; // Check that there are no repeated account ids From a903c221db93a8d19c1ff9c42c9a814051977232 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Wed, 29 Oct 2025 12:02:41 +0200 Subject: [PATCH 09/15] fix: tests fix --- integration_tests/src/test_suite_map.rs | 45 +++++++++++-------- .../src/cli/native_token_transfer_program.rs | 4 +- wallet/src/cli/token_program.rs | 6 +-- wallet/src/lib.rs | 4 +- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 0bf8f00..e0c7f86 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -31,7 +31,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success() { - info!("test_success"); + info!("########## test_success ##########"); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: make_public_account_input_from_str(ACC_SENDER), to: Some(make_public_account_input_from_str(ACC_RECEIVER)), @@ -70,7 +70,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_move_to_another_account() { - info!("test_success_move_to_another_account"); + info!("########## test_success_move_to_another_account ##########"); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {})); let wallet_config = fetch_config().await.unwrap(); @@ -134,7 +134,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_failure() { - info!("test_failure"); + info!("########## test_failure ##########"); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: make_public_account_input_from_str(ACC_SENDER), to: Some(make_public_account_input_from_str(ACC_RECEIVER)), @@ -175,7 +175,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_two_transactions() { - info!("test_success_two_transactions"); + info!("########## test_success_two_transactions ##########"); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: make_public_account_input_from_str(ACC_SENDER), to: Some(make_public_account_input_from_str(ACC_RECEIVER)), @@ -245,7 +245,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_get_account() { - info!("test_get_account"); + info!("########## test_get_account ##########"); let wallet_config = fetch_config().await.unwrap(); let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); @@ -268,6 +268,7 @@ pub fn prepare_function_map() -> HashMap { /// token transfer to a new account. #[nssa_integration_test] pub async fn test_success_token_program() { + info!("########## test_success_token_program ##########"); let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition @@ -424,6 +425,7 @@ pub fn prepare_function_map() -> HashMap { /// private token transfer to a new account. All accounts are owned except definition. #[nssa_integration_test] pub async fn test_success_token_program_private_owned() { + info!("########## test_success_token_program_private_owned ##########"); let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition (public) @@ -574,6 +576,7 @@ pub fn prepare_function_map() -> HashMap { /// private token transfer to a new account. #[nssa_integration_test] pub async fn test_success_token_program_private_claiming_path() { + info!("########## test_success_token_program_private_claiming_path ##########"); let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition (public) @@ -705,6 +708,7 @@ pub fn prepare_function_map() -> HashMap { /// shielded token transfer to a new account. All accounts are owned except definition. #[nssa_integration_test] pub async fn test_success_token_program_shielded_owned() { + info!("########## test_success_token_program_shielded_owned ##########"); let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition (public) @@ -835,6 +839,7 @@ pub fn prepare_function_map() -> HashMap { /// deshielded token transfer to a new account. All accounts are owned except definition. #[nssa_integration_test] pub async fn test_success_token_program_deshielded_owned() { + info!("########## test_success_token_program_deshielded_owned ##########"); let wallet_config = fetch_config().await.unwrap(); // Create new account for the token definition (public) @@ -973,7 +978,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_private_transfer_to_another_owned_account() { - info!("test_success_private_transfer_to_another_owned_account"); + info!("########## test_success_private_transfer_to_another_owned_account ##########"); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to: Address = ACC_RECEIVER_PRIVATE.parse().unwrap(); @@ -1009,7 +1014,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_private_transfer_to_another_foreign_account() { - info!("test_success_private_transfer_to_another_foreign_account"); + info!("########## test_success_private_transfer_to_another_foreign_account ##########"); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); @@ -1055,7 +1060,9 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_private_transfer_to_another_owned_account_claiming_path() { - info!("test_success_private_transfer_to_another_owned_account_claiming_path"); + info!( + "########## test_success_private_transfer_to_another_owned_account_claiming_path ##########" + ); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {})); @@ -1119,7 +1126,9 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_private_transfer_to_another_owned_account_cont_run_path() { - info!("test_success_private_transfer_to_another_owned_account_cont_run_path"); + info!( + "########## test_success_private_transfer_to_another_owned_account_cont_run_path ##########" + ); let continious_run_handle = tokio::spawn(wallet::execute_continious_run()); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); @@ -1189,7 +1198,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_deshielded_transfer_to_another_account() { - info!("test_success_deshielded_transfer_to_another_account"); + info!("########## test_success_deshielded_transfer_to_another_account ##########"); let from: Address = ACC_SENDER_PRIVATE.parse().unwrap(); let to: Address = ACC_RECEIVER.parse().unwrap(); @@ -1238,7 +1247,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_shielded_transfer_to_another_owned_account() { - info!("test_success_shielded_transfer_to_another_owned_account"); + info!("########## test_success_shielded_transfer_to_another_owned_account ##########"); let from: Address = ACC_SENDER.parse().unwrap(); let to: Address = ACC_RECEIVER_PRIVATE.parse().unwrap(); @@ -1280,7 +1289,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_success_shielded_transfer_to_another_foreign_account() { - info!("test_success_shielded_transfer_to_another_foreign_account"); + info!("########## test_success_shielded_transfer_to_another_foreign_account ##########"); let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); let to_ipk = Secp256k1Point::from_scalar(to_npk.0); @@ -1325,7 +1334,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_pinata() { - info!("test_pinata"); + info!("########## test_pinata ##########"); let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; @@ -1370,7 +1379,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_program_deployment() { - info!("test program deployment"); + info!("########## test program deployment ##########"); let bytecode = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.to_vec(); let message = nssa::program_deployment_transaction::Message::new(bytecode.clone()); let transaction = ProgramDeploymentTransaction::new(message); @@ -1417,7 +1426,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_authenticated_transfer_initialize_function() { - info!("test initialize account for authenticated transfer"); + info!("########## test initialize account for authenticated transfer ##########"); let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {})); let SubcommandReturnValue::RegisterAccount { addr } = wallet::execute_subcommand(command).await.unwrap() @@ -1426,7 +1435,7 @@ pub fn prepare_function_map() -> HashMap { }; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - addr: addr.to_string(), + addr: make_public_account_input_from_str(&addr.to_string()), }); wallet::execute_subcommand(command).await.unwrap(); @@ -1453,7 +1462,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_pinata_private_receiver() { - info!("test_pinata_private_receiver"); + info!("########## test_pinata_private_receiver ##########"); let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; @@ -1510,7 +1519,7 @@ pub fn prepare_function_map() -> HashMap { #[nssa_integration_test] pub async fn test_pinata_private_receiver_new_account() { - info!("test_pinata_private_receiver"); + info!("########## test_pinata_private_receiver ##########"); let pinata_addr = PINATA_BASE58; let pinata_prize = 150; let solution = 989106; diff --git a/wallet/src/cli/native_token_transfer_program.rs b/wallet/src/cli/native_token_transfer_program.rs index 73243d2..e286bb9 100644 --- a/wallet/src/cli/native_token_transfer_program.rs +++ b/wallet/src/cli/native_token_transfer_program.rs @@ -19,9 +19,9 @@ pub enum AuthTransferSubcommand { addr: String, }, ///Send native tokens from one account to another with variable privacy - /// + /// ///If receiver is private, then `to` and (`to_npk` , `to_ipk`) is a mutually exclusive patterns. - /// + /// ///First is used for owned accounts, second otherwise. Send { ///from - valid 32 byte base58 string with privacy prefix diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/token_program.rs index e077efb..483dee5 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/token_program.rs @@ -13,7 +13,7 @@ use crate::{ #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramAgnosticSubcommand { ///Produce new ERC-20 token - /// + /// ///Currently the only supported privacy options is for public definition New { ///definition_addr - valid 32 byte base58 string with privacy prefix @@ -28,9 +28,9 @@ pub enum TokenProgramAgnosticSubcommand { total_supply: u128, }, ///Send tokens from one account to another with variable privacy - /// + /// ///If receiver is private, then `to` and (`to_npk` , `to_ipk`) is a mutually exclusive patterns. - /// + /// ///First is used for owned accounts, second otherwise. Send { ///from - valid 32 byte base58 string with privacy prefix diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index dc17292..3beac67 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -219,9 +219,9 @@ pub enum Command { } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config -/// +/// /// All account adresses must be valid 32 byte base58 strings. -/// +/// /// All account addresses must be provided as {privacy_prefix}/{addr}, /// where valid options for `privacy_prefix` is `Public` and `Private` #[derive(Parser, Debug)] From 3a277193928b2343d291a8ff8a04783b209d5ca9 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 29 Oct 2025 01:51:09 -0300 Subject: [PATCH 10/15] add tail-chain logic for public transactions --- integration_tests/src/data_changer.bin | Bin 366692 -> 371388 bytes .../src/bin/privacy_preserving_circuit.rs | 7 +- nssa/src/program.rs | 12 ++-- nssa/src/public_transaction/transaction.rs | 67 +++++++++++++++--- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/integration_tests/src/data_changer.bin b/integration_tests/src/data_changer.bin index d201f91960c9f4739d4a4ee0c94fb4a356f50e24..c4fbec0f1c44d11a802a9f10e241da57d768ea88 100644 GIT binary patch delta 95675 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 Dh4?oR delta 92914 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 bSUtX~I!ejRNpDzx4emAaxmG#pb0_{k(z`m7 diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 1baa5a0..345198a 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -27,9 +27,14 @@ fn main() { let ProgramOutput { pre_states, post_states, - chained_call: _, + chained_call, } = program_output; + // TODO: implement tail calls for privacy preserving transactions + if chained_call.is_some() { + panic!("Privacy preserving transactions do not support yet tail calls.") + } + // Check that there are no repeated account ids if !validate_uniqueness_of_account_ids(&pre_states) { panic!("Repeated account ids found") diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 7771aaf..877b01b 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -48,7 +48,7 @@ impl Program { &self, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, - ) -> Result, NssaError> { + ) -> Result { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION)); @@ -62,12 +62,12 @@ impl Program { .map_err(|e| NssaError::ProgramExecutionFailed(e.to_string()))?; // Get outputs - let ProgramOutput { post_states, .. } = session_info + let program_output = session_info .journal .decode() .map_err(|e| NssaError::ProgramExecutionFailed(e.to_string()))?; - Ok(post_states) + Ok(program_output) } /// Writes inputs to `env_builder` in the order expected by the programs @@ -221,12 +221,12 @@ mod tests { balance: balance_to_move, ..Account::default() }; - let [sender_post, recipient_post] = program + let program_output = program .execute(&[sender, recipient], &instruction_data) - .unwrap() - .try_into() .unwrap(); + let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); + assert_eq!(sender_post, expected_sender_post); assert_eq!(recipient_post, expected_recipient_post); } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index b0b8f73..7079074 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use nssa_core::{ account::{Account, AccountWithMetadata}, address::Address, - program::validate_execution, + program::{ChainedCall, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -18,6 +18,7 @@ pub struct PublicTransaction { message: Message, witness_set: WitnessSet, } +const MAX_NUMBER_CHAINED_CALLS: usize = 10; impl PublicTransaction { pub fn new(message: Message, witness_set: WitnessSet) -> Self { @@ -100,21 +101,65 @@ impl PublicTransaction { }) .collect(); - // Check the `program_id` corresponds to a deployed program - let Some(program) = state.programs().get(&message.program_id) else { - return Err(NssaError::InvalidInput("Unknown program".into())); + let mut state_diff: HashMap = message + .addresses + .iter() + .cloned() + .zip(pre_states.iter().map(|pre| pre.account.clone())) + .collect(); + + let mut chained_call = ChainedCall { + program_id: message.program_id, + instruction_data: message.instruction_data.clone(), + account_indices: (0..pre_states.len()).collect(), }; - // // Execute program - let post_states = program.execute(&pre_states, &message.instruction_data)?; + for _ in 0..MAX_NUMBER_CHAINED_CALLS { + // Check the `program_id` corresponds to a deployed program + let Some(program) = state.programs().get(&chained_call.program_id) else { + return Err(NssaError::InvalidInput("Unknown program".into())); + }; - // Verify execution corresponds to a well-behaved program. - // See the # Programs section for the definition of the `validate_execution` method. - if !validate_execution(&pre_states, &post_states, message.program_id) { - return Err(NssaError::InvalidProgramBehavior); + let pre_states_chained_call = chained_call + .account_indices + .iter() + .map(|&i| { + pre_states + .get(i) + .ok_or_else(|| NssaError::InvalidInput("Invalid account indices".into())) + .cloned() + }) + .collect::, NssaError>>()?; + + let program_output = + program.execute(&pre_states_chained_call, &chained_call.instruction_data)?; + + // Verify execution corresponds to a well-behaved program. + // See the # Programs section for the definition of the `validate_execution` method. + if validate_execution( + &program_output.pre_states, + &program_output.post_states, + message.program_id, + ) { + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states) + { + state_diff.insert(pre.account_id, post); + } + } else { + return Err(NssaError::InvalidProgramBehavior); + } + + if let Some(next_chained_call) = program_output.chained_call { + chained_call = next_chained_call; + } else { + break; + }; } - Ok(message.addresses.iter().cloned().zip(post_states).collect()) + Ok(state_diff) } } From 48fc64395204df128f8d2d47270e16b9745c0d11 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 29 Oct 2025 15:34:11 -0300 Subject: [PATCH 11/15] add chained call test --- nssa/core/src/program.rs | 17 +++++++-- .../src/bin/privacy_preserving_circuit.rs | 4 +-- nssa/src/program.rs | 15 ++++++-- nssa/src/public_transaction/transaction.rs | 4 +-- nssa/src/state.rs | 36 +++++++++++++++++++ nssa/test_program_methods/guest/Cargo.lock | 2 ++ nssa/test_program_methods/guest/Cargo.toml | 2 ++ .../guest/src/bin/chain_caller.rs | 32 +++++++++++++++++ 8 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 nssa/test_program_methods/guest/src/bin/chain_caller.rs diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index a96d3cf..6582984 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -25,7 +25,7 @@ pub struct ChainedCall { pub struct ProgramOutput { pub pre_states: Vec, pub post_states: Vec, - pub chained_call: Option + pub chained_call: Option, } pub fn read_nssa_inputs() -> ProgramInput { @@ -42,7 +42,20 @@ pub fn write_nssa_outputs(pre_states: Vec, post_states: Vec let output = ProgramOutput { pre_states, post_states, - chained_call: None + chained_call: None, + }; + env::commit(&output); +} + +pub fn write_nssa_outputs_with_chained_call( + pre_states: Vec, + post_states: Vec, + chained_call: Option, +) { + let output = ProgramOutput { + pre_states, + post_states, + chained_call, }; env::commit(&output); } diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 345198a..d8ed15d 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -30,9 +30,9 @@ fn main() { chained_call, } = program_output; - // TODO: implement tail calls for privacy preserving transactions + // TODO: implement chained calls for privacy preserving transactions if chained_call.is_some() { - panic!("Privacy preserving transactions do not support yet tail calls.") + panic!("Privacy preserving transactions do not support yet chained calls.") } // Check that there are no repeated account ids diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 877b01b..11eb413 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,6 +1,6 @@ use crate::program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}; use nssa_core::{ - account::{Account, AccountWithMetadata}, + account::AccountWithMetadata, program::{InstructionData, ProgramId, ProgramOutput}, }; @@ -107,11 +107,11 @@ impl Program { #[cfg(test)] mod tests { - use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use program_methods::{ + use crate::program_methods::{ AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, }; + use nssa_core::account::{Account, AccountId, AccountWithMetadata}; use crate::program::Program; @@ -195,6 +195,15 @@ mod tests { elf: BURNER_ELF.to_vec(), } } + + pub fn chain_caller() -> Self { + use test_program_methods::{CHAIN_CALLER_ELF, CHAIN_CALLER_ID}; + + Program { + id: CHAIN_CALLER_ID, + elf: CHAIN_CALLER_ELF.to_vec(), + } + } } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 7079074..70d22a3 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -114,7 +114,7 @@ impl PublicTransaction { account_indices: (0..pre_states.len()).collect(), }; - for _ in 0..MAX_NUMBER_CHAINED_CALLS { + for _i in 0..MAX_NUMBER_CHAINED_CALLS { // Check the `program_id` corresponds to a deployed program let Some(program) = state.programs().get(&chained_call.program_id) else { return Err(NssaError::InvalidInput("Unknown program".into())); @@ -139,7 +139,7 @@ impl PublicTransaction { if validate_execution( &program_output.pre_states, &program_output.post_states, - message.program_id, + chained_call.program_id, ) { for (pre, post) in program_output .pre_states diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 83183f5..5a0b87b 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -263,6 +263,7 @@ pub mod tests { Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar}, + program::ProgramId, }; fn transfer_transaction( @@ -475,6 +476,7 @@ pub mod tests { self.insert_program(Program::data_changer()); self.insert_program(Program::minter()); self.insert_program(Program::burner()); + self.insert_program(Program::chain_caller()); self } @@ -2045,4 +2047,38 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + + #[test] + fn test_chained_call() { + let program = Program::chain_caller(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let address = Address::from(&PublicKey::new_from_private_key(&key)); + let initial_balance = 100; + let initial_data = [(address, initial_balance)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let from = address; + let from_key = key; + let to = Address::new([2; 32]); + let amount: u128 = 37; + let instruction: (u128, ProgramId) = + (amount, Program::authenticated_transfer_program().id()); + + let message = public_transaction::Message::try_new( + program.id(), + vec![to, from], + vec![0], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx).unwrap(); + + let from_post = state.get_account_by_address(&from); + let to_post = state.get_account_by_address(&to); + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post.balance, amount); + } } diff --git a/nssa/test_program_methods/guest/Cargo.lock b/nssa/test_program_methods/guest/Cargo.lock index 8cb2bec..d7e5b67 100644 --- a/nssa/test_program_methods/guest/Cargo.lock +++ b/nssa/test_program_methods/guest/Cargo.lock @@ -1824,6 +1824,8 @@ name = "programs" version = "0.1.0" dependencies = [ "nssa-core", + "risc0-zkvm", + "serde", ] [[package]] diff --git a/nssa/test_program_methods/guest/Cargo.toml b/nssa/test_program_methods/guest/Cargo.toml index 2289292..9e5f543 100644 --- a/nssa/test_program_methods/guest/Cargo.toml +++ b/nssa/test_program_methods/guest/Cargo.toml @@ -6,4 +6,6 @@ edition = "2024" [workspace] [dependencies] +risc0-zkvm = { version = "3.0.3", features = ['std'] } nssa-core = { path = "../../core" } +serde = { version = "1.0.219", default-features = false } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs new file mode 100644 index 0000000..321d032 --- /dev/null +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -0,0 +1,32 @@ +use nssa_core::program::{ + ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call, +}; +use risc0_zkvm::serde::to_vec; + +type Instruction = (u128, ProgramId); + +fn main() { + let ProgramInput { + pre_states, + instruction: (balance, program_id), + } = read_nssa_inputs::(); + + let [sender_pre, receiver_pre] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let instruction_data = to_vec(&balance).unwrap(); + + let chained_call = Some(ChainedCall { + program_id, + instruction_data, + account_indices: vec![1, 0], + }); + + write_nssa_outputs_with_chained_call( + vec![sender_pre.clone(), receiver_pre.clone()], + vec![sender_pre.account, receiver_pre.account], + chained_call, + ); +} From c2b8459645878fbd3c6c5e2e230417fd214602e1 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Thu, 30 Oct 2025 15:00:21 +0200 Subject: [PATCH 12/15] fix: suggestions fix 1 --- wallet/src/cli/account.rs | 18 +++++++++++++++--- wallet/src/cli/token_program.rs | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 54e383a..3ff4470 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -1,7 +1,7 @@ use anyhow::Result; use base58::ToBase58; use clap::Subcommand; -use nssa::{Address, program::Program}; +use nssa::{Account, Address, program::Program}; use serde::Serialize; use crate::{ @@ -103,7 +103,7 @@ impl WalletSubcommand for NewSubcommand { NewSubcommand::Public {} => { let addr = wallet_core.create_new_account_public(); - println!("Generated new account with addr {addr}"); + println!("Generated new account with addr Public/{addr}"); let path = wallet_core.store_persistent_data().await?; @@ -121,7 +121,7 @@ impl WalletSubcommand for NewSubcommand { .unwrap(); println!( - "Generated new account with addr {}", + "Generated new account with addr Private/{}", addr.to_bytes().to_base58() ); println!("With npk {}", hex::encode(key.nullifer_public_key.0)); @@ -205,6 +205,12 @@ impl WalletSubcommand for AccountSubcommand { .ok_or(anyhow::anyhow!("Private account not found in storage"))?, }; + if account == Account::default() { + println!("Account is Uninitialized"); + + return Ok(SubcommandReturnValue::Empty); + } + if raw { let account_hr: HumanReadableAccount = account.clone().into(); println!("{}", serde_json::to_string(&account_hr).unwrap()); @@ -219,16 +225,22 @@ impl WalletSubcommand for AccountSubcommand { _ if account.program_owner == auth_tr_prog_id => { let acc_view: AuthenticatedTransferAccountView = account.into(); + println!("Account owned by authenticated transfer program"); + 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(); + println!("Definition account owned by token program"); + serde_json::to_string(&acc_view)? } else if let Some(token_hold) = TokenHolding::parse(&account.data) { let acc_view: TokedHoldingAccountView = token_hold.into(); + println!("Holding account owned by token program"); + serde_json::to_string(&acc_view)? } else { anyhow::bail!("Invalid data for account {addr:#?} with token program"); diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/token_program.rs index 483dee5..6ce7dbe 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/token_program.rs @@ -12,7 +12,7 @@ use crate::{ ///Represents generic CLI subcommand for a wallet working with token program #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramAgnosticSubcommand { - ///Produce new ERC-20 token + ///Produce a new token /// ///Currently the only supported privacy options is for public definition New { @@ -94,7 +94,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { anyhow::bail!("Unavailable privacy pairing") } (AddressPrivacyKind::Private, AddressPrivacyKind::Public) => { - //Probably valid. If definition is not public, but supply is it is very suspicious. + //ToDo: Probably valid. If definition is not public, but supply is it is very suspicious. anyhow::bail!("Unavailable privacy pairing") } }; From 0fb72e452f5b021e365d7a2ba8ca144003b80e4a Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 30 Oct 2025 10:52:31 -0300 Subject: [PATCH 13/15] fix claiming mechanism for chained calls --- nssa/core/src/program.rs | 7 ++- nssa/src/public_transaction/transaction.rs | 29 ++++++---- nssa/src/state.rs | 54 ++++++++++++++++--- .../guest/src/bin/chain_caller.rs | 4 +- 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 6582984..3ecee30 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -102,9 +102,14 @@ pub fn validate_execution( { return false; } + + // 6. If a post state has default program owner, the pre state must have been a default account + if post.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { + return false; + } } - // 6. Total balance is preserved + // 7. Total balance is preserved let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum(); let total_balance_post_states: u128 = post_states.iter().map(|post| post.balance).sum(); if total_balance_pre_states != total_balance_post_states { diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 70d22a3..900a476 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use nssa_core::{ account::{Account, AccountWithMetadata}, address::Address, - program::{ChainedCall, validate_execution}, + program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -131,27 +131,34 @@ impl PublicTransaction { }) .collect::, NssaError>>()?; - let program_output = + let mut program_output = program.execute(&pre_states_chained_call, &chained_call.instruction_data)?; // Verify execution corresponds to a well-behaved program. // See the # Programs section for the definition of the `validate_execution` method. - if validate_execution( + if !validate_execution( &program_output.pre_states, &program_output.post_states, chained_call.program_id, ) { - for (pre, post) in program_output - .pre_states - .iter() - .zip(program_output.post_states) - { - state_diff.insert(pre.account_id, post); - } - } else { return Err(NssaError::InvalidProgramBehavior); } + for post in program_output.post_states.iter_mut() { + // The invoked program claims the accounts with default program id. + if post.program_owner == DEFAULT_PROGRAM_ID { + post.program_owner = chained_call.program_id; + } + } + + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states) + { + state_diff.insert(pre.account_id, post); + } + if let Some(next_chained_call) = program_output.chained_call { chained_call = next_chained_call; } else { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 5a0b87b..2119929 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -8,7 +8,7 @@ use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, account::Account, address::Address, - program::{DEFAULT_PROGRAM_ID, ProgramId}, + program::ProgramId, }; use std::collections::{HashMap, HashSet}; @@ -114,10 +114,6 @@ impl V02State { let current_account = self.get_account_by_address_mut(address); *current_account = post; - // The invoked program claims the accounts with default program id. - if current_account.program_owner == DEFAULT_PROGRAM_ID { - current_account.program_owner = tx.message().program_id; - } } for address in tx.signer_addresses() { @@ -437,7 +433,7 @@ pub mod tests { } #[test] - fn transition_from_chained_authenticated_transfer_program_invocations() { + fn transition_from_sequence_of_authenticated_transfer_program_invocations() { let key1 = PrivateKey::try_new([8; 32]).unwrap(); let address1 = Address::from(&PublicKey::new_from_private_key(&key1)); let key2 = PrivateKey::try_new([2; 32]).unwrap(); @@ -2048,6 +2044,42 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + #[test] + fn test_claiming_mechanism() { + let program = Program::authenticated_transfer_program(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let address = Address::from(&PublicKey::new_from_private_key(&key)); + let initial_balance = 100; + let initial_data = [(address, initial_balance)]; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let from = address; + let from_key = key; + let to = Address::new([2; 32]); + let amount: u128 = 37; + + // Check the recipient is an uninitialized account + assert_eq!(state.get_account_by_address(&to), Account::default()); + + let expected_recipient_post = Account { + program_owner: program.id(), + balance: amount, + ..Account::default() + }; + + let message = + public_transaction::Message::try_new(program.id(), vec![from, to], vec![0], amount) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx).unwrap(); + + let recipient_post = state.get_account_by_address(&to); + + assert_eq!(recipient_post, expected_recipient_post); + } + #[test] fn test_chained_call() { let program = Program::chain_caller(); @@ -2064,9 +2096,15 @@ pub mod tests { let instruction: (u128, ProgramId) = (amount, Program::authenticated_transfer_program().id()); + let expected_to_post = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: amount, + ..Account::default() + }; + let message = public_transaction::Message::try_new( program.id(), - vec![to, from], + vec![to, from], //The chain_caller program permutes the account order in the chain call vec![0], instruction, ) @@ -2079,6 +2117,6 @@ pub mod tests { let from_post = state.get_account_by_address(&from); let to_post = state.get_account_by_address(&to); assert_eq!(from_post.balance, initial_balance - amount); - assert_eq!(to_post.balance, amount); + assert_eq!(to_post, expected_to_post); } } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index 321d032..dfd77b1 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -5,6 +5,8 @@ use risc0_zkvm::serde::to_vec; type Instruction = (u128, ProgramId); +/// A program that calls another program. +/// It permutes the order of the input accounts on the subsequent call fn main() { let ProgramInput { pre_states, @@ -21,7 +23,7 @@ fn main() { let chained_call = Some(ChainedCall { program_id, instruction_data, - account_indices: vec![1, 0], + account_indices: vec![1, 0], // <- Account order permutation here }); write_nssa_outputs_with_chained_call( From 12974f6f6b8662d4107b8e04d3baaa28c7dc32ad Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 30 Oct 2025 13:47:52 -0300 Subject: [PATCH 14/15] refactor --- nssa/src/public_transaction/transaction.rs | 78 +++++++++++++--------- nssa/src/state.rs | 6 +- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 900a476..d118d0c 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use nssa_core::{ account::{Account, AccountWithMetadata}, address::Address, - program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution}, + program::{DEFAULT_PROGRAM_ID, validate_execution}, }; use sha2::{Digest, digest::FixedOutput}; @@ -89,7 +89,7 @@ impl PublicTransaction { } // Build pre_states for execution - let pre_states: Vec<_> = message + let mut input_pre_states: Vec<_> = message .addresses .iter() .map(|address| { @@ -101,66 +101,80 @@ impl PublicTransaction { }) .collect(); - let mut state_diff: HashMap = message - .addresses - .iter() - .cloned() - .zip(pre_states.iter().map(|pre| pre.account.clone())) - .collect(); + let mut state_diff: HashMap = HashMap::new(); - let mut chained_call = ChainedCall { - program_id: message.program_id, - instruction_data: message.instruction_data.clone(), - account_indices: (0..pre_states.len()).collect(), - }; + let mut program_id = message.program_id; + let mut instruction_data = message.instruction_data.clone(); for _i in 0..MAX_NUMBER_CHAINED_CALLS { // Check the `program_id` corresponds to a deployed program - let Some(program) = state.programs().get(&chained_call.program_id) else { + let Some(program) = state.programs().get(&program_id) else { return Err(NssaError::InvalidInput("Unknown program".into())); }; - let pre_states_chained_call = chained_call - .account_indices - .iter() - .map(|&i| { - pre_states - .get(i) - .ok_or_else(|| NssaError::InvalidInput("Invalid account indices".into())) - .cloned() - }) - .collect::, NssaError>>()?; + let mut program_output = program.execute(&input_pre_states, &instruction_data)?; - let mut program_output = - program.execute(&pre_states_chained_call, &chained_call.instruction_data)?; + // This check is equivalent to checking that the program output pre_states coinicide + // with the values in the public state or with any modifications to those values + // during the chain of calls. + if input_pre_states != program_output.pre_states { + return Err(NssaError::InvalidProgramBehavior); + } // Verify execution corresponds to a well-behaved program. // See the # Programs section for the definition of the `validate_execution` method. if !validate_execution( &program_output.pre_states, &program_output.post_states, - chained_call.program_id, + program_id, ) { return Err(NssaError::InvalidProgramBehavior); } + // The invoked program claims the accounts with default program id. for post in program_output.post_states.iter_mut() { - // The invoked program claims the accounts with default program id. if post.program_owner == DEFAULT_PROGRAM_ID { - post.program_owner = chained_call.program_id; + post.program_owner = program_id; } } + // Update the state diff for (pre, post) in program_output .pre_states .iter() - .zip(program_output.post_states) + .zip(program_output.post_states.iter()) { - state_diff.insert(pre.account_id, post); + state_diff.insert(pre.account_id, post.clone()); } if let Some(next_chained_call) = program_output.chained_call { - chained_call = next_chained_call; + program_id = next_chained_call.program_id; + instruction_data = next_chained_call.instruction_data; + + // Build post states with metadata for next call + let mut post_states_with_metadata = Vec::new(); + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states) + { + let mut post_with_metadata = pre.clone(); + post_with_metadata.account = post.clone(); + post_states_with_metadata.push(post_with_metadata); + } + + input_pre_states = next_chained_call + .account_indices + .iter() + .map(|&i| { + post_states_with_metadata + .get(i) + .ok_or_else(|| { + NssaError::InvalidInput("Invalid account indices".into()) + }) + .cloned() + }) + .collect::, NssaError>>()?; } else { break; }; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 2119929..4120824 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -6,9 +6,7 @@ use crate::{ }; use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, - account::Account, - address::Address, - program::ProgramId, + account::Account, address::Address, program::ProgramId, }; use std::collections::{HashMap, HashSet}; @@ -2097,7 +2095,7 @@ pub mod tests { (amount, Program::authenticated_transfer_program().id()); let expected_to_post = Account { - program_owner: Program::authenticated_transfer_program().id(), + program_owner: Program::chain_caller().id(), balance: amount, ..Account::default() }; From 18dca9407c5b017c8ec677cf93451e518c52a073 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Fri, 31 Oct 2025 10:16:15 +0200 Subject: [PATCH 15/15] fix: config update --- .../configs/debug/sequencer_config.json | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/sequencer_runner/configs/debug/sequencer_config.json b/sequencer_runner/configs/debug/sequencer_config.json index 19ff458..acd0caa 100644 --- a/sequencer_runner/configs/debug/sequencer_config.json +++ b/sequencer_runner/configs/debug/sequencer_config.json @@ -8,49 +8,49 @@ "port": 3040, "initial_accounts": [ { - "addr": "d07ad2e84b27fa00c262f0a1eea0ff35ca0973547e6a106f72f193c2dc838b44", + "addr": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy", "balance": 10000 }, { - "addr": "e7ae77c5ef1a05999344af499fc78a1705398d62ed06cf2e1479f6def89a39bc", + "addr": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw", "balance": 20000 } ], "initial_commitments": [ { "npk": [ - 193, - 209, - 150, - 113, - 47, - 241, - 48, - 145, - 250, - 79, - 235, - 51, - 119, - 40, - 184, - 232, - 5, + 63, + 202, + 178, + 231, + 183, + 82, + 237, + 212, + 216, 221, - 36, - 21, - 201, - 106, - 90, - 210, - 129, - 106, - 71, - 99, - 208, + 215, + 255, 153, - 75, - 215 + 101, + 177, + 161, + 254, + 210, + 128, + 122, + 54, + 190, + 230, + 151, + 183, + 64, + 225, + 229, + 113, + 1, + 228, + 97 ], "account": { "program_owner": [ @@ -70,38 +70,38 @@ }, { "npk": [ - 27, - 250, + 192, + 251, + 166, + 243, + 167, + 236, + 84, + 249, + 35, 136, - 142, - 88, - 128, - 138, - 21, - 49, - 183, - 118, - 160, - 117, - 114, - 110, - 47, - 136, - 87, - 60, - 70, - 59, - 60, - 18, - 223, - 23, - 147, - 241, - 5, - 184, - 103, + 130, + 172, + 219, 225, - 105 + 161, + 139, + 229, + 89, + 243, + 125, + 194, + 213, + 209, + 30, + 23, + 174, + 100, + 244, + 124, + 74, + 140, + 47 ], "account": { "program_owner": [ @@ -154,4 +154,4 @@ 37, 37 ] -} +} \ No newline at end of file