From f77481f3b59bd569c70aeb11a6ebdde881d0eb44 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 11 Dec 2025 14:46:16 +0200 Subject: [PATCH] feat: mint and burn --- integration_tests/src/test_suite_map.rs | 240 +++++ nssa/program_methods/guest/src/bin/token.rs | 4 +- .../src/cli/programs/native_token_transfer.rs | 11 +- wallet/src/cli/programs/pinata.rs | 3 +- wallet/src/cli/programs/token.rs | 824 +++++++++++++++++- wallet/src/lib.rs | 38 +- wallet/src/program_facades/token.rs | 388 +++++++++ 7 files changed, 1477 insertions(+), 31 deletions(-) diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index 8012e17..eb21438 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -1548,6 +1548,246 @@ pub fn prepare_function_map() -> HashMap { info!("Success!"); } + /// This test creates a new token using the token program. After creating the token, the test + /// executes a token transfer to a new account then mint, then burn. + #[nssa_integration_test] + pub async fn test_success_token_program_burn_mint() { + info!("########## test_success_token_program_burn_mint ##########"); + let wallet_config = fetch_config().await.unwrap(); + + // Create new account for the token definition + let SubcommandReturnValue::RegisterAccount { + account_id: definition_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { cci: None }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for the token supply holder + let SubcommandReturnValue::RegisterAccount { + account_id: supply_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { cci: None }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + // Create new account for receiving a token transaction + let SubcommandReturnValue::RegisterAccount { + account_id: recipient_account_id, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { cci: None }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + + // Create new token + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: make_public_account_input_from_str( + &definition_account_id.to_string(), + ), + supply_account_id: make_public_account_input_from_str(&supply_account_id.to_string()), + name: "A NAME".to_string(), + total_supply: 37, + }; + wallet::cli::execute_subcommand(Command::Token(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_account_id.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.as_ref(), + &[ + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // Check the status of the token holding account with the total supply is the expected after + // the execution + let supply_acc = seq_client + .get_account(supply_account_id.to_string()) + .await + .unwrap() + .account; + + // The account must be owned by the token program + assert_eq!(supply_acc.program_owner, Program::token().id()); + // The data of a token definition account has the following layout: + // [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16 + // bytes) ] First byte of the data equal to 1 means it's a token holding account + assert_eq!(supply_acc.data.as_ref()[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + // In this example, this is a token account of the newly created token, so it is expected + // to be equal to the account_id of the token definition account. + assert_eq!( + &supply_acc.data.as_ref()[1..33], + definition_account_id.to_bytes() + ); + assert_eq!( + u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), + 37 + ); + + // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id` + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: make_public_account_input_from_str(&supply_account_id.to_string()), + to: Some(make_public_account_input_from_str( + &recipient_account_id.to_string(), + )), + to_npk: None, + to_ipk: None, + amount: 7, + }; + + wallet::cli::execute_subcommand(Command::Token(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Check the status of the account at `supply_account_id` is the expected after the + // execution + let supply_acc = seq_client + .get_account(supply_account_id.to_string()) + .await + .unwrap() + .account; + // The account must be owned by the token program + assert_eq!(supply_acc.program_owner, Program::token().id()); + // First byte equal to 1 means it's a token holding account + assert_eq!(supply_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + assert_eq!(&supply_acc.data[1..33], definition_account_id.to_bytes()); + assert_eq!( + u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()), + 30 + ); + + // Check the status of the account at `recipient_account_id` is the expected after the + // execution + let recipient_acc = seq_client + .get_account(recipient_account_id.to_string()) + .await + .unwrap() + .account; + + // The account must be owned by the token program + assert_eq!(recipient_acc.program_owner, Program::token().id()); + // First byte equal to 1 means it's a token holding account + assert_eq!(recipient_acc.data[0], 1); + // Bytes from 1 to 33 represent the id of the token this account is associated with. + assert_eq!(&recipient_acc.data[1..33], definition_account_id.to_bytes()); + assert_eq!( + u128::from_le_bytes(recipient_acc.data[33..].try_into().unwrap()), + 7 + ); + + // Burn 3 tokens from `recipient_acc` + let subcommand = TokenProgramAgnosticSubcommand::Burn { + definition: Some(make_public_account_input_from_str(&definition_account_id.to_string())), + definition_npk: None, + definition_ipk: None, + holder: make_public_account_input_from_str(&recipient_account_id.to_string()), + amount: 3, + }; + + wallet::cli::execute_subcommand(Command::Token(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Check the status of the token definition account is the expected after the execution + let definition_acc = seq_client + .get_account(definition_account_id.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + definition_acc.data.as_ref(), + &[ + 0, 65, 32, 78, 65, 77, 69, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // Check the status of the account at `recipient_account_id` is the expected after the + // execution + let recipient_acc = seq_client + .get_account(recipient_account_id.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(recipient_acc.data[33..].try_into().unwrap()), + 4 + ); + + // Mint 10 tokens at `recipient_acc` + let subcommand = TokenProgramAgnosticSubcommand::Mint { + definition: make_public_account_input_from_str(&definition_account_id.to_string()), + holder: Some(make_public_account_input_from_str(&recipient_account_id.to_string())), + holder_npk: None, + holder_ipk: None, + amount: 10, + }; + + wallet::cli::execute_subcommand(Command::Token(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Check the status of the token definition account is the expected after the execution + let definition_acc = seq_client + .get_account(definition_account_id.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + definition_acc.data.as_ref(), + &[ + 0, 65, 32, 78, 65, 77, 69, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + // Check the status of the account at `recipient_account_id` is the expected after the + // execution + let recipient_acc = seq_client + .get_account(recipient_account_id.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(recipient_acc.data[33..].try_into().unwrap()), + 14 + ); + } + #[nssa_integration_test] pub async fn test_pinata() { info!("########## test_pinata ##########"); diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 7c9c0a4..4e9336b 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -26,13 +26,13 @@ use nssa_core::{ // Arguments to this function are: // * Two accounts: [definition_account, holding_account]. // * Authorization required: holding_account -// * An instruction data byte string of length 23, indicating the balance to burn with the folloiwng layout +// * An instruction data byte string of length 23, indicating the balance to burn with the following layout // [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. // 5. Mint additional supply of tokens tokens to a Token Holding account (thus increasing total supply) // Arguments to this function are: // * Two accounts: [definition_account, holding_account]. // * Authorization required: definition_account -// * An instruction data byte string of length 23, indicating the balance to mint with the folloiwng layout +// * An instruction data byte string of length 23, indicating the balance to mint with the following layout // [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. const TOKEN_DEFINITION_TYPE: u8 = 0; diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 9dc72ae..7868a7c 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -4,6 +4,7 @@ use common::transaction::NSSATransaction; use nssa::AccountId; use crate::{ + AccDecodeData::Decode, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, @@ -87,7 +88,7 @@ impl WalletSubcommand for AuthTransferSubcommand { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret, account_id)]; + let acc_decode_data = vec![Decode(secret, account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -328,7 +329,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_from, from), (secret_to, to)]; + let acc_decode_data = vec![Decode(secret_from, from), Decode(secret_to, to)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -372,7 +373,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_from, from)]; + let acc_decode_data = vec![Decode(secret_from, from)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -412,7 +413,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret, to)]; + let acc_decode_data = vec![Decode(secret, to)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -491,7 +492,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret, from)]; + let acc_decode_data = vec![Decode(secret, from)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index 7712a7c..e0b6570 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -3,6 +3,7 @@ use clap::Subcommand; use common::{PINATA_BASE58, transaction::NSSATransaction}; use crate::{ + AccDecodeData::Decode, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, @@ -159,7 +160,7 @@ impl WalletSubcommand for PinataProgramSubcommandPrivate { println!("Transaction data is {transfer_tx:?}"); if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_winner, winner_account_id)]; + let acc_decode_data = vec![Decode(secret_winner, winner_account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 4480a1e..24f314e 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -4,6 +4,7 @@ use common::transaction::NSSATransaction; use nssa::AccountId; use crate::{ + AccDecodeData::{Decode, Skip}, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, @@ -49,6 +50,56 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] amount: u128, }, + /// Burn tokens on `holder`, modify `definition`. + /// + /// `holder` is owned + /// + /// If `definition` is private, then `definition` and (`definition_npk` , `definition_ipk`) is a + /// mutually exclusive patterns. + /// + /// First is used for owned accounts, second otherwise. + Burn { + /// definition - valid 32 byte base58 string with privacy prefix + #[arg(long)] + definition: Option, + /// definition_npk - valid 32 byte hex string + #[arg(long)] + definition_npk: Option, + /// definition_ipk - valid 33 byte hex string + #[arg(long)] + definition_ipk: Option, + /// holder - valid 32 byte base58 string with privacy prefix + #[arg(long)] + holder: String, + /// amount - amount of balance to burn + #[arg(long)] + amount: u128, + }, + /// Mint tokens on `holder`, modify `definition`. + /// + /// `definition` is owned + /// + /// If `holder` is private, then `holder` and (`holder_npk` , `holder_ipk`) is a mutually + /// exclusive patterns. + /// + /// First is used for owned accounts, second otherwise. + Mint { + /// definition - valid 32 byte base58 string with privacy prefix + #[arg(long)] + definition: String, + /// holder - valid 32 byte base58 string with privacy prefix + #[arg(long)] + holder: Option, + /// holder_npk - valid 32 byte hex string + #[arg(long)] + holder_npk: Option, + /// to_ipk - valid 33 byte hex string + #[arg(long)] + holder_ipk: Option, + /// amount - amount of balance to mint + #[arg(long)] + amount: u128, + }, } impl WalletSubcommand for TokenProgramAgnosticSubcommand { @@ -201,6 +252,189 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { } }; + underlying_subcommand.handle_subcommand(wallet_core).await + } + TokenProgramAgnosticSubcommand::Burn { + definition, + definition_npk, + definition_ipk, + holder, + amount, + } => { + let underlying_subcommand = match (definition, definition_npk, definition_ipk) { + (None, None, None) => { + anyhow::bail!( + "Provide either account account_id of definition or their public keys" + ); + } + (Some(_), Some(_), Some(_)) => { + anyhow::bail!( + "Provide only one variant: either account_id of definition or their public keys" + ); + } + (_, Some(_), None) | (_, None, Some(_)) => { + anyhow::bail!("List of public keys is uncomplete"); + } + (Some(definition), None, None) => { + let (definition, definition_privacy) = + parse_addr_with_privacy_prefix(&definition)?; + let (holder, holder_privacy) = parse_addr_with_privacy_prefix(&holder)?; + + match (definition_privacy, holder_privacy) { + (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { + TokenProgramSubcommand::Public( + TokenProgramSubcommandPublic::BurnToken { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + (AccountPrivacyKind::Private, AccountPrivacyKind::Private) => { + TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::BurnTokenPrivateOwned { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + (AccountPrivacyKind::Private, AccountPrivacyKind::Public) => { + TokenProgramSubcommand::Deshielded( + TokenProgramSubcommandDeshielded::BurnTokenDeshieldedOwned { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + (AccountPrivacyKind::Public, AccountPrivacyKind::Private) => { + TokenProgramSubcommand::Shielded( + TokenProgramSubcommandShielded::BurnTokenShielded { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + } + } + (None, Some(definition_npk), Some(definition_ipk)) => { + let (holder, holder_privacy) = parse_addr_with_privacy_prefix(&holder)?; + + match holder_privacy { + AccountPrivacyKind::Private => TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::BurnTokenPrivateForeign { + definition_npk, + definition_ipk, + holder_account_id: holder, + amount, + }, + ), + AccountPrivacyKind::Public => TokenProgramSubcommand::Deshielded( + TokenProgramSubcommandDeshielded::BurnTokenDeshieldedForeign { + definition_npk, + definition_ipk, + holder_account_id: holder, + amount, + }, + ), + } + } + }; + + underlying_subcommand.handle_subcommand(wallet_core).await + } + TokenProgramAgnosticSubcommand::Mint { + definition, + holder, + holder_npk, + holder_ipk, + amount, + } => { + let underlying_subcommand = match (holder, holder_npk, holder_ipk) { + (None, None, None) => { + anyhow::bail!( + "Provide either account account_id of holder or their public keys" + ); + } + (Some(_), Some(_), Some(_)) => { + anyhow::bail!( + "Provide only one variant: either account_id of holder or their public keys" + ); + } + (_, Some(_), None) | (_, None, Some(_)) => { + anyhow::bail!("List of public keys is uncomplete"); + } + (Some(holder), None, None) => { + let (definition, definition_privacy) = + parse_addr_with_privacy_prefix(&definition)?; + let (holder, holder_privacy) = parse_addr_with_privacy_prefix(&holder)?; + + match (definition_privacy, holder_privacy) { + (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { + TokenProgramSubcommand::Public( + TokenProgramSubcommandPublic::MintToken { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + (AccountPrivacyKind::Private, AccountPrivacyKind::Private) => { + TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::MintTokenPrivateOwned { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + (AccountPrivacyKind::Private, AccountPrivacyKind::Public) => { + TokenProgramSubcommand::Deshielded( + TokenProgramSubcommandDeshielded::MintTokenDeshielded { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + (AccountPrivacyKind::Public, AccountPrivacyKind::Private) => { + TokenProgramSubcommand::Shielded( + TokenProgramSubcommandShielded::MintTokenShieldedOwned { + definition_account_id: definition, + holder_account_id: holder, + amount, + }, + ) + } + } + } + (None, Some(holder_npk), Some(holder_ipk)) => { + let (definition, definition_privacy) = + parse_addr_with_privacy_prefix(&definition)?; + + match definition_privacy { + AccountPrivacyKind::Private => TokenProgramSubcommand::Private( + TokenProgramSubcommandPrivate::MintTokenPrivateForeign { + definition_account_id: definition, + holder_npk, + holder_ipk, + amount, + }, + ), + AccountPrivacyKind::Public => TokenProgramSubcommand::Shielded( + TokenProgramSubcommandShielded::MintTokenShieldedForeign { + definition_account_id: definition, + holder_npk, + holder_ipk, + amount, + }, + ), + } + } + }; + underlying_subcommand.handle_subcommand(wallet_core).await } } @@ -239,6 +473,24 @@ pub enum TokenProgramSubcommandPublic { #[arg(short, long)] balance_to_move: u128, }, + // Burn tokens using the token program + BurnToken { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Transfer tokens using the token program + MintToken { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, } /// Represents generic private CLI subcommand for a wallet working with token_program @@ -266,6 +518,46 @@ pub enum TokenProgramSubcommandPrivate { #[arg(short, long)] balance_to_move: u128, }, + // Burn tokens using the token program + BurnTokenPrivateOwned { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Transfer tokens using the token program + MintTokenPrivateOwned { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Burn tokens using the token program + BurnTokenPrivateForeign { + #[arg(short, long)] + definition_npk: String, + #[arg(short, long)] + definition_ipk: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Transfer tokens using the token program + MintTokenPrivateForeign { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_npk: String, + #[arg(short, long)] + holder_ipk: String, + #[arg(short, long)] + amount: u128, + }, } /// Represents deshielded public CLI subcommand for a wallet working with token_program @@ -280,6 +572,35 @@ pub enum TokenProgramSubcommandDeshielded { #[arg(short, long)] balance_to_move: u128, }, + // Burn tokens using the token program + BurnTokenDeshieldedOwned { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Transfer tokens using the token program + MintTokenDeshielded { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Burn tokens using the token program + BurnTokenDeshieldedForeign { + #[arg(short, long)] + definition_npk: String, + #[arg(short, long)] + definition_ipk: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, } /// Represents generic shielded CLI subcommand for a wallet working with token_program @@ -307,6 +628,35 @@ pub enum TokenProgramSubcommandShielded { #[arg(short, long)] balance_to_move: u128, }, + // Burn tokens using the token program + BurnTokenShielded { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Transfer tokens using the token program + MintTokenShieldedOwned { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_account_id: String, + #[arg(short, long)] + amount: u128, + }, + // Transfer tokens using the token program + MintTokenShieldedForeign { + #[arg(short, long)] + definition_account_id: String, + #[arg(short, long)] + holder_npk: String, + #[arg(short, long)] + holder_ipk: String, + #[arg(short, long)] + amount: u128, + }, } /// Represents generic initialization subcommand for a wallet working with token_program @@ -386,6 +736,34 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { .await?; Ok(SubcommandReturnValue::Empty) } + TokenProgramSubcommandPublic::BurnToken { + definition_account_id, + holder_account_id, + amount, + } => { + Token(wallet_core) + .send_burn_transaction( + definition_account_id.parse().unwrap(), + holder_account_id.parse().unwrap(), + amount, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } + TokenProgramSubcommandPublic::MintToken { + definition_account_id, + holder_account_id, + amount, + } => { + Token(wallet_core) + .send_mint_transaction( + definition_account_id.parse().unwrap(), + holder_account_id.parse().unwrap(), + amount, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } } } } @@ -421,8 +799,8 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { let acc_decode_data = vec![ - (secret_sender, sender_account_id), - (secret_recipient, recipient_account_id), + Decode(secret_sender, sender_account_id), + Decode(secret_recipient, recipient_account_id), ]; wallet_core.decode_insert_privacy_preserving_transaction_results( @@ -473,7 +851,191 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_sender, sender_account_id)]; + let acc_decode_data = vec![Decode(secret_sender, sender_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandPrivate::BurnTokenPrivateOwned { + definition_account_id, + holder_account_id, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, [secret_definition, secret_holder]) = Token(wallet_core) + .send_burn_transaction_private_owned_account( + definition_account_id, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![ + Decode(secret_definition, definition_account_id), + Decode(secret_holder, holder_account_id), + ]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandPrivate::BurnTokenPrivateForeign { + definition_npk, + definition_ipk, + holder_account_id, + amount, + } => { + let definition_npk_res = hex::decode(definition_npk)?; + let mut definition_npk = [0; 32]; + definition_npk.copy_from_slice(&definition_npk_res); + let definition_npk = nssa_core::NullifierPublicKey(definition_npk); + + let definition_ipk_res = hex::decode(definition_ipk)?; + let mut definition_ipk = [0u8; 33]; + definition_ipk.copy_from_slice(&definition_ipk_res); + let definition_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( + definition_ipk.to_vec(), + ); + + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, [_, secret_holder]) = Token(wallet_core) + .send_burn_transaction_private_foreign_account( + definition_npk, + definition_ipk, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![Skip, Decode(secret_holder, holder_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandPrivate::MintTokenPrivateOwned { + definition_account_id, + holder_account_id, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, [secret_definition, secret_holder]) = Token(wallet_core) + .send_mint_transaction_private_owned_account( + definition_account_id, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![ + Decode(secret_definition, definition_account_id), + Decode(secret_holder, holder_account_id), + ]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandPrivate::MintTokenPrivateForeign { + definition_account_id, + holder_npk, + holder_ipk, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + + let holder_npk_res = hex::decode(holder_npk)?; + let mut holder_npk = [0; 32]; + holder_npk.copy_from_slice(&holder_npk_res); + let holder_npk = nssa_core::NullifierPublicKey(holder_npk); + + let holder_ipk_res = hex::decode(holder_ipk)?; + let mut holder_ipk = [0u8; 33]; + holder_ipk.copy_from_slice(&holder_ipk_res); + let holder_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( + holder_ipk.to_vec(), + ); + + let (res, [secret_definition, _]) = Token(wallet_core) + .send_mint_transaction_private_foreign_account( + definition_account_id, + holder_npk, + holder_ipk, + amount, + ) + .await?; + + println!("Results of tx send are {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![Decode(secret_definition, definition_account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -521,7 +1083,129 @@ impl WalletSubcommand for TokenProgramSubcommandDeshielded { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_sender, sender_account_id)]; + let acc_decode_data = vec![Decode(secret_sender, sender_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandDeshielded::BurnTokenDeshieldedOwned { + definition_account_id, + holder_account_id, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, secret_definition) = Token(wallet_core) + .send_burn_transaction_deshielded_owned_account( + definition_account_id, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![Decode(secret_definition, definition_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandDeshielded::BurnTokenDeshieldedForeign { + definition_npk, + definition_ipk, + holder_account_id, + amount, + } => { + let definition_npk_res = hex::decode(definition_npk)?; + let mut definition_npk = [0; 32]; + definition_npk.copy_from_slice(&definition_npk_res); + let definition_npk = nssa_core::NullifierPublicKey(definition_npk); + + let definition_ipk_res = hex::decode(definition_ipk)?; + let mut definition_ipk = [0u8; 33]; + definition_ipk.copy_from_slice(&definition_ipk_res); + let definition_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( + definition_ipk.to_vec(), + ); + + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, _) = Token(wallet_core) + .send_burn_transaction_deshielded_foreign_account( + definition_npk, + definition_ipk, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandDeshielded::MintTokenDeshielded { + definition_account_id, + holder_account_id, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, secret_definition) = Token(wallet_core) + .send_mint_transaction_deshielded( + definition_account_id, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![Decode(secret_definition, definition_account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -614,7 +1298,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_recipient, recipient_account_id)]; + let acc_decode_data = vec![Decode(secret_recipient, recipient_account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -626,6 +1310,128 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { println!("Stored persistent accounts at {path:#?}"); + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandShielded::BurnTokenShielded { + definition_account_id, + holder_account_id, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, secret_holder) = Token(wallet_core) + .send_burn_transaction_shielded( + definition_account_id, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![Decode(secret_holder, holder_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandShielded::MintTokenShieldedOwned { + definition_account_id, + holder_account_id, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + let holder_account_id: AccountId = holder_account_id.parse().unwrap(); + + let (res, secret_holder) = Token(wallet_core) + .send_mint_transaction_shielded_owned_account( + definition_account_id, + holder_account_id, + amount, + ) + .await?; + + println!("Results of tx send are {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![Decode(secret_holder, holder_account_id)]; + + wallet_core.decode_insert_privacy_preserving_transaction_results( + tx, + &acc_decode_data, + )?; + } + + let path = wallet_core.store_persistent_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + TokenProgramSubcommandShielded::MintTokenShieldedForeign { + definition_account_id, + holder_npk, + holder_ipk, + amount, + } => { + let definition_account_id: AccountId = definition_account_id.parse().unwrap(); + + let holder_npk_res = hex::decode(holder_npk)?; + let mut holder_npk = [0; 32]; + holder_npk.copy_from_slice(&holder_npk_res); + let holder_npk = nssa_core::NullifierPublicKey(holder_npk); + + let holder_ipk_res = hex::decode(holder_ipk)?; + let mut holder_ipk = [0u8; 33]; + holder_ipk.copy_from_slice(&holder_ipk_res); + let holder_ipk = nssa_core::encryption::shared_key_derivation::Secp256k1Point( + holder_ipk.to_vec(), + ); + + let (res, _) = Token(wallet_core) + .send_mint_transaction_shielded_foreign_account( + definition_account_id, + holder_npk, + holder_ipk, + amount, + ) + .await?; + + println!("Results of tx send are {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_data().await?; + + println!("Stored persistent accounts at {path:#?}"); + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) } } @@ -673,8 +1479,8 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { let acc_decode_data = vec![ - (secret_definition, definition_account_id), - (secret_supply, supply_account_id), + Decode(secret_definition, definition_account_id), + Decode(secret_supply, supply_account_id), ]; wallet_core.decode_insert_privacy_preserving_transaction_results( @@ -723,7 +1529,7 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_definition, definition_account_id)]; + let acc_decode_data = vec![Decode(secret_definition, definition_account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, @@ -771,7 +1577,7 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { .await?; if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx { - let acc_decode_data = vec![(secret_supply, supply_account_id)]; + let acc_decode_data = vec![Decode(secret_supply, supply_account_id)]; wallet_core.decode_insert_privacy_preserving_transaction_results( tx, diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 313fdcd..78cd00a 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -38,6 +38,11 @@ pub mod poller; mod privacy_preserving_tx; pub mod program_facades; +pub enum AccDecodeData { + Skip, + Decode(nssa_core::SharedSecretKey, AccountId), +} + pub struct WalletCore { pub storage: WalletChainStore, pub poller: TxPoller, @@ -218,24 +223,29 @@ impl WalletCore { pub fn decode_insert_privacy_preserving_transaction_results( &mut self, tx: nssa::privacy_preserving_transaction::PrivacyPreservingTransaction, - acc_decode_data: &[(nssa_core::SharedSecretKey, AccountId)], + acc_decode_mask: &[AccDecodeData], ) -> Result<()> { - for (output_index, (secret, acc_account_id)) in acc_decode_data.iter().enumerate() { - let acc_ead = tx.message.encrypted_private_post_states[output_index].clone(); - let acc_comm = tx.message.new_commitments[output_index].clone(); + for (output_index, acc_decode_data) in acc_decode_mask.iter().enumerate() { + match acc_decode_data { + AccDecodeData::Decode(secret, acc_account_id) => { + let acc_ead = tx.message.encrypted_private_post_states[output_index].clone(); + let acc_comm = tx.message.new_commitments[output_index].clone(); - let res_acc = nssa_core::EncryptionScheme::decrypt( - &acc_ead.ciphertext, - secret, - &acc_comm, - output_index as u32, - ) - .unwrap(); + let res_acc = nssa_core::EncryptionScheme::decrypt( + &acc_ead.ciphertext, + secret, + &acc_comm, + output_index as u32, + ) + .unwrap(); - println!("Received new acc {res_acc:#?}"); + println!("Received new acc {res_acc:#?}"); - self.storage - .insert_private_account_data(*acc_account_id, res_acc); + self.storage + .insert_private_account_data(*acc_account_id, res_acc); + } + AccDecodeData::Skip => {} + } } println!("Transaction data is {:?}", tx.message); diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 4ec9c12..f5b13ce 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -301,6 +301,370 @@ impl Token<'_> { (resp, first) }) } + + pub async fn send_burn_transaction( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result { + let account_ids = vec![definition_account_id, holder_account_id]; + let (instruction, program) = token_program_preparation_burn(amount); + + // ToDo: Fix this by updating `nssa::public_transaction::Message::try_new` to get raw bytes + let instruction: [u32; 23] = instruction.try_into().unwrap(); + + let Ok(nonces) = self.0.get_accounts_nonces(vec![holder_account_id]).await else { + return Err(ExecutionFailureKind::SequencerError); + }; + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let Some(signing_key) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&holder_account_id) + 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.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_burn_transaction_private_owned_account( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_burn(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::PrivateOwned(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected definition's secret"); + let second = iter.next().expect("expected holder's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_burn_transaction_private_foreign_account( + &self, + definition_npk: NullifierPublicKey, + definition_ipk: IncomingViewingPublicKey, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_burn(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateForeign { + npk: definition_npk, + ipk: definition_ipk, + }, + PrivacyPreservingAccount::PrivateOwned(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected definition's secret"); + let second = iter.next().expect("expected holder's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_burn_transaction_deshielded_owned_account( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_burn(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::Public(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected definition's secret"); + (resp, first) + }) + } + + pub async fn send_burn_transaction_shielded( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_burn(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(definition_account_id), + PrivacyPreservingAccount::PrivateOwned(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected holder's secret"); + (resp, first) + }) + } + + pub async fn send_burn_transaction_deshielded_foreign_account( + &self, + definition_npk: NullifierPublicKey, + definition_ipk: IncomingViewingPublicKey, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_burn(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateForeign { + npk: definition_npk, + ipk: definition_ipk, + }, + PrivacyPreservingAccount::Public(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected definition's secret"); + (resp, first) + }) + } + + pub async fn send_mint_transaction( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result { + let account_ids = vec![definition_account_id, holder_account_id]; + let (instruction, program) = token_program_preparation_mint(amount); + + // ToDo: Fix this by updating `nssa::public_transaction::Message::try_new` to get raw bytes + let instruction: [u32; 23] = instruction.try_into().unwrap(); + + let Ok(nonces) = self + .0 + .get_accounts_nonces(vec![definition_account_id]) + .await + else { + return Err(ExecutionFailureKind::SequencerError); + }; + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let Some(signing_key) = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&definition_account_id) + 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.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_mint_transaction_private_owned_account( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_mint(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::PrivateOwned(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected definition's secret"); + let second = iter.next().expect("expected holder's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_mint_transaction_private_foreign_account( + &self, + definition_account_id: AccountId, + holder_npk: NullifierPublicKey, + holder_ipk: IncomingViewingPublicKey, + amount: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_mint(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: holder_npk, + ipk: holder_ipk, + }, + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut iter = secrets.into_iter(); + let first = iter.next().expect("expected definition's secret"); + let second = iter.next().expect("expected holder's secret"); + (resp, [first, second]) + }) + } + + pub async fn send_mint_transaction_deshielded( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_mint(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivateOwned(definition_account_id), + PrivacyPreservingAccount::Public(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected definition's secret"); + (resp, first) + }) + } + + pub async fn send_mint_transaction_shielded_owned_account( + &self, + definition_account_id: AccountId, + holder_account_id: AccountId, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_mint(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(definition_account_id), + PrivacyPreservingAccount::PrivateOwned(holder_account_id), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected holder's secret"); + (resp, first) + }) + } + + pub async fn send_mint_transaction_shielded_foreign_account( + &self, + definition_account_id: AccountId, + holder_npk: NullifierPublicKey, + holder_ipk: IncomingViewingPublicKey, + amount: u128, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + let (instruction_data, program) = token_program_preparation_mint(amount); + + self.0 + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(definition_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: holder_npk, + ipk: holder_ipk, + }, + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let first = secrets + .into_iter() + .next() + .expect("expected holder's secret"); + (resp, first) + }) + } } fn token_program_preparation_transfer(amount: u128) -> (InstructionData, Program) { @@ -328,3 +692,27 @@ fn token_program_preparation_definition( (instruction_data, program) } + +fn token_program_preparation_burn(amount: u128) -> (InstructionData, Program) { + // Instruction must be: [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || + // 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x03; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + let instruction_data = Program::serialize_instruction(instruction).unwrap(); + let program = Program::token(); + + (instruction_data, program) +} + +fn token_program_preparation_mint(amount: u128) -> (InstructionData, Program) { + // Instruction must be: [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || + // 0x00 || 0x00 || 0x00]. + let mut instruction = [0; 23]; + instruction[0] = 0x04; + instruction[1..17].copy_from_slice(&amount.to_le_bytes()); + let instruction_data = Program::serialize_instruction(instruction).unwrap(); + let program = Program::token(); + + (instruction_data, program) +}