use std::{fs::File, io::Write, path::PathBuf, str::FromStr, sync::Arc}; use base64::Engine; use common::{ ExecutionFailureKind, sequencer_client::{SequencerClient, json::SendTxResponse}, transaction::{EncodedTransaction, NSSATransaction}, }; use anyhow::Result; use chain_storage::WalletChainStore; use config::WalletConfig; use k256::elliptic_curve::group::GroupEncoding; use k256::elliptic_curve::sec1::ToEncodedPoint; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use log::info; use nssa::Address; use clap::{Parser, Subcommand}; use crate::{ helperfunctions::{ fetch_config, fetch_persistent_accounts, get_home, produce_account_addr_from_hex, produce_data_for_storage, }, poller::TxPoller, }; pub const HOME_DIR_ENV_VAR: &str = "NSSA_WALLET_HOME_DIR"; pub mod chain_storage; pub mod config; pub mod helperfunctions; pub mod poller; pub struct WalletCore { pub storage: WalletChainStore, pub poller: TxPoller, pub sequencer_client: Arc, } impl WalletCore { pub fn start_from_config_update_chain(config: WalletConfig) -> Result { let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); let tx_poller = TxPoller::new(config.clone(), client.clone()); let mut storage = WalletChainStore::new(config)?; let persistent_accounts = fetch_persistent_accounts()?; for pers_acc_data in persistent_accounts { storage.insert_account_data(pers_acc_data); } Ok(Self { storage, poller: tx_poller, sequencer_client: client.clone(), }) } ///Store persistent accounts at home pub fn store_persistent_accounts(&self) -> Result { let home = get_home()?; let accs_path = home.join("curr_accounts.json"); let data = produce_data_for_storage(&self.storage.user_data); let accs = serde_json::to_vec_pretty(&data)?; let mut accs_file = File::create(accs_path.as_path())?; accs_file.write_all(&accs)?; info!("Stored accounts data at {accs_path:#?}"); Ok(accs_path) } pub fn create_new_account_public(&mut self) -> Address { self.storage .user_data .generate_new_public_transaction_private_key() } pub fn create_new_account_private(&mut self) -> Address { self.storage .user_data .generate_new_privacy_preserving_transaction_key_chain() } pub async fn send_public_native_token_transfer( &self, from: Address, to: Address, balance_to_move: u128, ) -> Result { let Ok(balance) = self.get_account_balance(from).await else { return Err(ExecutionFailureKind::SequencerError); }; if balance >= balance_to_move { let Ok(nonces) = self.get_accounts_nonces(vec![from]).await else { return Err(ExecutionFailureKind::SequencerError); }; let addresses = vec![from, to]; let program_id = nssa::program::Program::authenticated_transfer_program().id(); let message = nssa::public_transaction::Message::try_new( program_id, addresses, nonces, balance_to_move, ) .unwrap(); let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); let Some(signing_key) = signing_key else { return Err(ExecutionFailureKind::KeyNotFoundError); }; let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); let tx = nssa::PublicTransaction::new(message, witness_set); Ok(self.sequencer_client.send_tx_public(tx).await?) } else { Err(ExecutionFailureKind::InsufficientFundsError) } } pub async fn send_private_native_token_transfer( &self, from: Address, to: Address, balance_to_move: u128, ) -> Result { let from_data = self.storage.user_data.get_private_account(&from); let to_data = self.storage.user_data.get_private_account(&to); let Some((from_keys, from_acc)) = from_data else { return Err(ExecutionFailureKind::KeyNotFoundError); }; let Some((to_keys, to_acc)) = to_data else { return Err(ExecutionFailureKind::KeyNotFoundError); }; if from_acc.balance >= balance_to_move { let program = nssa::program::Program::authenticated_transfer_program(); let sender_commitment = nssa_core::Commitment::new( &nssa_core::NullifierPublicKey(from_keys.nullifer_public_key), from_acc, ); let sender_pre = nssa_core::account::AccountWithMetadata { account: from_acc.clone(), is_authorized: true, }; let recipient_pre = nssa_core::account::AccountWithMetadata { account: to_acc.clone(), is_authorized: false, }; let eph_holder = EphemeralKeyHolder::new( to_keys.nullifer_public_key, from_keys.private_key_holder.outgoing_viewing_secret_key, from_acc.nonce.try_into().unwrap(), ); let shared_secret = eph_holder.calculate_shared_secret_sender(to_keys.incoming_viewing_public_key); let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( &[sender_pre, recipient_pre], &nssa::program::Program::serialize_instruction(balance_to_move).unwrap(), &[1, 2], &[from_acc.nonce + 1, to_acc.nonce + 1], &[ ( nssa_core::NullifierPublicKey(from_keys.nullifer_public_key), nssa_core::SharedSecretKey( shared_secret.to_bytes().as_slice().try_into().unwrap(), ), ), ( nssa_core::NullifierPublicKey(to_keys.nullifer_public_key), nssa_core::SharedSecretKey( shared_secret.to_bytes().as_slice().try_into().unwrap(), ), ), ], &[( from_keys.private_key_holder.nullifier_secret_key, self.sequencer_client .get_proof_for_commitment(sender_commitment) .await .unwrap() .unwrap(), )], &program, ) .unwrap(); let message = nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( vec![], vec![], vec![ ( nssa_core::NullifierPublicKey(from_keys.nullifer_public_key), nssa_core::encryption::shared_key_derivation::Secp256k1Point( from_keys .incoming_viewing_public_key .to_encoded_point(true) .as_bytes() .to_vec(), ), nssa_core::encryption::shared_key_derivation::Secp256k1Point( eph_holder .generate_ephemeral_public_key() .to_encoded_point(true) .as_bytes() .to_vec(), ), ), ( nssa_core::NullifierPublicKey(to_keys.nullifer_public_key), nssa_core::encryption::shared_key_derivation::Secp256k1Point( to_keys .incoming_viewing_public_key .to_encoded_point(true) .as_bytes() .to_vec(), ), nssa_core::encryption::shared_key_derivation::Secp256k1Point( eph_holder .generate_ephemeral_public_key() .to_encoded_point(true) .as_bytes() .to_vec(), ), ), ], output, ) .unwrap(); let witness_set = nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( &message, proof, &[], ); let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( message, witness_set, ); Ok(self.sequencer_client.send_tx_private(tx).await?) } else { Err(ExecutionFailureKind::InsufficientFundsError) } } ///Get account balance pub async fn get_account_balance(&self, acc: Address) -> Result { Ok(self .sequencer_client .get_account_balance(acc.to_string()) .await? .balance) } ///Get accounts nonces pub async fn get_accounts_nonces(&self, accs: Vec
) -> Result> { Ok(self .sequencer_client .get_accounts_nonces(accs.into_iter().map(|acc| acc.to_string()).collect()) .await? .nonces) } ///Poll transactions pub async fn poll_native_token_transfer(&self, hash: String) -> Result { let transaction_encoded = self.poller.poll_tx(hash).await?; let tx_base64_decode = base64::engine::general_purpose::STANDARD.decode(transaction_encoded)?; let pub_tx = EncodedTransaction::from_bytes(tx_base64_decode); Ok(NSSATransaction::try_from(&pub_tx)?) } } ///Represents CLI command for a wallet #[derive(Subcommand, Debug, Clone)] #[clap(about)] pub enum Command { ///Send native token transfer from `from` to `to` for `amount` /// /// Public operation SendNativeTokenTransferPublic { ///from - valid 32 byte hex string #[arg(long)] from: String, ///to - valid 32 byte hex string #[arg(long)] to: String, ///amount - amount of balance to move #[arg(long)] amount: u128, }, ///Send native token transfer from `from` to `to` for `amount` /// /// Private operation SendNativeTokenTransferPrivate { ///from - valid 32 byte hex string #[arg(long)] from: String, ///to - valid 32 byte hex string #[arg(long)] to: String, ///amount - amount of balance to move #[arg(long)] amount: u128, }, ///Register new public account RegisterAccountPublic {}, ///Register new private account RegisterAccountPrivate {}, ///Fetch transaction by `hash` FetchTx { #[arg(short, long)] tx_hash: String, }, ///Get account `addr` balance GetAccountBalance { #[arg(short, long)] addr: String, }, ///Get account `addr` nonce GetAccountNonce { #[arg(short, long)] addr: String, }, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config #[derive(Parser, Debug)] #[clap(version, about)] pub struct Args { /// Wallet command #[command(subcommand)] pub command: Command, } pub async fn execute_subcommand(command: Command) -> Result<()> { let wallet_config = fetch_config()?; let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config)?; match command { Command::SendNativeTokenTransferPublic { from, to, amount } => { let from = produce_account_addr_from_hex(from)?; let to = produce_account_addr_from_hex(to)?; let res = wallet_core .send_public_native_token_transfer(from, to, amount) .await?; info!("Results of tx send is {res:#?}"); let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; info!("Transaction data is {transfer_tx:?}"); } Command::SendNativeTokenTransferPrivate { from, to, amount } => { let from = produce_account_addr_from_hex(from)?; let to = produce_account_addr_from_hex(to)?; let res = wallet_core .send_private_native_token_transfer(from, to, amount) .await?; info!("Results of tx send is {res:#?}"); let transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; info!("Transaction data is {transfer_tx:?}"); } Command::RegisterAccountPublic {} => { let addr = wallet_core.create_new_account_public(); let key = wallet_core .storage .user_data .get_pub_account_signing_key(&addr); info!("Generated new account with addr {addr:#?}"); info!("With key {key:#?}"); } Command::RegisterAccountPrivate {} => { let addr = wallet_core.create_new_account_private(); let (key, account) = wallet_core .storage .user_data .get_private_account(&addr) .unwrap(); info!("Generated new account with addr {addr:#?}"); info!("With key {key:#?}"); info!("With account {account:#?}"); } Command::FetchTx { tx_hash } => { let tx_obj = wallet_core .sequencer_client .get_transaction_by_hash(tx_hash) .await?; info!("Transaction object {tx_obj:#?}"); } Command::GetAccountBalance { addr } => { let addr = Address::from_str(&addr)?; let balance = wallet_core.get_account_balance(addr).await?; info!("Accounts {addr:#?} balance is {balance}"); } Command::GetAccountNonce { addr } => { let addr = Address::from_str(&addr)?; let nonce = wallet_core.get_accounts_nonces(vec![addr]).await?[0]; info!("Accounts {addr:#?} nonce is {nonce}"); } } wallet_core.store_persistent_accounts()?; Ok(()) }