From d4a471e94890dbacc3ba598dc3cc44506c0a6a1f Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Tue, 16 Dec 2025 14:05:34 +0200 Subject: [PATCH] feat: swaps, add/rem liq --- nssa/core/src/encryption/mod.rs | 2 +- nssa/program_methods/guest/src/bin/amm.rs | 1 + .../privacy_preserving_transaction/circuit.rs | 6 +- nssa/src/state.rs | 69 +-- wallet/src/cli/mod.rs | 8 +- wallet/src/cli/programs/amm.rs | 452 +++++++++++++++ wallet/src/cli/programs/mod.rs | 1 + wallet/src/lib.rs | 2 +- wallet/src/privacy_preserving_tx.rs | 26 +- wallet/src/program_facades/amm.rs | 522 ++++++++++++++++++ wallet/src/program_facades/mod.rs | 1 + 11 files changed, 1035 insertions(+), 55 deletions(-) create mode 100644 wallet/src/cli/programs/amm.rs create mode 100644 wallet/src/program_facades/amm.rs diff --git a/nssa/core/src/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index 8f0c6be..c953d4d 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -16,7 +16,7 @@ use crate::{Commitment, account::Account}; pub type Scalar = [u8; 32]; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Copy)] pub struct SharedSecretKey(pub [u8; 32]); pub struct EncryptionScheme; diff --git a/nssa/program_methods/guest/src/bin/amm.rs b/nssa/program_methods/guest/src/bin/amm.rs index 0882a0d..275defa 100644 --- a/nssa/program_methods/guest/src/bin/amm.rs +++ b/nssa/program_methods/guest/src/bin/amm.rs @@ -55,6 +55,7 @@ use nssa_core::{ // * compute_pool_pda_seed: token definitions for the pool pair // * compute_vault_pda_seed: pool definition id, definition token id, // * compute_liquidity_token_pda_seed: pool definition id +// const POOL_DEFINITION_DATA_SIZE: usize = 225; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 95933a3..c8aad62 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -216,7 +216,7 @@ mod tests { &Program::serialize_instruction(balance_to_move).unwrap(), &[0, 2], &[0xdeadbeef], - &[(recipient_keys.npk(), shared_secret.clone())], + &[(recipient_keys.npk(), shared_secret)], &[], &Program::authenticated_transfer_program().into(), ) @@ -312,8 +312,8 @@ mod tests { &[1, 2], &[0xdeadbeef1, 0xdeadbeef2], &[ - (sender_keys.npk(), shared_secret_1.clone()), - (recipient_keys.npk(), shared_secret_2.clone()), + (sender_keys.npk(), shared_secret_1), + (recipient_keys.npk(), shared_secret_2), ], &[( sender_keys.nsk, diff --git a/nssa/src/state.rs b/nssa/src/state.rs index ab802c7..bd2ddc0 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2094,7 +2094,7 @@ pub mod tests { &visibility_mask, &[0xdeadbeef1, 0xdeadbeef2], &[ - (sender_keys.npk(), shared_secret.clone()), + (sender_keys.npk(), shared_secret), (sender_keys.npk(), shared_secret), ], &private_account_auth, @@ -2288,12 +2288,12 @@ pub mod tests { let mut i: usize = 0; let (token_1, token_2) = loop { if definition_token_a_id.value()[i] > definition_token_b_id.value()[i] { - let token_1 = definition_token_a_id.clone(); - let token_2 = definition_token_b_id.clone(); + let token_1 = definition_token_a_id; + let token_2 = definition_token_b_id; break (token_1, token_2); } else if definition_token_a_id.value()[i] < definition_token_b_id.value()[i] { - let token_1 = definition_token_b_id.clone(); - let token_2 = definition_token_a_id.clone(); + let token_1 = definition_token_b_id; + let token_2 = definition_token_a_id; break (token_1, token_2); } @@ -2396,6 +2396,7 @@ pub mod tests { .expect("225 bytes should fit into Data") } + #[allow(unused)] fn parse(data: &[u8]) -> Option { if data.len() != POOL_DEFINITION_DATA_SIZE { None @@ -2531,6 +2532,7 @@ pub mod tests { UserTokenBHoldingNewDef, } + #[allow(clippy::enum_variant_names)] enum IdEnum { PoolDefinitionId, TokenLPDefinitionId, @@ -2543,6 +2545,7 @@ pub mod tests { VaultBId, } + #[allow(clippy::enum_variant_names)] enum PrivateKeysEnum { UserTokenAKey, UserTokenBKey, @@ -2680,9 +2683,7 @@ pub mod tests { reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceInit), fees: 0u128, active: true, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::TokenADefinitionAcc => Account { @@ -2692,9 +2693,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: helper_balances_constructor(BalancesEnum::TokenASupply), - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::TokenBDefinitionAcc => Account { @@ -2704,9 +2703,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: helper_balances_constructor(BalancesEnum::TokenBSupply), - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::TokenLPDefinitionAcc => Account { @@ -2716,9 +2713,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: helper_balances_constructor(BalancesEnum::TokenLPSupply), - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::VaultAInit => Account { @@ -2787,9 +2782,7 @@ pub mod tests { reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceSwap1), fees: 0u128, active: true, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::UserTokenAHoldingSwap1 => Account { @@ -2848,9 +2841,7 @@ pub mod tests { reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceSwap2), fees: 0u128, active: true, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::UserTokenAHoldingSwap2 => Account { @@ -2909,9 +2900,7 @@ pub mod tests { reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceAdd), fees: 0u128, active: true, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::UserTokenAHoldingAdd => Account { @@ -2951,9 +2940,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: helper_balances_constructor(BalancesEnum::TokenLPSupplyAdd), - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::VaultARemove => Account { @@ -2992,9 +2979,7 @@ pub mod tests { reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceRemove), fees: 0u128, active: true, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::UserTokenAHoldingRemove => Account { @@ -3034,9 +3019,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: helper_balances_constructor(BalancesEnum::TokenLPSupplyRemove), - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::TokenLPDefinitionInitInactive => Account { @@ -3046,9 +3029,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: 0, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::VaultAInitInactive => Account { @@ -3085,9 +3066,7 @@ pub mod tests { reserve_b: 0, fees: 0u128, active: false, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::UserTokenAHoldingNewInit => Account { @@ -3127,9 +3106,7 @@ pub mod tests { account_type: 0u8, name: [1u8; 6], total_supply: helper_balances_constructor(BalancesEnum::VaultABalanceInit), - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::PoolDefinitionNewInit => Account { @@ -3148,9 +3125,7 @@ pub mod tests { reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceInit), fees: 0u128, active: true, - }) - .try_into() - .expect("Data too big"), + }), nonce: 0, }, AccountsEnum::UserTokenLPHoldingInitZero => Account { diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 61b8697..cf3b2f1 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -11,8 +11,8 @@ use crate::{ chain::ChainSubcommand, config::ConfigSubcommand, programs::{ - native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, - token::TokenProgramAgnosticSubcommand, + amm::AmmProgramAgnosticSubcommand, native_token_transfer::AuthTransferSubcommand, + pinata::PinataProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand, }, }, helperfunctions::{fetch_config, fetch_persistent_storage, merge_auth_config}, @@ -47,6 +47,9 @@ pub enum Command { /// Token program interaction subcommand #[command(subcommand)] Token(TokenProgramAgnosticSubcommand), + /// AMM program interaction subcommand + #[command(subcommand)] + AMM(AmmProgramAgnosticSubcommand), /// Check the wallet can connect to the node and builtin local programs /// match the remote versions CheckHealth {}, @@ -165,6 +168,7 @@ pub async fn execute_subcommand_with_auth( Command::Token(token_subcommand) => { token_subcommand.handle_subcommand(&mut wallet_core).await? } + Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(&mut wallet_core).await?, Command::Config(config_subcommand) => { config_subcommand .handle_subcommand(&mut wallet_core) diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs new file mode 100644 index 0000000..3878c94 --- /dev/null +++ b/wallet/src/cli/programs/amm.rs @@ -0,0 +1,452 @@ +use anyhow::Result; +use clap::Subcommand; + +use crate::{ + PrivacyPreservingAccount, WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, + helperfunctions::parse_addr_with_privacy_prefix, + program_facades::amm::AMM, +}; + +/// Represents generic CLI subcommand for a wallet working with amm program +#[derive(Subcommand, Debug, Clone)] +pub enum AmmProgramAgnosticSubcommand { + /// Produce a new token + /// + /// user_holding_a and user_holding_b must be owned. + New { + /// amm_pool - valid 32 byte base58 string with privacy prefix + #[arg(long)] + amm_pool: String, + /// vault_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_a: String, + /// vault_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_b: String, + /// pool_lp - valid 32 byte base58 string with privacy prefix + #[arg(long)] + pool_lp: String, + /// user_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_a: String, + /// user_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_b: String, + /// user_holding_lp - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_lp: String, + #[arg(long)] + balance_a: u128, + #[arg(long)] + balance_b: u128, + }, + /// Swap with variable privacy + /// + /// The account associated with swapping token must be owned + Swap { + /// amm_pool - valid 32 byte base58 string with privacy prefix + #[arg(long)] + amm_pool: String, + /// vault_holding_1 - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_1: String, + /// vault_holding_2 - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_2: String, + /// user_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_a: String, + /// user_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_b: String, + #[arg(long)] + amount_in: u128, + #[arg(long)] + min_amount_out: u128, + /// token_definition - valid 32 byte base58 string WITHOUT privacy prefix + #[arg(long)] + token_definition: String, + }, + /// Add liquidity with variable privacy + /// + /// user_holding_a and user_holding_b must be owned. + AddLiquidity { + /// amm_pool - valid 32 byte base58 string with privacy prefix + #[arg(long)] + amm_pool: String, + /// vault_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_a: String, + /// vault_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_b: String, + /// pool_lp - valid 32 byte base58 string with privacy prefix + #[arg(long)] + pool_lp: String, + /// user_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_a: String, + /// user_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_b: String, + /// user_holding_lp - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_lp: String, + #[arg(long)] + min_amount_lp: u128, + #[arg(long)] + max_amount_a: u128, + #[arg(long)] + max_amount_b: u128, + }, + /// Remove liquidity with variable privacy + /// + /// user_holding_lp must be owned. + RemoveLiquidity { + /// amm_pool - valid 32 byte base58 string with privacy prefix + #[arg(long)] + amm_pool: String, + /// vault_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_a: String, + /// vault_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + vault_holding_b: String, + /// pool_lp - valid 32 byte base58 string with privacy prefix + #[arg(long)] + pool_lp: String, + /// user_holding_a - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_a: String, + /// user_holding_b - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_b: String, + /// user_holding_lp - valid 32 byte base58 string with privacy prefix + #[arg(long)] + user_holding_lp: String, + #[arg(long)] + balance_lp: u128, + #[arg(long)] + max_amount_a: u128, + #[arg(long)] + max_amount_b: u128, + }, +} + +impl WalletSubcommand for AmmProgramAgnosticSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + AmmProgramAgnosticSubcommand::New { + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + balance_a, + balance_b, + } => { + let amm_pool = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&amm_pool)?, + )?; + let vault_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_a)?, + )?; + let vault_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_b)?, + )?; + let pool_lp = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&pool_lp)?, + )?; + let user_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_a)?, + )?; + let user_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_b)?, + )?; + let user_holding_lp = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_lp)?, + )?; + + let is_public_tx = [ + &amm_pool, + &vault_holding_a, + &vault_holding_b, + &pool_lp, + &user_holding_a, + &user_holding_b, + &user_holding_lp, + ] + .into_iter() + .all(|acc| acc.is_public()); + + if is_public_tx { + AMM(wallet_core) + .send_new_amm_definition( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + balance_a, + balance_b, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } else { + AMM(wallet_core) + .send_new_amm_definition_privacy_preserving( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + balance_a, + balance_b, + ) + .await?; + // ToDo: change into correct return value + Ok(SubcommandReturnValue::Empty) + } + } + AmmProgramAgnosticSubcommand::Swap { + amm_pool, + vault_holding_1, + vault_holding_2, + user_holding_a, + user_holding_b, + amount_in, + min_amount_out, + token_definition, + } => { + let amm_pool = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&amm_pool)?, + )?; + let vault_holding_1 = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_1)?, + )?; + let vault_holding_2 = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_2)?, + )?; + let user_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_a)?, + )?; + let user_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_b)?, + )?; + + let is_public_tx = [ + &amm_pool, + &vault_holding_1, + &vault_holding_2, + &user_holding_a, + &user_holding_b, + ] + .into_iter() + .all(|acc| acc.is_public()); + + if is_public_tx { + AMM(wallet_core) + .send_swap( + amm_pool, + vault_holding_1, + vault_holding_2, + user_holding_a, + user_holding_b, + amount_in, + min_amount_out, + token_definition.parse()?, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } else { + AMM(wallet_core) + .send_swap_privacy_preserving( + amm_pool, + vault_holding_1, + vault_holding_2, + user_holding_a, + user_holding_b, + amount_in, + min_amount_out, + token_definition.parse()?, + ) + .await?; + // ToDo: change into correct return value + Ok(SubcommandReturnValue::Empty) + } + } + AmmProgramAgnosticSubcommand::AddLiquidity { + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + min_amount_lp, + max_amount_a, + max_amount_b, + } => { + let amm_pool = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&amm_pool)?, + )?; + let vault_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_a)?, + )?; + let vault_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_b)?, + )?; + let pool_lp = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&pool_lp)?, + )?; + let user_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_a)?, + )?; + let user_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_b)?, + )?; + let user_holding_lp = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_lp)?, + )?; + + let is_public_tx = [ + &amm_pool, + &vault_holding_a, + &vault_holding_b, + &pool_lp, + &user_holding_a, + &user_holding_b, + &user_holding_lp, + ] + .into_iter() + .all(|acc| acc.is_public()); + + if is_public_tx { + AMM(wallet_core) + .send_add_liq( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + min_amount_lp, + max_amount_a, + max_amount_b, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } else { + AMM(wallet_core) + .send_add_liq_privacy_preserving( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + min_amount_lp, + max_amount_a, + max_amount_b, + ) + .await?; + // ToDo: change into correct return value + Ok(SubcommandReturnValue::Empty) + } + } + AmmProgramAgnosticSubcommand::RemoveLiquidity { + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + balance_lp, + max_amount_a, + max_amount_b, + } => { + let amm_pool = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&amm_pool)?, + )?; + let vault_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_a)?, + )?; + let vault_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&vault_holding_b)?, + )?; + let pool_lp = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&pool_lp)?, + )?; + let user_holding_a = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_a)?, + )?; + let user_holding_b = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_b)?, + )?; + let user_holding_lp = PrivacyPreservingAccount::parse_with_privacy( + parse_addr_with_privacy_prefix(&user_holding_lp)?, + )?; + + let is_public_tx = [ + &amm_pool, + &vault_holding_a, + &vault_holding_b, + &pool_lp, + &user_holding_a, + &user_holding_b, + &user_holding_lp, + ] + .into_iter() + .all(|acc| acc.is_public()); + + if is_public_tx { + AMM(wallet_core) + .send_remove_liq( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + balance_lp, + max_amount_a, + max_amount_b, + ) + .await?; + Ok(SubcommandReturnValue::Empty) + } else { + AMM(wallet_core) + .send_remove_liq_privacy_preserving( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + balance_lp, + max_amount_a, + max_amount_b, + ) + .await?; + // ToDo: change into correct return value + Ok(SubcommandReturnValue::Empty) + } + } + } + } +} diff --git a/wallet/src/cli/programs/mod.rs b/wallet/src/cli/programs/mod.rs index 3ffb7bb..96a4e76 100644 --- a/wallet/src/cli/programs/mod.rs +++ b/wallet/src/cli/programs/mod.rs @@ -1,3 +1,4 @@ +pub mod amm; pub mod native_token_transfer; pub mod pinata; pub mod token; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index bc28311..1218d25 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -280,7 +280,7 @@ impl WalletCore { &produce_random_nonces(private_account_keys.len()), &private_account_keys .iter() - .map(|keys| (keys.npk.clone(), keys.ssk.clone())) + .map(|keys| (keys.npk.clone(), keys.ssk)) .collect::>(), &acc_manager.private_account_auth(), &program.to_owned().into(), diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index e79bbac..d0d6629 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use common::error::ExecutionFailureKind; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use nssa::{AccountId, PrivateKey}; @@ -7,8 +8,9 @@ use nssa_core::{ encryption::{EphemeralPublicKey, IncomingViewingPublicKey}, }; -use crate::WalletCore; +use crate::{WalletCore, helperfunctions::AccountPrivacyKind}; +#[derive(Clone)] pub enum PrivacyPreservingAccount { Public(AccountId), PrivateOwned(AccountId), @@ -18,6 +20,28 @@ pub enum PrivacyPreservingAccount { }, } +impl PrivacyPreservingAccount { + pub fn parse_with_privacy(input: (String, AccountPrivacyKind)) -> Result { + let acc_id: AccountId = input.0.parse()?; + + match input.1 { + AccountPrivacyKind::Public => Ok(Self::Public(acc_id)), + AccountPrivacyKind::Private => Ok(Self::PrivateOwned(acc_id)), + } + } + + pub fn is_public(&self) -> bool { + matches!(&self, Self::Public(_)) + } + + pub fn is_private(&self) -> bool { + matches!( + &self, + Self::PrivateOwned(_) | Self::PrivateForeign { npk: _, ipk: _ } + ) + } +} + pub struct PrivateAccountKeys { pub npk: NullifierPublicKey, pub ssk: SharedSecretKey, diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs new file mode 100644 index 0000000..13e4940 --- /dev/null +++ b/wallet/src/program_facades/amm.rs @@ -0,0 +1,522 @@ +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; +use nssa::{AccountId, program::Program}; +use nssa_core::{SharedSecretKey, program::InstructionData}; +use serde::Serialize; + +use crate::{PrivacyPreservingAccount, WalletCore}; + +struct OrphanHack65BytesInput([u8; 65]); + +impl Serialize for OrphanHack65BytesInput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +struct OrphanHack49BytesInput([u8; 49]); + +impl Serialize for OrphanHack49BytesInput { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.0) + } +} + +pub struct AMM<'w>(pub &'w WalletCore); + +impl AMM<'_> { + #[allow(clippy::too_many_arguments)] + pub async fn send_new_amm_definition( + &self, + _amm_pool: PrivacyPreservingAccount, + _vault_holding_a: PrivacyPreservingAccount, + _vault_holding_b: PrivacyPreservingAccount, + _pool_lp: PrivacyPreservingAccount, + _user_holding_a: PrivacyPreservingAccount, + _user_holding_b: PrivacyPreservingAccount, + _user_holding_lp: PrivacyPreservingAccount, + _balance_a: u128, + _balance_b: u128, + ) -> Result { + todo!() + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_new_amm_definition_privacy_preserving( + &self, + _amm_pool: PrivacyPreservingAccount, + _vault_holding_a: PrivacyPreservingAccount, + _vault_holding_b: PrivacyPreservingAccount, + _pool_lp: PrivacyPreservingAccount, + _user_holding_a: PrivacyPreservingAccount, + _user_holding_b: PrivacyPreservingAccount, + _user_holding_lp: PrivacyPreservingAccount, + _balance_a: u128, + _balance_b: u128, + ) -> Result<(SendTxResponse, [Option; 7]), ExecutionFailureKind> { + todo!() + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_swap( + &self, + amm_pool: PrivacyPreservingAccount, + vault_holding_1: PrivacyPreservingAccount, + vault_holding_2: PrivacyPreservingAccount, + user_holding_a: PrivacyPreservingAccount, + user_holding_b: PrivacyPreservingAccount, + amount_in: u128, + min_amount_out: u128, + token_definition_id: AccountId, + ) -> Result { + let (instruction, program) = + amm_program_preparation_swap(amount_in, min_amount_out, token_definition_id); + + match ( + amm_pool, + vault_holding_1, + vault_holding_2, + user_holding_a, + user_holding_b, + ) { + ( + PrivacyPreservingAccount::Public(amm_pool), + PrivacyPreservingAccount::Public(vault_holding_1), + PrivacyPreservingAccount::Public(vault_holding_2), + PrivacyPreservingAccount::Public(user_holding_a), + PrivacyPreservingAccount::Public(user_holding_b), + ) => { + let account_ids = vec![ + amm_pool, + vault_holding_1, + vault_holding_2, + user_holding_a, + user_holding_b, + ]; + + // ToDo: Correct authorization + // ToDo: Also correct instruction serialization + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + vec![], + instruction, + ) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + _ => unreachable!(), + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_swap_privacy_preserving( + &self, + amm_pool: PrivacyPreservingAccount, + vault_holding_1: PrivacyPreservingAccount, + vault_holding_2: PrivacyPreservingAccount, + user_holding_a: PrivacyPreservingAccount, + user_holding_b: PrivacyPreservingAccount, + amount_in: u128, + min_amount_out: u128, + token_definition_id: AccountId, + ) -> Result<(SendTxResponse, [Option; 5]), ExecutionFailureKind> { + let (instruction_data, program) = + amm_program_preparation_swap(amount_in, min_amount_out, token_definition_id); + + self.0 + .send_privacy_preserving_tx( + vec![ + amm_pool.clone(), + vault_holding_1.clone(), + vault_holding_2.clone(), + user_holding_a.clone(), + user_holding_b.clone(), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut secrets = secrets.into_iter(); + let mut secrets_res = [None; 5]; + + for acc_id in [ + amm_pool, + vault_holding_1, + vault_holding_2, + user_holding_a, + user_holding_b, + ] + .iter() + .enumerate() + { + if acc_id.1.is_private() { + let secret = secrets.next().expect("expected next secret"); + + secrets_res[acc_id.0] = Some(secret); + } + } + + (resp, secrets_res) + }) + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_add_liq( + &self, + amm_pool: PrivacyPreservingAccount, + vault_holding_a: PrivacyPreservingAccount, + vault_holding_b: PrivacyPreservingAccount, + pool_lp: PrivacyPreservingAccount, + user_holding_a: PrivacyPreservingAccount, + user_holding_b: PrivacyPreservingAccount, + user_holding_lp: PrivacyPreservingAccount, + min_amount_lp: u128, + max_amount_a: u128, + max_amount_b: u128, + ) -> Result { + let (instruction, program) = + amm_program_preparation_add_liq(min_amount_lp, max_amount_a, max_amount_b); + + match ( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ) { + ( + PrivacyPreservingAccount::Public(amm_pool), + PrivacyPreservingAccount::Public(vault_holding_a), + PrivacyPreservingAccount::Public(vault_holding_b), + PrivacyPreservingAccount::Public(pool_lp), + PrivacyPreservingAccount::Public(user_holding_a), + PrivacyPreservingAccount::Public(user_holding_b), + PrivacyPreservingAccount::Public(user_holding_lp), + ) => { + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ]; + + // ToDo: Correct authorization + // ToDo: Also correct instruction serialization + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + vec![], + instruction, + ) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + _ => unreachable!(), + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_add_liq_privacy_preserving( + &self, + amm_pool: PrivacyPreservingAccount, + vault_holding_a: PrivacyPreservingAccount, + vault_holding_b: PrivacyPreservingAccount, + pool_lp: PrivacyPreservingAccount, + user_holding_a: PrivacyPreservingAccount, + user_holding_b: PrivacyPreservingAccount, + user_holding_lp: PrivacyPreservingAccount, + min_amount_lp: u128, + max_amount_a: u128, + max_amount_b: u128, + ) -> Result<(SendTxResponse, [Option; 7]), ExecutionFailureKind> { + let (instruction_data, program) = + amm_program_preparation_add_liq(min_amount_lp, max_amount_a, max_amount_b); + + self.0 + .send_privacy_preserving_tx( + vec![ + amm_pool.clone(), + vault_holding_a.clone(), + vault_holding_b.clone(), + pool_lp.clone(), + user_holding_a.clone(), + user_holding_b.clone(), + user_holding_lp.clone(), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut secrets = secrets.into_iter(); + let mut secrets_res = [None; 7]; + + for acc_id in [ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ] + .iter() + .enumerate() + { + if acc_id.1.is_private() { + let secret = secrets.next().expect("expected next secret"); + + secrets_res[acc_id.0] = Some(secret); + } + } + + (resp, secrets_res) + }) + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_remove_liq( + &self, + amm_pool: PrivacyPreservingAccount, + vault_holding_a: PrivacyPreservingAccount, + vault_holding_b: PrivacyPreservingAccount, + pool_lp: PrivacyPreservingAccount, + user_holding_a: PrivacyPreservingAccount, + user_holding_b: PrivacyPreservingAccount, + user_holding_lp: PrivacyPreservingAccount, + balance_lp: u128, + max_amount_a: u128, + max_amount_b: u128, + ) -> Result { + let (instruction, program) = + amm_program_preparation_remove_liq(balance_lp, max_amount_a, max_amount_b); + + match ( + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ) { + ( + PrivacyPreservingAccount::Public(amm_pool), + PrivacyPreservingAccount::Public(vault_holding_a), + PrivacyPreservingAccount::Public(vault_holding_b), + PrivacyPreservingAccount::Public(pool_lp), + PrivacyPreservingAccount::Public(user_holding_a), + PrivacyPreservingAccount::Public(user_holding_b), + PrivacyPreservingAccount::Public(user_holding_lp), + ) => { + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ]; + + // ToDo: Correct authorization + // ToDo: Also correct instruction serialization + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + vec![], + instruction, + ) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + _ => unreachable!(), + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn send_remove_liq_privacy_preserving( + &self, + amm_pool: PrivacyPreservingAccount, + vault_holding_a: PrivacyPreservingAccount, + vault_holding_b: PrivacyPreservingAccount, + pool_lp: PrivacyPreservingAccount, + user_holding_a: PrivacyPreservingAccount, + user_holding_b: PrivacyPreservingAccount, + user_holding_lp: PrivacyPreservingAccount, + balance_lp: u128, + max_amount_a: u128, + max_amount_b: u128, + ) -> Result<(SendTxResponse, [Option; 7]), ExecutionFailureKind> { + let (instruction_data, program) = + amm_program_preparation_remove_liq(balance_lp, max_amount_a, max_amount_b); + + self.0 + .send_privacy_preserving_tx( + vec![ + amm_pool.clone(), + vault_holding_a.clone(), + vault_holding_b.clone(), + pool_lp.clone(), + user_holding_a.clone(), + user_holding_b.clone(), + user_holding_lp.clone(), + ], + &instruction_data, + &program, + ) + .await + .map(|(resp, secrets)| { + let mut secrets = secrets.into_iter(); + let mut secrets_res = [None; 7]; + + for acc_id in [ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ] + .iter() + .enumerate() + { + if acc_id.1.is_private() { + let secret = secrets.next().expect("expected next secret"); + + secrets_res[acc_id.0] = Some(secret); + } + } + + (resp, secrets_res) + }) + } +} + +#[allow(unused)] +fn amm_program_preparation_definition( + balance_a: u128, + balance_b: u128, +) -> (InstructionData, Program) { + // An instruction data of 65-bytes, indicating the initial amm reserves' balances and + // token_program_id with the following layout: + // [0x00 || array of balances (little-endian 16 bytes) || AMM_PROGRAM_ID)] + let amm_program_id = Program::token().id(); + + let mut instruction = [0; 65]; + instruction[1..17].copy_from_slice(&balance_a.to_le_bytes()); + instruction[17..33].copy_from_slice(&balance_b.to_le_bytes()); + + // This can be done less verbose, but it is better to use same way, as in amm program + instruction[33..37].copy_from_slice(&amm_program_id[0].to_le_bytes()); + instruction[37..41].copy_from_slice(&amm_program_id[1].to_le_bytes()); + instruction[41..45].copy_from_slice(&amm_program_id[2].to_le_bytes()); + instruction[45..49].copy_from_slice(&amm_program_id[3].to_le_bytes()); + instruction[49..53].copy_from_slice(&amm_program_id[4].to_le_bytes()); + instruction[53..57].copy_from_slice(&amm_program_id[5].to_le_bytes()); + instruction[57..61].copy_from_slice(&amm_program_id[6].to_le_bytes()); + instruction[61..].copy_from_slice(&amm_program_id[7].to_le_bytes()); + + let instruction_data = + Program::serialize_instruction(OrphanHack65BytesInput(instruction)).unwrap(); + let program = Program::token(); + + (instruction_data, program) +} + +fn amm_program_preparation_swap( + amount_in: u128, + min_amount_out: u128, + token_definition_id: AccountId, +) -> (InstructionData, Program) { + // An instruction data byte string of length 65, indicating which token type to swap, quantity + // of tokens put into the swap (of type TOKEN_DEFINITION_ID) and min_amount_out. + // [0x01 || amount (little-endian 16 bytes) || TOKEN_DEFINITION_ID]. + let mut instruction = [0; 65]; + instruction[1..17].copy_from_slice(&amount_in.to_le_bytes()); + instruction[17..33].copy_from_slice(&min_amount_out.to_le_bytes()); + + // This can be done less verbose, but it is better to use same way, as in amm program + instruction[33..].copy_from_slice(&token_definition_id.to_bytes()); + + let instruction_data = + Program::serialize_instruction(OrphanHack65BytesInput(instruction)).unwrap(); + let program = Program::token(); + + (instruction_data, program) +} + +fn amm_program_preparation_add_liq( + min_amount_lp: u128, + max_amount_a: u128, + max_amount_b: u128, +) -> (InstructionData, Program) { + // An instruction data byte string of length 49, amounts for minimum amount of liquidity from + // add (min_amount_lp), max amount added for each token (max_amount_a and max_amount_b); + // indicate [0x02 || array of of balances (little-endian 16 bytes)]. + let mut instruction = [0; 49]; + instruction[0] = 0x02; + + instruction[1..17].copy_from_slice(&min_amount_lp.to_le_bytes()); + instruction[17..33].copy_from_slice(&max_amount_a.to_le_bytes()); + instruction[33..49].copy_from_slice(&max_amount_b.to_le_bytes()); + + let instruction_data = + Program::serialize_instruction(OrphanHack49BytesInput(instruction)).unwrap(); + let program = Program::token(); + + (instruction_data, program) +} + +fn amm_program_preparation_remove_liq( + balance_lp: u128, + max_amount_a: u128, + max_amount_b: u128, +) -> (InstructionData, Program) { + // An instruction data byte string of length 49, amounts for minimum amount of liquidity to + // redeem (balance_lp), minimum balance of each token to remove (min_amount_a and + // min_amount_b); indicate [0x03 || array of balances (little-endian 16 bytes)]. + let mut instruction = [0; 49]; + instruction[0] = 0x03; + + instruction[1..17].copy_from_slice(&balance_lp.to_le_bytes()); + instruction[17..33].copy_from_slice(&max_amount_a.to_le_bytes()); + instruction[33..49].copy_from_slice(&max_amount_b.to_le_bytes()); + + let instruction_data = + Program::serialize_instruction(OrphanHack49BytesInput(instruction)).unwrap(); + let program = Program::token(); + + (instruction_data, program) +} diff --git a/wallet/src/program_facades/mod.rs b/wallet/src/program_facades/mod.rs index 27d30ce..5fdcdb3 100644 --- a/wallet/src/program_facades/mod.rs +++ b/wallet/src/program_facades/mod.rs @@ -1,6 +1,7 @@ //! This module contains [`WalletCore`](crate::WalletCore) facades for interacting with various //! on-chain programs. +pub mod amm; pub mod native_token_transfer; pub mod pinata; pub mod token;