diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index a1aa3e0..f86553a 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -18,7 +18,6 @@ use sequencer_runner::startup_sequencer; use tempfile::TempDir; use tokio::task::JoinHandle; use wallet::{ - Command, SubcommandReturnValue, WalletCore, cli::{ chain::{ChainSubcommand, FetchSubcommand, RegisterSubcommand}, native_token_transfer_program::{ @@ -29,11 +28,9 @@ use wallet::{ PinataProgramSubcommand, PinataProgramSubcommandPrivate, PinataProgramSubcommandPublic, }, token_program::{ - TokenProgramSubcommand, TokenProgramSubcommandPrivate, TokenProgramSubcommandPublic, + TokenProgramSubcommand, TokenProgramSubcommandDeshielded, TokenProgramSubcommandPrivate, TokenProgramSubcommandPublic, TokenProgramSubcommandShielded }, - }, - config::PersistentAccountData, - helperfunctions::{fetch_config, fetch_persistent_accounts}, + }, config::PersistentAccountData, helperfunctions::{fetch_config, fetch_persistent_accounts}, Command, SubcommandReturnValue, WalletCore }; #[derive(Parser, Debug)] @@ -764,6 +761,246 @@ pub async fn test_success_token_program_private_claiming_path() { assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); } +/// This test creates a new public token using the token program. After creating the token, the test executes a +/// shielded token transfer to a new account. All accounts are owned except definition. +pub async fn test_success_token_program_shielded_owned() { + let wallet_config = fetch_config().await.unwrap(); + + // Create new account for the token definition (public) + let SubcommandReturnValue::RegisterAccount { + addr: definition_addr, + } = wallet::execute_subcommand(Command::Chain(ChainSubcommand::Register(RegisterSubcommand::RegisterAccountPublic {}))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for the token supply holder (private) + let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = + wallet::execute_subcommand(Command::Chain(ChainSubcommand::Register(RegisterSubcommand::RegisterAccountPublic {}))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for receiving a token transaction + let SubcommandReturnValue::RegisterAccount { + addr: recipient_addr, + } = wallet::execute_subcommand(Command::Chain(ChainSubcommand::Register(RegisterSubcommand::RegisterAccountPrivate {}))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + + // 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, + }); + + wallet::execute_subcommand(Command::TokenProgram(subcommand)) + .await + .unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + + // Check the status of the token definition account is the expected after the execution + let definition_acc = seq_client + .get_account(definition_addr.to_string()) + .await + .unwrap() + .account; + + assert_eq!(definition_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ] + assert_eq!( + definition_acc.data, + vec![ + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // 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, + }, + ); + + wallet::execute_subcommand(Command::TokenProgram(subcommand)) + .await + .unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config).await.unwrap(); + + let new_commitment2 = wallet_storage + .get_private_account_commitment(&recipient_addr) + .unwrap(); + 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, + }, + ); + + wallet::execute_subcommand(Command::TokenProgram(subcommand)) + .await + .unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config).await.unwrap(); + + let new_commitment2 = wallet_storage + .get_private_account_commitment(&recipient_addr) + .unwrap(); + assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await); +} + +/// This test creates a new private token using the token program. After creating the token, the test executes a +/// deshielded token transfer to a new account. All accounts are owned except definition. +pub async fn test_success_token_program_deshielded_owned() { + let wallet_config = fetch_config().await.unwrap(); + + // Create new account for the token definition (public) + let SubcommandReturnValue::RegisterAccount { + addr: definition_addr, + } = wallet::execute_subcommand(Command::Chain(ChainSubcommand::Register(RegisterSubcommand::RegisterAccountPublic {}))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for the token supply holder (private) + let SubcommandReturnValue::RegisterAccount { addr: supply_addr } = + wallet::execute_subcommand(Command::Chain(ChainSubcommand::Register(RegisterSubcommand::RegisterAccountPrivate {}))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for receiving a token transaction + let SubcommandReturnValue::RegisterAccount { + addr: recipient_addr, + } = wallet::execute_subcommand(Command::Chain(ChainSubcommand::Register(RegisterSubcommand::RegisterAccountPublic {}))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + + // 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, + }, + ); + + wallet::execute_subcommand(Command::TokenProgram(subcommand)) + .await + .unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + + // Check the status of the token definition account is the expected after the execution + let definition_acc = seq_client + .get_account(definition_addr.to_string()) + .await + .unwrap() + .account; + + assert_eq!(definition_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ] + assert_eq!( + definition_acc.data, + vec![ + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config).await.unwrap(); + + let new_commitment1 = wallet_storage + .get_private_account_commitment(&supply_addr) + .unwrap(); + 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, + }, + ); + + wallet::execute_subcommand(Command::TokenProgram(subcommand)) + .await + .unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config).await.unwrap(); + + let new_commitment1 = wallet_storage + .get_private_account_commitment(&supply_addr) + .unwrap(); + 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, + }, + ); + + wallet::execute_subcommand(Command::TokenProgram(subcommand)) + .await + .unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let wallet_config = fetch_config().await.unwrap(); + let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config).await.unwrap(); + + let new_commitment1 = wallet_storage + .get_private_account_commitment(&supply_addr) + .unwrap(); + assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await); +} + pub async fn 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(); @@ -1393,6 +1630,12 @@ pub async fn main_tests_runner() -> Result<()> { test_success_private_transfer_to_another_owned_account_cont_run_path ); } + "test_success_token_program_shielded_owned" => { + test_cleanup_wrap!(home_dir, test_success_token_program_shielded_owned); + } + "test_success_token_program_deshielded_owned" => { + test_cleanup_wrap!(home_dir, test_success_token_program_deshielded_owned); + } "all" => { test_cleanup_wrap!(home_dir, test_success_move_to_another_account); test_cleanup_wrap!(home_dir, test_success); diff --git a/wallet/src/cli/token_program.rs b/wallet/src/cli/token_program.rs index 7bd3e88..25de77d 100644 --- a/wallet/src/cli/token_program.rs +++ b/wallet/src/cli/token_program.rs @@ -14,6 +14,12 @@ pub enum TokenProgramSubcommand { ///Private execution #[command(subcommand)] Private(TokenProgramSubcommandPrivate), + ///Deshielded execution + #[command(subcommand)] + Deshielded(TokenProgramSubcommandDeshielded), + ///Shielded execution + #[command(subcommand)] + Shielded(TokenProgramSubcommandShielded), } ///Represents generic public CLI subcommand for a wallet working with token_program @@ -41,7 +47,7 @@ pub enum TokenProgramSubcommandPublic { }, } -///Represents generic public CLI subcommand for a wallet working with token_program +///Represents generic private CLI subcommand for a wallet working with token_program #[derive(Subcommand, Debug, Clone)] pub enum TokenProgramSubcommandPrivate { //Create a new token using the token program @@ -79,6 +85,47 @@ pub enum TokenProgramSubcommandPrivate { }, } +///Represents deshielded public CLI subcommand for a wallet working with token_program +#[derive(Subcommand, Debug, Clone)] +pub enum TokenProgramSubcommandDeshielded { + //Transfer tokens using the token program + TransferTokenDeshielded { + #[arg(short, long)] + sender_addr: String, + #[arg(short, long)] + recipient_addr: String, + #[arg(short, long)] + balance_to_move: u128, + }, +} + +///Represents generic shielded CLI subcommand for a wallet working with token_program +#[derive(Subcommand, Debug, Clone)] +pub enum TokenProgramSubcommandShielded { + //Transfer tokens using the token program + TransferTokenShieldedOwned { + #[arg(short, long)] + sender_addr: String, + #[arg(short, long)] + recipient_addr: String, + #[arg(short, long)] + balance_to_move: u128, + }, + //Transfer tokens using the token program + TransferTokenShieldedForeign { + #[arg(short, long)] + sender_addr: String, + ///recipient_npk - valid 32 byte hex string + #[arg(long)] + recipient_npk: String, + ///recipient_ipk - valid 33 byte hex string + #[arg(long)] + recipient_ipk: String, + #[arg(short, long)] + balance_to_move: u128, + }, +} + impl WalletSubcommand for TokenProgramSubcommandPublic { async fn handle_subcommand( self, @@ -291,6 +338,163 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { } } +impl WalletSubcommand for TokenProgramSubcommandDeshielded { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + TokenProgramSubcommandDeshielded::TransferTokenDeshielded { + sender_addr, + recipient_addr, + balance_to_move, + } => { + let sender_addr: Address = sender_addr.parse().unwrap(); + let recipient_addr: Address = recipient_addr.parse().unwrap(); + + let (res, [secret_sender]) = wallet_core + .send_transfer_token_transaction_deshielded( + sender_addr, + recipient_addr, + balance_to_move, + ) + .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_sender, sender_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::PrivacyPreservingTransfer { tx_hash }) + } + } + } +} + +impl WalletSubcommand for TokenProgramSubcommandShielded { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + TokenProgramSubcommandShielded::TransferTokenShieldedForeign { + sender_addr, + recipient_npk, + recipient_ipk, + balance_to_move, + } => { + let sender_addr: Address = sender_addr.parse().unwrap(); + let recipient_npk_res = hex::decode(recipient_npk)?; + let mut recipient_npk = [0; 32]; + recipient_npk.copy_from_slice(&recipient_npk_res); + let recipient_npk = nssa_core::NullifierPublicKey(recipient_npk); + + let recipient_ipk_res = hex::decode(recipient_ipk)?; + let mut recipient_ipk = [0u8; 33]; + recipient_ipk.copy_from_slice(&recipient_ipk_res); + let recipient_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( + recipient_ipk.to_vec(), + ); + + let res = wallet_core + .send_transfer_token_transaction_shielded_foreign_account( + sender_addr, + recipient_npk, + recipient_ipk, + balance_to_move, + ) + .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 { + println!("Transaction data is {:?}", tx.message); + } + + let path = wallet_core.store_persistent_accounts().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandShielded::TransferTokenShieldedOwned { + sender_addr, + recipient_addr, + balance_to_move, + } => { + let sender_addr: Address = sender_addr.parse().unwrap(); + let recipient_addr: Address = recipient_addr.parse().unwrap(); + + let recipient_initialization = wallet_core + .check_private_account_initialized(&recipient_addr) + .await?; + + let (res, [secret_recipient]) = + if let Some(recipient_proof) = recipient_initialization { + wallet_core + .send_transfer_token_transaction_shielded_owned_account_already_initialized( + sender_addr, + recipient_addr, + balance_to_move, + recipient_proof, + ) + .await? + } else { + wallet_core + .send_transfer_token_transaction_shielded_owned_account_not_initialized( + sender_addr, + recipient_addr, + balance_to_move, + ) + .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_recipient, recipient_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::PrivacyPreservingTransfer { tx_hash }) + } + } + } +} + impl WalletSubcommand for TokenProgramSubcommand { async fn handle_subcommand( self, @@ -303,6 +507,12 @@ impl WalletSubcommand for TokenProgramSubcommand { TokenProgramSubcommand::Public(public_subcommand) => { public_subcommand.handle_subcommand(wallet_core).await } + TokenProgramSubcommand::Deshielded(deshielded_subcommand) => { + deshielded_subcommand.handle_subcommand(wallet_core).await + } + TokenProgramSubcommand::Shielded(shielded_subcommand) => { + shielded_subcommand.handle_subcommand(wallet_core).await + } } } } diff --git a/wallet/src/token_program_interactions.rs b/wallet/src/token_program_interactions.rs index 76bd36b..a77a512 100644 --- a/wallet/src/token_program_interactions.rs +++ b/wallet/src/token_program_interactions.rs @@ -448,4 +448,325 @@ impl WalletCore { [shared_secret_sender, shared_secret_recipient], )) } + + pub async fn send_transfer_token_transaction_deshielded( + &self, + sender_address: Address, + recipient_address: Address, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let Some((sender_keys, sender_acc)) = self + .storage + .user_data + .get_private_account(&sender_address) + .cloned() + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let Ok(recipient_acc) = self.get_account_public(recipient_address).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let sender_npk = sender_keys.nullifer_public_key; + let sender_ipk = sender_keys.incoming_viewing_public_key; + + let program = Program::token(); + + let sender_commitment = Commitment::new(&sender_npk, &sender_acc); + + let sender_pre = AccountWithMetadata::new(sender_acc.clone(), true, &sender_npk); + let recipient_pre = + AccountWithMetadata::new(recipient_acc.clone(), false, recipient_address); + + let eph_holder_sender = EphemeralKeyHolder::new(&sender_npk); + let shared_secret_sender = eph_holder_sender.calculate_shared_secret_sender(&sender_ipk); + + // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x01; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &Program::serialize_instruction(instruction).unwrap(), + &[1, 0], + &produce_random_nonces(1), + &[(sender_npk.clone(), shared_secret_sender.clone())], + &[( + sender_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![recipient_address], + vec![], + vec![( + sender_npk.clone(), + sender_ipk.clone(), + eph_holder_sender.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[], + ); + let tx = nssa::PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_sender], + )) + } + + pub async fn send_transfer_token_transaction_shielded_owned_account_already_initialized( + &self, + sender_address: Address, + recipient_address: Address, + amount: u128, + recipient_proof: MembershipProof, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let Ok(sender_acc) = self.get_account_public(sender_address).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let Some(sender_priv_key) = self + .storage + .user_data + .get_pub_account_signing_key(&sender_address) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let Some((recipient_keys, recipient_acc)) = self + .storage + .user_data + .get_private_account(&recipient_address) + .cloned() + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let recipient_npk = recipient_keys.nullifer_public_key.clone(); + let recipient_ipk = recipient_keys.incoming_viewing_public_key.clone(); + + let program = Program::token(); + + let sender_pre = AccountWithMetadata::new(sender_acc.clone(), true, sender_address); + let recipient_pre = AccountWithMetadata::new(recipient_acc.clone(), true, &recipient_npk); + + let eph_holder_recipient = EphemeralKeyHolder::new(&recipient_npk); + let shared_secret_recipient = + eph_holder_recipient.calculate_shared_secret_sender(&recipient_ipk); + + // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x01; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &Program::serialize_instruction(instruction).unwrap(), + &[0, 1], + &produce_random_nonces(1), + &[(recipient_npk.clone(), shared_secret_recipient.clone())], + &[( + recipient_keys.private_key_holder.nullifier_secret_key, + recipient_proof, + )], + &program, + ) + .unwrap(); + + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + vec![sender_address], + vec![sender_acc.nonce], + vec![( + recipient_npk.clone(), + recipient_ipk.clone(), + eph_holder_recipient.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[sender_priv_key], + ); + let tx = nssa::PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_recipient], + )) + } + + pub async fn send_transfer_token_transaction_shielded_owned_account_not_initialized( + &self, + sender_address: Address, + recipient_address: Address, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let Ok(sender_acc) = self.get_account_public(sender_address).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let Some(sender_priv_key) = self + .storage + .user_data + .get_pub_account_signing_key(&sender_address) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let Some((recipient_keys, recipient_acc)) = self + .storage + .user_data + .get_private_account(&recipient_address) + .cloned() + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let recipient_npk = recipient_keys.nullifer_public_key.clone(); + let recipient_ipk = recipient_keys.incoming_viewing_public_key.clone(); + + let program = Program::token(); + + let sender_pre = AccountWithMetadata::new(sender_acc.clone(), true, sender_address); + let recipient_pre = AccountWithMetadata::new(recipient_acc.clone(), false, &recipient_npk); + + let eph_holder_recipient = EphemeralKeyHolder::new(&recipient_npk); + let shared_secret_recipient = + eph_holder_recipient.calculate_shared_secret_sender(&recipient_ipk); + + // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x01; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &Program::serialize_instruction(instruction).unwrap(), + &[0, 2], + &produce_random_nonces(1), + &[(recipient_npk.clone(), shared_secret_recipient.clone())], + &[], + &program, + ) + .unwrap(); + + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + vec![sender_address], + vec![sender_acc.nonce], + vec![( + recipient_npk.clone(), + recipient_ipk.clone(), + eph_holder_recipient.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[sender_priv_key], + ); + let tx = nssa::PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_recipient], + )) + } + + pub async fn send_transfer_token_transaction_shielded_foreign_account( + &self, + sender_address: Address, + recipient_npk: NullifierPublicKey, + recipient_ipk: IncomingViewingPublicKey, + amount: u128, + ) -> Result { + let Ok(sender_acc) = self.get_account_public(sender_address).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let Some(sender_priv_key) = self + .storage + .user_data + .get_pub_account_signing_key(&sender_address) + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let recipient_acc = nssa_core::account::Account::default(); + + let program = Program::token(); + + let sender_pre = AccountWithMetadata::new(sender_acc.clone(), true, sender_address); + let recipient_pre = AccountWithMetadata::new(recipient_acc.clone(), false, &recipient_npk); + + let eph_holder_recipient = EphemeralKeyHolder::new(&recipient_npk); + let shared_secret_recipient = + eph_holder_recipient.calculate_shared_secret_sender(&recipient_ipk); + + // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x01; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &Program::serialize_instruction(instruction).unwrap(), + &[0, 2], + &produce_random_nonces(1), + &[(recipient_npk.clone(), shared_secret_recipient.clone())], + &[], + &program, + ) + .unwrap(); + + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + vec![sender_address], + vec![sender_acc.nonce], + vec![( + recipient_npk.clone(), + recipient_ipk.clone(), + eph_holder_recipient.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[sender_priv_key], + ); + let tx = nssa::PrivacyPreservingTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx_private(tx).await?) + } }