diff --git a/Cargo.lock b/Cargo.lock index c8719b0..e75a679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2657,6 +2657,8 @@ name = "nssa" version = "0.1.0" dependencies = [ "borsh", + "bytemuck", + "env_logger", "hex", "hex-literal 1.1.0", "log", @@ -4756,6 +4758,7 @@ dependencies = [ "nssa_core", "paste", "rand 0.8.5", + "risc0-zkvm", "serde", "serde_json", "sha2", diff --git a/README.md b/README.md index cb8628d..cc20d22 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ Commands: account Account view and sync subcommand pinata Pinata program interaction subcommand token Token program interaction subcommand + amm AMM program interaction subcommand check-health Check the wallet can connect to the node and builtin local programs match the remote versions ``` @@ -604,13 +605,13 @@ wallet account new public Generated new account with account_id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 ``` -Let's send 10 B tokens to this new account. We'll debit this from the supply account used in the creation of the token. +Let's send 1000 B tokens to this new account. We'll debit this from the supply account used in the creation of the token. ```bash wallet token send \ --from Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF \ --to Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \ - --amount 10 + --amount 1000 ``` Let's inspect the public account: @@ -620,7 +621,7 @@ wallet account get --account-id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6 # Output: Holding account owned by token program -{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":10} +{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":1000} ``` ### Chain information @@ -644,3 +645,107 @@ Last block id is 65537 ``` +### Automated Market Maker (AMM) + +NSSA includes an AMM program that manages liquidity pools and enables swaps between custom tokens. To test this functionality, we first need to create a liquidity pool. + +#### Creating a liquidity pool for a token pair + +We start by creating a new pool for the tokens previously created. In return for providing liquidity, we will receive liquidity provider (LP) tokens, which represent our share of the pool and are required to withdraw liquidity later. + +>[!NOTE] +> The AMM program does not currently charge swap fees or distribute rewards to liquidity providers. LP tokens therefore only represent a proportional share of the pool reserves and do not provide additional value from swap activity. Fee support for liquidity providers will be added in future versions of the AMM program. + +To hold these LP tokens, we first create a new account: + +```bash +wallet account new public + +# Output: +Generated new account with account_id Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf +``` + +Next, we initialize the liquidity pool by depositing tokens A and B and specifying the account that will receive the LP tokens: + +```bash +wallet amm new \ + --user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \ + --user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \ + --user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \ + --balance-a 100 \ + --balance-b 200 +``` + +The newly created account is owned by the token program, meaning that LP tokens are managed by the same token infrastructure as regular tokens. + +```bash +wallet account get --account-id Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf + +# Output: +Holding account owned by token program +{"account_type":"Token holding","definition_id":"7BeDS3e28MA5Err7gBswmR1fUKdHXqmUpTefNPu3pJ9i","balance":100} +``` + +If you inspect the `user-holding-a` and `user-holding-b` accounts passed to the `wallet amm new` command, you will see that 100 and 200 tokens were deducted, respectively. These tokens now reside in the liquidity pool and are available for swaps by any user. + + +#### Swaping + +Token swaps can be performed using the wallet amm swap command: + +```bash +wallet amm swap \ + --user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \ + --user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \ + # The amount of tokens to swap + --amount-in 5 \ + # The minimum number of tokens expected in return + --min-amount-out 8 \ + # The definition ID of the token being provided to the swap + # In this case, we are swapping from TOKENA to TOKENB, and so this is the definition ID of TOKENA + --token-definition 4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 +``` + +Once executed, 5 tokens are deducted from the Token A holding account and the corresponding amount (determined by the pool’s pricing function) is credited to the Token B holding account. + + +#### Withdrawing liquidity from the pool + +Liquidity providers can withdraw assets from the pool by redeeming (burning) LP tokens. The amount of tokens received is proportional to the share of LP tokens being redeemed relative to the total LP supply. + +This operation is performed using the `wallet amm remove-liquidity` command: + +```bash +wallet amm remove-liquidity \ + --user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \ + --user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \ + --user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \ + --balance-lp 20 \ + --min-amount-a 1 \ + --min-amount-b 1 +``` + +This instruction burns `balance-lp` LP tokens from the user’s LP holding account. In exchange, the AMM transfers tokens A and B from the pool’s vault accounts to the user’s holding accounts, according to the current pool reserves. + +The `min-amount-a` and `min-amount-b` parameters specify the minimum acceptable amounts of tokens A and B to be received. If the computed outputs fall below either threshold, the instruction fails, protecting the user against unfavorable pool state changes. + +#### Adding liquidity to the pool + +Additional liquidity can be added to an existing pool by depositing tokens A and B in the ratio implied by the current pool reserves. In return, new LP tokens are minted to represent the user’s proportional share of the pool. + +This is done using the `wallet amm add-liquidity` command: + +```bash +wallet amm add-liquidity \ + --user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \ + --user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \ + --user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \ + --min-amount-lp 1 \ + --max-amount-a 10 \ + --max-amount-b 10 +``` + +In this instruction, `max-amount-a` and `max-amount-b` define upper bounds on the number of tokens A and B that may be withdrawn from the user’s accounts. The AMM computes the actual required amounts based on the pool’s reserve ratio. + +The `min-amount-lp` parameter specifies the minimum number of LP tokens that must be minted for the transaction to succeed. If the resulting LP token amount is below this threshold, the instruction fails. + diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin new file mode 100644 index 0000000..354e655 Binary files /dev/null and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 347342c..1b72c46 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 2c1364f..91d2b93 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 5c02b2f..8da8b89 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index c7e9483..404f32b 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 151240c..2269341 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 0c0ed65..6bbc507 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 5d20ca1..56b017e 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index ae53240..0f42aae 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 52025c8..99089d3 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 46a13eb..ffb622f 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index b026dd0..08a57ea 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index ba5d6a4..891acd5 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index f85b7ba..1779383 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 2cde3f6..9dcb36a 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index ebcc2be..fba8457 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index fce9231..6daa6a1 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index c9788ab..4fa42b2 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/common/src/error.rs b/common/src/error.rs index e1634a6..5c81a10 100644 --- a/common/src/error.rs +++ b/common/src/error.rs @@ -1,3 +1,4 @@ +use nssa::AccountId; use serde::Deserialize; use crate::rpc_primitives::errors::RpcError; @@ -49,4 +50,6 @@ pub enum ExecutionFailureKind { SequencerClientError(#[from] SequencerClientError), #[error("Can not pay for operation")] InsufficientFundsError, + #[error("Account {0} data is invalid")] + AccountDataError(AccountId), } diff --git a/integration_tests/src/test_suite_map.rs b/integration_tests/src/test_suite_map.rs index f7dc44a..eec9bdf 100644 --- a/integration_tests/src/test_suite_map.rs +++ b/integration_tests/src/test_suite_map.rs @@ -24,8 +24,9 @@ use wallet::{ config::ConfigSubcommand, programs::{ ArgsDefinitionOwned, ArgsHolderMaybeUnowned, ArgsHolderOwned, ArgsReceiverMaybeUnowned, - ArgsSenderOwned, ArgsSupplyOwned, native_token_transfer::AuthTransferSubcommand, - pinata::PinataProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand, + ArgsSenderOwned, ArgsSupplyOwned, amm::AmmProgramAgnosticSubcommand, + native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, + token::TokenProgramAgnosticSubcommand, }, }, config::PersistentStorage, @@ -374,11 +375,14 @@ pub fn prepare_function_map() -> HashMap { 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) ] + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 + // 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 + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -497,7 +501,9 @@ pub fn prepare_function_map() -> HashMap { 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 + 0, 65, 32, 78, 65, 77, 69, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -547,7 +553,9 @@ pub fn prepare_function_map() -> HashMap { 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 + 0, 65, 32, 78, 65, 77, 69, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -641,11 +649,14 @@ pub fn prepare_function_map() -> HashMap { 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) ] + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 + // 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 + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -765,7 +776,9 @@ pub fn prepare_function_map() -> HashMap { 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 + 0, 65, 32, 78, 65, 77, 69, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -823,7 +836,9 @@ pub fn prepare_function_map() -> HashMap { 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 + 0, 65, 32, 78, 65, 77, 69, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -911,7 +926,9 @@ pub fn prepare_function_map() -> HashMap { assert_eq!( definition_acc.data.as_ref(), &[ - 0, 65, 32, 78, 65, 77, 69, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 65, 32, 78, 65, 77, 69, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1014,8 +1031,8 @@ pub fn prepare_function_map() -> HashMap { .account; assert_eq!(supply_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) ] + // The data of a token holding account has the following layout: + // [ 0x01 || definition id (32 bytes) || balance (little endian 16 bytes) ] assert_eq!( supply_acc.data.as_ref(), &[ @@ -1085,7 +1102,9 @@ pub fn prepare_function_map() -> HashMap { assert_eq!( definition_acc.data.as_ref(), &[ - 0, 65, 32, 78, 65, 77, 69, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 65, 32, 78, 65, 77, 69, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1149,7 +1168,9 @@ pub fn prepare_function_map() -> HashMap { assert_eq!( definition_acc.data.as_ref(), &[ - 0, 65, 32, 78, 65, 77, 69, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 65, 32, 78, 65, 77, 69, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1205,7 +1226,9 @@ pub fn prepare_function_map() -> HashMap { assert_eq!( definition_acc.data.as_ref(), &[ - 0, 65, 32, 78, 65, 77, 69, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 65, 32, 78, 65, 77, 69, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1259,7 +1282,9 @@ pub fn prepare_function_map() -> HashMap { assert_eq!( definition_acc.data.as_ref(), &[ - 0, 65, 32, 78, 65, 77, 69, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 65, 32, 78, 65, 77, 69, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1310,7 +1335,9 @@ pub fn prepare_function_map() -> HashMap { assert_eq!( definition_acc.data.as_ref(), &[ - 0, 65, 32, 78, 65, 77, 69, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 65, 32, 78, 65, 77, 69, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1416,17 +1443,20 @@ pub fn prepare_function_map() -> HashMap { 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) ] + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 + // 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 + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); assert_eq!(supply_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) ] + // The data of a token holding account has the following layout: + // [ 0x01 || definition id (32 bytes) || balance (little endian 16 bytes) ] assert_eq!( supply_acc.data.as_ref(), &[ @@ -1512,11 +1542,14 @@ pub fn prepare_function_map() -> HashMap { 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) ] + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 + // 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 + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1658,11 +1691,14 @@ pub fn prepare_function_map() -> HashMap { 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) ] + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 + // 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 + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -1808,11 +1844,14 @@ pub fn prepare_function_map() -> HashMap { 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) ] + // [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) || metadata id (32 + // 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 + 0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 ] ); @@ -2829,6 +2868,438 @@ pub fn prepare_function_map() -> HashMap { info!("Success!"); } + #[nssa_integration_test] + pub async fn test_amm_public() { + info!("########## test_amm_public ##########"); + let wallet_config = fetch_config().await.unwrap(); + + // Create new account for the token definition + let SubcommandReturnValue::RegisterAccount { + account_id: definition_account_id_1, + } = 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_1, + } = 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_1, + } = 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 definition + let SubcommandReturnValue::RegisterAccount { + account_id: definition_account_id_2, + } = 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_2, + } = 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_2, + } = 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: ArgsDefinitionOwned { + definition_account_id: make_public_account_input_from_str( + &definition_account_id_1.to_string(), + ), + }, + supply: ArgsSupplyOwned { + supply_account_id: make_public_account_input_from_str( + &supply_account_id_1.to_string(), + ), + }, + name: "A NAM1".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; + + // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1` + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: ArgsSenderOwned { + from: make_public_account_input_from_str(&supply_account_id_1.to_string()), + }, + to: ArgsReceiverMaybeUnowned { + to: Some(make_public_account_input_from_str( + &recipient_account_id_1.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; + + // Create new token + let subcommand = TokenProgramAgnosticSubcommand::New { + definition: ArgsDefinitionOwned { + definition_account_id: make_public_account_input_from_str( + &definition_account_id_2.to_string(), + ), + }, + supply: ArgsSupplyOwned { + supply_account_id: make_public_account_input_from_str( + &supply_account_id_2.to_string(), + ), + }, + name: "A NAM2".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(); + + // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1` + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: ArgsSenderOwned { + from: make_public_account_input_from_str(&supply_account_id_2.to_string()), + }, + to: ArgsReceiverMaybeUnowned { + to: Some(make_public_account_input_from_str( + &recipient_account_id_2.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; + + info!("=================== SETUP FINISHED ==============="); + + // Create new AMM + + // Setup accounts + // Create new account for the user holding lp + let SubcommandReturnValue::RegisterAccount { + account_id: user_holding_lp, + } = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New( + NewSubcommand::Public { cci: None }, + ))) + .await + .unwrap() + else { + panic!("invalid subcommand return value"); + }; + + // Send creation tx + let subcommand = AmmProgramAgnosticSubcommand::New { + user_holding_a: make_public_account_input_from_str(&recipient_account_id_1.to_string()), + user_holding_b: make_public_account_input_from_str(&recipient_account_id_2.to_string()), + user_holding_lp: make_public_account_input_from_str(&user_holding_lp.to_string()), + balance_a: 3, + balance_b: 3, + }; + + wallet::cli::execute_subcommand(Command::AMM(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let user_holding_a_acc = seq_client + .get_account(recipient_account_id_1.to_string()) + .await + .unwrap() + .account; + + let user_holding_b_acc = seq_client + .get_account(recipient_account_id_2.to_string()) + .await + .unwrap() + .account; + + let user_holding_lp_acc = seq_client + .get_account(user_holding_lp.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), + 4 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_b_acc.data[33..].try_into().unwrap()), + 4 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_lp_acc.data[33..].try_into().unwrap()), + 3 + ); + + info!("=================== AMM DEFINITION FINISHED ==============="); + + // Make swap + + let subcommand = AmmProgramAgnosticSubcommand::Swap { + user_holding_a: make_public_account_input_from_str(&recipient_account_id_1.to_string()), + user_holding_b: make_public_account_input_from_str(&recipient_account_id_2.to_string()), + amount_in: 2, + min_amount_out: 1, + token_definition: definition_account_id_1.to_string(), + }; + + wallet::cli::execute_subcommand(Command::AMM(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let user_holding_a_acc = seq_client + .get_account(recipient_account_id_1.to_string()) + .await + .unwrap() + .account; + + let user_holding_b_acc = seq_client + .get_account(recipient_account_id_2.to_string()) + .await + .unwrap() + .account; + + let user_holding_lp_acc = seq_client + .get_account(user_holding_lp.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), + 2 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_b_acc.data[33..].try_into().unwrap()), + 5 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_lp_acc.data[33..].try_into().unwrap()), + 3 + ); + + info!("=================== FIRST SWAP FINISHED ==============="); + + // Make swap + + let subcommand = AmmProgramAgnosticSubcommand::Swap { + user_holding_a: make_public_account_input_from_str(&recipient_account_id_1.to_string()), + user_holding_b: make_public_account_input_from_str(&recipient_account_id_2.to_string()), + amount_in: 2, + min_amount_out: 1, + token_definition: definition_account_id_2.to_string(), + }; + + wallet::cli::execute_subcommand(Command::AMM(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let user_holding_a_acc = seq_client + .get_account(recipient_account_id_1.to_string()) + .await + .unwrap() + .account; + + let user_holding_b_acc = seq_client + .get_account(recipient_account_id_2.to_string()) + .await + .unwrap() + .account; + + let user_holding_lp_acc = seq_client + .get_account(user_holding_lp.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), + 4 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_b_acc.data[33..].try_into().unwrap()), + 3 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_lp_acc.data[33..].try_into().unwrap()), + 3 + ); + + info!("=================== SECOND SWAP FINISHED ==============="); + + // Add liquidity + + let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity { + user_holding_a: make_public_account_input_from_str(&recipient_account_id_1.to_string()), + user_holding_b: make_public_account_input_from_str(&recipient_account_id_2.to_string()), + user_holding_lp: make_public_account_input_from_str(&user_holding_lp.to_string()), + min_amount_lp: 1, + max_amount_a: 2, + max_amount_b: 2, + }; + + wallet::cli::execute_subcommand(Command::AMM(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let user_holding_a_acc = seq_client + .get_account(recipient_account_id_1.to_string()) + .await + .unwrap() + .account; + + let user_holding_b_acc = seq_client + .get_account(recipient_account_id_2.to_string()) + .await + .unwrap() + .account; + + let user_holding_lp_acc = seq_client + .get_account(user_holding_lp.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), + 3 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_b_acc.data[33..].try_into().unwrap()), + 1 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_lp_acc.data[33..].try_into().unwrap()), + 4 + ); + + info!("=================== ADD LIQ FINISHED ==============="); + + // Remove liquidity + + let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity { + user_holding_a: make_public_account_input_from_str(&recipient_account_id_1.to_string()), + user_holding_b: make_public_account_input_from_str(&recipient_account_id_2.to_string()), + user_holding_lp: make_public_account_input_from_str(&user_holding_lp.to_string()), + balance_lp: 2, + min_amount_a: 1, + min_amount_b: 1, + }; + + wallet::cli::execute_subcommand(Command::AMM(subcommand)) + .await + .unwrap(); + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let user_holding_a_acc = seq_client + .get_account(recipient_account_id_1.to_string()) + .await + .unwrap() + .account; + + let user_holding_b_acc = seq_client + .get_account(recipient_account_id_2.to_string()) + .await + .unwrap() + .account; + + let user_holding_lp_acc = seq_client + .get_account(user_holding_lp.to_string()) + .await + .unwrap() + .account; + + assert_eq!( + u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), + 5 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_b_acc.data[33..].try_into().unwrap()), + 4 + ); + + assert_eq!( + u128::from_le_bytes(user_holding_lp_acc.data[33..].try_into().unwrap()), + 2 + ); + + info!("Success!"); + } + println!("{function_map:#?}"); function_map diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index 4b13c70..f1c7709 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -15,6 +15,7 @@ borsh.workspace = true hex.workspace = true secp256k1 = "0.31.1" risc0-binfmt = "3.0.2" +bytemuck = "1.24.0" log.workspace = true [build-dependencies] @@ -24,6 +25,7 @@ risc0-binfmt = "3.0.2" [dev-dependencies] test_program_methods.workspace = true hex-literal = "1.0.0" +env_logger.workspace = true [features] default = [] diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index f317d34..473cde9 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -6,17 +6,18 @@ edition = "2024" [dependencies] risc0-zkvm.workspace = true borsh.workspace = true -serde = { workspace = true } +serde.workspace = true thiserror.workspace = true -chacha20 = { version = "0.9", default-features = false } -bytemuck = { workspace = true, optional = true } +bytemuck.workspace = true k256 = { workspace = true, optional = true } base58 = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } +chacha20 = { version = "0.9", default-features = false } + [dev-dependencies] serde_json.workspace = true [features] default = [] -host = ["dep:bytemuck", "dep:k256", "dep:base58", "dep:anyhow"] +host = ["dep:k256", "dep:base58", "dep:anyhow"] diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index bd18c80..51467af 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -15,7 +15,7 @@ pub type Nonce = u128; /// Account to be used both in public and private contexts #[derive( - Serialize, Deserialize, Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, + Clone, Default, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, )] #[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct Account { @@ -25,7 +25,7 @@ pub struct Account { pub nonce: Nonce, } -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct AccountWithMetadata { pub account: Account, @@ -45,9 +45,18 @@ impl AccountWithMetadata { } #[derive( - Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize, + Default, + Copy, + Clone, + Serialize, + Deserialize, + PartialEq, + Eq, + Hash, + BorshSerialize, + BorshDeserialize, )] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialOrd, Ord, Default))] +#[cfg_attr(any(feature = "host", test), derive(Debug, PartialOrd, Ord))] pub struct AccountId { value: [u8; 32], } @@ -180,4 +189,11 @@ mod tests { let result = base58_str.parse::().unwrap_err(); assert!(matches!(result, AccountIdError::InvalidLength(_))); } + + #[test] + fn default_account_id() { + let default_account_id = AccountId::default(); + let expected_account_id = AccountId::new([0; 32]); + assert!(default_account_id == expected_account_id); + } } 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/core/src/program.rs b/nssa/core/src/program.rs index 51ac487..357a4a5 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -3,9 +3,7 @@ use std::collections::HashSet; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "host")] -use crate::account::AccountId; -use crate::account::{Account, AccountWithMetadata}; +use crate::account::{Account, AccountId, AccountWithMetadata}; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; @@ -22,8 +20,8 @@ pub struct ProgramInput { /// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct PdaSeed([u8; 32]); impl PdaSeed { @@ -32,7 +30,6 @@ impl PdaSeed { } } -#[cfg(feature = "host")] impl From<(&ProgramId, &PdaSeed)> for AccountId { fn from(value: (&ProgramId, &PdaSeed)) -> Self { use risc0_zkvm::sha::{Impl, Sha256}; @@ -54,8 +51,8 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId { } } -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Debug,))] pub struct ChainedCall { /// The program ID of the program to execute pub program_id: ProgramId, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index c698b1b..f29eb1c 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -219,7 +219,7 @@ mod tests { &Program::serialize_instruction(balance_to_move).unwrap(), &[0, 2], &[0xdeadbeef], - &[(recipient_keys.npk(), shared_secret.clone())], + &[(recipient_keys.npk(), shared_secret)], &[], &[None], &Program::authenticated_transfer_program().into(), @@ -316,8 +316,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], &[commitment_set.get_proof_for(&commitment_sender), None], diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 335bcb5..69cb02c 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -7,7 +7,7 @@ use serde::Serialize; use crate::{ error::NssaError, - program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, + program_methods::{AMM_ELF, AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, }; /// Maximum number of cycles for a public execution. @@ -95,6 +95,10 @@ impl Program { // `program_methods` Self::new(TOKEN_ELF.to_vec()).unwrap() } + + pub fn amm() -> Self { + Self::new(AMM_ELF.to_vec()).expect("The AMM program must be a valid Risc0 program") + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. diff --git a/nssa/src/public_transaction/message.rs b/nssa/src/public_transaction/message.rs index 63ed03f..d8bd2da 100644 --- a/nssa/src/public_transaction/message.rs +++ b/nssa/src/public_transaction/message.rs @@ -23,6 +23,7 @@ impl Message { instruction: T, ) -> Result { let instruction_data = Program::serialize_instruction(instruction)?; + Ok(Self { program_id, account_ids, @@ -30,4 +31,18 @@ impl Message { instruction_data, }) } + + pub fn new_preserialized( + program_id: ProgramId, + account_ids: Vec, + nonces: Vec, + instruction_data: InstructionData, + ) -> Self { + Self { + program_id, + account_ids, + nonces, + instruction_data, + } + } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index d5c138d..bc0ff62 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -99,6 +99,7 @@ impl V02State { this.insert_program(Program::authenticated_transfer_program()); this.insert_program(Program::token()); + this.insert_program(Program::amm()); this } @@ -340,6 +341,7 @@ pub mod tests { authenticated_transfers_program, ); this.insert(Program::token().id(), Program::token()); + this.insert(Program::amm().id(), Program::amm()); this }; @@ -500,6 +502,7 @@ pub mod tests { self.insert_program(Program::minter()); self.insert_program(Program::burner()); self.insert_program(Program::chain_caller()); + self.insert_program(Program::amm()); self.insert_program(Program::claimer()); self } @@ -2151,7 +2154,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_nsks, @@ -2280,6 +2283,1566 @@ pub mod tests { )); } + // TODO: repeated code needs to be cleaned up + // from token.rs (also repeated in amm.rs) + const TOKEN_DEFINITION_DATA_SIZE: usize = 55; + + const TOKEN_HOLDING_DATA_SIZE: usize = 49; + + struct TokenDefinition { + account_type: u8, + name: [u8; 6], + total_supply: u128, + metadata_id: AccountId, + } + + struct TokenHolding { + account_type: u8, + definition_id: AccountId, + balance: u128, + } + impl TokenDefinition { + fn into_data(self) -> Data { + let mut bytes = Vec::::new(); + bytes.extend_from_slice(&[self.account_type]); + bytes.extend_from_slice(&self.name); + bytes.extend_from_slice(&self.total_supply.to_le_bytes()); + bytes.extend_from_slice(&self.metadata_id.to_bytes()); + + if bytes.len() != TOKEN_DEFINITION_DATA_SIZE { + panic!("Invalid Token Definition data"); + } + + Data::try_from(bytes).expect("Token definition data size must fit into data") + } + } + + impl TokenHolding { + fn into_data(self) -> Data { + let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; + bytes[0] = self.account_type; + bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); + bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); + bytes + .to_vec() + .try_into() + .expect("33 bytes should fit into Data") + } + } + + // TODO repeated code should ultimately be removed; + fn compute_pool_pda( + amm_program_id: ProgramId, + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, + ) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), + )) + } + + fn compute_pool_pda_seed( + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, + ) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + 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; + 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; + let token_2 = definition_token_a_id; + break (token_1, token_2); + } + + if i == 32 { + panic!("Definitions match"); + } else { + i += 1; + } + }; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&token_1.to_bytes()); + bytes[32..].copy_from_slice(&token_2.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) + } + + fn compute_vault_pda( + amm_program_id: ProgramId, + pool_id: AccountId, + definition_token_id: AccountId, + ) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_vault_pda_seed(pool_id, definition_token_id), + )) + } + + fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) + } + + fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { + AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) + } + + fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&[0; 32]); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) + } + + const POOL_DEFINITION_DATA_SIZE: usize = 225; + + #[derive(Default)] + struct PoolDefinition { + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, + vault_a_id: AccountId, + vault_b_id: AccountId, + liquidity_pool_id: AccountId, + liquidity_pool_supply: u128, + reserve_a: u128, + reserve_b: u128, + fees: u128, + active: bool, + } + + impl PoolDefinition { + fn into_data(self) -> Data { + let mut bytes = [0; POOL_DEFINITION_DATA_SIZE]; + bytes[0..32].copy_from_slice(&self.definition_token_a_id.to_bytes()); + bytes[32..64].copy_from_slice(&self.definition_token_b_id.to_bytes()); + bytes[64..96].copy_from_slice(&self.vault_a_id.to_bytes()); + bytes[96..128].copy_from_slice(&self.vault_b_id.to_bytes()); + bytes[128..160].copy_from_slice(&self.liquidity_pool_id.to_bytes()); + bytes[160..176].copy_from_slice(&self.liquidity_pool_supply.to_le_bytes()); + bytes[176..192].copy_from_slice(&self.reserve_a.to_le_bytes()); + bytes[192..208].copy_from_slice(&self.reserve_b.to_le_bytes()); + bytes[208..224].copy_from_slice(&self.fees.to_le_bytes()); + bytes[224] = self.active as u8; + + bytes + .to_vec() + .try_into() + .expect("225 bytes should fit into Data") + } + } + + struct PrivateKeysForTests; + + impl PrivateKeysForTests { + fn user_token_a_key() -> PrivateKey { + PrivateKey::try_new([31; 32]).expect("Keys constructor expects valid private key") + } + + fn user_token_b_key() -> PrivateKey { + PrivateKey::try_new([32; 32]).expect("Keys constructor expects valid private key") + } + + fn user_token_lp_key() -> PrivateKey { + PrivateKey::try_new([33; 32]).expect("Keys constructor expects valid private key") + } + } + + struct BalanceForTests; + + impl BalanceForTests { + fn user_token_a_holding_init() -> u128 { + 10_000 + } + + fn user_token_b_holding_init() -> u128 { + 10_000 + } + + fn user_token_lp_holding_init() -> u128 { + 2_000 + } + + fn vault_a_balance_init() -> u128 { + 5_000 + } + + fn vault_b_balance_init() -> u128 { + 2_500 + } + + fn pool_lp_supply_init() -> u128 { + 5_000 + } + + fn token_a_supply() -> u128 { + 100_000 + } + + fn token_b_supply() -> u128 { + 100_000 + } + + fn token_lp_supply() -> u128 { + 5_000 + } + + fn remove_lp() -> u128 { + 1_000 + } + + fn remove_min_amount_a() -> u128 { + 500 + } + + fn remove_min_amount_b() -> u128 { + 500 + } + + fn add_min_amount_lp() -> u128 { + 1_000 + } + + fn add_max_amount_a() -> u128 { + 2_000 + } + + fn add_max_amount_b() -> u128 { + 1_000 + } + + fn swap_amount_in() -> u128 { + 1_000 + } + + fn swap_min_amount_out() -> u128 { + 200 + } + + fn vault_a_balance_swap_1() -> u128 { + 3_572 + } + + fn vault_b_balance_swap_1() -> u128 { + 3_500 + } + + fn user_token_a_holding_swap_1() -> u128 { + 11_428 + } + + fn user_token_b_holding_swap_1() -> u128 { + 9_000 + } + + fn vault_a_balance_swap_2() -> u128 { + 6_000 + } + + fn vault_b_balance_swap_2() -> u128 { + 2_084 + } + + fn user_token_a_holding_swap_2() -> u128 { + 9_000 + } + + fn user_token_b_holding_swap_2() -> u128 { + 10_416 + } + + fn vault_a_balance_add() -> u128 { + 7_000 + } + + fn vault_b_balance_add() -> u128 { + 3_500 + } + + fn user_token_a_holding_add() -> u128 { + 8_000 + } + + fn user_token_b_holding_add() -> u128 { + 9_000 + } + + fn user_token_lp_holding_add() -> u128 { + 4_000 + } + + fn token_lp_supply_add() -> u128 { + 7_000 + } + + fn vault_a_balance_remove() -> u128 { + 4_000 + } + + fn vault_b_balance_remove() -> u128 { + 2_000 + } + + fn user_token_a_holding_remove() -> u128 { + 11_000 + } + + fn user_token_b_holding_remove() -> u128 { + 10_500 + } + + fn user_token_lp_holding_remove() -> u128 { + 1_000 + } + + fn token_lp_supply_remove() -> u128 { + 4_000 + } + + fn user_token_a_holding_new_definition() -> u128 { + 5_000 + } + + fn user_token_b_holding_new_definition() -> u128 { + 7_500 + } + } + + struct IdForTests; + + impl IdForTests { + fn pool_definition_id() -> AccountId { + compute_pool_pda( + Program::amm().id(), + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id(), + ) + } + + fn token_lp_definition_id() -> AccountId { + compute_liquidity_token_pda(Program::amm().id(), IdForTests::pool_definition_id()) + } + + fn token_a_definition_id() -> AccountId { + AccountId::new([3; 32]) + } + + fn token_b_definition_id() -> AccountId { + AccountId::new([4; 32]) + } + + fn user_token_a_id() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key( + &PrivateKeysForTests::user_token_a_key(), + )) + } + + fn user_token_b_id() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key( + &PrivateKeysForTests::user_token_b_key(), + )) + } + + fn user_token_lp_id() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key( + &PrivateKeysForTests::user_token_lp_key(), + )) + } + + fn vault_a_id() -> AccountId { + compute_vault_pda( + Program::amm().id(), + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + ) + } + + fn vault_b_id() -> AccountId { + compute_vault_pda( + Program::amm().id(), + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + ) + } + } + + struct AccountForTests; + + impl AccountForTests { + fn user_token_a_holding() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_holding_init(), + }), + nonce: 0, + } + } + + fn user_token_b_holding() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_holding_init(), + }), + nonce: 0, + } + } + + fn pool_definition_init() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::pool_lp_supply_init(), + reserve_a: BalanceForTests::vault_a_balance_init(), + reserve_b: BalanceForTests::vault_b_balance_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + } + } + + fn token_a_definition_account() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: BalanceForTests::token_a_supply(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn token_b_definition_acc() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: BalanceForTests::token_b_supply(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn token_lp_definition_acc() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: BalanceForTests::token_lp_supply(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn vault_a_init() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_balance_init(), + }), + nonce: 0, + } + } + + fn vault_b_init() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_balance_init(), + }), + nonce: 0, + } + } + + fn user_token_lp_holding() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: BalanceForTests::user_token_lp_holding_init(), + }), + nonce: 0, + } + } + + fn vault_a_swap_1() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_balance_swap_1(), + }), + nonce: 0, + } + } + + fn vault_b_swap_1() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_balance_swap_1(), + }), + nonce: 0, + } + } + + fn pool_definition_swap_1() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::pool_lp_supply_init(), + reserve_a: BalanceForTests::vault_a_balance_swap_1(), + reserve_b: BalanceForTests::vault_b_balance_swap_1(), + fees: 0u128, + active: true, + }), + nonce: 0, + } + } + + fn user_token_a_holding_swap_1() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_holding_swap_1(), + }), + nonce: 0, + } + } + + fn user_token_b_holding_swap_1() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_holding_swap_1(), + }), + nonce: 1, + } + } + + fn vault_a_swap_2() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_balance_swap_2(), + }), + nonce: 0, + } + } + + fn vault_b_swap_2() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_balance_swap_2(), + }), + nonce: 0, + } + } + + fn pool_definition_swap_2() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::pool_lp_supply_init(), + reserve_a: BalanceForTests::vault_a_balance_swap_2(), + reserve_b: BalanceForTests::vault_b_balance_swap_2(), + fees: 0u128, + active: true, + }), + nonce: 0, + } + } + + fn user_token_a_holding_swap_2() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_holding_swap_2(), + }), + nonce: 1, + } + } + + fn user_token_b_holding_swap_2() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_holding_swap_2(), + }), + nonce: 0, + } + } + + fn vault_a_add() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_balance_add(), + }), + nonce: 0, + } + } + + fn vault_b_add() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_balance_add(), + }), + nonce: 0, + } + } + + fn pool_definition_add() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::token_lp_supply_add(), + reserve_a: BalanceForTests::vault_a_balance_add(), + reserve_b: BalanceForTests::vault_b_balance_add(), + fees: 0u128, + active: true, + }), + nonce: 0, + } + } + + fn user_token_a_holding_add() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_holding_add(), + }), + nonce: 1, + } + } + + fn user_token_b_holding_add() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_holding_add(), + }), + nonce: 1, + } + } + + fn user_token_lp_holding_add() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: BalanceForTests::user_token_lp_holding_add(), + }), + nonce: 0, + } + } + + fn token_lp_definition_add() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: BalanceForTests::token_lp_supply_add(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn vault_a_remove() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_balance_remove(), + }), + nonce: 0, + } + } + + fn vault_b_remove() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_balance_remove(), + }), + nonce: 0, + } + } + + fn pool_definition_remove() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::token_lp_supply_remove(), + reserve_a: BalanceForTests::vault_a_balance_remove(), + reserve_b: BalanceForTests::vault_b_balance_remove(), + fees: 0u128, + active: true, + }), + nonce: 0, + } + } + + fn user_token_a_holding_remove() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_holding_remove(), + }), + nonce: 0, + } + } + + fn user_token_b_holding_remove() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_holding_remove(), + }), + nonce: 0, + } + } + + fn user_token_lp_holding_remove() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: BalanceForTests::user_token_lp_holding_remove(), + }), + nonce: 1, + } + } + + fn token_lp_definition_remove() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: BalanceForTests::token_lp_supply_remove(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn token_lp_definition_init_inactive() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: 0, + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn vault_a_init_inactive() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: 0, + }), + nonce: 0, + } + } + + fn vault_b_init_inactive() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: 0, + }), + nonce: 0, + } + } + + fn pool_definition_inactive() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: 0, + reserve_a: 0, + reserve_b: 0, + fees: 0u128, + active: false, + }), + nonce: 0, + } + } + + fn user_token_a_holding_new_init() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_holding_new_definition(), + }), + nonce: 1, + } + } + + fn user_token_b_holding_new_init() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_holding_new_definition(), + }), + nonce: 1, + } + } + + fn user_token_lp_holding_new_init() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: BalanceForTests::user_token_a_holding_new_definition(), + }), + nonce: 0, + } + } + + fn token_lp_definition_new_init() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1u8; 6], + total_supply: BalanceForTests::vault_a_balance_init(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + } + } + + fn pool_definition_new_init() -> Account { + Account { + program_owner: Program::amm().id(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::user_token_a_holding_new_definition(), + reserve_a: BalanceForTests::vault_a_balance_init(), + reserve_b: BalanceForTests::vault_b_balance_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + } + } + + fn user_token_lp_holding_init_zero() -> Account { + Account { + program_owner: Program::token().id(), + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: 0, + }), + nonce: 0, + } + } + } + + const AMM_NEW_DEFINITION: u8 = 0; + const AMM_SWAP: u8 = 1; + const AMM_ADD_LIQUIDITY: u8 = 2; + const AMM_REMOVE_LIQUIDITY: u8 = 3; + + fn state_for_amm_tests() -> V02State { + let initial_data = []; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + state.force_insert_account( + IdForTests::pool_definition_id(), + AccountForTests::pool_definition_init(), + ); + state.force_insert_account( + IdForTests::token_a_definition_id(), + AccountForTests::token_a_definition_account(), + ); + state.force_insert_account( + IdForTests::token_b_definition_id(), + AccountForTests::token_b_definition_acc(), + ); + state.force_insert_account( + IdForTests::token_lp_definition_id(), + AccountForTests::token_lp_definition_acc(), + ); + state.force_insert_account( + IdForTests::user_token_a_id(), + AccountForTests::user_token_a_holding(), + ); + state.force_insert_account( + IdForTests::user_token_b_id(), + AccountForTests::user_token_b_holding(), + ); + state.force_insert_account( + IdForTests::user_token_lp_id(), + AccountForTests::user_token_lp_holding(), + ); + state.force_insert_account(IdForTests::vault_a_id(), AccountForTests::vault_a_init()); + state.force_insert_account(IdForTests::vault_b_id(), AccountForTests::vault_b_init()); + + state + } + + fn state_for_amm_tests_with_new_def() -> V02State { + let initial_data = []; + let mut state = + V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + state.force_insert_account( + IdForTests::token_a_definition_id(), + AccountForTests::token_a_definition_account(), + ); + state.force_insert_account( + IdForTests::token_b_definition_id(), + AccountForTests::token_b_definition_acc(), + ); + state.force_insert_account( + IdForTests::user_token_a_id(), + AccountForTests::user_token_a_holding(), + ); + state.force_insert_account( + IdForTests::user_token_b_id(), + AccountForTests::user_token_b_holding(), + ); + state + } + + #[test] + fn test_simple_amm_remove() { + let mut state = state_for_amm_tests(); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_REMOVE_LIQUIDITY); + instruction.extend_from_slice(&BalanceForTests::remove_lp().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::remove_min_amount_a().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::remove_min_amount_b().to_le_bytes()); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::token_lp_definition_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + IdForTests::user_token_lp_id(), + ], + vec![0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&PrivateKeysForTests::user_token_lp_key()], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let token_lp_post = state.get_account_by_id(&IdForTests::token_lp_definition_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + let user_token_lp_post = state.get_account_by_id(&IdForTests::user_token_lp_id()); + + let expected_pool = AccountForTests::pool_definition_remove(); + let expected_vault_a = AccountForTests::vault_a_remove(); + let expected_vault_b = AccountForTests::vault_b_remove(); + let expected_token_lp = AccountForTests::token_lp_definition_remove(); + let expected_user_token_a = AccountForTests::user_token_a_holding_remove(); + let expected_user_token_b = AccountForTests::user_token_b_holding_remove(); + let expected_user_token_lp = AccountForTests::user_token_lp_holding_remove(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(token_lp_post, expected_token_lp); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + assert_eq!(user_token_lp_post, expected_user_token_lp); + } + + #[test] + fn test_simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { + let mut state = state_for_amm_tests_with_new_def(); + + // Uninitialized in constructor + state.force_insert_account( + IdForTests::vault_a_id(), + AccountForTests::vault_a_init_inactive(), + ); + state.force_insert_account( + IdForTests::vault_b_id(), + AccountForTests::vault_b_init_inactive(), + ); + state.force_insert_account( + IdForTests::pool_definition_id(), + AccountForTests::pool_definition_inactive(), + ); + state.force_insert_account( + IdForTests::token_lp_definition_id(), + AccountForTests::token_lp_definition_init_inactive(), + ); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_NEW_DEFINITION); + instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes()); + let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id()); + instruction.extend_from_slice(&amm_program_u8); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::token_lp_definition_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + IdForTests::user_token_lp_id(), + ], + vec![0, 0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[ + &PrivateKeysForTests::user_token_a_key(), + &PrivateKeysForTests::user_token_b_key(), + ], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let token_lp_post = state.get_account_by_id(&IdForTests::token_lp_definition_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + let user_token_lp_post = state.get_account_by_id(&IdForTests::user_token_lp_id()); + + let expected_pool = AccountForTests::pool_definition_new_init(); + let expected_vault_a = AccountForTests::vault_a_init(); + let expected_vault_b = AccountForTests::vault_b_init(); + let expected_token_lp = AccountForTests::token_lp_definition_new_init(); + let expected_user_token_a = AccountForTests::user_token_a_holding_new_init(); + let expected_user_token_b = AccountForTests::user_token_b_holding_new_init(); + let expected_user_token_lp = AccountForTests::user_token_lp_holding_new_init(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(token_lp_post, expected_token_lp); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + assert_eq!(user_token_lp_post, expected_user_token_lp); + } + + #[test] + fn test_simple_amm_new_definition_inactive_initialized_pool_init_user_lp() { + let mut state = state_for_amm_tests_with_new_def(); + + // Uninitialized in constructor + state.force_insert_account( + IdForTests::vault_a_id(), + AccountForTests::vault_a_init_inactive(), + ); + state.force_insert_account( + IdForTests::vault_b_id(), + AccountForTests::vault_b_init_inactive(), + ); + state.force_insert_account( + IdForTests::pool_definition_id(), + AccountForTests::pool_definition_inactive(), + ); + state.force_insert_account( + IdForTests::token_lp_definition_id(), + AccountForTests::token_lp_definition_init_inactive(), + ); + state.force_insert_account( + IdForTests::user_token_lp_id(), + AccountForTests::user_token_lp_holding_init_zero(), + ); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_NEW_DEFINITION); + instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes()); + let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id()); + instruction.extend_from_slice(&amm_program_u8); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::token_lp_definition_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + IdForTests::user_token_lp_id(), + ], + vec![0, 0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[ + &PrivateKeysForTests::user_token_a_key(), + &PrivateKeysForTests::user_token_b_key(), + ], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let token_lp_post = state.get_account_by_id(&IdForTests::token_lp_definition_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + let user_token_lp_post = state.get_account_by_id(&IdForTests::user_token_lp_id()); + + let expected_pool = AccountForTests::pool_definition_init(); + let expected_vault_a = AccountForTests::vault_a_init(); + let expected_vault_b = AccountForTests::vault_b_init(); + let expected_token_lp = AccountForTests::token_lp_definition_new_init(); + let expected_user_token_a = AccountForTests::user_token_a_holding_new_init(); + let expected_user_token_b = AccountForTests::user_token_b_holding_new_init(); + let expected_user_token_lp = AccountForTests::user_token_lp_holding_new_init(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(token_lp_post, expected_token_lp); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + assert_eq!(user_token_lp_post, expected_user_token_lp); + } + + #[test] + fn test_simple_amm_new_definition_uninitialized_pool() { + let mut state = state_for_amm_tests_with_new_def(); + + // Uninitialized in constructor + state.force_insert_account( + IdForTests::vault_a_id(), + AccountForTests::vault_a_init_inactive(), + ); + state.force_insert_account( + IdForTests::vault_b_id(), + AccountForTests::vault_b_init_inactive(), + ); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_NEW_DEFINITION); + instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes()); + let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id()); + instruction.extend_from_slice(&amm_program_u8); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::token_lp_definition_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + IdForTests::user_token_lp_id(), + ], + vec![0, 0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[ + &PrivateKeysForTests::user_token_a_key(), + &PrivateKeysForTests::user_token_b_key(), + ], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let token_lp_post = state.get_account_by_id(&IdForTests::token_lp_definition_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + let user_token_lp_post = state.get_account_by_id(&IdForTests::user_token_lp_id()); + + let expected_pool = AccountForTests::pool_definition_new_init(); + let expected_vault_a = AccountForTests::vault_a_init(); + let expected_vault_b = AccountForTests::vault_b_init(); + let expected_token_lp = AccountForTests::token_lp_definition_new_init(); + let expected_user_token_a = AccountForTests::user_token_a_holding_new_init(); + let expected_user_token_b = AccountForTests::user_token_b_holding_new_init(); + let expected_user_token_lp = AccountForTests::user_token_lp_holding_new_init(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(token_lp_post, expected_token_lp); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + assert_eq!(user_token_lp_post, expected_user_token_lp); + } + + #[test] + fn test_simple_amm_add() { + env_logger::init(); + let mut state = state_for_amm_tests(); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_ADD_LIQUIDITY); + instruction.extend_from_slice(&BalanceForTests::add_min_amount_lp().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::add_max_amount_a().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::add_max_amount_b().to_le_bytes()); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::token_lp_definition_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + IdForTests::user_token_lp_id(), + ], + vec![0, 0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[ + &PrivateKeysForTests::user_token_a_key(), + &PrivateKeysForTests::user_token_b_key(), + ], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let token_lp_post = state.get_account_by_id(&IdForTests::token_lp_definition_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + let user_token_lp_post = state.get_account_by_id(&IdForTests::user_token_lp_id()); + + let expected_pool = AccountForTests::pool_definition_add(); + let expected_vault_a = AccountForTests::vault_a_add(); + let expected_vault_b = AccountForTests::vault_b_add(); + let expected_token_lp = AccountForTests::token_lp_definition_add(); + let expected_user_token_a = AccountForTests::user_token_a_holding_add(); + let expected_user_token_b = AccountForTests::user_token_b_holding_add(); + let expected_user_token_lp = AccountForTests::user_token_lp_holding_add(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(token_lp_post, expected_token_lp); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + assert_eq!(user_token_lp_post, expected_user_token_lp); + } + + #[test] + fn test_simple_amm_swap_1() { + let mut state = state_for_amm_tests(); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_SWAP); + instruction.extend_from_slice(&BalanceForTests::swap_amount_in().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::swap_min_amount_out().to_le_bytes()); + instruction.extend_from_slice(&IdForTests::token_b_definition_id().to_bytes()); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + ], + vec![0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&PrivateKeysForTests::user_token_b_key()], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + + let expected_pool = AccountForTests::pool_definition_swap_1(); + let expected_vault_a = AccountForTests::vault_a_swap_1(); + let expected_vault_b = AccountForTests::vault_b_swap_1(); + let expected_user_token_a = AccountForTests::user_token_a_holding_swap_1(); + let expected_user_token_b = AccountForTests::user_token_b_holding_swap_1(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + } + + #[test] + fn test_simple_amm_swap_2() { + let mut state = state_for_amm_tests(); + + let mut instruction: Vec = Vec::new(); + instruction.push(AMM_SWAP); + instruction.extend_from_slice(&BalanceForTests::swap_amount_in().to_le_bytes()); + instruction.extend_from_slice(&BalanceForTests::swap_min_amount_out().to_le_bytes()); + instruction.extend_from_slice(&IdForTests::token_a_definition_id().to_bytes()); + + let message = public_transaction::Message::try_new( + Program::amm().id(), + vec![ + IdForTests::pool_definition_id(), + IdForTests::vault_a_id(), + IdForTests::vault_b_id(), + IdForTests::user_token_a_id(), + IdForTests::user_token_b_id(), + ], + vec![0], + instruction, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message( + &message, + &[&PrivateKeysForTests::user_token_a_key()], + ); + + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let pool_post = state.get_account_by_id(&IdForTests::pool_definition_id()); + let vault_a_post = state.get_account_by_id(&IdForTests::vault_a_id()); + let vault_b_post = state.get_account_by_id(&IdForTests::vault_b_id()); + let user_token_a_post = state.get_account_by_id(&IdForTests::user_token_a_id()); + let user_token_b_post = state.get_account_by_id(&IdForTests::user_token_b_id()); + + let expected_pool = AccountForTests::pool_definition_swap_2(); + let expected_vault_a = AccountForTests::vault_a_swap_2(); + let expected_vault_b = AccountForTests::vault_b_swap_2(); + let expected_user_token_a = AccountForTests::user_token_a_holding_swap_2(); + let expected_user_token_b = AccountForTests::user_token_b_holding_swap_2(); + + assert_eq!(pool_post, expected_pool); + assert_eq!(vault_a_post, expected_vault_a); + assert_eq!(vault_b_post, expected_vault_b); + assert_eq!(user_token_a_post, expected_user_token_a); + assert_eq!(user_token_b_post, expected_user_token_b); + } + #[test] fn test_execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() { let chain_caller = Program::chain_caller(); @@ -2523,7 +4086,7 @@ pub mod tests { // definition and supply accounts let total_supply: u128 = 10_000_000; // instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] - let mut instruction: [u8; 23] = [0; 23]; + let mut instruction = vec![0; 23]; instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); instruction[17..].copy_from_slice(b"PINATA"); let message = public_transaction::Message::try_new( @@ -2538,7 +4101,7 @@ pub mod tests { state.transition_from_public_transaction(&tx).unwrap(); // Execution of the token program transfer just to initialize the winner token account - let mut instruction: [u8; 23] = [0; 23]; + let mut instruction = vec![0; 23]; instruction[0] = 2; let message = public_transaction::Message::try_new( token.id(), @@ -2663,8 +4226,8 @@ pub mod tests { this }; - assert!(expected_sender_post == sender_post); - assert!(expected_recipient_post == recipient_post); + assert_eq!(expected_sender_post, sender_post); + assert_eq!(expected_recipient_post, recipient_post); } #[test] diff --git a/program_methods/guest/src/bin/amm.rs b/program_methods/guest/src/bin/amm.rs new file mode 100644 index 0000000..9488db1 --- /dev/null +++ b/program_methods/guest/src/bin/amm.rs @@ -0,0 +1,3587 @@ +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, + }, +}; + +// The AMM program has five functions (four directly accessible via instructions): +// 1. New AMM definition. Arguments to this function are: +// * Seven accounts: [amm_pool, vault_holding_a, vault_holding_b, pool_lp, user_holding_a, +// user_holding_b, user_holding_lp]. For new AMM Pool: amm_pool, vault_holding_a, +// vault_holding_b, pool_lp and user_holding_lp are default accounts. amm_pool is a default +// account that will initiate the amm definition account values vault_holding_a is a token +// holding account for token a vault_holding_b is a token holding account for token b pool_lp +// is a token holding account for the pool's lp token user_holding_a is a token holding +// account for token a user_holding_b is a token holding account for token b user_holding_lp +// is a token holding account for lp token +// * PDA remark: Accounts amm_pool, vault_holding_a, vault_holding_b and pool_lp are PDA. The +// AccountId for these accounts must be computed using: amm_pool AccountId <- +// compute_pool_pda vault_holding_a, vault_holding_b <- compute_vault_pda pool_lp +// <-compute_liquidity_token_pda +// * Requires authorization: user_holding_a, user_holding_b +// * 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)] +// * Internally, calls compute_liquidity_token_pda_seed, compute_vault_pda_seed to authorize +// transfers. +// * Internally, calls compute_pool_da, compute_vault_pda and compute_vault_pda to check +// various AccountIds are correct. +// 2. Swap assets Arguments to this function are: +// * Five accounts: [amm_pool, vault_holding_a, vault_holding_b, user_holding_a, +// user_holding_b]. +// * Requires authorization: user holding account associated to TOKEN_DEFINITION_ID (either +// user_holding_a or user_holding_b) +// * 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]. +// * Internally, calls swap logic. +// * Four accounts: [user_deposit, vault_deposit, vault_withdraw, user_withdraw]. +// user_deposit and vault_deposit define deposit transaction. vault_withdraw and +// user_withdraw define withdraw transaction. +// * deposit_amount is the amount for user_deposit -> vault_deposit transfer. +// * reserve_amounts is the pool's reserves; used to compute the withdraw amount. +// * Outputs the token transfers as a Vec and the withdraw amount. +// 3. Add liquidity Arguments to this function are: +// * Seven accounts: [amm_pool, vault_holding_a, vault_holding_b, pool_lp, user_holding_a, +// user_holding_a, user_holding_lp]. +// * Requires authorization: user_holding_a, user_holding_b +// * 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)]. +// * Internally, calls compute_liquidity_token_pda_seed to compute liquidity pool PDA seed. +// 4. Remove liquidity +// * Seven accounts: [amm_pool, vault_holding_a, vault_holding_b, pool_lp, user_holding_a, +// user_holding_a, user_holding_lp]. +// * Requires authorization: user_holding_lp +// * 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)]. +// * Internally, calls compute_vault_pda_seed to compute vault_a and vault_b's PDA seed. + +const POOL_DEFINITION_DATA_SIZE: usize = 225; + +#[derive(Clone, Default)] +struct PoolDefinition { + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, + vault_a_id: AccountId, + vault_b_id: AccountId, + liquidity_pool_id: AccountId, + liquidity_pool_supply: u128, + reserve_a: u128, + reserve_b: u128, + /// Fees are currently not used + fees: u128, + /// A pool becomes inactive (active = false) + /// once all of its liquidity has been removed (e.g., reserves are emptied and + /// liquidity_pool_supply = 0) + active: bool, +} + +impl PoolDefinition { + fn into_data(self) -> Data { + let mut bytes = [0; POOL_DEFINITION_DATA_SIZE]; + bytes[0..32].copy_from_slice(&self.definition_token_a_id.to_bytes()); + bytes[32..64].copy_from_slice(&self.definition_token_b_id.to_bytes()); + bytes[64..96].copy_from_slice(&self.vault_a_id.to_bytes()); + bytes[96..128].copy_from_slice(&self.vault_b_id.to_bytes()); + bytes[128..160].copy_from_slice(&self.liquidity_pool_id.to_bytes()); + bytes[160..176].copy_from_slice(&self.liquidity_pool_supply.to_le_bytes()); + bytes[176..192].copy_from_slice(&self.reserve_a.to_le_bytes()); + bytes[192..208].copy_from_slice(&self.reserve_b.to_le_bytes()); + bytes[208..224].copy_from_slice(&self.fees.to_le_bytes()); + bytes[224] = self.active as u8; + + bytes + .to_vec() + .try_into() + .expect("225 bytes should fit into Data") + } + + fn parse(data: &[u8]) -> Option { + if data.len() != POOL_DEFINITION_DATA_SIZE { + None + } else { + let definition_token_a_id = AccountId::new(data[0..32].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Token A definition")); + let definition_token_b_id = AccountId::new(data[32..64].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Vault B definition")); + let vault_a_id = AccountId::new(data[64..96].try_into().expect( + "Parse data: The AMM program must be provided a valid AccountId for Vault A", + )); + let vault_b_id = AccountId::new(data[96..128].try_into().expect( + "Parse data: The AMM program must be provided a valid AccountId for Vault B", + )); + let liquidity_pool_id = AccountId::new(data[128..160].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Token liquidity pool definition")); + let liquidity_pool_supply = u128::from_le_bytes(data[160..176].try_into().expect( + "Parse data: The AMM program must be provided a valid u128 for liquidity cap", + )); + let reserve_a = u128::from_le_bytes(data[176..192].try_into().expect( + "Parse data: The AMM program must be provided a valid u128 for reserve A balance", + )); + let reserve_b = u128::from_le_bytes(data[192..208].try_into().expect( + "Parse data: The AMM program must be provided a valid u128 for reserve B balance", + )); + let fees = u128::from_le_bytes( + data[208..224] + .try_into() + .expect("Parse data: The AMM program must be provided a valid u128 for fees"), + ); + + let active = match data[224] { + 0 => false, + 1 => true, + _ => panic!("Parse data: The AMM program must be provided a valid bool for active"), + }; + + Some(Self { + definition_token_a_id, + definition_token_b_id, + vault_a_id, + vault_b_id, + liquidity_pool_id, + liquidity_pool_supply, + reserve_a, + reserve_b, + fees, + active, + }) + } + } +} + +// TODO: remove repeated code for Token_Definition and TokenHoldling + +const TOKEN_HOLDING_TYPE: u8 = 1; +const TOKEN_HOLDING_DATA_SIZE: usize = 49; + +struct TokenHolding { + #[cfg_attr(not(test), expect(dead_code, reason = "TODO: fix later"))] + account_type: u8, + definition_id: AccountId, + balance: u128, +} + +impl TokenHolding { + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { + None + } else { + let account_type = data[0]; + let definition_id = AccountId::new( + data[1..33] + .try_into() + .expect("Defintion ID must be 32 bytes long"), + ); + let balance = u128::from_le_bytes( + data[33..] + .try_into() + .expect("balance must be 16 bytes little-endian"), + ); + Some(Self { + definition_id, + balance, + account_type, + }) + } + } + + #[cfg(test)] + fn into_data(self) -> Data { + let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; + bytes[0] = self.account_type; + bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); + bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); + + bytes + .to_vec() + .try_into() + .expect("49 bytes should fit into Data") + } +} + +type Instruction = Vec; +fn main() { + let ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); + + let (post_states, chained_calls) = + match instruction[0] { + 0 => { + let balance_a: u128 = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("New definition: AMM Program expects u128 for balance a"), + ); + let balance_b: u128 = u128::from_le_bytes( + instruction[17..33] + .try_into() + .expect("New definition: AMM Program expects u128 for balance b"), + ); + + // Convert Vec to ProgramId ([u32;8]) + let mut amm_program_id: [u32; 8] = [0; 8]; + amm_program_id[0] = u32::from_le_bytes( + instruction[33..37] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[1] = u32::from_le_bytes( + instruction[37..41] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[2] = u32::from_le_bytes( + instruction[41..45] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[3] = u32::from_le_bytes( + instruction[45..49] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[4] = u32::from_le_bytes( + instruction[49..53] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[5] = u32::from_le_bytes( + instruction[53..57] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[6] = u32::from_le_bytes( + instruction[57..61] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + amm_program_id[7] = u32::from_le_bytes( + instruction[61..65] + .try_into() + .expect("New definition: AMM Program expects valid u32"), + ); + + new_definition(&pre_states, &[balance_a, balance_b], amm_program_id) + } + 1 => { + let mut token_in_id: [u8; 32] = [0; 32]; + token_in_id[0..].copy_from_slice(&instruction[33..65]); + let token_in_id = AccountId::new(token_in_id); + + let amount_in = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Swap: AMM Program expects valid u128 for balance to move"), + ); + let min_amount_out = u128::from_le_bytes( + instruction[17..33] + .try_into() + .expect("Swap: AMM Program expects valid u128 for balance to move"), + ); + + swap(&pre_states, &[amount_in, min_amount_out], token_in_id) + } + 2 => { + let min_amount_lp = u128::from_le_bytes(instruction[1..17].try_into().expect( + "Add liquidity: AMM Program expects valid u128 for min amount liquidity", + )); + let max_amount_a = u128::from_le_bytes( + instruction[17..33] + .try_into() + .expect("Add liquidity: AMM Program expects valid u128 for max amount a"), + ); + let max_amount_b = u128::from_le_bytes( + instruction[33..49] + .try_into() + .expect("Add liquidity: AMM Program expects valid u128 for max amount b"), + ); + + add_liquidity(&pre_states, &[min_amount_lp, max_amount_a, max_amount_b]) + } + 3 => { + let balance_lp = u128::from_le_bytes(instruction[1..17].try_into().expect( + "Remove liquidity: AMM Program expects valid u128 for balance liquidity", + )); + let min_amount_a = u128::from_le_bytes( + instruction[17..33] + .try_into() + .expect("Remove liquidity: AMM Program expects valid u128 for balance a"), + ); + let min_amount_b = u128::from_le_bytes( + instruction[33..49] + .try_into() + .expect("Remove liquidity: AMM Program expects valid u128 for balance b"), + ); + + remove_liquidity(&pre_states, &[balance_lp, min_amount_a, min_amount_b]) + } + _ => panic!("Invalid instruction"), + }; + + write_nssa_outputs_with_chained_call(instruction_words, pre_states, post_states, chained_calls); +} + +fn compute_pool_pda( + amm_program_id: ProgramId, + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, +) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), + )) +} + +fn compute_pool_pda_seed( + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, +) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let (token_1, token_2) = match definition_token_a_id + .value() + .cmp(definition_token_b_id.value()) + { + std::cmp::Ordering::Less => (definition_token_b_id, definition_token_a_id), + std::cmp::Ordering::Greater => (definition_token_a_id, definition_token_b_id), + std::cmp::Ordering::Equal => panic!("Definitions match"), + }; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&token_1.to_bytes()); + bytes[32..].copy_from_slice(&token_2.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +fn compute_vault_pda( + amm_program_id: ProgramId, + pool_id: AccountId, + definition_token_id: AccountId, +) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_vault_pda_seed(pool_id, definition_token_id), + )) +} + +fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { + AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) +} + +fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&[0; 32]); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +const TOKEN_PROGRAM_NEW: u8 = 0; +const TOKEN_PROGRAM_TRANSFER: u8 = 1; +const TOKEN_PROGRAM_MINT: u8 = 4; +const TOKEN_PROGRAM_BURN: u8 = 3; + +fn initialize_token_transfer_chained_call( + token_program_command: u8, + sender: AccountWithMetadata, + recipient: AccountWithMetadata, + amount_to_move: u128, + pda_seed: Vec, +) -> ChainedCall { + let mut instruction_data = vec![0u8; 23]; + instruction_data[0] = token_program_command; + instruction_data[1..17].copy_from_slice(&amount_to_move.to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) + .expect("AMM Program expects valid token transfer instruction data"); + + ChainedCall { + program_id: sender.account.program_owner, + instruction_data, + pre_states: vec![sender, recipient], + pda_seeds: pda_seed, + } +} + +fn new_definition( + pre_states: &[AccountWithMetadata], + balance_in: &[u128], + amm_program_id: ProgramId, +) -> (Vec, Vec) { + // Pool accounts: pool itself, and its 2 vaults and LP token + // 2 accounts for funding tokens + // initial funder's LP account + if pre_states.len() != 7 { + panic!("Invalid number of input accounts") + } + + if balance_in.len() != 2 { + panic!("Invalid number of input balances") + } + + let pool = &pre_states[0]; + let vault_a = &pre_states[1]; + let vault_b = &pre_states[2]; + let pool_lp = &pre_states[3]; + let user_holding_a = &pre_states[4]; + let user_holding_b = &pre_states[5]; + let user_holding_lp = &pre_states[6]; + + let amount_a = balance_in[0]; + let amount_b = balance_in[1]; + + // Prevents pool constant coefficient (k) from being 0. + if amount_a == 0 || amount_b == 0 { + panic!("Balances must be nonzero") + } + + // Verify token_a and token_b are different + let definition_token_a_id = TokenHolding::parse(&user_holding_a.account.data) + .expect("New definition: AMM Program expects valid Token Holding account for Token A") + .definition_id; + let definition_token_b_id = TokenHolding::parse(&user_holding_b.account.data) + .expect("New definition: AMM Program expects valid Token Holding account for Token B") + .definition_id; + + // both instances of the same token program + let token_program = user_holding_a.account.program_owner; + + if user_holding_b.account.program_owner != token_program { + panic!("User Token holdings must use the same Token Program"); + } + + if definition_token_a_id == definition_token_b_id { + panic!("Cannot set up a swap for a token with itself") + } + + if pool.account_id + != compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id) + { + panic!("Pool Definition Account ID does not match PDA"); + } + + if vault_a.account_id + != compute_vault_pda(amm_program_id, pool.account_id, definition_token_a_id) + || vault_b.account_id + != compute_vault_pda(amm_program_id, pool.account_id, definition_token_b_id) + { + panic!("Vault ID does not match PDA"); + } + + if pool_lp.account_id != compute_liquidity_token_pda(amm_program_id, pool.account_id) { + panic!("Liquidity pool Token Definition Account ID does not match PDA"); + } + + // Verify that Pool Account is not active + let pool_account_data = if pool.account == Account::default() { + PoolDefinition::default() + } else { + PoolDefinition::parse(&pool.account.data).expect("AMM program expects a valid Pool account") + }; + + if pool_account_data.active { + panic!("Cannot initialize an active Pool Definition") + } + + // LP Token minting calculation + // We assume LP is based on the initial deposit amount for Token_A. + + // Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + definition_token_a_id, + definition_token_b_id, + vault_a_id: vault_a.account_id, + vault_b_id: vault_b.account_id, + liquidity_pool_id: pool_lp.account_id, + liquidity_pool_supply: amount_a, + reserve_a: amount_a, + reserve_b: amount_b, + fees: 0u128, // TODO: we assume all fees are 0 for now. + active: true, + }; + + pool_post.data = pool_post_definition.into_data(); + let pool_post: AccountPostState = if pool.account == Account::default() { + AccountPostState::new_claimed(pool_post.clone()) + } else { + AccountPostState::new(pool_post.clone()) + }; + + let mut chained_calls = Vec::::new(); + + // Chain call for Token A (user_holding_a -> Vault_A) + let call_token_a = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + user_holding_a.clone(), + vault_a.clone(), + amount_a, + Vec::::new(), + ); + // Chain call for Token B (user_holding_b -> Vault_B) + let call_token_b = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + user_holding_b.clone(), + vault_b.clone(), + amount_b, + Vec::::new(), + ); + + // Chain call for liquidity token (TokenLP definition -> User LP Holding) + let mut instruction_data = vec![0u8; 23]; + instruction_data[0] = if pool.account == Account::default() { + TOKEN_PROGRAM_NEW + } else { + TOKEN_PROGRAM_MINT + }; //new or mint + let nme = if pool.account == Account::default() { + [1u8; 6] + } else { + [0u8; 6] + }; + + instruction_data[1..17].copy_from_slice(&amount_a.to_le_bytes()); + instruction_data[17..].copy_from_slice(&nme); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) + .expect("New definition: AMM Program expects valid instruction_data"); + + let mut pool_lp_auth = pool_lp.clone(); + pool_lp_auth.is_authorized = true; + + let token_program_id = user_holding_a.account.program_owner; + let call_token_lp = ChainedCall { + program_id: token_program_id, + instruction_data, + pre_states: vec![pool_lp_auth.clone(), user_holding_lp.clone()], + pda_seeds: vec![compute_liquidity_token_pda_seed(pool.account_id)], + }; + + chained_calls.push(call_token_lp); + chained_calls.push(call_token_b); + chained_calls.push(call_token_a); + + let post_states = vec![ + pool_post.clone(), + AccountPostState::new(pre_states[1].account.clone()), + AccountPostState::new(pre_states[2].account.clone()), + AccountPostState::new(pre_states[3].account.clone()), + AccountPostState::new(pre_states[4].account.clone()), + AccountPostState::new(pre_states[5].account.clone()), + AccountPostState::new(pre_states[6].account.clone()), + ]; + + (post_states.clone(), chained_calls) +} + +fn swap( + pre_states: &[AccountWithMetadata], + amounts: &[u128], + token_in_id: AccountId, +) -> (Vec, Vec) { + if pre_states.len() != 5 { + panic!("Invalid number of input accounts"); + } + + if amounts.len() != 2 { + panic!("Invalid number of amounts provided"); + } + + let pool = &pre_states[0]; + let vault_a = &pre_states[1]; + let vault_b = &pre_states[2]; + let user_holding_a = &pre_states[3]; + let user_holding_b = &pre_states[4]; + + // Verify vaults are in fact vaults + let pool_def_data = PoolDefinition::parse(&pool.account.data) + .expect("Swap: AMM Program expects a valid Pool Definition Account"); + + if !pool_def_data.active { + panic!("Pool is inactive"); + } + + if vault_a.account_id != pool_def_data.vault_a_id { + panic!("Vault A was not provided"); + } + + if vault_b.account_id != pool_def_data.vault_b_id { + panic!("Vault B was not provided"); + } + + // fetch pool reserves + // validates reserves is at least the vaults' balances + if TokenHolding::parse(&vault_a.account.data) + .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A") + .balance + < pool_def_data.reserve_a + { + panic!("Reserve for Token A exceeds vault balance"); + } + if TokenHolding::parse(&vault_b.account.data) + .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B") + .balance + < pool_def_data.reserve_b + { + panic!("Reserve for Token B exceeds vault balance"); + } + + let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = + if token_in_id == pool_def_data.definition_token_a_id { + let (chained_calls, deposit_a, withdraw_b) = swap_logic( + user_holding_a.clone(), + vault_a.clone(), + vault_b.clone(), + user_holding_b.clone(), + amounts[0], + amounts[1], + &[pool_def_data.reserve_a, pool_def_data.reserve_b], + pool.account_id, + ); + + (chained_calls, [deposit_a, 0], [0, withdraw_b]) + } else if token_in_id == pool_def_data.definition_token_b_id { + let (chained_calls, deposit_b, withdraw_a) = swap_logic( + user_holding_b.clone(), + vault_b.clone(), + vault_a.clone(), + user_holding_a.clone(), + amounts[0], + amounts[1], + &[pool_def_data.reserve_b, pool_def_data.reserve_a], + pool.account_id, + ); + + (chained_calls, [0, withdraw_a], [deposit_b, 0]) + } else { + panic!("AccountId is not a token type for the pool"); + }; + + // Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, + reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, + ..pool_def_data + }; + + pool_post.data = pool_post_definition.into_data(); + + let post_states = vec![ + AccountPostState::new(pool_post.clone()), + AccountPostState::new(pre_states[1].account.clone()), + AccountPostState::new(pre_states[2].account.clone()), + AccountPostState::new(pre_states[3].account.clone()), + AccountPostState::new(pre_states[4].account.clone()), + ]; + + (post_states, chained_calls) +} + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +fn swap_logic( + user_deposit: AccountWithMetadata, + vault_deposit: AccountWithMetadata, + vault_withdraw: AccountWithMetadata, + user_withdraw: AccountWithMetadata, + deposit_amount: u128, + min_amount_out: u128, + reserve_amounts: &[u128], + pool_id: AccountId, +) -> (Vec, u128, u128) { + let reserve_deposit_vault_amount = reserve_amounts[0]; + let reserve_withdraw_vault_amount = reserve_amounts[1]; + + // Compute withdraw amount + // Maintains pool constant product + // k = pool_def_data.reserve_a * pool_def_data.reserve_b; + let withdraw_amount = (reserve_withdraw_vault_amount * deposit_amount) + / (reserve_deposit_vault_amount + deposit_amount); + + // Slippage check + if min_amount_out > withdraw_amount { + panic!("Withdraw amount is less than minimal amount out"); + } + + if withdraw_amount == 0 { + panic!("Withdraw amount should be nonzero"); + } + + let mut chained_calls = Vec::new(); + chained_calls.push(initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + user_deposit.clone(), + vault_deposit.clone(), + deposit_amount, + Vec::::new(), + )); + + let mut vault_withdraw = vault_withdraw.clone(); + vault_withdraw.is_authorized = true; + + chained_calls.push(initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + vault_withdraw.clone(), + user_withdraw.clone(), + withdraw_amount, + vec![compute_vault_pda_seed( + pool_id, + TokenHolding::parse(&vault_withdraw.account.data) + .expect("Swap Logic: AMM Program expects valid token data") + .definition_id, + )], + )); + + (chained_calls, deposit_amount, withdraw_amount) +} + +fn add_liquidity( + pre_states: &[AccountWithMetadata], + balances: &[u128], +) -> (Vec, Vec) { + if pre_states.len() != 7 { + panic!("Invalid number of input accounts"); + } + + let pool = &pre_states[0]; + let vault_a = &pre_states[1]; + let vault_b = &pre_states[2]; + let pool_definition_lp = &pre_states[3]; + let user_holding_a = &pre_states[4]; + let user_holding_b = &pre_states[5]; + let user_holding_lp = &pre_states[6]; + + // 1. Fetch Pool state + let pool_def_data = PoolDefinition::parse(&pool.account.data) + .expect("Add liquidity: AMM Program expects valid Pool Definition Account"); + if vault_a.account_id != pool_def_data.vault_a_id { + panic!("Vault A was not provided"); + } + + if pool_def_data.liquidity_pool_id != pool_definition_lp.account_id { + panic!("LP definition mismatch"); + } + + if vault_b.account_id != pool_def_data.vault_b_id { + panic!("Vault B was not provided"); + } + if balances.len() != 3 { + panic!("Invalid number of input balances"); + } + + let min_amount_lp = balances[0]; + let max_amount_a = balances[1]; + let max_amount_b = balances[2]; + + if max_amount_a == 0 || max_amount_b == 0 { + panic!("Both max-balances must be nonzero"); + } + + if min_amount_lp == 0 { + panic!("Min-lp must be nonzero"); + } + + // 2. Determine deposit amount + let vault_b_balance = TokenHolding::parse(&vault_b.account.data) + .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B") + .balance; + let vault_a_balance = TokenHolding::parse(&vault_a.account.data) + .expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A") + .balance; + + if pool_def_data.reserve_a == 0 || pool_def_data.reserve_b == 0 { + panic!("Reserves must be nonzero"); + } + + if vault_a_balance < pool_def_data.reserve_a || vault_b_balance < pool_def_data.reserve_b { + panic!("Vaults' balances must be at least the reserve amounts"); + } + + // Calculate actual_amounts + let ideal_a: u128 = (pool_def_data.reserve_a * max_amount_b) / pool_def_data.reserve_b; + let ideal_b: u128 = (pool_def_data.reserve_b * max_amount_a) / pool_def_data.reserve_a; + + let actual_amount_a = if ideal_a > max_amount_a { + max_amount_a + } else { + ideal_a + }; + let actual_amount_b = if ideal_b > max_amount_b { + max_amount_b + } else { + ideal_b + }; + + // 3. Validate amounts + if max_amount_a < actual_amount_a || max_amount_b < actual_amount_b { + panic!("Actual trade amounts cannot exceed max_amounts"); + } + + if actual_amount_a == 0 || actual_amount_b == 0 { + panic!("A trade amount is 0"); + } + + // 4. Calculate LP to mint + let delta_lp = std::cmp::min( + pool_def_data.liquidity_pool_supply * actual_amount_a / pool_def_data.reserve_a, + pool_def_data.liquidity_pool_supply * actual_amount_b / pool_def_data.reserve_b, + ); + + if delta_lp == 0 { + panic!("Payable LP must be nonzero"); + } + + if delta_lp < min_amount_lp { + panic!("Payable LP is less than provided minimum LP amount"); + } + + // 5. Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + liquidity_pool_supply: pool_def_data.liquidity_pool_supply + delta_lp, + reserve_a: pool_def_data.reserve_a + actual_amount_a, + reserve_b: pool_def_data.reserve_b + actual_amount_b, + ..pool_def_data + }; + + pool_post.data = pool_post_definition.into_data(); + let mut chained_call = Vec::new(); + + // Chain call for Token A (UserHoldingA -> Vault_A) + let call_token_a = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + user_holding_a.clone(), + vault_a.clone(), + actual_amount_a, + Vec::::new(), + ); + // Chain call for Token B (UserHoldingB -> Vault_B) + let call_token_b = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + user_holding_b.clone(), + vault_b.clone(), + actual_amount_b, + Vec::::new(), + ); + // Chain call for LP (mint new tokens for user_holding_lp) + let mut pool_definition_lp_auth = pool_definition_lp.clone(); + pool_definition_lp_auth.is_authorized = true; + let call_token_lp = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_MINT, + pool_definition_lp_auth.clone(), + user_holding_lp.clone(), + delta_lp, + vec![compute_liquidity_token_pda_seed(pool.account_id)], + ); + + chained_call.push(call_token_lp); + chained_call.push(call_token_b); + chained_call.push(call_token_a); + + let post_states = vec![ + AccountPostState::new(pool_post), + AccountPostState::new(pre_states[1].account.clone()), + AccountPostState::new(pre_states[2].account.clone()), + AccountPostState::new(pre_states[3].account.clone()), + AccountPostState::new(pre_states[4].account.clone()), + AccountPostState::new(pre_states[5].account.clone()), + AccountPostState::new(pre_states[6].account.clone()), + ]; + + (post_states, chained_call) +} + +fn remove_liquidity( + pre_states: &[AccountWithMetadata], + amounts: &[u128], +) -> (Vec, Vec) { + if pre_states.len() != 7 { + panic!("Invalid number of input accounts"); + } + + let pool = &pre_states[0]; + let vault_a = &pre_states[1]; + let vault_b = &pre_states[2]; + let pool_definition_lp = &pre_states[3]; + let user_holding_a = &pre_states[4]; + let user_holding_b = &pre_states[5]; + let user_holding_lp = &pre_states[6]; + + if amounts.len() != 3 { + panic!("Invalid number of balances"); + } + + let amount_lp = amounts[0]; + let amount_min_a = amounts[1]; + let amount_min_b = amounts[2]; + + // 1. Fetch Pool state + let pool_def_data = PoolDefinition::parse(&pool.account.data) + .expect("Remove liquidity: AMM Program expects a valid Pool Definition Account"); + + if !pool_def_data.active { + panic!("Pool is inactive"); + } + + if pool_def_data.liquidity_pool_id != pool_definition_lp.account_id { + panic!("LP definition mismatch"); + } + + if vault_a.account_id != pool_def_data.vault_a_id { + panic!("Vault A was not provided"); + } + + if vault_b.account_id != pool_def_data.vault_b_id { + panic!("Vault B was not provided"); + } + + // Vault addresses do not need to be checked with PDA + // calculation for setting authorization since stored + // in the Pool Definition. + let mut running_vault_a = vault_a.clone(); + let mut running_vault_b = vault_b.clone(); + running_vault_a.is_authorized = true; + running_vault_b.is_authorized = true; + + if amount_min_a == 0 || amount_min_b == 0 { + panic!("Minimum withdraw amount must be nonzero"); + } + + if amount_lp == 0 { + panic!("Liquidity amount must be nonzero"); + } + + // 2. Compute withdrawal amounts + let user_holding_lp_data = TokenHolding::parse(&user_holding_lp.account.data) + .expect("Remove liquidity: AMM Program expects a valid Token Account for liquidity token"); + + if user_holding_lp_data.balance > pool_def_data.liquidity_pool_supply + || user_holding_lp_data.definition_id != pool_def_data.liquidity_pool_id + { + panic!("Invalid liquidity account provided"); + } + + let withdraw_amount_a = + (pool_def_data.reserve_a * amount_lp) / pool_def_data.liquidity_pool_supply; + let withdraw_amount_b = + (pool_def_data.reserve_b * amount_lp) / pool_def_data.liquidity_pool_supply; + + // 3. Validate and slippage check + if withdraw_amount_a < amount_min_a { + panic!("Insufficient minimal withdraw amount (Token A) provided for liquidity amount"); + } + if withdraw_amount_b < amount_min_b { + panic!("Insufficient minimal withdraw amount (Token B) provided for liquidity amount"); + } + + // 4. Calculate LP to reduce cap by + let delta_lp: u128 = + (pool_def_data.liquidity_pool_supply * amount_lp) / pool_def_data.liquidity_pool_supply; + + let active: bool = pool_def_data.liquidity_pool_supply - delta_lp != 0; + + // 5. Update pool account + let mut pool_post = pool.account.clone(); + let pool_post_definition = PoolDefinition { + liquidity_pool_supply: pool_def_data.liquidity_pool_supply - delta_lp, + reserve_a: pool_def_data.reserve_a - withdraw_amount_a, + reserve_b: pool_def_data.reserve_b - withdraw_amount_b, + active, + ..pool_def_data.clone() + }; + + pool_post.data = pool_post_definition.into_data(); + + let mut chained_calls = Vec::new(); + + // Chaincall for Token A withdraw + let call_token_a = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + running_vault_a, + user_holding_a.clone(), + withdraw_amount_a, + vec![compute_vault_pda_seed( + pool.account_id, + pool_def_data.definition_token_a_id, + )], + ); + // Chaincall for Token B withdraw + let call_token_b = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_TRANSFER, + running_vault_b, + user_holding_b.clone(), + withdraw_amount_b, + vec![compute_vault_pda_seed( + pool.account_id, + pool_def_data.definition_token_b_id, + )], + ); + // Chaincall for LP adjustment + let mut pool_definition_lp_auth = pool_definition_lp.clone(); + pool_definition_lp_auth.is_authorized = true; + let call_token_lp = initialize_token_transfer_chained_call( + TOKEN_PROGRAM_BURN, + pool_definition_lp_auth.clone(), + user_holding_lp.clone(), + delta_lp, + vec![compute_liquidity_token_pda_seed(pool.account_id)], + ); + + chained_calls.push(call_token_lp); + chained_calls.push(call_token_b); + chained_calls.push(call_token_a); + + let post_states = vec![ + AccountPostState::new(pool_post.clone()), + AccountPostState::new(pre_states[1].account.clone()), + AccountPostState::new(pre_states[2].account.clone()), + AccountPostState::new(pre_states[3].account.clone()), + AccountPostState::new(pre_states[4].account.clone()), + AccountPostState::new(pre_states[5].account.clone()), + AccountPostState::new(pre_states[6].account.clone()), + ]; + + (post_states, chained_calls) +} + +#[cfg(test)] +mod tests { + use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ChainedCall, PdaSeed, ProgramId}, + }; + + use crate::{ + PoolDefinition, TokenHolding, add_liquidity, compute_liquidity_token_pda, + compute_liquidity_token_pda_seed, compute_pool_pda, compute_vault_pda, + compute_vault_pda_seed, new_definition, remove_liquidity, swap, + }; + + const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; + const AMM_PROGRAM_ID: ProgramId = [42; 8]; + const TOKEN_DEFINITION_DATA_SIZE: usize = 55; + + struct TokenDefinition { + account_type: u8, + name: [u8; 6], + total_supply: u128, + metadata_id: AccountId, + } + + impl TokenDefinition { + fn into_data(self) -> Data { + let mut bytes = Vec::::new(); + bytes.extend_from_slice(&[self.account_type]); + bytes.extend_from_slice(&self.name); + bytes.extend_from_slice(&self.total_supply.to_le_bytes()); + bytes.extend_from_slice(&self.metadata_id.to_bytes()); + + if bytes.len() != TOKEN_DEFINITION_DATA_SIZE { + panic!("Invalid Token Definition data"); + } + + Data::try_from(bytes).expect("Token definition data size must fit into data") + } + } + + struct BalanceForTests; + + impl BalanceForTests { + fn vault_a_reserve_init() -> u128 { + 1_000 + } + + fn vault_b_reserve_init() -> u128 { + 500 + } + + fn vault_a_reserve_low() -> u128 { + 10 + } + + fn vault_b_reserve_low() -> u128 { + 10 + } + + fn vault_a_reserve_high() -> u128 { + 500_000 + } + + fn vault_b_reserve_high() -> u128 { + 500_000 + } + + fn user_token_a_balance() -> u128 { + 1_000 + } + + fn user_token_b_balance() -> u128 { + 500 + } + + fn user_token_lp_balance() -> u128 { + 100 + } + + fn remove_min_amount_a() -> u128 { + 50 + } + + fn remove_min_amount_b() -> u128 { + 100 + } + + fn remove_actual_a_successful() -> u128 { + 100 + } + + fn remove_min_amount_b_low() -> u128 { + 50 + } + + fn remove_amount_lp() -> u128 { + 100 + } + + fn remove_amount_lp_1() -> u128 { + 30 + } + + fn add_max_amount_a() -> u128 { + 500 + } + + fn add_max_amount_b() -> u128 { + 200 + } + + fn add_max_amount_a_low() -> u128 { + 10 + } + + fn add_max_amount_b_low() -> u128 { + 10 + } + + fn add_min_amount_lp() -> u128 { + 20 + } + + fn vault_a_swap_test_1() -> u128 { + 1_500 + } + + fn vault_a_swap_test_2() -> u128 { + 715 + } + + fn vault_b_swap_test_1() -> u128 { + 334 + } + + fn vault_b_swap_test_2() -> u128 { + 700 + } + + fn min_amount_out() -> u128 { + 200 + } + + fn vault_a_add_successful() -> u128 { + 1_400 + } + + fn vault_b_add_successful() -> u128 { + 700 + } + + fn add_successful_amount_a() -> u128 { + 400 + } + + fn add_successful_amount_b() -> u128 { + 200 + } + + fn vault_a_remove_successful() -> u128 { + 900 + } + + fn vault_b_remove_successful() -> u128 { + 450 + } + } + + struct ChainedCallForTests; + + impl ChainedCallForTests { + fn cc_swap_token_a_test_1() -> ChainedCall { + let mut instruction_data = vec![0; 23]; + instruction_data[0] = 1; + instruction_data[1..17] + .copy_from_slice(&BalanceForTests::add_max_amount_a().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::user_holding_a(), + AccountForTests::vault_a_init(), + ], + pda_seeds: Vec::::new(), + } + } + + fn cc_swap_token_b_test_1() -> ChainedCall { + let swap_amount: u128 = 166; + + let mut vault_b_auth = AccountForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17].copy_from_slice(&swap_amount.to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![vault_b_auth, AccountForTests::user_holding_b()], + pda_seeds: vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )], + } + } + + fn cc_swap_token_a_test_2() -> ChainedCall { + let swap_amount: u128 = 285; + + let mut vault_a_auth = AccountForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + + let mut instruction_data = vec![0; 23]; + instruction_data[0] = 1; + instruction_data[1..17].copy_from_slice(&swap_amount.to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction_data) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![vault_a_auth, AccountForTests::user_holding_a()], + pda_seeds: vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )], + } + } + + fn cc_swap_token_b_test_2() -> ChainedCall { + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17].copy_from_slice(&BalanceForTests::add_max_amount_b().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::user_holding_b(), + AccountForTests::vault_b_init(), + ], + pda_seeds: Vec::::new(), + } + } + + fn cc_add_token_a() -> ChainedCall { + let mut instruction = vec![0u8; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::user_holding_a(), + AccountForTests::vault_a_init(), + ], + pda_seeds: Vec::::new(), + } + } + + fn cc_add_token_b() -> ChainedCall { + let mut instruction = vec![0u8; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::add_successful_amount_b().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("Swap Logic: AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::user_holding_b(), + AccountForTests::vault_b_init(), + ], + pda_seeds: Vec::::new(), + } + } + + fn cc_add_pool_lp() -> ChainedCall { + let mut pool_lp_auth = AccountForTests::pool_lp_init(); + pool_lp_auth.is_authorized = true; + + let mut instruction = vec![0u8; 23]; + instruction[0] = 4; + instruction[1..17] + .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("Swap Logic: AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![pool_lp_auth, AccountForTests::user_holding_lp_init()], + pda_seeds: vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )], + } + } + + fn cc_remove_token_a() -> ChainedCall { + let mut vault_a_auth = AccountForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::remove_actual_a_successful().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![vault_a_auth, AccountForTests::user_holding_a()], + pda_seeds: vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )], + } + } + + fn cc_remove_token_b() -> ChainedCall { + let mut vault_b_auth = AccountForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::remove_min_amount_b_low().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![vault_b_auth, AccountForTests::user_holding_b()], + pda_seeds: vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )], + } + } + + fn cc_remove_pool_lp() -> ChainedCall { + let mut pool_lp_auth = AccountForTests::pool_lp_init(); + pool_lp_auth.is_authorized = true; + + let mut instruction = vec![0; 23]; + instruction[0] = 3; + instruction[1..17] + .copy_from_slice(&BalanceForTests::remove_actual_a_successful().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_lp_init(), + ], + pda_seeds: vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )], + } + } + + fn cc_new_definition_token_a() -> ChainedCall { + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::user_holding_a(), + AccountForTests::vault_a_init(), + ], + pda_seeds: Vec::::new(), + } + } + + fn cc_new_definition_token_b() -> ChainedCall { + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::add_successful_amount_b().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("Swap Logic: AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::user_holding_b(), + AccountForTests::vault_b_init(), + ], + pda_seeds: Vec::::new(), + } + } + + fn cc_new_definition_token_lp() -> ChainedCall { + let mut instruction = vec![0; 23]; + instruction[0] = 1; + instruction[1..17] + .copy_from_slice(&BalanceForTests::add_successful_amount_a().to_le_bytes()); + let instruction_data = risc0_zkvm::serde::to_vec(&instruction) + .expect("AMM Program expects valid transaction instruction data"); + ChainedCall { + program_id: TOKEN_PROGRAM_ID, + instruction_data, + pre_states: vec![ + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_lp_uninit(), + ], + pda_seeds: vec![compute_liquidity_token_pda_seed( + IdForTests::pool_definition_id(), + )], + } + } + } + + struct IdForTests; + + impl IdForTests { + fn token_a_definition_id() -> AccountId { + AccountId::new([42; 32]) + } + + fn token_b_definition_id() -> AccountId { + AccountId::new([43; 32]) + } + + fn token_lp_definition_id() -> AccountId { + compute_liquidity_token_pda(AMM_PROGRAM_ID, IdForTests::pool_definition_id()) + } + + fn user_token_a_id() -> AccountId { + AccountId::new([45; 32]) + } + + fn user_token_b_id() -> AccountId { + AccountId::new([46; 32]) + } + + fn user_token_lp_id() -> AccountId { + AccountId::new([47; 32]) + } + + fn pool_definition_id() -> AccountId { + compute_pool_pda( + AMM_PROGRAM_ID, + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id(), + ) + } + + fn vault_a_id() -> AccountId { + compute_vault_pda( + AMM_PROGRAM_ID, + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + ) + } + + fn vault_b_id() -> AccountId { + compute_vault_pda( + AMM_PROGRAM_ID, + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + ) + } + } + + struct AccountForTests; + + impl AccountForTests { + fn user_holding_a() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::user_token_a_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_a_id(), + } + } + + fn user_holding_b() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::user_token_b_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_b_id(), + } + } + + fn vault_a_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn vault_a_init_high() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_high(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init_high() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_high(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn vault_a_init_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_low(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_low(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn vault_a_init_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn vault_b_init_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + } + } + + fn pool_lp_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1; 6], + total_supply: BalanceForTests::vault_a_reserve_init(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::token_lp_definition_id(), + } + } + + fn pool_lp_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: 0u8, + name: [1; 6], + total_supply: BalanceForTests::vault_a_reserve_init(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + } + } + + fn user_holding_lp_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_lp_id(), + } + } + + fn user_holding_lp_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_lp_definition_id(), + balance: BalanceForTests::user_token_lp_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::user_token_lp_id(), + } + } + + fn pool_definition_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_a_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: 0, + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_b_zero() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: 0, + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_a_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(), + reserve_a: BalanceForTests::vault_a_reserve_low(), + reserve_b: BalanceForTests::vault_b_reserve_high(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_init_reserve_b_low() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_high(), + reserve_a: BalanceForTests::vault_a_reserve_high(), + reserve_b: BalanceForTests::vault_b_reserve_low(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_swap_test_1() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_swap_test_1(), + reserve_b: BalanceForTests::vault_b_swap_test_1(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_swap_test_2() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_swap_test_2(), + reserve_b: BalanceForTests::vault_b_swap_test_2(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_add_zero_lp() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_low(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_add_successful() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_add_successful(), + reserve_a: BalanceForTests::vault_a_add_successful(), + reserve_b: BalanceForTests::vault_b_add_successful(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_remove_successful() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_remove_successful(), + reserve_a: BalanceForTests::vault_a_remove_successful(), + reserve_b: BalanceForTests::vault_b_remove_successful(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_inactive() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: false, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: false, + }), + nonce: 0, + }, + is_authorized: true, + account_id: AccountId::new([4; 32]), + } + } + + fn vault_a_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_a_definition_id(), + balance: BalanceForTests::vault_a_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: AccountId::new([4; 32]), + } + } + + fn vault_b_with_wrong_id() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: 1u8, + definition_id: IdForTests::token_b_definition_id(), + balance: BalanceForTests::vault_b_reserve_init(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: AccountId::new([4; 32]), + } + } + + fn pool_definition_active() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0u128, + data: PoolDefinition::into_data(PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::vault_a_reserve_init(), + reserve_a: BalanceForTests::vault_a_reserve_init(), + reserve_b: BalanceForTests::vault_b_reserve_init(), + fees: 0u128, + active: true, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + } + + #[test] + fn test_pool_pda_produces_unique_id_for_token_pair() { + // compute_pool_pda(amm_program_id: ProgramId, definition_token_a_id: AccountId, + // definition_token_b_id: AccountId) + assert!( + compute_pool_pda( + AMM_PROGRAM_ID, + IdForTests::token_a_definition_id(), + IdForTests::token_b_definition_id() + ) == compute_pool_pda( + AMM_PROGRAM_ID, + IdForTests::token_b_definition_id(), + IdForTests::token_a_definition_id() + ) + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountForTests::pool_definition_uninit()]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_3() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_4() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_5() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_with_invalid_number_of_accounts_6() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Invalid number of input balances")] + #[test] + fn test_call_new_definition_with_invalid_number_of_balances() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[BalanceForTests::vault_a_reserve_init()], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Balances must be nonzero")] + #[test] + fn test_call_new_definition_with_zero_balance_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[0, BalanceForTests::vault_b_reserve_init()], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Balances must be nonzero")] + #[test] + fn test_call_new_definition_with_zero_balance_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[BalanceForTests::vault_a_reserve_init(), 0], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Cannot set up a swap for a token with itself")] + #[test] + fn test_call_new_definition_same_token_definition() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Liquidity pool Token Definition Account ID does not match PDA")] + #[test] + fn test_call_new_definition_wrong_liquidity_id() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Pool Definition Account ID does not match PDA")] + #[test] + fn test_call_new_definition_wrong_pool_id() { + let pre_states = vec![ + AccountForTests::pool_definition_with_wrong_id(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Vault ID does not match PDA")] + #[test] + fn test_call_new_definition_wrong_vault_id_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Vault ID does not match PDA")] + #[test] + fn test_call_new_definition_wrong_vault_id_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Cannot initialize an active Pool Definition")] + #[test] + fn test_call_new_definition_cannot_initialize_active_pool() { + let pre_states = vec![ + AccountForTests::pool_definition_active(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let _post_states = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + } + + #[should_panic(expected = "Cannot initialize an active Pool Definition")] + #[test] + fn test_call_new_definition_chained_call_successful() { + let pre_states = vec![ + AccountForTests::pool_definition_active(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_uninit(), + ]; + let (post_states, chained_calls) = new_definition( + &pre_states, + &[ + BalanceForTests::vault_a_reserve_init(), + BalanceForTests::vault_b_reserve_init(), + ], + AMM_PROGRAM_ID, + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_add_successful().account == *pool_post.account()); + + let chained_call_lp = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + let chained_call_a = chained_calls[2].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_new_definition_token_a()); + assert!(chained_call_b == ChainedCallForTests::cc_new_definition_token_b()); + assert!(chained_call_lp == ChainedCallForTests::cc_new_definition_token_lp()); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_remove_liquidity_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_remove_liquidity_with_invalid_number_of_accounts_3() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_remove_liquidity_with_invalid_number_of_accounts_4() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_remove_liquidity_with_invalid_number_of_accounts_5() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_remove_liquidity_with_invalid_number_of_accounts_6() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Vault A was not provided")] + #[test] + fn test_call_remove_liquidity_vault_a_omitted() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Vault B was not provided")] + #[test] + fn test_call_remove_liquidity_vault_b_omitted() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "LP definition mismatch")] + #[test] + fn test_call_remove_liquidity_lp_def_mismatch() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid liquidity account provided")] + #[test] + fn test_call_remove_liquidity_insufficient_liquidity_amount() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_a(), /* different token account than lp to create + * desired error */ + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic( + expected = "Insufficient minimal withdraw amount (Token A) provided for liquidity amount" + )] + #[test] + fn test_call_remove_liquidity_insufficient_balance_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp_1(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic( + expected = "Insufficient minimal withdraw amount (Token B) provided for liquidity amount" + )] + #[test] + fn test_call_remove_liquidity_insufficient_balance_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Minimum withdraw amount must be nonzero")] + #[test] + fn test_call_remove_liquidity_min_bal_zero_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + 0, + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[should_panic(expected = "Minimum withdraw amount must be nonzero")] + #[test] + fn test_call_remove_liquidity_min_bal_zero_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + 0, + ], + ); + } + + #[should_panic(expected = "Liquidity amount must be nonzero")] + #[test] + fn test_call_remove_liquidity_lp_bal_zero() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = remove_liquidity( + &pre_states, + &[ + 0, + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b(), + ], + ); + } + + #[test] + fn test_call_remove_liquidity_chained_call_successful() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let (post_states, chained_calls) = remove_liquidity( + &pre_states, + &[ + BalanceForTests::remove_amount_lp(), + BalanceForTests::remove_min_amount_a(), + BalanceForTests::remove_min_amount_b_low(), + ], + ); + + let pool_post = post_states[0].clone(); + + assert!( + AccountForTests::pool_definition_remove_successful().account == *pool_post.account() + ); + + let chained_call_lp = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + let chained_call_a = chained_calls[2].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_remove_token_a()); + assert!(chained_call_b == ChainedCallForTests::cc_remove_token_b()); + assert!(chained_call_lp == ChainedCallForTests::cc_remove_pool_lp()); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_add_liquidity_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountForTests::pool_definition_init()]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_add_liquidity_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_add_liquidity_with_invalid_number_of_accounts_3() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_add_liquidity_with_invalid_number_of_accounts_4() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_add_liquidity_with_invalid_number_of_accounts_5() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_add_liquidity_with_invalid_number_of_accounts_6() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Invalid number of input balances")] + #[test] + fn test_call_add_liquidity_invalid_number_of_balances_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity(&pre_states, &[BalanceForTests::add_min_amount_lp()]); + } + + #[should_panic(expected = "Invalid number of input balances")] + #[test] + fn test_call_add_liquidity_invalid_number_of_balances_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + ], + ); + } + + #[should_panic(expected = "Vault A was not provided")] + #[test] + fn test_call_add_liquidity_vault_a_omitted() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Vault B was not provided")] + #[test] + fn test_call_add_liquidity_vault_b_omitted() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "LP definition mismatch")] + #[test] + fn test_call_add_liquidity_lp_definition_mismatch() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Both max-balances must be nonzero")] + #[test] + fn test_call_add_liquidity_zero_balance_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + 0, + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Both max-balances must be nonzero")] + #[test] + fn test_call_add_liquidity_zero_balance_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + 0, + BalanceForTests::add_max_amount_a(), + ], + ); + } + + #[should_panic(expected = "Min-lp must be nonzero")] + #[test] + fn test_call_add_liquidity_zero_min_lp() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + 0, + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Vaults' balances must be at least the reserve amounts")] + #[test] + fn test_call_add_liquidity_vault_insufficient_balance_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init_zero(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + BalanceForTests::add_min_amount_lp(), + ], + ); + } + + #[should_panic(expected = "Vaults' balances must be at least the reserve amounts")] + #[test] + fn test_call_add_liquidity_vault_insufficient_balance_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init_zero(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + BalanceForTests::add_min_amount_lp(), + ], + ); + } + + #[should_panic(expected = "A trade amount is 0")] + #[test] + fn test_call_add_liquidity_actual_amount_zero_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init_reserve_a_low(), + AccountForTests::vault_a_init_low(), + AccountForTests::vault_b_init_high(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "A trade amount is 0")] + #[test] + fn test_call_add_liquidity_actual_amount_zero_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init_reserve_b_low(), + AccountForTests::vault_a_init_high(), + AccountForTests::vault_b_init_low(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a_low(), + BalanceForTests::add_max_amount_b_low(), + ], + ); + } + + #[should_panic(expected = "Reserves must be nonzero")] + #[test] + fn test_call_add_liquidity_reserves_zero_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init_reserve_a_zero(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Reserves must be nonzero")] + #[test] + fn test_call_add_liquidity_reserves_zero_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init_reserve_b_zero(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + } + + #[should_panic(expected = "Payable LP must be nonzero")] + #[test] + fn test_call_add_liquidity_payable_lp_zero() { + let pre_states = vec![ + AccountForTests::pool_definition_add_zero_lp(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let _post_states = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a_low(), + BalanceForTests::add_max_amount_b_low(), + ], + ); + } + + #[test] + fn test_call_add_liquidity_chained_call_successsful() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::pool_lp_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + AccountForTests::user_holding_lp_init(), + ]; + let (post_states, chained_calls) = add_liquidity( + &pre_states, + &[ + BalanceForTests::add_min_amount_lp(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_b(), + ], + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_add_successful().account == *pool_post.account()); + + let chained_call_lp = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + let chained_call_a = chained_calls[2].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_add_token_a()); + assert!(chained_call_b == ChainedCallForTests::cc_add_token_b()); + assert!(chained_call_lp == ChainedCallForTests::cc_add_pool_lp()); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_swap_with_invalid_number_of_accounts_1() { + let pre_states = vec![AccountForTests::pool_definition_init()]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_swap_with_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_swap_with_invalid_number_of_accounts_3() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_swap_with_invalid_number_of_accounts_4() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Invalid number of amounts provided")] + #[test] + fn test_call_swap_with_invalid_number_of_amounts() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[BalanceForTests::add_max_amount_a()], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "AccountId is not a token type for the pool")] + #[test] + fn test_call_swap_incorrect_token_type() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_lp_definition_id(), + ); + } + + #[should_panic(expected = "Vault A was not provided")] + #[test] + fn test_call_swap_vault_a_omitted() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_with_wrong_id(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Vault B was not provided")] + #[test] + fn test_call_swap_vault_b_omitted() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_with_wrong_id(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Reserve for Token A exceeds vault balance")] + #[test] + fn test_call_swap_reserves_vault_mismatch_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init_low(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Reserve for Token B exceeds vault balance")] + #[test] + fn test_call_swap_reserves_vault_mismatch_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init_low(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Pool is inactive")] + #[test] + fn test_call_swap_ianctive() { + let pre_states = vec![ + AccountForTests::pool_definition_inactive(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[should_panic(expected = "Withdraw amount is less than minimal amount out")] + #[test] + fn test_call_swap_below_min_out() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let _post_states = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_a_definition_id(), + ); + } + + #[test] + fn test_call_swap_chained_call_successful_1() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let (post_states, chained_calls) = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_a(), + BalanceForTests::add_max_amount_a_low(), + ], + IdForTests::token_a_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_swap_test_1().account == *pool_post.account()); + + let chained_call_a = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_swap_token_a_test_1()); + assert!(chained_call_b == ChainedCallForTests::cc_swap_token_b_test_1()); + } + + #[test] + fn test_call_swap_chained_call_successful_2() { + let pre_states = vec![ + AccountForTests::pool_definition_init(), + AccountForTests::vault_a_init(), + AccountForTests::vault_b_init(), + AccountForTests::user_holding_a(), + AccountForTests::user_holding_b(), + ]; + let (post_states, chained_calls) = swap( + &pre_states, + &[ + BalanceForTests::add_max_amount_b(), + BalanceForTests::min_amount_out(), + ], + IdForTests::token_b_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!(AccountForTests::pool_definition_swap_test_2().account == *pool_post.account()); + + let chained_call_a = chained_calls[1].clone(); + let chained_call_b = chained_calls[0].clone(); + + assert!(chained_call_a == ChainedCallForTests::cc_swap_token_a_test_2()); + assert!(chained_call_b == ChainedCallForTests::cc_swap_token_b_test_2()); + } +} diff --git a/program_methods/guest/src/bin/pinata_token.rs b/program_methods/guest/src/bin/pinata_token.rs index f988be9..0461379 100644 --- a/program_methods/guest/src/bin/pinata_token.rs +++ b/program_methods/guest/src/bin/pinata_token.rs @@ -82,7 +82,7 @@ fn main() { let winner_token_holding_post = winner_token_holding.account.clone(); pinata_definition_post.data = data.next_data(); - let mut instruction_data: [u8; 23] = [0; 23]; + let mut instruction_data = vec![0; 23]; instruction_data[0] = 1; instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes()); diff --git a/program_methods/guest/src/bin/token.rs b/program_methods/guest/src/bin/token.rs index 3fdfc7a..0f7b628 100644 --- a/program_methods/guest/src/bin/token.rs +++ b/program_methods/guest/src/bin/token.rs @@ -36,17 +36,115 @@ use nssa_core::{ // * An instruction data byte string of length 23, indicating the balance to mint with the // folloiwng layout // [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. +// 6. New token definition with metadata. Arguments to this function are: +// * Three **default** accounts: [definition_account, metadata_account. holding_account]. The +// first default account will be initialized with the token definition account values. The +// second account will be initialized to a token metadata account for the new token +// definition. The third account will be initialized to a token holding account for the new +// token, holding the entire total supply. +// * An instruction data of 474-bytes, indicating the token name, total supply, token standard, +// metadata standard and metadata_values (uri and creators). the following layout: [0x05 || +// total_supply (little-endian 16 bytes) || name (6 bytes) || token_standard || +// metadata_standard || metadata_values] The name cannot be equal to [0x00, 0x00, 0x00, 0x00, +// 0x00, 0x00] +// 7. Print NFT copy from Master NFT Arguments to this function are: +// * Two accounts: [master_nft, printed_account (default)]. +// * Authorization required: master_nft +// * An dummy byte string of length 23, with the following layout [0x06 || 0x00 || 0x00 || 0x00 +// || ... || 0x00 || 0x00]. +const TOKEN_STANDARD_FUNGIBLE_TOKEN: u8 = 0; +const TOKEN_STANDARD_FUNGIBLE_ASSET: u8 = 1; +const TOKEN_STANDARD_NONFUNGIBLE: u8 = 2; +const TOKEN_STANDARD_NONFUNGIBLE_PRINTABLE: u8 = 3; -const TOKEN_DEFINITION_TYPE: u8 = 0; -const TOKEN_DEFINITION_DATA_SIZE: usize = 23; +const METADATA_TYPE_SIMPLE: u8 = 0; +const METADATA_TYPE_EXPANDED: u8 = 1; + +const TOKEN_DEFINITION_DATA_SIZE: usize = 55; + +const TOKEN_HOLDING_STANDARD: u8 = 1; +const TOKEN_HOLDING_NFT_MASTER: u8 = 2; +const TOKEN_HOLDING_NFT_PRINTED_COPY: u8 = 3; -const TOKEN_HOLDING_TYPE: u8 = 1; const TOKEN_HOLDING_DATA_SIZE: usize = 49; +const CURRENT_VERSION: u8 = 1; + +const TOKEN_METADATA_DATA_SIZE: usize = 463; + +fn is_token_standard_valid(standard: u8) -> bool { + matches!( + standard, + TOKEN_STANDARD_FUNGIBLE_TOKEN + | TOKEN_STANDARD_FUNGIBLE_ASSET + | TOKEN_STANDARD_NONFUNGIBLE + | TOKEN_STANDARD_NONFUNGIBLE_PRINTABLE + ) +} + +fn is_metadata_type_valid(standard: u8) -> bool { + matches!(standard, METADATA_TYPE_SIMPLE | METADATA_TYPE_EXPANDED) +} + +fn is_token_holding_type_valid(standard: u8) -> bool { + matches!(standard, |TOKEN_HOLDING_STANDARD| TOKEN_HOLDING_NFT_MASTER + | TOKEN_HOLDING_NFT_PRINTED_COPY) +} struct TokenDefinition { account_type: u8, name: [u8; 6], total_supply: u128, + metadata_id: AccountId, +} + +impl TokenDefinition { + fn into_data(self) -> Data { + let mut bytes = Vec::::new(); + bytes.extend_from_slice(&[self.account_type]); + bytes.extend_from_slice(&self.name); + bytes.extend_from_slice(&self.total_supply.to_le_bytes()); + bytes.extend_from_slice(&self.metadata_id.to_bytes()); + + if bytes.len() != TOKEN_DEFINITION_DATA_SIZE { + panic!("Invalid Token Definition data"); + } + + Data::try_from(bytes).expect("Token definition data size must fit into data") + } + + fn parse(data: &Data) -> Option { + let data = Vec::::from(data.clone()); + + if data.len() != TOKEN_DEFINITION_DATA_SIZE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().expect("Name must be a 6 bytes"); + let total_supply = u128::from_le_bytes( + data[7..23] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + let metadata_id = AccountId::new( + data[23..TOKEN_DEFINITION_DATA_SIZE] + .try_into() + .expect("Token Program expects valid Account Id for Metadata"), + ); + + let this = Some(Self { + account_type, + name, + total_supply, + metadata_id, + }); + + match account_type { + TOKEN_STANDARD_NONFUNGIBLE if total_supply != 1 => None, + TOKEN_STANDARD_FUNGIBLE_TOKEN if metadata_id != AccountId::new([0; 32]) => None, + _ => this, + } + } + } } struct TokenHolding { @@ -55,49 +153,24 @@ struct TokenHolding { balance: u128, } -impl TokenDefinition { - fn into_data(self) -> Data { - let mut bytes = [0; TOKEN_DEFINITION_DATA_SIZE]; - bytes[0] = self.account_type; - bytes[1..7].copy_from_slice(&self.name); - bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); - bytes - .to_vec() - .try_into() - .expect("23 bytes should fit into Data") - } - - fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { - None - } else { - let account_type = data[0]; - let name = data[1..7].try_into().unwrap(); - let total_supply = u128::from_le_bytes( - data[7..] - .try_into() - .expect("Total supply must be 16 bytes little-endian"), - ); - Some(Self { - account_type, - name, - total_supply, - }) - } - } -} - impl TokenHolding { - fn new(definition_id: AccountId) -> Self { + fn new(definition_id: &AccountId) -> Self { Self { - account_type: TOKEN_HOLDING_TYPE, - definition_id, + account_type: TOKEN_HOLDING_STANDARD, + definition_id: *definition_id, balance: 0, } } - fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { + fn parse(data: &Data) -> Option { + let data = Vec::::from(data.clone()); + + if data.len() != TOKEN_HOLDING_DATA_SIZE { + return None; + } + + // Check account_type + if !is_token_holding_type_valid(data[0]) { return None; } @@ -112,6 +185,7 @@ impl TokenHolding { .try_into() .expect("balance must be 16 bytes little-endian"), ); + Some(Self { definition_id, balance, @@ -120,14 +194,52 @@ impl TokenHolding { } fn into_data(self) -> Data { - let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE]; - bytes[0] = self.account_type; - bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); - bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); - bytes - .to_vec() - .try_into() - .expect("33 bytes should fit into Data") + if !is_token_holding_type_valid(self.account_type) { + panic!("Invalid Token Holding type"); + } + + let mut bytes = Vec::::new(); + bytes.extend_from_slice(&[self.account_type]); + bytes.extend_from_slice(&self.definition_id.to_bytes()); + bytes.extend_from_slice(&self.balance.to_le_bytes()); + + if bytes.len() != TOKEN_HOLDING_DATA_SIZE { + panic!("Invalid Token Holding data"); + } + + Data::try_from(bytes).expect("Invalid data") + } +} + +struct TokenMetadata { + account_type: u8, + version: u8, + definition_id: AccountId, + uri: [u8; 200], + creators: [u8; 250], + /// Block id + primary_sale_date: u64, +} + +impl TokenMetadata { + fn into_data(self) -> Data { + if !is_metadata_type_valid(self.account_type) { + panic!("Invalid Metadata type"); + } + + let mut bytes = Vec::::new(); + bytes.extend_from_slice(&[self.account_type]); + bytes.extend_from_slice(&[self.version]); + bytes.extend_from_slice(&self.definition_id.to_bytes()); + bytes.extend_from_slice(&self.uri); + bytes.extend_from_slice(&self.creators); + bytes.extend_from_slice(&self.primary_sale_date.to_le_bytes()); + + if bytes.len() != TOKEN_METADATA_DATA_SIZE { + panic!("Invalid Token Definition data length"); + } + + Data::try_from(bytes).expect("Invalid data") } } @@ -138,10 +250,14 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec Vec Vec (TokenHolding, TokenHolding) { + let mut sender_holding = sender_holding; + let mut recipient_holding = recipient_holding; + + if sender_holding.balance < balance_to_move { + panic!("Insufficient balance"); + } + + sender_holding.balance = sender_holding + .balance + .checked_sub(balance_to_move) + .expect("Checked above"); + recipient_holding.balance = recipient_holding + .balance + .checked_add(balance_to_move) + .expect("Recipient balance overflow"); + + recipient_holding.account_type = sender_holding.account_type; + + (sender_holding, recipient_holding) +} + +fn nft_master_transfer( + sender_holding: TokenHolding, + recipient_holding: TokenHolding, + balance_to_move: u128, +) -> (TokenHolding, TokenHolding) { + let mut sender_holding = sender_holding; + let mut recipient_holding = recipient_holding; + + if recipient_holding.balance != 0 { + panic!("Invalid balance in recipient account for NFT transfer"); + } + + if sender_holding.balance != balance_to_move { + panic!("Invalid balance for NFT Master transfer"); + } + + sender_holding.balance = 0; + recipient_holding.balance = balance_to_move; + recipient_holding.account_type = sender_holding.account_type; + + (sender_holding, recipient_holding) +} + fn new_definition( pre_states: &[AccountWithMetadata], name: [u8; 6], @@ -193,6 +351,7 @@ fn new_definition( if pre_states.len() != 2 { panic!("Invalid number of input accounts"); } + let definition_target_account = &pre_states[0]; let holding_target_account = &pre_states[1]; @@ -205,13 +364,14 @@ fn new_definition( } let token_definition = TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, + account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, name, total_supply, + metadata_id: AccountId::new([0; 32]), }; let token_holding = TokenHolding { - account_type: TOKEN_HOLDING_TYPE, + account_type: TOKEN_HOLDING_STANDARD, definition_id: definition_target_account.account_id, balance: total_supply, }; @@ -228,6 +388,99 @@ fn new_definition( ] } +fn new_definition_with_metadata( + pre_states: &[AccountWithMetadata], + name: [u8; 6], + total_supply: u128, + token_standard: u8, + metadata_standard: u8, + metadata_values: &Data, +) -> Vec { + if pre_states.len() != 3 { + panic!("Invalid number of input accounts"); + } + + let definition_target_account = &pre_states[0]; + let metadata_target_account = &pre_states[1]; + let holding_target_account = &pre_states[2]; + + if definition_target_account.account != Account::default() { + panic!("Definition target account must have default values"); + } + + if metadata_target_account.account != Account::default() { + panic!("Metadata target account must have default values"); + } + + if holding_target_account.account != Account::default() { + panic!("Holding target account must have default values"); + } + + if !is_token_standard_valid(token_standard) { + panic!("Invalid Token Standard provided"); + } + + if !is_metadata_type_valid(metadata_standard) { + panic!("Invalid Metadata Standadard provided"); + } + + if !valid_total_supply_for_token_standard(total_supply, token_standard) { + panic!("Invalid total supply for the specified token supply"); + } + + let token_definition = TokenDefinition { + account_type: token_standard, + name, + total_supply, + metadata_id: metadata_target_account.account_id, + }; + + let token_holding = TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: definition_target_account.account_id, + balance: total_supply, + }; + + if metadata_values.len() != 450 { + panic!("Metadata values data should be 450 bytes"); + } + + let uri: [u8; 200] = metadata_values[0..200] + .try_into() + .expect("Token program expects valid uri for Metadata"); + let creators: [u8; 250] = metadata_values[200..450] + .try_into() + .expect("Token program expects valid creators for Metadata"); + + let token_metadata = TokenMetadata { + account_type: metadata_standard, + version: CURRENT_VERSION, + definition_id: definition_target_account.account_id, + uri, + creators, + primary_sale_date: 0u64, // TODO #261: future works to implement this + }; + + let mut definition_target_account_post = definition_target_account.account.clone(); + definition_target_account_post.data = token_definition.into_data(); + + let mut holding_target_account_post = holding_target_account.account.clone(); + holding_target_account_post.data = token_holding.into_data(); + + let mut metadata_target_account_post = metadata_target_account.account.clone(); + metadata_target_account_post.data = token_metadata.into_data(); + + vec![ + AccountPostState::new_claimed(definition_target_account_post), + AccountPostState::new_claimed(holding_target_account_post), + AccountPostState::new_claimed(metadata_target_account_post), + ] +} + +fn valid_total_supply_for_token_standard(total_supply: u128, token_standard: u8) -> bool { + token_standard != TOKEN_STANDARD_NONFUNGIBLE || total_supply == 1 +} + fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec { if pre_states.len() != 2 { panic!("Invalid number of accounts"); @@ -246,7 +499,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec Vec Vec Vec bool { + account_type != TOKEN_STANDARD_NONFUNGIBLE +} + fn mint_additional_supply( pre_states: &[AccountWithMetadata], amount_to_mint: u128, @@ -329,13 +587,17 @@ fn mint_additional_supply( TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); let token_holding_values: TokenHolding = if token_holding.account == Account::default() { - TokenHolding::new(definition.account_id) + TokenHolding::new(&definition.account_id) } else { TokenHolding::parse(&token_holding.account.data).expect("Holding account must be valid") }; + if !is_mintable(definition_values.account_type) { + panic!("Token Definition's standard does not permit minting additional supply"); + } + if definition.account_id != token_holding_values.definition_id { - panic!("Mismatch token definition and token holding"); + panic!("Mismatch Token Definition and Token Holding"); } let token_holding_post_data = TokenHolding { @@ -356,6 +618,7 @@ fn mint_additional_supply( account_type: definition_values.account_type, name: definition_values.name, total_supply: post_total_supply, + metadata_id: definition_values.metadata_id, }; let post_definition = { @@ -378,7 +641,60 @@ fn mint_additional_supply( vec![post_definition, token_holding_post] } -type Instruction = [u8; 23]; +fn print_nft(pre_states: &[AccountWithMetadata]) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let master_account = &pre_states[0]; + let printed_account = &pre_states[1]; + + if !master_account.is_authorized { + panic!("Master NFT Account must be authorized"); + } + + if printed_account.account != Account::default() { + panic!("Printed Account must be uninitialized"); + } + + let mut master_account_data = + TokenHolding::parse(&master_account.account.data).expect("Invalid Token Holding data"); + + if master_account_data.account_type != TOKEN_HOLDING_NFT_MASTER { + panic!("Invalid Token Holding provided as NFT Master Account"); + } + + if master_account_data.balance < 2 { + panic!("Insufficient balance to print another NFT copy"); + } + + let definition_id = master_account_data.definition_id; + + let post_master_account = { + let mut this = master_account.account.clone(); + master_account_data.balance -= 1; + this.data = master_account_data.into_data(); + AccountPostState::new(this) + }; + + let post_printed_account = { + let mut this = printed_account.account.clone(); + + let printed_data = TokenHolding { + account_type: TOKEN_HOLDING_NFT_PRINTED_COPY, + definition_id, + balance: 1, + }; + + this.data = TokenHolding::into_data(printed_data); + + AccountPostState::new_claimed(this) + }; + + vec![post_master_account, post_printed_account] +} + +type Instruction = Vec; fn main() { let ( @@ -455,6 +771,48 @@ fn main() { // Execute mint_additional_supply(&pre_states, balance_to_mint) } + 5 => { + if instruction.len() != 474 { + panic!("Invalid instruction length") + } + + // Parse instruction + let total_supply = u128::from_le_bytes( + instruction[1..17] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + let name = instruction[17..23] + .try_into() + .expect("Name must be 6 bytes long"); + assert_ne!(name, [0; 6]); + let token_standard = instruction[23]; + let metadata_standard = instruction[24]; + let metadata_values: Data = + Data::try_from(instruction[25..474].to_vec()).expect("Invalid metadata"); + + // Execute + new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ) + } + 6 => { + if instruction.len() != 23 { + panic!("Invalid instruction length"); + } + + // Initialize account + if instruction[1..] != [0; 22] { + panic!("Invalid instruction for initialize account"); + } + + print_nft(&pre_states) + } _ => panic!("Invalid instruction"), }; @@ -463,14 +821,545 @@ fn main() { #[cfg(test)] mod tests { - use nssa_core::account::{Account, AccountId, AccountWithMetadata}; + use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data}; use crate::{ - TOKEN_DEFINITION_DATA_SIZE, TOKEN_DEFINITION_TYPE, TOKEN_HOLDING_DATA_SIZE, - TOKEN_HOLDING_TYPE, TokenDefinition, TokenHolding, burn, initialize_account, - mint_additional_supply, new_definition, transfer, + TOKEN_DEFINITION_DATA_SIZE, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_NFT_MASTER, + TOKEN_HOLDING_NFT_PRINTED_COPY, TOKEN_HOLDING_STANDARD, TOKEN_STANDARD_FUNGIBLE_TOKEN, + TOKEN_STANDARD_NONFUNGIBLE, TokenDefinition, TokenHolding, burn, mint_additional_supply, + new_definition, new_definition_with_metadata, print_nft, transfer, }; + struct BalanceForTests; + struct IdForTests; + + struct AccountForTests; + + impl AccountForTests { + fn definition_account_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, + name: [2; 6], + total_supply: BalanceForTests::init_supply(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_without_auth() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, + name: [2; 6], + total_supply: BalanceForTests::init_supply(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_different_definition() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id_diff(), + balance: BalanceForTests::holding_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_same_definition_with_authorization() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_same_definition_without_authorization() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_same_definition_without_authorization_overflow() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn definition_account_post_burn() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, + name: [2; 6], + total_supply: BalanceForTests::init_supply_burned(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_account_post_burn() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance_burned(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: IdForTests::holding_id_2(), + } + } + + fn init_mint() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::mint_success(), + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_same_definition_mint() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::holding_balance_mint(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_mint() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, + name: [2; 6], + total_supply: BalanceForTests::init_supply_mint(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_same_definition_with_authorization_and_large_balance() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::mint_overflow(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_with_authorization_nonfungible() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_STANDARD_NONFUNGIBLE, + name: [2; 6], + total_supply: 1, + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn definition_account_uninit() -> AccountWithMetadata { + AccountWithMetadata { + account: Account::default(), + is_authorized: false, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_account_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn definition_account_unclaimed() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: TokenDefinition::into_data(TokenDefinition { + account_type: TOKEN_STANDARD_FUNGIBLE_TOKEN, + name: [2; 6], + total_supply: BalanceForTests::init_supply(), + metadata_id: AccountId::new([0; 32]), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn holding_account_unclaimed() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account2_init() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::init_supply(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + + fn holding_account2_init_post_transfer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::recipient_post_transfer(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + + fn holding_account_init_post_transfer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_STANDARD, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::sender_post_transfer(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_master_nft() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_NFT_MASTER, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::printable_copies(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_master_nft_insufficient_balance() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_NFT_MASTER, + definition_id: IdForTests::pool_definition_id(), + balance: 1, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_master_nft_after_print() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_NFT_MASTER, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::printable_copies() - 1, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_printed_nft() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_NFT_PRINTED_COPY, + definition_id: IdForTests::pool_definition_id(), + balance: 1, + }), + nonce: 0, + }, + is_authorized: false, + account_id: IdForTests::holding_id(), + } + } + + fn holding_account_with_master_nft_transferred_to() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [0u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_NFT_MASTER, + definition_id: IdForTests::pool_definition_id(), + balance: BalanceForTests::printable_copies(), + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id_2(), + } + } + + fn holding_account_master_nft_post_transfer() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: [5u32; 8], + balance: 0u128, + data: TokenHolding::into_data(TokenHolding { + account_type: TOKEN_HOLDING_NFT_MASTER, + definition_id: IdForTests::pool_definition_id(), + balance: 0, + }), + nonce: 0, + }, + is_authorized: true, + account_id: IdForTests::holding_id(), + } + } + } + + impl BalanceForTests { + fn init_supply() -> u128 { + 100_000 + } + + fn holding_balance() -> u128 { + 1_000 + } + + fn init_supply_burned() -> u128 { + 99_500 + } + + fn holding_balance_burned() -> u128 { + 500 + } + + fn burn_success() -> u128 { + 500 + } + + fn burn_insufficient() -> u128 { + 1_500 + } + + fn mint_success() -> u128 { + 50_000 + } + + fn holding_balance_mint() -> u128 { + 51_000 + } + + fn mint_overflow() -> u128 { + u128::MAX - 40_000 + } + + fn init_supply_mint() -> u128 { + 150_000 + } + + fn sender_post_transfer() -> u128 { + 95_000 + } + + fn recipient_post_transfer() -> u128 { + 105_000 + } + + fn transfer_amount() -> u128 { + 5_000 + } + + fn printable_copies() -> u128 { + 10 + } + } + + impl IdForTests { + fn pool_definition_id() -> AccountId { + AccountId::new([15; 32]) + } + + fn pool_definition_id_diff() -> AccountId { + AccountId::new([16; 32]) + } + + fn holding_id() -> AccountId { + AccountId::new([17; 32]) + } + + fn holding_id_2() -> AccountId { + AccountId::new([42; 32]) + } + } + #[should_panic(expected = "Invalid number of input accounts")] #[test] fn test_call_new_definition_with_invalid_number_of_accounts_1() { @@ -550,40 +1439,19 @@ mod tests { #[test] fn test_new_definition_with_valid_inputs_succeeds() { let pre_states = vec![ - AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: AccountId::new([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - ]), - }, - AccountWithMetadata { - account: Account { - ..Account::default() - }, - is_authorized: false, - account_id: AccountId::new([2; 32]), - }, + AccountForTests::definition_account_uninit(), + AccountForTests::holding_account_uninit(), ]; - let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10); + let post_states = new_definition(&pre_states, [2u8; 6], BalanceForTests::init_supply()); + let [definition_account, holding_account] = post_states.try_into().ok().unwrap(); - assert_eq!( - definition_account.account().data.as_ref(), - &[ - 0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0 - ] - ); - assert_eq!( - holding_account.account().data.as_ref(), - &[ - 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, - 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0 - ] + assert!( + *definition_account.account() + == AccountForTests::definition_account_unclaimed().account ); + + assert!(*holding_account.account() == AccountForTests::holding_account_unclaimed().account); } #[should_panic(expected = "Invalid number of input accounts")] @@ -623,14 +1491,13 @@ mod tests { #[should_panic(expected = "Invalid sender data")] #[test] fn test_transfer_invalid_instruction_type_should_fail() { - let invalid_type = TOKEN_HOLDING_TYPE ^ 1; + let invalid_type = TOKEN_HOLDING_STANDARD ^ 1; let pre_states = vec![ AccountWithMetadata { account: Account { - // First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts - data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE] - .try_into() - .unwrap(), + // First byte should be `TOKEN_HOLDING_STANDARD` for token holding accounts + data: Data::try_from(vec![invalid_type; TOKEN_HOLDING_DATA_SIZE]) + .expect("Invalid data"), ..Account::default() }, is_authorized: true, @@ -652,7 +1519,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1].try_into().unwrap(), + data: Data::try_from(vec![1; TOKEN_HOLDING_DATA_SIZE - 1]).unwrap(), ..Account::default() }, is_authorized: true, @@ -674,7 +1541,7 @@ mod tests { AccountWithMetadata { account: Account { // Data must be of exact length `TOKEN_HOLDING_DATA_SIZE` - data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1].try_into().unwrap(), + data: Data::try_from(vec![1; TOKEN_HOLDING_DATA_SIZE - 1]).unwrap(), ..Account::default() }, is_authorized: true, @@ -693,27 +1560,8 @@ mod tests { #[test] fn test_transfer_with_different_definition_ids_should_fail() { let pre_states = vec![ - AccountWithMetadata { - account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account { - data: [1] - .into_iter() - .chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1]) - .collect::>() - .try_into() - .unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, + AccountForTests::holding_same_definition_with_authorization(), + AccountForTests::holding_different_definition(), ]; let _post_states = transfer(&pre_states, 10); } @@ -722,46 +1570,25 @@ mod tests { #[test] fn test_transfer_with_insufficient_balance_should_fail() { let pre_states = vec![ - AccountWithMetadata { - account: Account { - // Account with balance 37 - data: [1; TOKEN_HOLDING_DATA_SIZE - 16] - .into_iter() - .chain(u128::to_le_bytes(37)) - .collect::>() - .try_into() - .unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, + AccountForTests::holding_same_definition_with_authorization(), + AccountForTests::holding_account_same_definition_mint(), ]; // Attempt to transfer 38 tokens - let _post_states = transfer(&pre_states, 38); + let _post_states = transfer(&pre_states, BalanceForTests::burn_insufficient()); } #[should_panic(expected = "Sender authorization is missing")] #[test] fn test_transfer_without_sender_authorization_should_fail() { + let mut def_data = Vec::::new(); + def_data.extend_from_slice(&[1; TOKEN_DEFINITION_DATA_SIZE - 16]); + def_data.extend_from_slice(&u128::to_le_bytes(37)); + let pre_states = vec![ AccountWithMetadata { account: Account { // Account with balance 37 - data: [1; TOKEN_HOLDING_DATA_SIZE - 16] - .into_iter() - .chain(u128::to_le_bytes(37)) - .collect::>() - .try_into() - .unwrap(), + data: Data::try_from(def_data).unwrap(), ..Account::default() }, is_authorized: false, @@ -769,7 +1596,7 @@ mod tests { }, AccountWithMetadata { account: Account { - data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(), + data: Data::try_from(vec![1; TOKEN_HOLDING_DATA_SIZE - 1]).unwrap(), ..Account::default() }, is_authorized: true, @@ -782,511 +1609,229 @@ mod tests { #[test] fn test_transfer_with_valid_inputs_succeeds() { let pre_states = vec![ - AccountWithMetadata { - account: Account { - // Account with balance 37 - data: [1; TOKEN_HOLDING_DATA_SIZE - 16] - .into_iter() - .chain(u128::to_le_bytes(37)) - .collect::>() - .try_into() - .unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account { - // Account with balance 255 - data: [1; TOKEN_HOLDING_DATA_SIZE - 16] - .into_iter() - .chain(u128::to_le_bytes(255)) - .collect::>() - .try_into() - .unwrap(), - ..Account::default() - }, - is_authorized: true, - account_id: AccountId::new([2; 32]), - }, + AccountForTests::holding_account_init(), + AccountForTests::holding_account2_init(), ]; - let post_states = transfer(&pre_states, 11); + let post_states = transfer(&pre_states, BalanceForTests::transfer_amount()); let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); - assert_eq!( - sender_post.account().data.as_ref(), - [ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + + assert!( + *sender_post.account() == AccountForTests::holding_account_init_post_transfer().account ); - assert_eq!( - recipient_post.account().data.as_ref(), - [ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + assert!( + *recipient_post.account() + == AccountForTests::holding_account2_init_post_transfer().account + ); + } + + #[should_panic(expected = "Invalid balance for NFT Master transfer")] + #[test] + fn test_transfer_with_master_nft_invalid_balance() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft(), + AccountForTests::holding_account_uninit(), + ]; + let _post_states = transfer(&pre_states, BalanceForTests::transfer_amount()); + } + + #[should_panic(expected = "Invalid balance in recipient account for NFT transfer")] + #[test] + fn test_transfer_with_master_nft_invalid_recipient_balance() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft(), + AccountForTests::holding_account_with_master_nft_transferred_to(), + ]; + let _post_states = transfer(&pre_states, BalanceForTests::printable_copies()); + } + + #[test] + fn test_transfer_with_master_nft_success() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft(), + AccountForTests::holding_account_uninit(), + ]; + let post_states = transfer(&pre_states, BalanceForTests::printable_copies()); + let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); + + assert!( + *sender_post.account() + == AccountForTests::holding_account_master_nft_post_transfer().account + ); + assert!( + *recipient_post.account() + == AccountForTests::holding_account_with_master_nft_transferred_to().account ); } #[test] fn test_token_initialize_account_succeeds() { let pre_states = vec![ - AccountWithMetadata { - account: Account { - // Definition ID with - data: [0; TOKEN_DEFINITION_DATA_SIZE - 16] - .into_iter() - .chain(u128::to_le_bytes(1000)) - .collect::>() - .try_into() - .unwrap(), - ..Account::default() - }, - is_authorized: false, - account_id: AccountId::new([1; 32]), - }, - AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: AccountId::new([2; 32]), - }, + AccountForTests::holding_account_init(), + AccountForTests::holding_account2_init(), ]; - let post_states = initialize_account(&pre_states); - let [definition, holding] = post_states.try_into().ok().unwrap(); - assert_eq!( - definition.account().data.as_ref(), - pre_states[0].account.data.as_ref() + let post_states = transfer(&pre_states, BalanceForTests::transfer_amount()); + let [sender_post, recipient_post] = post_states.try_into().ok().unwrap(); + + assert!( + *sender_post.account() == AccountForTests::holding_account_init_post_transfer().account ); - assert_eq!( - holding.account().data.as_ref(), - [ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] + assert!( + *recipient_post.account() + == AccountForTests::holding_account2_init_post_transfer().account ); } - enum BalanceEnum { - InitSupply, - HoldingBalance, - InitSupplyBurned, - HoldingBalanceBurned, - BurnSuccess, - BurnInsufficient, - MintSuccess, - InitSupplyMint, - HoldingBalanceMint, - MintOverflow, - } - - enum AccountsEnum { - DefinitionAccountAuth, - DefinitionAccountNotAuth, - HoldingDiffDef, - HoldingSameDefAuth, - HoldingSameDefNotAuth, - HoldingSameDefNotAuthOverflow, - DefinitionAccountPostBurn, - HoldingAccountPostBurn, - Uninit, - InitMint, - DefinitionAccountMint, - HoldingSameDefMint, - HoldingSameDefAuthLargeBalance, - } - - enum IdEnum { - PoolDefinitionId, - PoolDefinitionIdDiff, - HoldingId, - } - - fn helper_account_constructor(selection: AccountsEnum) -> AccountWithMetadata { - match selection { - AccountsEnum::DefinitionAccountAuth => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupply), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::PoolDefinitionId), - }, - AccountsEnum::DefinitionAccountNotAuth => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupply), - }), - nonce: 0, - }, - is_authorized: false, - account_id: helper_id_constructor(IdEnum::PoolDefinitionId), - }, - AccountsEnum::HoldingDiffDef => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionIdDiff), - balance: helper_balance_constructor(BalanceEnum::HoldingBalance), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::HoldingSameDefAuth => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalance), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::HoldingSameDefNotAuth => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalance), - }), - nonce: 0, - }, - is_authorized: false, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::HoldingSameDefNotAuthOverflow => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::InitSupply), - }), - nonce: 0, - }, - is_authorized: false, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::DefinitionAccountPostBurn => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupplyBurned), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::PoolDefinitionId), - }, - AccountsEnum::HoldingAccountPostBurn => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalanceBurned), - }), - nonce: 0, - }, - is_authorized: false, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::Uninit => AccountWithMetadata { - account: Account::default(), - is_authorized: false, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::InitMint => AccountWithMetadata { - account: Account { - program_owner: [0u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::MintSuccess), - }), - nonce: 0, - }, - is_authorized: false, - account_id: helper_id_constructor(IdEnum::HoldingId), - }, - AccountsEnum::HoldingSameDefMint => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::HoldingBalanceMint), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::PoolDefinitionId), - }, - AccountsEnum::DefinitionAccountMint => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenDefinition::into_data(TokenDefinition { - account_type: TOKEN_DEFINITION_TYPE, - name: [2; 6], - total_supply: helper_balance_constructor(BalanceEnum::InitSupplyMint), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::PoolDefinitionId), - }, - AccountsEnum::HoldingSameDefAuthLargeBalance => AccountWithMetadata { - account: Account { - program_owner: [5u32; 8], - balance: 0u128, - data: TokenHolding::into_data(TokenHolding { - account_type: TOKEN_HOLDING_TYPE, - definition_id: helper_id_constructor(IdEnum::PoolDefinitionId), - balance: helper_balance_constructor(BalanceEnum::MintOverflow), - }), - nonce: 0, - }, - is_authorized: true, - account_id: helper_id_constructor(IdEnum::PoolDefinitionId), - }, - } - } - - fn helper_balance_constructor(selection: BalanceEnum) -> u128 { - match selection { - BalanceEnum::InitSupply => 100_000, - BalanceEnum::HoldingBalance => 1_000, - BalanceEnum::InitSupplyBurned => 99_500, - BalanceEnum::HoldingBalanceBurned => 500, - BalanceEnum::BurnSuccess => 500, - BalanceEnum::BurnInsufficient => 1_500, - BalanceEnum::MintSuccess => 50_000, - BalanceEnum::InitSupplyMint => 150_000, - BalanceEnum::HoldingBalanceMint => 51_000, - BalanceEnum::MintOverflow => u128::MAX - 40_000, - } - } - - fn helper_id_constructor(selection: IdEnum) -> AccountId { - match selection { - IdEnum::PoolDefinitionId => AccountId::new([15; 32]), - IdEnum::PoolDefinitionIdDiff => AccountId::new([16; 32]), - IdEnum::HoldingId => AccountId::new([17; 32]), - } - } - #[test] #[should_panic(expected = "Invalid number of accounts")] fn test_burn_invalid_number_of_accounts() { - let pre_states = vec![helper_account_constructor( - AccountsEnum::DefinitionAccountAuth, - )]; - let _post_states = burn( - &pre_states, - helper_balance_constructor(BalanceEnum::BurnSuccess), - ); + let pre_states = vec![AccountForTests::definition_account_auth()]; + let _post_states = burn(&pre_states, BalanceForTests::burn_success()); } #[test] - #[should_panic(expected = "Mismatch token definition and token holding")] + #[should_panic(expected = "Mismatch Token Definition and Token Holding")] fn test_burn_mismatch_def() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingDiffDef), + AccountForTests::definition_account_auth(), + AccountForTests::holding_different_definition(), ]; - let _post_states = burn( - &pre_states, - helper_balance_constructor(BalanceEnum::BurnSuccess), - ); + let _post_states = burn(&pre_states, BalanceForTests::burn_success()); } #[test] #[should_panic(expected = "Authorization is missing")] fn test_burn_missing_authorization() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_without_authorization(), ]; - let _post_states = burn( - &pre_states, - helper_balance_constructor(BalanceEnum::BurnSuccess), - ); + let _post_states = burn(&pre_states, BalanceForTests::burn_success()); } #[test] #[should_panic(expected = "Insufficient balance to burn")] fn test_burn_insufficient_balance() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefAuth), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_with_authorization(), ]; - let _post_states = burn( - &pre_states, - helper_balance_constructor(BalanceEnum::BurnInsufficient), - ); + let _post_states = burn(&pre_states, BalanceForTests::burn_insufficient()); } #[test] #[should_panic(expected = "Total supply underflow")] fn test_burn_total_supply_underflow() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefAuthLargeBalance), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_with_authorization_and_large_balance(), ]; - let _post_states = burn( - &pre_states, - helper_balance_constructor(BalanceEnum::MintOverflow), - ); + let _post_states = burn(&pre_states, BalanceForTests::mint_overflow()); } #[test] fn test_burn_success() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefAuth), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_with_authorization(), ]; - let post_states = burn( - &pre_states, - helper_balance_constructor(BalanceEnum::BurnSuccess), - ); + let post_states = burn(&pre_states, BalanceForTests::burn_success()); let def_post = post_states[0].clone(); let holding_post = post_states[1].clone(); - assert!( - *def_post.account() - == helper_account_constructor(AccountsEnum::DefinitionAccountPostBurn).account - ); - assert!( - *holding_post.account() - == helper_account_constructor(AccountsEnum::HoldingAccountPostBurn).account - ); + assert!(*def_post.account() == AccountForTests::definition_account_post_burn().account); + assert!(*holding_post.account() == AccountForTests::holding_account_post_burn().account); } #[test] #[should_panic(expected = "Invalid number of accounts")] - fn test_mint_invalid_number_of_accounts() { - let pre_states = vec![helper_account_constructor( - AccountsEnum::DefinitionAccountAuth, - )]; - let _post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintSuccess), - ); + fn test_mint_invalid_number_of_accounts_1() { + let pre_states = vec![AccountForTests::definition_account_auth()]; + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); + } + + #[test] + #[should_panic(expected = "Invalid number of accounts")] + fn test_mint_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountForTests::definition_account_auth(), + AccountForTests::holding_account_same_definition_mint(), + AccountForTests::holding_same_definition_with_authorization(), + ]; + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); } #[test] #[should_panic(expected = "Holding account must be valid")] fn test_mint_not_valid_holding_account() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), + AccountForTests::definition_account_auth(), + AccountForTests::definition_account_without_auth(), ]; - let _post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintSuccess), - ); + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); + } + + #[test] + #[should_panic(expected = "Definition account must be valid")] + fn test_mint_not_valid_definition_account() { + let pre_states = vec![ + AccountForTests::holding_same_definition_with_authorization(), + AccountForTests::holding_same_definition_without_authorization(), + ]; + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); } #[test] #[should_panic(expected = "Definition authorization is missing")] fn test_mint_missing_authorization() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountNotAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + AccountForTests::definition_account_without_auth(), + AccountForTests::holding_same_definition_without_authorization(), ]; - let _post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintSuccess), - ); + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); } #[test] - #[should_panic(expected = "Mismatch token definition and token holding")] + #[should_panic(expected = "Mismatch Token Definition and Token Holding")] fn test_mint_mismatched_token_definition() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingDiffDef), + AccountForTests::definition_account_auth(), + AccountForTests::holding_different_definition(), ]; - let _post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintSuccess), - ); + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); } #[test] fn test_mint_success() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_without_authorization(), ]; - let post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintSuccess), - ); + let post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); let def_post = post_states[0].clone(); let holding_post = post_states[1].clone(); - assert!( - *def_post.account() - == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account - ); + assert!(*def_post.account() == AccountForTests::definition_account_mint().account); assert!( *holding_post.account() - == helper_account_constructor(AccountsEnum::HoldingSameDefMint).account + == AccountForTests::holding_account_same_definition_mint().account ); } #[test] fn test_mint_uninit_holding_success() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::Uninit), + AccountForTests::definition_account_auth(), + AccountForTests::holding_account_uninit(), ]; - let post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintSuccess), - ); + let post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); let def_post = post_states[0].clone(); let holding_post = post_states[1].clone(); - assert!( - *def_post.account() - == helper_account_constructor(AccountsEnum::DefinitionAccountMint).account - ); - assert!( - *holding_post.account() == helper_account_constructor(AccountsEnum::InitMint).account - ); + assert!(*def_post.account() == AccountForTests::definition_account_mint().account); + assert!(*holding_post.account() == AccountForTests::init_mint().account); assert!(holding_post.requires_claim()); } @@ -1294,25 +1839,488 @@ mod tests { #[should_panic(expected = "Total supply overflow")] fn test_mint_total_supply_overflow() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_without_authorization(), ]; - let _post_states = mint_additional_supply( - &pre_states, - helper_balance_constructor(BalanceEnum::MintOverflow), - ); + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_overflow()); } #[test] #[should_panic(expected = "New balance overflow")] fn test_mint_holding_account_overflow() { let pre_states = vec![ - helper_account_constructor(AccountsEnum::DefinitionAccountAuth), - helper_account_constructor(AccountsEnum::HoldingSameDefNotAuthOverflow), + AccountForTests::definition_account_auth(), + AccountForTests::holding_same_definition_without_authorization_overflow(), ]; - let _post_states = mint_additional_supply( + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_overflow()); + } + + #[test] + #[should_panic( + expected = "Token Definition's standard does not permit minting additional supply" + )] + fn test_mint_cannot_mint_unmintable_tokens() { + let pre_states = vec![ + AccountForTests::definition_account_with_authorization_nonfungible(), + AccountForTests::holding_same_definition_without_authorization(), + ]; + let _post_states = mint_additional_supply(&pre_states, BalanceForTests::mint_success()); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_metadata_with_invalid_number_of_accounts_1() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }]; + let _post_states = new_definition_with_metadata( &pre_states, - helper_balance_constructor(BalanceEnum::MintOverflow), + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, ); } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_metadata_with_invalid_number_of_accounts_2() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Invalid number of input accounts")] + #[test] + fn test_call_new_definition_metadata_with_invalid_number_of_accounts_3() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([4; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Definition target account must have default values")] + #[test] + fn test_call_new_definition_metadata_with_init_definition() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountForTests::definition_account_auth(), + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Metadata target account must have default values")] + #[test] + fn test_call_new_definition_metadata_with_init_metadata() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountForTests::holding_account_same_definition_mint(), + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Holding target account must have default values")] + #[test] + fn test_call_new_definition_metadata_with_init_holding() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountForTests::holding_account_same_definition_mint(), + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Metadata values data should be 450 bytes")] + #[test] + fn test_call_new_definition_metadata_with_too_short_metadata_length() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 449].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Metadata values data should be 450 bytes")] + #[test] + fn test_call_new_definition_metadata_with_too_long_metadata_length() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 451].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Invalid Token Standard provided")] + #[test] + fn test_call_new_definition_metadata_with_invalid_token_standard() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 14u8; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Invalid Metadata Standadard provided")] + #[test] + fn test_call_new_definition_metadata_with_invalid_metadata_standard() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = 0u8; + let metadata_standard = 14u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Invalid total supply for the specified token supply")] + #[test] + fn test_call_new_definition_metadata_invalid_supply_for_nonfungible() { + let name = [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe]; + let total_supply = 15u128; + let token_standard = TOKEN_STANDARD_NONFUNGIBLE; + let metadata_standard = 0u8; + let metadata_values: Data = Data::try_from([1u8; 450].to_vec()).unwrap(); + + let pre_states = vec![ + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([1; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([2; 32]), + }, + AccountWithMetadata { + account: Account::default(), + is_authorized: true, + account_id: AccountId::new([3; 32]), + }, + ]; + let _post_states = new_definition_with_metadata( + &pre_states, + name, + total_supply, + token_standard, + metadata_standard, + &metadata_values, + ); + } + + #[should_panic(expected = "Invalid number of accounts")] + #[test] + fn test_print_nft_invalid_number_of_accounts_1() { + let pre_states = vec![AccountForTests::holding_account_master_nft()]; + let _post_states = print_nft(&pre_states); + } + + #[should_panic(expected = "Invalid number of accounts")] + #[test] + fn test_print_nft_invalid_number_of_accounts_2() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft(), + AccountForTests::definition_account_auth(), + AccountForTests::holding_account_uninit(), + ]; + let _post_states = print_nft(&pre_states); + } + + #[should_panic(expected = "Master NFT Account must be authorized")] + #[test] + fn test_print_nft_master_account_must_be_authorized() { + let pre_states = vec![ + AccountForTests::holding_account_uninit(), + AccountForTests::holding_account_uninit(), + ]; + let _post_states = print_nft(&pre_states); + } + + #[should_panic(expected = "Printed Account must be uninitialized")] + #[test] + fn test_print_nft_print_account_initialized() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft(), + AccountForTests::holding_account_init(), + ]; + let _post_states = print_nft(&pre_states); + } + + #[should_panic(expected = "Invalid Token Holding data")] + #[test] + fn test_print_nft_master_nft_invalid_token_holding() { + let pre_states = vec![ + AccountForTests::definition_account_auth(), + AccountForTests::holding_account_uninit(), + ]; + let _post_states = print_nft(&pre_states); + } + + #[should_panic(expected = "Invalid Token Holding provided as NFT Master Account")] + #[test] + fn test_print_nft_master_nft_not_nft_master_account() { + let pre_states = vec![ + AccountForTests::holding_account_init(), + AccountForTests::holding_account_uninit(), + ]; + let _post_states = print_nft(&pre_states); + } + + #[should_panic(expected = "Insufficient balance to print another NFT copy")] + #[test] + fn test_print_nft_master_nft_insufficient_balance() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft_insufficient_balance(), + AccountForTests::holding_account_uninit(), + ]; + let _post_states = print_nft(&pre_states); + } + + #[test] + fn test_print_nft_success() { + let pre_states = vec![ + AccountForTests::holding_account_master_nft(), + AccountForTests::holding_account_uninit(), + ]; + let post_states = print_nft(&pre_states); + + let post_master_nft = post_states[0].account(); + let post_printed = post_states[1].account(); + + assert!( + *post_master_nft == AccountForTests::holding_account_master_nft_after_print().account + ); + assert!(*post_printed == AccountForTests::holding_account_printed_nft().account); + } } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 8e4a5cd..0794266 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -28,3 +28,4 @@ futures.workspace = true async-stream = "0.3.6" indicatif = { version = "0.18.3", features = ["improved_unicode"] } paste.workspace = true +risc0-zkvm.workspace = true diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 480480d..d10c8c5 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -3,70 +3,15 @@ use base58::ToBase58; use clap::Subcommand; use itertools::Itertools as _; use key_protocol::key_management::key_tree::chain_index::ChainIndex; -use nssa::{Account, AccountId, program::Program}; +use nssa::{Account, program::Program}; use serde::Serialize; use crate::{ - WalletCore, + TokenDefinition, TokenHolding, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, }; -const TOKEN_DEFINITION_TYPE: u8 = 0; -const TOKEN_DEFINITION_DATA_SIZE: usize = 23; - -const TOKEN_HOLDING_TYPE: u8 = 1; -const TOKEN_HOLDING_DATA_SIZE: usize = 49; - -struct TokenDefinition { - #[allow(unused)] - account_type: u8, - name: [u8; 6], - total_supply: u128, -} - -struct TokenHolding { - #[allow(unused)] - account_type: u8, - definition_id: AccountId, - balance: u128, -} - -impl TokenDefinition { - fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { - None - } else { - let account_type = data[0]; - let name = data[1..7].try_into().unwrap(); - let total_supply = u128::from_le_bytes(data[7..].try_into().unwrap()); - - Some(Self { - account_type, - name, - total_supply, - }) - } - } -} - -impl TokenHolding { - fn parse(data: &[u8]) -> Option { - if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { - None - } else { - let account_type = data[0]; - let definition_id = AccountId::new(data[1..33].try_into().unwrap()); - let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); - Some(Self { - definition_id, - balance, - account_type, - }) - } - } -} - /// Represents generic chain CLI subcommand #[derive(Subcommand, Debug, Clone)] pub enum AccountSubcommand { @@ -344,6 +289,8 @@ impl WalletSubcommand for AccountSubcommand { #[cfg(test)] mod tests { + use nssa::AccountId; + use crate::cli::account::{TokedDefinitionAccountView, TokenDefinition}; #[test] @@ -352,6 +299,7 @@ mod tests { account_type: 1, name: [137, 12, 14, 3, 5, 4], total_supply: 100, + metadata_id: AccountId::new([0; 32]), }; let token_def_view: TokedDefinitionAccountView = token_def.into(); @@ -365,6 +313,7 @@ mod tests { account_type: 1, name: [240, 159, 146, 150, 66, 66], total_supply: 100, + metadata_id: AccountId::new([0; 32]), }; let token_def_view: TokedDefinitionAccountView = token_def.into(); @@ -378,6 +327,7 @@ mod tests { account_type: 1, name: [78, 65, 77, 69, 0, 0], total_supply: 100, + metadata_id: AccountId::new([0; 32]), }; let token_def_view: TokedDefinitionAccountView = token_def.into(); 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..ce919b7 --- /dev/null +++ b/wallet/src/cli/programs/amm.rs @@ -0,0 +1,286 @@ +use anyhow::Result; +use clap::Subcommand; +use nssa::AccountId; + +use crate::{ + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, + helperfunctions::{AccountPrivacyKind, 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 pool + /// + /// user_holding_a and user_holding_b must be owned. + /// + /// Only public execution allowed + New { + /// 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 + /// + /// The account associated with swapping token must be owned + /// + /// Only public execution allowed + Swap { + /// 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 + /// + /// user_holding_a and user_holding_b must be owned. + /// + /// Only public execution allowed + AddLiquidity { + /// 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 + /// + /// user_holding_lp must be owned. + /// + /// Only public execution allowed + RemoveLiquidity { + /// 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)] + min_amount_a: u128, + #[arg(long)] + min_amount_b: u128, + }, +} + +impl WalletSubcommand for AmmProgramAgnosticSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + AmmProgramAgnosticSubcommand::New { + user_holding_a, + user_holding_b, + user_holding_lp, + balance_a, + balance_b, + } => { + let (user_holding_a, user_holding_a_privacy) = + parse_addr_with_privacy_prefix(&user_holding_a)?; + let (user_holding_b, user_holding_b_privacy) = + parse_addr_with_privacy_prefix(&user_holding_b)?; + let (user_holding_lp, user_holding_lp_privacy) = + parse_addr_with_privacy_prefix(&user_holding_lp)?; + + let user_holding_a: AccountId = user_holding_a.parse()?; + let user_holding_b: AccountId = user_holding_b.parse()?; + let user_holding_lp: AccountId = user_holding_lp.parse()?; + + match ( + user_holding_a_privacy, + user_holding_b_privacy, + user_holding_lp_privacy, + ) { + ( + AccountPrivacyKind::Public, + AccountPrivacyKind::Public, + AccountPrivacyKind::Public, + ) => { + Amm(wallet_core) + .send_new_definition( + user_holding_a, + user_holding_b, + user_holding_lp, + balance_a, + balance_b, + ) + .await?; + + Ok(SubcommandReturnValue::Empty) + } + _ => { + // ToDo: Implement after private multi-chain calls is available + anyhow::bail!("Only public execution allowed for Amm calls"); + } + } + } + AmmProgramAgnosticSubcommand::Swap { + user_holding_a, + user_holding_b, + amount_in, + min_amount_out, + token_definition, + } => { + let (user_holding_a, user_holding_a_privacy) = + parse_addr_with_privacy_prefix(&user_holding_a)?; + let (user_holding_b, user_holding_b_privacy) = + parse_addr_with_privacy_prefix(&user_holding_b)?; + + let user_holding_a: AccountId = user_holding_a.parse()?; + let user_holding_b: AccountId = user_holding_b.parse()?; + + match (user_holding_a_privacy, user_holding_b_privacy) { + (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { + Amm(wallet_core) + .send_swap( + user_holding_a, + user_holding_b, + amount_in, + min_amount_out, + token_definition.parse()?, + ) + .await?; + + Ok(SubcommandReturnValue::Empty) + } + _ => { + // ToDo: Implement after private multi-chain calls is available + anyhow::bail!("Only public execution allowed for Amm calls"); + } + } + } + AmmProgramAgnosticSubcommand::AddLiquidity { + user_holding_a, + user_holding_b, + user_holding_lp, + min_amount_lp, + max_amount_a, + max_amount_b, + } => { + let (user_holding_a, user_holding_a_privacy) = + parse_addr_with_privacy_prefix(&user_holding_a)?; + let (user_holding_b, user_holding_b_privacy) = + parse_addr_with_privacy_prefix(&user_holding_b)?; + let (user_holding_lp, user_holding_lp_privacy) = + parse_addr_with_privacy_prefix(&user_holding_lp)?; + + let user_holding_a: AccountId = user_holding_a.parse()?; + let user_holding_b: AccountId = user_holding_b.parse()?; + let user_holding_lp: AccountId = user_holding_lp.parse()?; + + match ( + user_holding_a_privacy, + user_holding_b_privacy, + user_holding_lp_privacy, + ) { + ( + AccountPrivacyKind::Public, + AccountPrivacyKind::Public, + AccountPrivacyKind::Public, + ) => { + Amm(wallet_core) + .send_add_liquidity( + user_holding_a, + user_holding_b, + user_holding_lp, + min_amount_lp, + max_amount_a, + max_amount_b, + ) + .await?; + + Ok(SubcommandReturnValue::Empty) + } + _ => { + // ToDo: Implement after private multi-chain calls is available + anyhow::bail!("Only public execution allowed for Amm calls"); + } + } + } + AmmProgramAgnosticSubcommand::RemoveLiquidity { + user_holding_a, + user_holding_b, + user_holding_lp, + balance_lp, + min_amount_a, + min_amount_b, + } => { + let (user_holding_a, user_holding_a_privacy) = + parse_addr_with_privacy_prefix(&user_holding_a)?; + let (user_holding_b, user_holding_b_privacy) = + parse_addr_with_privacy_prefix(&user_holding_b)?; + let (user_holding_lp, user_holding_lp_privacy) = + parse_addr_with_privacy_prefix(&user_holding_lp)?; + + let user_holding_a: AccountId = user_holding_a.parse()?; + let user_holding_b: AccountId = user_holding_b.parse()?; + let user_holding_lp: AccountId = user_holding_lp.parse()?; + + match ( + user_holding_a_privacy, + user_holding_b_privacy, + user_holding_lp_privacy, + ) { + ( + AccountPrivacyKind::Public, + AccountPrivacyKind::Public, + AccountPrivacyKind::Public, + ) => { + Amm(wallet_core) + .send_remove_liquidity( + user_holding_a, + user_holding_b, + user_holding_lp, + balance_lp, + min_amount_a, + min_amount_b, + ) + .await?; + + Ok(SubcommandReturnValue::Empty) + } + _ => { + // ToDo: Implement after private multi-chain calls is available + anyhow::bail!("Only public execution allowed for Amm calls"); + } + } + } + } + } +} diff --git a/wallet/src/cli/programs/mod.rs b/wallet/src/cli/programs/mod.rs index da7904d..e74a336 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 c6bb8c2..bad6435 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -18,7 +18,9 @@ use nssa::{ circuit::ProgramWithDependencies, message::EncryptedAccountData, }, }; -use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData}; +use nssa_core::{ + Commitment, MembershipProof, SharedSecretKey, account::Data, program::InstructionData, +}; pub use privacy_preserving_tx::PrivacyPreservingAccount; use tokio::io::AsyncWriteExt; @@ -45,6 +47,82 @@ pub enum AccDecodeData { Decode(nssa_core::SharedSecretKey, AccountId), } +const TOKEN_DEFINITION_DATA_SIZE: usize = 55; + +const TOKEN_HOLDING_TYPE: u8 = 1; +const TOKEN_HOLDING_DATA_SIZE: usize = 49; +const TOKEN_STANDARD_FUNGIBLE_TOKEN: u8 = 0; +const TOKEN_STANDARD_NONFUNGIBLE: u8 = 2; + +struct TokenDefinition { + #[allow(unused)] + account_type: u8, + name: [u8; 6], + total_supply: u128, + #[allow(unused)] + metadata_id: AccountId, +} + +struct TokenHolding { + #[allow(unused)] + account_type: u8, + definition_id: AccountId, + balance: u128, +} + +impl TokenDefinition { + fn parse(data: &Data) -> Option { + let data = Vec::::from(data.clone()); + + if data.len() != TOKEN_DEFINITION_DATA_SIZE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().expect("Name must be a 6 bytes"); + let total_supply = u128::from_le_bytes( + data[7..23] + .try_into() + .expect("Total supply must be 16 bytes little-endian"), + ); + let metadata_id = AccountId::new( + data[23..TOKEN_DEFINITION_DATA_SIZE] + .try_into() + .expect("Token Program expects valid Account Id for Metadata"), + ); + + let this = Some(Self { + account_type, + name, + total_supply, + metadata_id, + }); + + match account_type { + TOKEN_STANDARD_NONFUNGIBLE if total_supply != 1 => None, + TOKEN_STANDARD_FUNGIBLE_TOKEN if metadata_id != AccountId::new([0; 32]) => None, + _ => this, + } + } + } +} + +impl TokenHolding { + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE { + None + } else { + let account_type = data[0]; + let definition_id = AccountId::new(data[1..33].try_into().unwrap()); + let balance = u128::from_le_bytes(data[33..].try_into().unwrap()); + Some(Self { + definition_id, + balance, + account_type, + }) + } + } +} + pub struct WalletCore { pub storage: WalletChainStore, pub poller: TxPoller, @@ -251,7 +329,6 @@ impl WalletCore { } println!("Transaction data is {:?}", tx.message); - Ok(()) } @@ -292,7 +369,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(), &acc_manager.private_account_membership_proofs(), diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 96c1562..c063ca3 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}; @@ -27,6 +28,10 @@ impl PrivacyPreservingAccount { } } + pub fn is_public(&self) -> bool { + matches!(&self, Self::Public(_)) + } + pub fn is_private(&self) -> bool { matches!( &self, diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs new file mode 100644 index 0000000..3beb92c --- /dev/null +++ b/wallet/src/program_facades/amm.rs @@ -0,0 +1,539 @@ +use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; +use nssa::{AccountId, ProgramId, program::Program}; +use nssa_core::program::PdaSeed; + +use crate::{TokenHolding, WalletCore}; + +fn compute_pool_pda( + amm_program_id: ProgramId, + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, +) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), + )) +} + +fn compute_pool_pda_seed( + definition_token_a_id: AccountId, + definition_token_b_id: AccountId, +) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + 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; + 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; + let token_2 = definition_token_a_id; + break (token_1, token_2); + } + + if i == 32 { + panic!("Definitions match"); + } else { + i += 1; + } + }; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&token_1.to_bytes()); + bytes[32..].copy_from_slice(&token_2.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +fn compute_vault_pda( + amm_program_id: ProgramId, + pool_id: AccountId, + definition_token_id: AccountId, +) -> AccountId { + AccountId::from(( + &amm_program_id, + &compute_vault_pda_seed(pool_id, definition_token_id), + )) +} + +fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&definition_token_id.to_bytes()); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { + AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) +} + +fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed { + use risc0_zkvm::sha::{Impl, Sha256}; + + let mut bytes = [0; 64]; + bytes[0..32].copy_from_slice(&pool_id.to_bytes()); + bytes[32..].copy_from_slice(&[0; 32]); + + PdaSeed::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) +} + +pub struct Amm<'w>(pub &'w WalletCore); + +impl Amm<'_> { + pub async fn send_new_definition( + &self, + user_holding_a: AccountId, + user_holding_b: AccountId, + user_holding_lp: AccountId, + balance_a: u128, + balance_b: u128, + ) -> Result { + let (instruction, program) = amm_program_preparation_definition(balance_a, balance_b); + + let amm_program_id = Program::amm().id(); + + let user_a_acc = self + .0 + .get_account_public(user_holding_a) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + let user_b_acc = self + .0 + .get_account_public(user_holding_b) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + + let amm_pool = + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); + let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); + let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool); + + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ]; + + let nonces = self + .0 + .get_accounts_nonces(vec![user_holding_a, user_holding_b]) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let signing_key_a = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&user_holding_a) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let signing_key_b = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&user_holding_b) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message( + &message, + &[signing_key_a, signing_key_b], + ); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_swap( + &self, + user_holding_a: AccountId, + user_holding_b: AccountId, + 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); + + let amm_program_id = Program::amm().id(); + + let user_a_acc = self + .0 + .get_account_public(user_holding_a) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + let user_b_acc = self + .0 + .get_account_public(user_holding_b) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id; + + let amm_pool = + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); + let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); + let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + user_holding_a, + user_holding_b, + ]; + + let account_id_auth; + + // Checking, which account are associated with TokenDefinition + let token_holder_acc_a = self + .0 + .get_account_public(user_holding_a) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + let token_holder_acc_b = self + .0 + .get_account_public(user_holding_b) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let token_holder_a = TokenHolding::parse(&token_holder_acc_a.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))?; + let token_holder_b = TokenHolding::parse(&token_holder_acc_b.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_b))?; + + if token_holder_a.definition_id == token_definition_id { + account_id_auth = user_holding_a; + } else if token_holder_b.definition_id == token_definition_id { + account_id_auth = user_holding_b; + } else { + return Err(ExecutionFailureKind::AccountDataError(token_definition_id)); + } + + let nonces = self + .0 + .get_accounts_nonces(vec![account_id_auth]) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let signing_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&account_id_auth) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + 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_add_liquidity( + &self, + user_holding_a: AccountId, + user_holding_b: AccountId, + user_holding_lp: AccountId, + 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); + + let amm_program_id = Program::amm().id(); + + let user_a_acc = self + .0 + .get_account_public(user_holding_a) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + let user_b_acc = self + .0 + .get_account_public(user_holding_b) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + + let amm_pool = + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); + let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); + let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool); + + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ]; + + let nonces = self + .0 + .get_accounts_nonces(vec![user_holding_a, user_holding_b]) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let signing_key_a = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&user_holding_a) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let signing_key_b = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&user_holding_b) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message( + &message, + &[signing_key_a, signing_key_b], + ); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn send_remove_liquidity( + &self, + user_holding_a: AccountId, + user_holding_b: AccountId, + user_holding_lp: AccountId, + balance_lp: u128, + min_amount_a: u128, + min_amount_b: u128, + ) -> Result { + let (instruction, program) = + amm_program_preparation_remove_liq(balance_lp, min_amount_a, min_amount_b); + + let amm_program_id = Program::amm().id(); + + let user_a_acc = self + .0 + .get_account_public(user_holding_a) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + let user_b_acc = self + .0 + .get_account_public(user_holding_b) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let definition_token_a_id = TokenHolding::parse(&user_a_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + let definition_token_b_id = TokenHolding::parse(&user_b_acc.data) + .ok_or(ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id; + + let amm_pool = + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); + let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); + let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + let pool_lp = compute_liquidity_token_pda(amm_program_id, amm_pool); + + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + pool_lp, + user_holding_a, + user_holding_b, + user_holding_lp, + ]; + + let nonces = self + .0 + .get_accounts_nonces(vec![user_holding_lp]) + .await + .map_err(|_| ExecutionFailureKind::SequencerError)?; + + let signing_key_lp = self + .0 + .storage + .user_data + .get_pub_account_signing_key(&user_holding_lp) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key_lp]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.0.sequencer_client.send_tx_public(tx).await?) + } +} + +fn amm_program_preparation_definition(balance_a: u128, balance_b: u128) -> (Vec, 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::amm().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 = instruction.to_vec(); + let program = Program::amm(); + + (instruction_data, program) +} + +fn amm_program_preparation_swap( + amount_in: u128, + min_amount_out: u128, + token_definition_id: AccountId, +) -> (Vec, 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[0] = 0x01; + 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 = instruction.to_vec(); + let program = Program::amm(); + + (instruction_data, program) +} + +fn amm_program_preparation_add_liq( + min_amount_lp: u128, + max_amount_a: u128, + max_amount_b: u128, +) -> (Vec, 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 = instruction.to_vec(); + let program = Program::amm(); + + (instruction_data, program) +} + +fn amm_program_preparation_remove_liq( + balance_lp: u128, + min_amount_a: u128, + min_amount_b: u128, +) -> (Vec, 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(&min_amount_a.to_le_bytes()); + instruction[33..49].copy_from_slice(&min_amount_b.to_le_bytes()); + + let instruction_data = instruction.to_vec(); + let program = Program::amm(); + + (instruction_data, program) +} diff --git a/wallet/src/program_facades/mod.rs b/wallet/src/program_facades/mod.rs index e48442f..c9eed32 100644 --- a/wallet/src/program_facades/mod.rs +++ b/wallet/src/program_facades/mod.rs @@ -7,6 +7,7 @@ use nssa_core::program::InstructionData; use crate::{AccDecodeData, PrivacyPreservingAccount, WalletCore}; +pub mod amm; pub mod native_token_transfer; pub mod pinata; pub mod token; diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 551074f..e9ba997 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -17,7 +17,7 @@ impl Token<'_> { let account_ids = vec![definition_account_id, supply_account_id]; let program_id = nssa::program::Program::token().id(); // Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] - let mut instruction = [0; 23]; + let mut instruction = vec![0u8; 23]; instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); instruction[17..].copy_from_slice(&name); let message = nssa::public_transaction::Message::try_new( @@ -45,7 +45,7 @@ impl Token<'_> { let program_id = nssa::program::Program::token().id(); // Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || // 0x00 || 0x00 || 0x00]. - let mut instruction = [0; 23]; + let mut instruction = vec![0u8; 23]; instruction[0] = 0x01; instruction[1..17].copy_from_slice(&amount.to_le_bytes()); let Ok(nonces) = self.0.get_accounts_nonces(vec![sender_account_id]).await else {