diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml deleted file mode 100644 index d0622a6712..0000000000 --- a/.github/workflows/examples.yml +++ /dev/null @@ -1,59 +0,0 @@ -on: - push: - branches: - - main - paths: - - "examples/token-escrow/**" - - pull_request: - branches: - - main - paths: - - "examples/token-escrow/**" - - types: - - opened - - synchronize - - reopened - - ready_for_review - -name: examples/token-escrow - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - if: github.event.pull_request.draft == false - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v4 - with: - submodules: true - - - name: Cache .local directory - uses: actions/cache@v3 - with: - path: .local - key: ${{ runner.os }}-local-${{ hashFiles('**/install.sh') }} - - - name: Install dependencies - shell: bash - run: | - ./scripts/install.sh - source ./scripts/devenv.sh - - - name: Build - run: | - cd gnark-prover - go build - - - name: Build and test - run: | - source ./scripts/devenv.sh - anchor build - cp third-party/solana-program-library/spl_noop.so target/deploy/spl_noop.so - cd examples/token-escrow/programs/token-escrow - cargo test-sbf diff --git a/.github/workflows/light-system-programs-tests.yml b/.github/workflows/light-system-programs-tests.yml index 55ac219373..80c6fbc4cc 100644 --- a/.github/workflows/light-system-programs-tests.yml +++ b/.github/workflows/light-system-programs-tests.yml @@ -33,19 +33,27 @@ jobs: include: - program: account-compression sub-tests: '[ - "@lightprotocol/programs:test-account-compression" + "cargo-test-sbf -p account-compression" ]' - program: light-compressed-pda sub-tests: '[ - "@lightprotocol/programs:test-compressed-pda" + "cargo-test-sbf -p light-compressed-pda -- --test-threads=1" ]' - program: light-registry sub-tests: '[ - "@lightprotocol/programs:test-registry" + "cargo-test-sbf -p light-registry" ]' - program: light-compressed-token sub-tests: '[ - "@lightprotocol/programs:test-compressed-token" + "cargo-test-sbf -p light-compressed-token -- --test-threads=1" + ]' + - program: token-escrow + sub-tests: '[ + "cargo-test-sbf -p token-escrow" + ]' + - program: program-owned-account-test + sub-tests: '[ + "cargo-test-sbf -p program-owned-account-test" ]' steps: @@ -65,8 +73,9 @@ jobs: anchor build - IFS=', ' read -r -a sub_tests <<< "${{ join(fromJSON(matrix['sub-tests']), ', ') }}" + IFS=',' read -r -a sub_tests <<< "${{ join(fromJSON(matrix['sub-tests']), ', ') }}" for subtest in "${sub_tests[@]}" do - npx nx run "$subtest" + echo "$subtest" + eval "$subtest" done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 225500ef16..60c6db87c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: elif [[ "$CRATE_NAME" == *"light-compressed-pda"* ]]; then ARTIFACT="light_compressed_pda.so" elif [[ "$CRATE_NAME" == *"light-compressed-token"* ]]; then - ARTIFACT="psp_compressed_token.so" + ARTIFACT="light_compressed_token.so" elif [[ "$CRATE_NAME" == *"light-user-registry"* ]]; then ARTIFACT="light_user_registry.so" elif [[ "$CRATE_NAME" == *"light-registry"* ]]; then diff --git a/Cargo.lock b/Cargo.lock index b593a2adf4..0901ea0373 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2829,6 +2829,7 @@ dependencies = [ "light-compressed-token", "light-hash-set", "light-hasher", + "light-indexed-merkle-tree", "light-macros", "light-merkle-tree-reference", "light-registry", @@ -3710,6 +3711,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "program-owned-account-test" +version = "0.1.0" +dependencies = [ + "account-compression", + "anchor-lang", + "anchor-spl", + "light-circuitlib-rs", + "light-compressed-pda", + "light-compressed-token", + "light-hasher", + "light-test-utils", + "light-utils", + "num-bigint 0.4.4", + "num-traits", + "reqwest", + "solana-program-test", + "solana-sdk", + "spl-token 3.5.0", + "tokio", +] + [[package]] name = "qstring" version = "0.7.2" @@ -6531,6 +6554,7 @@ dependencies = [ "light-circuitlib-rs", "light-compressed-pda", "light-compressed-token", + "light-hasher", "light-test-utils", "num-bigint 0.4.4", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 2813a0de5f..d468bf8f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "utils", "xtask", "examples/token-escrow/programs/*", + "test-programs/*", ] [profile.release] diff --git a/circuit-lib/circuitlib-rs/src/gnark/helpers.rs b/circuit-lib/circuitlib-rs/src/gnark/helpers.rs index c58a431096..727ae4f375 100644 --- a/circuit-lib/circuitlib-rs/src/gnark/helpers.rs +++ b/circuit-lib/circuitlib-rs/src/gnark/helpers.rs @@ -14,7 +14,7 @@ use sysinfo::{Signal, System}; use crate::gnark::constants::{HEALTH_CHECK, SERVER_ADDRESS}; static IS_LOADING: AtomicBool = AtomicBool::new(false); - +#[derive(Debug)] pub enum ProofType { Inclusion, NonInclusion, diff --git a/examples/token-escrow/package.json b/examples/token-escrow/package.json index 02bbea3f8a..43131561e7 100644 --- a/examples/token-escrow/package.json +++ b/examples/token-escrow/package.json @@ -1,7 +1,8 @@ { "scripts": { "lint:fix": "prettier \"*/**/*{.js,.ts}\" -w", - "lint": "prettier \"*/**/*{.js,.ts}\" --check" + "lint": "prettier \"*/**/*{.js,.ts}\" --check", + "test": "cargo test-sbf -p token-escrow -- --test-threads 1" }, "dependencies": { "@coral-xyz/anchor": "^0.29.0" diff --git a/examples/token-escrow/programs/token-escrow/Cargo.toml b/examples/token-escrow/programs/token-escrow/Cargo.toml index 0e81e75517..70ed8ce61b 100644 --- a/examples/token-escrow/programs/token-escrow/Cargo.toml +++ b/examples/token-escrow/programs/token-escrow/Cargo.toml @@ -22,6 +22,7 @@ anchor-lang ={ version="0.29.0", features = ["init-if-needed"] } light-compressed-token = { path = "../../../../programs/compressed-token" , features = ["cpi"]} light-compressed-pda = { path = "../../../../programs/compressed-pda" , features = ["cpi"]} account-compression = { path = "../../../../programs/account-compression" , features = ["cpi"] } +light-hasher = { path = "../../../../merkle-tree/hasher" } [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = "1.18.11" diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs new file mode 100644 index 0000000000..675c43943f --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs @@ -0,0 +1,245 @@ +use crate::{create_change_output_compressed_token_account, EscrowTimeLock}; +use anchor_lang::prelude::*; +use light_compressed_pda::{ + compressed_account::{ + derive_address, CompressedAccount, CompressedAccountData, PackedMerkleContext, + }, + compressed_cpi::CompressedCpiContext, + utils::CompressedProof, + InstructionDataTransfer, NewAddressParamsPacked, +}; +use light_compressed_token::{ + CompressedTokenInstructionDataTransfer, InputTokenDataWithContext, TokenTransferOutputData, +}; +use light_hasher::{errors::HasherError, DataHasher, Hasher, Poseidon}; + +/// create compressed pda data +/// transfer tokens +/// execute complete transaction +pub fn process_escrow_compressed_tokens_with_compressed_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + lock_up_time: u64, + escrow_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, + new_address_params: NewAddressParamsPacked, + cpi_context: CompressedCpiContext, + bump: u8, +) -> Result<()> { + let compressed_pda = create_compressed_pda_data(lock_up_time, &ctx, &new_address_params)?; + let escrow_token_data = TokenTransferOutputData { + amount: escrow_amount, + owner: ctx.accounts.token_owner_pda.key(), + lamports: None, + }; + let change_token_data = create_change_output_compressed_token_account( + &input_token_data_with_context, + &[escrow_token_data], + &ctx.accounts.signer.key(), + ); + let output_compressed_accounts = vec![escrow_token_data, change_token_data]; + + cpi_compressed_token_transfer_pda( + &ctx, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + root_indices, + proof.clone(), + &cpi_context, + )?; + msg!("escrow compressed tokens with compressed pda"); + cpi_compressed_pda_transfer( + &ctx, + proof, + new_address_params, + compressed_pda, + cpi_context, + bump, + )?; + Ok(()) +} + +fn cpi_compressed_pda_transfer<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + proof: Option, + new_address_params: NewAddressParamsPacked, + compressed_pda: CompressedAccount, + cpi_context: CompressedCpiContext, + bump: u8, +) -> Result<()> { + let bump = &[bump]; + let signer_bytes = ctx.accounts.signer.key.to_bytes(); + let seeds = [b"escrow".as_slice(), signer_bytes.as_slice(), bump]; + let inputs_struct = InstructionDataTransfer { + relay_fee: None, + input_compressed_accounts_with_merkle_context: Vec::new(), + output_compressed_accounts: vec![compressed_pda], + input_root_indices: Vec::new(), + output_state_merkle_tree_account_indices: vec![0], + proof, + new_address_params: vec![new_address_params], + compression_lamports: None, + is_compress: false, + signer_seeds: Some(seeds.iter().map(|x| x.to_vec()).collect::>>()), + }; + + let mut inputs = Vec::new(); + InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { + signer: ctx.accounts.token_owner_pda.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + invoking_program: Some(ctx.accounts.self_program.to_account_info()), + compressed_sol_pda: None, + compression_recipient: None, + system_program: None, + cpi_signature_account: Some( + ctx.remaining_accounts[cpi_context.cpi_signature_account_index as usize] + .to_account_info(), + ), + }; + let seeds = [seeds.as_slice()]; + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_pda_program.to_account_info(), + cpi_accounts, + &seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} + +fn create_compressed_pda_data( + lock_up_time: u64, + ctx: &Context<'_, '_, '_, '_, EscrowCompressedTokensWithCompressedPda<'_>>, + new_address_params: &NewAddressParamsPacked, +) -> Result { + let current_slot = Clock::get()?.slot; + let timelock_compressed_pda = EscrowTimeLock { + slot: current_slot.checked_add(lock_up_time).unwrap(), + }; + let compressed_account_data = CompressedAccountData { + discriminator: 1u64.to_le_bytes(), + data: timelock_compressed_pda.try_to_vec().unwrap(), + data_hash: timelock_compressed_pda.hash().map_err(ProgramError::from)?, + }; + let derive_address = derive_address( + &ctx.remaining_accounts[new_address_params.address_merkle_tree_account_index as usize] + .key(), + &new_address_params.seed, + ) + .map_err(|_| ProgramError::InvalidArgument)?; + Ok(CompressedAccount { + owner: crate::ID, + lamports: 0, + address: Some(derive_address), + data: Some(compressed_account_data), + }) +} + +impl light_hasher::DataHasher for EscrowTimeLock { + fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { + Poseidon::hash(&self.slot.to_le_bytes()) + } +} + +#[derive(Accounts)] +pub struct EscrowCompressedTokensWithCompressedPda<'info> { + #[account(mut)] + pub signer: Signer<'info>, + /// CHECK: + #[account(seeds = [b"escrow".as_slice(), signer.key.to_bytes().as_slice()], bump)] + pub token_owner_pda: AccountInfo<'info>, + pub compressed_token_program: + Program<'info, light_compressed_token::program::LightCompressedToken>, + pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, + pub account_compression_program: + Program<'info, account_compression::program::AccountCompression>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub compressed_token_cpi_authority_pda: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: + pub noop_program: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::TokenEscrow>, +} +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedInputCompressedPda { + pub old_lock_up_time: u64, + pub new_lock_up_time: u64, + pub address: [u8; 32], + pub merkle_context: PackedMerkleContext, +} +// TODO: add functionality to deposit into an existing escrow account +#[inline(never)] +pub fn cpi_compressed_token_transfer_pda<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + output_state_merkle_tree_account_indices: Vec, + root_indices: Vec, + proof: Option, + cpi_context: &CompressedCpiContext, +) -> Result<()> { + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + is_compress: false, + compression_amount: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.signer.to_account_info(), + authority: ctx.accounts.signer.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + self_program: ctx.accounts.compressed_token_program.to_account_info(), + cpi_authority_pda: ctx + .accounts + .compressed_token_cpi_authority_pda + .to_account_info(), + compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), + token_pool_pda: None, + decompress_token_account: None, + token_program: None, + }; + + let mut cpi_ctx = CpiContext::new( + ctx.accounts.compressed_token_program.to_account_info(), + cpi_accounts, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + let cpi_context = CompressedCpiContext { + cpi_signature_account_index: cpi_context.cpi_signature_account_index, + execute: false, + }; + light_compressed_token::cpi::transfer(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/mod.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/mod.rs new file mode 100644 index 0000000000..d0b6284591 --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/mod.rs @@ -0,0 +1,5 @@ +pub mod escrow; +pub mod sdk; +pub mod withdrawal; + +pub use escrow::*; diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs new file mode 100644 index 0000000000..f8a7d3f73e --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/sdk.rs @@ -0,0 +1,234 @@ +#![cfg(not(target_os = "solana"))] + +use crate::escrow_with_compressed_pda::escrow::PackedInputCompressedPda; +use account_compression::{Pubkey, NOOP_PROGRAM_ID}; +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressed_pda::{ + compressed_account::{pack_merkle_context, MerkleContext}, + compressed_cpi::CompressedCpiContext, + pack_new_address_params, + utils::CompressedProof, + NewAddressParams, +}; +use light_compressed_token::{ + transfer_sdk::{create_inputs_and_remaining_accounts_checked, to_account_metas}, + TokenTransferOutputData, +}; +use solana_sdk::instruction::Instruction; + +#[derive(Debug, Clone)] +pub struct CreateCompressedPdaEscrowInstructionInputs<'a> { + pub lock_up_time: u64, + pub signer: &'a Pubkey, + pub input_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], + pub nullifier_array_pubkeys: &'a [Pubkey], + pub output_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], + pub output_compressed_accounts: &'a [TokenTransferOutputData], + pub root_indices: &'a [u16], + pub leaf_indices: &'a [u32], + pub proof: &'a CompressedProof, + pub input_token_data: &'a [light_compressed_token::TokenData], + pub mint: &'a Pubkey, + pub new_address_params: NewAddressParams, + pub cpi_signature_account: &'a Pubkey, +} + +pub fn create_escrow_instruction( + input_params: CreateCompressedPdaEscrowInstructionInputs, + escrow_amount: u64, +) -> Instruction { + let token_owner_pda = get_token_owner_pda(input_params.signer); + let (mut remaining_accounts, inputs) = create_inputs_and_remaining_accounts_checked( + input_params.input_compressed_account_merkle_tree_pubkeys, + input_params.leaf_indices, + input_params.input_token_data, + input_params.nullifier_array_pubkeys, + input_params.output_compressed_account_merkle_tree_pubkeys, + None, + input_params.output_compressed_accounts, + input_params.root_indices, + input_params.proof, + *input_params.mint, + input_params.signer, + false, + None, + ) + .unwrap(); + let new_address_params = + pack_new_address_params(&[input_params.new_address_params], &mut remaining_accounts); + let cpi_signature_account_index: u8 = + match remaining_accounts.get(input_params.cpi_signature_account) { + Some(entry) => (*entry).try_into().unwrap(), + None => { + remaining_accounts.insert( + *input_params.cpi_signature_account, + remaining_accounts.len(), + ); + (remaining_accounts.len() - 1) as u8 + } + }; + + let cpi_context = CompressedCpiContext { + execute: true, + cpi_signature_account_index, + }; + let instruction_data = crate::instruction::EscrowCompressedTokensWithCompressedPda { + lock_up_time: input_params.lock_up_time, + escrow_amount, + proof: Some(input_params.proof.clone()), + root_indices: input_params.root_indices.to_vec(), + mint: *input_params.mint, + signer_is_delegate: false, + input_token_data_with_context: inputs.input_token_data_with_context, + output_state_merkle_tree_account_indices: inputs.output_state_merkle_tree_account_indices, + new_address_params: new_address_params[0].clone(), + cpi_context, + bump: token_owner_pda.1, + }; + + let registered_program_pda = Pubkey::find_program_address( + &[light_compressed_pda::ID.to_bytes().as_slice()], + &account_compression::ID, + ) + .0; + let compressed_token_cpi_authority_pda = light_compressed_token::get_cpi_authority_pda().0; + let account_compression_authority = + light_compressed_pda::utils::get_cpi_authority_pda(&light_compressed_pda::ID); + + let accounts = crate::accounts::EscrowCompressedTokensWithCompressedPda { + signer: *input_params.signer, + noop_program: NOOP_PROGRAM_ID, + compressed_token_program: light_compressed_token::ID, + compressed_pda_program: light_compressed_pda::ID, + account_compression_program: account_compression::ID, + registered_program_pda, + compressed_token_cpi_authority_pda, + account_compression_authority, + self_program: crate::ID, + token_owner_pda: token_owner_pda.0, + }; + let remaining_accounts = to_account_metas(remaining_accounts); + + Instruction { + program_id: crate::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + + data: instruction_data.data(), + } +} + +pub fn get_token_owner_pda(signer: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[b"escrow".as_ref(), signer.to_bytes().as_ref()], + &crate::id(), + ) +} + +#[derive(Debug, Clone)] +pub struct CreateCompressedPdaWithdrawalInstructionInputs<'a> { + pub signer: &'a Pubkey, + pub input_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], + pub nullifier_array_pubkeys: &'a [Pubkey], + pub output_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], + pub output_compressed_accounts: &'a [TokenTransferOutputData], + pub root_indices: &'a [u16], + pub leaf_indices: &'a [u32], + pub proof: &'a CompressedProof, + pub input_token_data: &'a [light_compressed_token::TokenData], + pub mint: &'a Pubkey, + pub old_lock_up_time: u64, + pub new_lock_up_time: u64, + pub address: [u8; 32], + pub merkle_context: MerkleContext, + pub cpi_signature_account: &'a Pubkey, +} + +pub fn create_withdrawal_instruction( + input_params: CreateCompressedPdaWithdrawalInstructionInputs, + withdrawal_amount: u64, +) -> Instruction { + let (token_owner_pda, bump) = get_token_owner_pda(input_params.signer); + let (mut remaining_accounts, inputs) = create_inputs_and_remaining_accounts_checked( + input_params.input_compressed_account_merkle_tree_pubkeys, + &input_params.leaf_indices[1..], + input_params.input_token_data, + input_params.nullifier_array_pubkeys, + input_params.output_compressed_account_merkle_tree_pubkeys, + None, + input_params.output_compressed_accounts, + input_params.root_indices, + input_params.proof, + *input_params.mint, + &token_owner_pda, + false, + None, + ) + .unwrap(); + let merkle_context_packed = + pack_merkle_context(&[input_params.merkle_context], &mut remaining_accounts); + let cpi_signature_account_index: u8 = + match remaining_accounts.get(input_params.cpi_signature_account) { + Some(entry) => (*entry).try_into().unwrap(), + None => { + remaining_accounts.insert( + *input_params.cpi_signature_account, + remaining_accounts.len(), + ); + (remaining_accounts.len() - 1) as u8 + } + }; + + let cpi_context = CompressedCpiContext { + execute: true, + cpi_signature_account_index, + }; + + let input_compressed_pda = PackedInputCompressedPda { + old_lock_up_time: input_params.old_lock_up_time, + new_lock_up_time: input_params.new_lock_up_time, + address: input_params.address, + merkle_context: merkle_context_packed[0].clone(), + }; + let instruction_data = crate::instruction::WithdrawCompressedTokensWithCompressedPda { + proof: Some(input_params.proof.clone()), + root_indices: input_params.root_indices.to_vec(), + mint: *input_params.mint, + signer_is_delegate: false, + input_token_data_with_context: inputs.input_token_data_with_context, + output_state_merkle_tree_account_indices: inputs.output_state_merkle_tree_account_indices, + cpi_context, + input_compressed_pda, + withdrawal_amount, + bump, + }; + + let registered_program_pda = Pubkey::find_program_address( + &[light_compressed_pda::ID.to_bytes().as_slice()], + &account_compression::ID, + ) + .0; + let compressed_token_cpi_authority_pda = light_compressed_token::get_cpi_authority_pda().0; + let account_compression_authority = + light_compressed_pda::utils::get_cpi_authority_pda(&light_compressed_pda::ID); + + let accounts = crate::accounts::EscrowCompressedTokensWithCompressedPda { + signer: *input_params.signer, + noop_program: NOOP_PROGRAM_ID, + compressed_token_program: light_compressed_token::ID, + compressed_pda_program: light_compressed_pda::ID, + account_compression_program: account_compression::ID, + registered_program_pda, + compressed_token_cpi_authority_pda, + account_compression_authority, + self_program: crate::ID, + token_owner_pda, + }; + let remaining_accounts = to_account_metas(remaining_accounts); + + Instruction { + program_id: crate::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + + data: instruction_data.data(), + } +} diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs new file mode 100644 index 0000000000..0d0014e889 --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs @@ -0,0 +1,243 @@ +use anchor_lang::prelude::*; +use light_compressed_pda::{ + compressed_account::{ + CompressedAccount, CompressedAccountData, CompressedAccountWithMerkleContext, + }, + compressed_cpi::CompressedCpiContext, + utils::CompressedProof, + InstructionDataTransfer, +}; +use light_compressed_token::{ + CompressedTokenInstructionDataTransfer, InputTokenDataWithContext, TokenTransferOutputData, +}; +use light_hasher::DataHasher; + +use crate::{ + create_change_output_compressed_token_account, EscrowCompressedTokensWithCompressedPda, + EscrowError, EscrowTimeLock, PackedInputCompressedPda, +}; + +pub fn process_withdraw_compressed_tokens_with_compressed_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + withdrawal_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, + cpi_context: CompressedCpiContext, + input_compressed_pda: PackedInputCompressedPda, + bump: u8, +) -> Result<()> { + let current_slot = Clock::get()?.slot; + if current_slot < input_compressed_pda.old_lock_up_time { + return err!(EscrowError::EscrowLocked); + } + let (old_state, new_state) = create_compressed_pda_data_based_on_diff(&input_compressed_pda)?; + let withdrawal_token_data = TokenTransferOutputData { + amount: withdrawal_amount, + owner: ctx.accounts.signer.key(), + lamports: None, + }; + let escrow_change_token_data = create_change_output_compressed_token_account( + &input_token_data_with_context, + &[withdrawal_token_data], + &ctx.accounts.token_owner_pda.key(), + ); + let output_compressed_accounts = vec![withdrawal_token_data, escrow_change_token_data]; + cpi_compressed_token_withdrawal( + &ctx, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + vec![root_indices[1]], + proof.clone(), + &cpi_context, + bump, + )?; + + cpi_compressed_pda_withdrawal( + &ctx, + proof, + old_state, + new_state, + cpi_context, + vec![root_indices[0]], + bump, + )?; + Ok(()) +} + +fn create_compressed_pda_data_based_on_diff( + input_compressed_pda: &PackedInputCompressedPda, +) -> Result<(CompressedAccountWithMerkleContext, CompressedAccount)> { + let current_slot = Clock::get()?.slot; + + let old_timelock_compressed_pda = EscrowTimeLock { + slot: input_compressed_pda.old_lock_up_time, + }; + let old_compressed_account_data = CompressedAccountData { + discriminator: 1u64.to_le_bytes(), + data: old_timelock_compressed_pda.try_to_vec().unwrap(), + data_hash: old_timelock_compressed_pda + .hash() + .map_err(ProgramError::from)?, + }; + let old_compressed_account = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: Some(input_compressed_pda.address), + data: Some(old_compressed_account_data), + }; + let old_compressed_account_with_context = CompressedAccountWithMerkleContext { + compressed_account: old_compressed_account, + merkle_tree_pubkey_index: input_compressed_pda.merkle_context.merkle_tree_pubkey_index, + nullifier_queue_pubkey_index: input_compressed_pda + .merkle_context + .nullifier_queue_pubkey_index, + leaf_index: input_compressed_pda.merkle_context.leaf_index, + }; + let new_timelock_compressed_pda = EscrowTimeLock { + slot: current_slot + .checked_add(input_compressed_pda.new_lock_up_time) + .unwrap(), + }; + let new_compressed_account_data = CompressedAccountData { + discriminator: 1u64.to_le_bytes(), + data: new_timelock_compressed_pda.try_to_vec().unwrap(), + data_hash: new_timelock_compressed_pda + .hash() + .map_err(ProgramError::from)?, + }; + let new_state = CompressedAccount { + owner: crate::ID, + lamports: 0, + address: Some(input_compressed_pda.address), + data: Some(new_compressed_account_data), + }; + Ok((old_compressed_account_with_context, new_state)) +} + +fn cpi_compressed_pda_withdrawal<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + proof: Option, + old_state: CompressedAccountWithMerkleContext, + compressed_pda: CompressedAccount, + cpi_context: CompressedCpiContext, + root_indices: Vec, + bump: u8, +) -> Result<()> { + let bump = &[bump]; + let signer_bytes = ctx.accounts.signer.key.to_bytes(); + let seeds: [&[u8]; 3] = [b"escrow".as_slice(), signer_bytes.as_slice(), bump]; + let inputs_struct = InstructionDataTransfer { + relay_fee: None, + input_compressed_accounts_with_merkle_context: vec![old_state], + output_compressed_accounts: vec![compressed_pda], + input_root_indices: root_indices, + output_state_merkle_tree_account_indices: vec![0], + proof, + new_address_params: Vec::new(), + compression_lamports: None, + is_compress: false, + signer_seeds: Some(seeds.iter().map(|seed| seed.to_vec()).collect()), + }; + + let mut inputs = Vec::new(); + InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { + signer: ctx.accounts.token_owner_pda.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + invoking_program: Some(ctx.accounts.self_program.to_account_info()), + compressed_sol_pda: None, + compression_recipient: None, + system_program: None, + cpi_signature_account: Some( + ctx.remaining_accounts[cpi_context.cpi_signature_account_index as usize] + .to_account_info(), + ), + }; + let signer_seeds: [&[&[u8]]; 1] = [&seeds[..]]; + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_pda_program.to_account_info(), + cpi_accounts, + &signer_seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} + +#[inline(never)] +pub fn cpi_compressed_token_withdrawal<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + output_state_merkle_tree_account_indices: Vec, + root_indices: Vec, + proof: Option, + cpi_context: &CompressedCpiContext, + bump: u8, +) -> Result<()> { + let bump = &[bump]; + let signer_bytes = ctx.accounts.signer.key.to_bytes(); + let seeds: [&[u8]; 3] = [b"escrow".as_slice(), signer_bytes.as_slice(), bump]; + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + is_compress: false, + compression_amount: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.token_owner_pda.to_account_info(), + authority: ctx.accounts.token_owner_pda.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + self_program: ctx.accounts.compressed_token_program.to_account_info(), + cpi_authority_pda: ctx + .accounts + .compressed_token_cpi_authority_pda + .to_account_info(), + compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), + token_pool_pda: None, + decompress_token_account: None, + token_program: None, + }; + let signer_seeds: [&[&[u8]]; 1] = [&seeds[..]]; + + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_token_program.to_account_info(), + cpi_accounts, + &signer_seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + let cpi_context = CompressedCpiContext { + cpi_signature_account_index: cpi_context.cpi_signature_account_index, + execute: false, + }; + light_compressed_token::cpi::transfer(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/escrow.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/escrow.rs new file mode 100644 index 0000000000..471c243fe1 --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/escrow.rs @@ -0,0 +1,235 @@ +use anchor_lang::prelude::*; +use light_compressed_pda::utils::CompressedProof; +use light_compressed_token::{ + CompressedTokenInstructionDataTransfer, InputTokenDataWithContext, TokenTransferOutputData, +}; + +use crate::{create_change_output_compressed_token_account, EscrowError}; + +pub fn process_escrow_compressed_tokens_with_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + lock_up_time: u64, + escrow_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + // set timelock + let current_slot = Clock::get()?.slot; + ctx.accounts.timelock_pda.slot = current_slot.checked_add(lock_up_time).unwrap(); + + let escrow_token_data = TokenTransferOutputData { + amount: escrow_amount, + owner: ctx.accounts.token_owner_pda.key(), + lamports: None, + }; + let change_token_data = create_change_output_compressed_token_account( + &input_token_data_with_context, + &[escrow_token_data], + &ctx.accounts.signer.key(), + ); + let output_compressed_accounts = vec![escrow_token_data, change_token_data]; + + cpi_compressed_token_transfer( + &ctx, + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + ) +} + +/// Allows the owner to withdraw compressed tokens from the escrow account, +/// provided the lockup time has expired. +pub fn process_withdraw_compressed_escrow_tokens_with_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + bump: u8, + withdrawal_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + let current_slot = Clock::get()?.slot; + if current_slot < ctx.accounts.timelock_pda.slot { + return err!(EscrowError::EscrowLocked); + } + + let escrow_token_data = TokenTransferOutputData { + amount: withdrawal_amount, + owner: ctx.accounts.signer.key(), + lamports: None, + }; + let change_token_data = create_change_output_compressed_token_account( + &input_token_data_with_context, + &[escrow_token_data], + &ctx.accounts.token_owner_pda.key(), + ); + let output_compressed_accounts = vec![escrow_token_data, change_token_data]; + + withdrawal_cpi_compressed_token_transfer( + &ctx, + bump, + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + ) +} + +#[derive(Accounts)] +pub struct EscrowCompressedTokensWithPda<'info> { + #[account(mut)] + pub signer: Signer<'info>, + /// CHECK: + #[account(seeds = [b"escrow".as_slice(), signer.key.to_bytes().as_slice()], bump)] + pub token_owner_pda: AccountInfo<'info>, + pub compressed_token_program: + Program<'info, light_compressed_token::program::LightCompressedToken>, + pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, + pub account_compression_program: + Program<'info, account_compression::program::AccountCompression>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub compressed_token_cpi_authority_pda: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: + pub noop_program: AccountInfo<'info>, + #[account(init_if_needed, seeds = [b"timelock".as_slice(), signer.key.to_bytes().as_slice()],bump, payer = signer, space = 8 + 8)] + pub timelock_pda: Account<'info, EscrowTimeLock>, + pub system_program: Program<'info, System>, +} + +#[derive(Debug)] +#[account] +pub struct EscrowTimeLock { + pub slot: u64, +} + +#[inline(never)] +pub fn cpi_compressed_token_transfer<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + is_compress: false, + compression_amount: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.signer.to_account_info(), + authority: ctx.accounts.signer.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + self_program: ctx.accounts.compressed_token_program.to_account_info(), + cpi_authority_pda: ctx + .accounts + .compressed_token_cpi_authority_pda + .to_account_info(), + compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), + token_pool_pda: None, + decompress_token_account: None, + token_program: None, + }; + + let mut cpi_ctx = CpiContext::new( + ctx.accounts.compressed_token_program.to_account_info(), + cpi_accounts, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + light_compressed_token::cpi::transfer(cpi_ctx, inputs, None)?; + Ok(()) +} + +#[inline(never)] +pub fn withdrawal_cpi_compressed_token_transfer<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + bump: u8, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + is_compress: false, + compression_amount: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let bump = &[bump]; + let signer_bytes = ctx.accounts.signer.key.to_bytes(); + let seeds = [b"escrow".as_slice(), signer_bytes.as_slice(), bump]; + + let signer_seeds = &[&seeds[..]]; + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.token_owner_pda.to_account_info(), + authority: ctx.accounts.token_owner_pda.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + self_program: ctx.accounts.compressed_token_program.to_account_info(), + cpi_authority_pda: ctx + .accounts + .compressed_token_cpi_authority_pda + .to_account_info(), + compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), + token_pool_pda: None, + decompress_token_account: None, + token_program: None, + }; + + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_token_program.to_account_info(), + cpi_accounts, + signer_seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + light_compressed_token::cpi::transfer(cpi_ctx, inputs, None)?; + Ok(()) +} diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/mod.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/mod.rs new file mode 100644 index 0000000000..d0b6284591 --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/mod.rs @@ -0,0 +1,5 @@ +pub mod escrow; +pub mod sdk; +pub mod withdrawal; + +pub use escrow::*; diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs new file mode 100644 index 0000000000..a872ccdf67 --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs @@ -0,0 +1,165 @@ +#![cfg(not(target_os = "solana"))] + +use account_compression::{Pubkey, NOOP_PROGRAM_ID}; +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressed_pda::utils::CompressedProof; +use light_compressed_token::{ + transfer_sdk::{ + create_inputs_and_remaining_accounts, create_inputs_and_remaining_accounts_checked, + to_account_metas, + }, + TokenTransferOutputData, +}; +use solana_sdk::instruction::Instruction; + +use crate::escrow_with_compressed_pda::sdk::get_token_owner_pda; + +#[derive(Debug, Clone, Copy)] +pub struct CreateEscrowInstructionInputs<'a> { + pub lock_up_time: u64, + pub signer: &'a Pubkey, + pub input_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], + pub nullifier_array_pubkeys: &'a [Pubkey], + pub output_compressed_account_merkle_tree_pubkeys: &'a [Pubkey], + pub output_compressed_accounts: &'a [TokenTransferOutputData], + pub root_indices: &'a [u16], + pub leaf_indices: &'a [u32], + pub proof: &'a CompressedProof, + pub input_token_data: &'a [light_compressed_token::TokenData], + pub mint: &'a Pubkey, +} + +pub fn get_timelock_pda(signer: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&[b"timelock".as_ref(), signer.as_ref()], &crate::id()).0 +} + +pub fn create_escrow_instruction( + input_params: CreateEscrowInstructionInputs, + escrow_amount: u64, +) -> Instruction { + let token_owner_pda = get_token_owner_pda(input_params.signer); + let timelock_pda = get_timelock_pda(input_params.signer); + let (remaining_accounts, inputs) = create_inputs_and_remaining_accounts_checked( + input_params.input_compressed_account_merkle_tree_pubkeys, + input_params.leaf_indices, + input_params.input_token_data, + input_params.nullifier_array_pubkeys, + input_params.output_compressed_account_merkle_tree_pubkeys, + None, + input_params.output_compressed_accounts, + input_params.root_indices, + input_params.proof, + *input_params.mint, + input_params.signer, + false, + None, + ) + .unwrap(); + + let instruction_data = crate::instruction::EscrowCompressedTokensWithPda { + lock_up_time: input_params.lock_up_time, + escrow_amount, + proof: Some(input_params.proof.clone()), + root_indices: input_params.root_indices.to_vec(), + mint: *input_params.mint, + signer_is_delegate: false, + input_token_data_with_context: inputs.input_token_data_with_context, + output_state_merkle_tree_account_indices: inputs.output_state_merkle_tree_account_indices, + }; + + let registered_program_pda = Pubkey::find_program_address( + &[light_compressed_pda::ID.to_bytes().as_slice()], + &account_compression::ID, + ) + .0; + let compressed_token_cpi_authority_pda = light_compressed_token::get_cpi_authority_pda().0; + let account_compression_authority = + light_compressed_pda::utils::get_cpi_authority_pda(&light_compressed_pda::ID); + let accounts = crate::accounts::EscrowCompressedTokensWithPda { + signer: *input_params.signer, + noop_program: NOOP_PROGRAM_ID, + compressed_token_program: light_compressed_token::ID, + compressed_pda_program: light_compressed_pda::ID, + account_compression_program: account_compression::ID, + registered_program_pda, + compressed_token_cpi_authority_pda, + account_compression_authority, + timelock_pda, + system_program: solana_sdk::system_program::ID, + token_owner_pda: token_owner_pda.0, + }; + let remaining_accounts = to_account_metas(remaining_accounts); + + Instruction { + program_id: crate::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + + data: instruction_data.data(), + } +} + +pub fn create_withdrawal_escrow_instruction( + input_params: CreateEscrowInstructionInputs, + withdrawal_amount: u64, +) -> Instruction { + let token_owner_pda = get_token_owner_pda(input_params.signer); + let timelock_pda = get_timelock_pda(input_params.signer); + // Token transactions with an invalid signer will just fail with invalid proof verification. + // Thus, it's recommented to use create_inputs_and_remaining_accounts_checked, which returns a descriptive error in case of a wrong signer. + // We use unchecked here to perform a failing test with an invalid signer. + let (remaining_accounts, inputs) = create_inputs_and_remaining_accounts( + input_params.input_compressed_account_merkle_tree_pubkeys, + input_params.leaf_indices, + input_params.input_token_data, + input_params.nullifier_array_pubkeys, + input_params.output_compressed_account_merkle_tree_pubkeys, + None, + input_params.output_compressed_accounts, + input_params.root_indices, + input_params.proof, + *input_params.mint, + false, + None, + ); + + let instruction_data = crate::instruction::WithdrawCompressedEscrowTokensWithPda { + bump: token_owner_pda.1, + withdrawal_amount, + proof: Some(input_params.proof.clone()), + root_indices: input_params.root_indices.to_vec(), + mint: *input_params.mint, + signer_is_delegate: false, + input_token_data_with_context: inputs.input_token_data_with_context, + output_state_merkle_tree_account_indices: inputs.output_state_merkle_tree_account_indices, + }; + + let registered_program_pda = Pubkey::find_program_address( + &[light_compressed_pda::ID.to_bytes().as_slice()], + &account_compression::ID, + ) + .0; + let compressed_token_cpi_authority_pda = light_compressed_token::get_cpi_authority_pda().0; + let account_compression_authority = + light_compressed_pda::utils::get_cpi_authority_pda(&light_compressed_pda::ID); + let accounts = crate::accounts::EscrowCompressedTokensWithPda { + signer: *input_params.signer, + token_owner_pda: token_owner_pda.0, + noop_program: NOOP_PROGRAM_ID, + compressed_token_program: light_compressed_token::ID, + compressed_pda_program: light_compressed_pda::ID, + account_compression_program: account_compression::ID, + registered_program_pda, + compressed_token_cpi_authority_pda, + account_compression_authority, + timelock_pda, + system_program: solana_sdk::system_program::ID, + }; + let remaining_accounts = to_account_metas(remaining_accounts); + + Instruction { + program_id: crate::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + + data: instruction_data.data(), + } +} diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/withdrawal.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/withdrawal.rs new file mode 100644 index 0000000000..1df5f3055f --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/withdrawal.rs @@ -0,0 +1,234 @@ +use anchor_lang::prelude::*; +use light_compressed_pda::utils::CompressedProof; +use light_compressed_token::{ + CompressedTokenInstructionDataTransfer, InputTokenDataWithContext, TokenTransferOutputData, +}; + +use crate::{create_change_output_compressed_token_account, EscrowError}; + +pub fn process_escrow_compressed_tokens_with_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + lock_up_time: u64, + escrow_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + // set timelock + let current_slot = Clock::get()?.slot; + ctx.accounts.timelock_pda.slot = current_slot.checked_add(lock_up_time).unwrap(); + + let escrow_token_data = TokenTransferOutputData { + amount: escrow_amount, + owner: ctx.accounts.token_owner_pda.key(), + lamports: None, + }; + let change_token_data = create_change_output_compressed_token_account( + &input_token_data_with_context, + &[escrow_token_data], + &ctx.accounts.signer.key(), + ); + let output_compressed_accounts = vec![escrow_token_data, change_token_data]; + + cpi_compressed_token_transfer( + &ctx, + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + ) +} + +/// Allows the owner to withdraw compressed tokens from the escrow account, +/// provided the lockup time has expired. +pub fn process_withdraw_compressed_escrow_tokens_with_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + bump: u8, + withdrawal_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + let current_slot = Clock::get()?.slot; + if current_slot < ctx.accounts.timelock_pda.slot { + return err!(EscrowError::EscrowLocked); + } + + let escrow_token_data = TokenTransferOutputData { + amount: withdrawal_amount, + owner: ctx.accounts.signer.key(), + lamports: None, + }; + let change_token_data = create_change_output_compressed_token_account( + &input_token_data_with_context, + &[escrow_token_data], + &ctx.accounts.token_owner_pda.key(), + ); + let output_compressed_accounts = vec![escrow_token_data, change_token_data]; + + withdrawal_cpi_compressed_token_transfer( + &ctx, + bump, + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + ) +} + +#[derive(Accounts)] +pub struct EscrowCompressedTokensWithPda<'info> { + #[account(mut)] + pub signer: Signer<'info>, + /// CHECK: + #[account(seeds = [b"escrow".as_slice(), signer.key.to_bytes().as_slice()], bump)] + pub token_owner_pda: AccountInfo<'info>, + pub compressed_token_program: + Program<'info, light_compressed_token::program::LightCompressedToken>, + pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, + pub account_compression_program: + Program<'info, account_compression::program::AccountCompression>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub compressed_token_cpi_authority_pda: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: + pub noop_program: AccountInfo<'info>, + #[account(init_if_needed, seeds = [b"timelock".as_slice(), signer.key.to_bytes().as_slice()],bump, payer = signer, space = 8 + 8)] + pub timelock_pda: Account<'info, EscrowTimeLock>, + pub system_program: Program<'info, System>, +} + +#[account] +pub struct EscrowTimeLock { + pub slot: u64, +} + +#[inline(never)] +pub fn cpi_compressed_token_transfer<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + is_compress: false, + compression_amount: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.signer.to_account_info(), + authority: ctx.accounts.signer.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + self_program: ctx.accounts.compressed_token_program.to_account_info(), + cpi_authority_pda: ctx + .accounts + .compressed_token_cpi_authority_pda + .to_account_info(), + compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), + token_pool_pda: None, + decompress_token_account: None, + token_program: None, + }; + + let mut cpi_ctx = CpiContext::new( + ctx.accounts.compressed_token_program.to_account_info(), + cpi_accounts, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + light_compressed_token::cpi::transfer(cpi_ctx, inputs, None)?; + Ok(()) +} + +#[inline(never)] +pub fn withdrawal_cpi_compressed_token_transfer<'info>( + ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, + bump: u8, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_compressed_accounts: Vec, + output_state_merkle_tree_account_indices: Vec, +) -> Result<()> { + let inputs_struct = CompressedTokenInstructionDataTransfer { + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_compressed_accounts, + output_state_merkle_tree_account_indices, + is_compress: false, + compression_amount: None, + }; + + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let bump = &[bump]; + let signer_bytes = ctx.accounts.signer.key.to_bytes(); + let seeds = [b"escrow".as_slice(), signer_bytes.as_slice(), bump]; + + let signer_seeds = &[&seeds[..]]; + let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { + fee_payer: ctx.accounts.token_owner_pda.to_account_info(), + authority: ctx.accounts.token_owner_pda.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + self_program: ctx.accounts.compressed_token_program.to_account_info(), + cpi_authority_pda: ctx + .accounts + .compressed_token_cpi_authority_pda + .to_account_info(), + compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), + token_pool_pda: None, + decompress_token_account: None, + token_program: None, + }; + + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_token_program.to_account_info(), + cpi_accounts, + signer_seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + light_compressed_token::cpi::transfer(cpi_ctx, inputs, None)?; + Ok(()) +} diff --git a/examples/token-escrow/programs/token-escrow/src/lib.rs b/examples/token-escrow/programs/token-escrow/src/lib.rs index 84c5ba451b..519784c638 100644 --- a/examples/token-escrow/programs/token-escrow/src/lib.rs +++ b/examples/token-escrow/programs/token-escrow/src/lib.rs @@ -2,9 +2,15 @@ use anchor_lang::prelude::*; use anchor_lang::solana_program::pubkey::Pubkey; use light_compressed_pda::utils::CompressedProof; +use light_compressed_token::InputTokenDataWithContext; use light_compressed_token::TokenTransferOutputData; -use light_compressed_token::{CompressedTokenInstructionDataTransfer, InputTokenDataWithContext}; -pub mod sdk; +pub mod escrow_with_compressed_pda; +pub mod escrow_with_pda; + +pub use escrow_with_compressed_pda::escrow::*; +pub use escrow_with_pda::escrow::*; +use light_compressed_pda::compressed_cpi::CompressedCpiContext; +use light_compressed_pda::NewAddressParamsPacked; #[error_code] pub enum EscrowError { @@ -17,6 +23,8 @@ declare_id!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); #[program] pub mod token_escrow { + use self::escrow_with_compressed_pda::withdrawal::process_withdraw_compressed_tokens_with_compressed_pda; + use super::*; /// Escrows compressed tokens, for a certain number of slots. @@ -33,30 +41,15 @@ pub mod token_escrow { input_token_data_with_context: Vec, output_state_merkle_tree_account_indices: Vec, ) -> Result<()> { - // set timelock - let current_slot = Clock::get()?.slot; - ctx.accounts.timelock_pda.slot = current_slot.checked_add(lock_up_time).unwrap(); - - let escrow_token_data = TokenTransferOutputData { - amount: escrow_amount, - owner: ctx.accounts.cpi_signer.key(), - lamports: None, - }; - let change_token_data = create_change_output_compressed_token_account( - &input_token_data_with_context, - &[escrow_token_data], - &ctx.accounts.signer.key(), - ); - let output_compressed_accounts = vec![escrow_token_data, change_token_data]; - - cpi_compressed_token_transfer( - &ctx, + process_escrow_compressed_tokens_with_pda( + ctx, + lock_up_time, + escrow_amount, proof, root_indices, mint, signer_is_delegate, input_token_data_with_context, - output_compressed_accounts, output_state_merkle_tree_account_indices, ) } @@ -74,60 +67,82 @@ pub mod token_escrow { input_token_data_with_context: Vec, output_state_merkle_tree_account_indices: Vec, ) -> Result<()> { - let current_slot = Clock::get()?.slot; - if current_slot > ctx.accounts.timelock_pda.slot { - return err!(EscrowError::EscrowLocked); - } - - let escrow_token_data = TokenTransferOutputData { - amount: withdrawal_amount, - owner: ctx.accounts.signer.key(), - lamports: None, - }; - let change_token_data = create_change_output_compressed_token_account( - &input_token_data_with_context, - &[escrow_token_data], - &ctx.accounts.cpi_signer.key(), - ); - let output_compressed_accounts = vec![escrow_token_data, change_token_data]; - - withdrawal_cpi_compressed_token_transfer( - &ctx, + process_withdraw_compressed_escrow_tokens_with_pda( + ctx, bump, + withdrawal_amount, proof, root_indices, mint, signer_is_delegate, input_token_data_with_context, - output_compressed_accounts, output_state_merkle_tree_account_indices, ) } -} -#[derive(Accounts)] -pub struct EscrowCompressedTokensWithPda<'info> { - #[account(mut)] - pub signer: Signer<'info>, - #[account(seeds = [b"escrow".as_slice(), signer.key.to_bytes().as_slice()], bump)] - pub cpi_signer: AccountInfo<'info>, - pub compressed_token_program: - Program<'info, light_compressed_token::program::LightCompressedToken>, - pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, - pub account_compression_program: - Program<'info, account_compression::program::AccountCompression>, - pub account_compression_authority: AccountInfo<'info>, - pub compressed_token_cpi_authority_pda: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - #[account(init_if_needed, seeds = [b"timelock".as_slice(), signer.key.to_bytes().as_slice()],bump, payer = signer, space = 8 + 8)] - pub timelock_pda: Account<'info, EscrowTimeLock>, - pub system_program: Program<'info, System>, -} + /// Escrows compressed tokens, for a certain number of slots. + /// Transfers compressed tokens to compressed token account owned by cpi_signer. + /// Tokens are locked for lock_up_time slots. + pub fn escrow_compressed_tokens_with_compressed_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + lock_up_time: u64, + escrow_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, + new_address_params: NewAddressParamsPacked, + cpi_context: CompressedCpiContext, + bump: u8, + ) -> Result<()> { + process_escrow_compressed_tokens_with_compressed_pda( + ctx, + lock_up_time, + escrow_amount, + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_state_merkle_tree_account_indices, + new_address_params, + cpi_context, + bump, + ) + } -#[account] -pub struct EscrowTimeLock { - pub slot: u64, + /// Escrows compressed tokens, for a certain number of slots. + /// Transfers compressed tokens to compressed token account owned by cpi_signer. + /// Tokens are locked for lock_up_time slots. + pub fn withdraw_compressed_tokens_with_compressed_pda<'info>( + ctx: Context<'_, '_, '_, 'info, EscrowCompressedTokensWithCompressedPda<'info>>, + withdrawal_amount: u64, + proof: Option, + root_indices: Vec, + mint: Pubkey, + signer_is_delegate: bool, + input_token_data_with_context: Vec, + output_state_merkle_tree_account_indices: Vec, + cpi_context: CompressedCpiContext, + input_compressed_pda: PackedInputCompressedPda, + bump: u8, + ) -> Result<()> { + process_withdraw_compressed_tokens_with_compressed_pda( + ctx, + withdrawal_amount, + proof, + root_indices, + mint, + signer_is_delegate, + input_token_data_with_context, + output_state_merkle_tree_account_indices, + cpi_context, + input_compressed_pda, + bump, + ) + } } // TODO: add to light_sdk @@ -154,117 +169,3 @@ fn create_change_output_compressed_token_account( lamports: None, } } - -#[inline(never)] -pub fn cpi_compressed_token_transfer<'info>( - ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, - proof: Option, - root_indices: Vec, - mint: Pubkey, - signer_is_delegate: bool, - input_token_data_with_context: Vec, - output_compressed_accounts: Vec, - output_state_merkle_tree_account_indices: Vec, -) -> Result<()> { - let inputs_struct = CompressedTokenInstructionDataTransfer { - proof, - root_indices, - mint, - signer_is_delegate, - input_token_data_with_context, - output_compressed_accounts, - output_state_merkle_tree_account_indices, - is_compress: false, - compression_amount: None, - }; - - let inputs = inputs_struct.try_to_vec()?; - - let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { - fee_payer: ctx.accounts.signer.to_account_info(), - authority: ctx.accounts.signer.to_account_info(), - registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), - noop_program: ctx.accounts.noop_program.to_account_info(), - account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), - account_compression_program: ctx.accounts.account_compression_program.to_account_info(), - self_program: ctx.accounts.compressed_token_program.to_account_info(), - cpi_authority_pda: ctx - .accounts - .compressed_token_cpi_authority_pda - .to_account_info(), - compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), - token_pool_pda: None, - decompress_token_account: None, - token_program: None, - }; - - let mut cpi_ctx = CpiContext::new( - ctx.accounts.compressed_token_program.to_account_info(), - cpi_accounts, - ); - - cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); - light_compressed_token::cpi::transfer(cpi_ctx, inputs)?; - Ok(()) -} - -#[inline(never)] -pub fn withdrawal_cpi_compressed_token_transfer<'info>( - ctx: &Context<'_, '_, '_, 'info, EscrowCompressedTokensWithPda<'info>>, - bump: u8, - proof: Option, - root_indices: Vec, - mint: Pubkey, - signer_is_delegate: bool, - input_token_data_with_context: Vec, - output_compressed_accounts: Vec, - output_state_merkle_tree_account_indices: Vec, -) -> Result<()> { - let inputs_struct = CompressedTokenInstructionDataTransfer { - proof, - root_indices, - mint, - signer_is_delegate, - input_token_data_with_context, - output_compressed_accounts, - output_state_merkle_tree_account_indices, - is_compress: false, - compression_amount: None, - }; - - let mut inputs = Vec::new(); - CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); - - let bump = &[bump]; - let signer_bytes = ctx.accounts.signer.key.to_bytes(); - let seeds = [b"escrow".as_slice(), signer_bytes.as_slice(), bump]; - - let signer_seeds = &[&seeds[..]]; - let cpi_accounts = light_compressed_token::cpi::accounts::TransferInstruction { - fee_payer: ctx.accounts.cpi_signer.to_account_info(), - authority: ctx.accounts.cpi_signer.to_account_info(), - registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), - noop_program: ctx.accounts.noop_program.to_account_info(), - account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), - account_compression_program: ctx.accounts.account_compression_program.to_account_info(), - self_program: ctx.accounts.compressed_token_program.to_account_info(), - cpi_authority_pda: ctx - .accounts - .compressed_token_cpi_authority_pda - .to_account_info(), - compressed_pda_program: ctx.accounts.compressed_pda_program.to_account_info(), - token_pool_pda: None, - decompress_token_account: None, - token_program: None, - }; - - let mut cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.compressed_token_program.to_account_info(), - cpi_accounts, - signer_seeds, - ); - - cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); - light_compressed_token::cpi::transfer(cpi_ctx, inputs)?; - Ok(()) -} diff --git a/examples/token-escrow/programs/token-escrow/tests/test.rs b/examples/token-escrow/programs/token-escrow/tests/test.rs index cafbde89a6..5a1da87952 100644 --- a/examples/token-escrow/programs/token-escrow/tests/test.rs +++ b/examples/token-escrow/programs/token-escrow/tests/test.rs @@ -10,52 +10,59 @@ // - create escrow pda and just prove that utxo exists -> read utxo from compressed token account // release compressed tokens -// TODO: 2. escrow tokens with compressed pda -// create test env -// create mint and mint tokens -// escrow compressed tokens - with compressed pda -// release compressed tokens - -// TODO: 3. escrow tokens by decompression with compressed pda -// this design pattern can be used to use compressed accounts with an AMMM -// create test env -// create mint and mint tokens -// decomcompress compressed tokens into program owned token account - with compressed pda -// release compressed tokens - -use light_test_utils::test_env::setup_test_programs_with_accounts; +use account_compression::Pubkey; +use light_compressed_pda::event::PublicTransactionEvent; +use light_test_utils::test_env::{setup_test_programs_with_accounts, EnvAccounts}; use light_test_utils::test_indexer::{create_mint_helper, mint_tokens_helper, TestIndexer}; +use light_test_utils::{airdrop_lamports, create_and_send_transaction_with_event, get_account}; -use solana_sdk::{pubkey::Pubkey, signer::Signer, transaction::Transaction}; -use token_escrow::sdk::{ - create_escrow_instruction, create_withdrawal_escrow_instruction, CreateEscrowInstructionInputs, +use solana_program_test::{ + BanksClientError, BanksTransactionResultWithMetadata, ProgramTestContext, +}; +use solana_sdk::instruction::{Instruction, InstructionError}; +use solana_sdk::signature::Keypair; +use solana_sdk::{signer::Signer, transaction::Transaction}; +use token_escrow::escrow_with_compressed_pda::sdk::get_token_owner_pda; +use token_escrow::escrow_with_pda::sdk::{ + create_escrow_instruction, create_withdrawal_escrow_instruction, get_timelock_pda, + CreateEscrowInstructionInputs, }; +use token_escrow::{EscrowError, EscrowTimeLock}; -/// Steps: +/// Tests: /// 1. create test env /// 2. create mint and mint tokens /// 3. escrow compressed tokens /// 4. withdraw compressed tokens +/// 5. mint tokens to second payer +/// 6. escrow compressed tokens with lockup time +/// 7. try to withdraw before lockup time +/// 8. try to withdraw with invalid signer +/// 9. withdraw after lockup time #[tokio::test] -async fn test_escrow() { - let env: light_test_utils::test_env::EnvWithAccounts = setup_test_programs_with_accounts(Some( - vec![(String::from("token_escrow"), token_escrow::ID)], - )) +async fn test_escrow_pda() { + let (mut context, env) = setup_test_programs_with_accounts(Some(vec![( + String::from("token_escrow"), + token_escrow::ID, + )])) .await; - let mut context = env.context; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); let merkle_tree_pubkey = env.merkle_tree_pubkey; let nullifier_queue_pubkey = env.nullifier_queue_pubkey; + let address_merkle_tree_pubkey = env.address_merkle_tree_pubkey; let test_indexer = TestIndexer::new( merkle_tree_pubkey, nullifier_queue_pubkey, + address_merkle_tree_pubkey, payer.insecure_clone(), + true, + false, + "../../../../circuit-lib/circuitlib-rs/scripts/prover.sh", ); let mint = create_mint_helper(&mut context, &payer).await; let mut test_indexer = test_indexer.await; - // big footgun signer check of token account is done with zkp onchain thus no conclusive error message - // let recipient_keypair = Keypair::new(); + let amount = 10000u64; mint_tokens_helper( &mut context, @@ -67,141 +74,430 @@ async fn test_escrow() { vec![payer.pubkey()], ) .await; - let input_compressed_token_account_data = test_indexer.token_compressed_accounts[0].clone(); + println!("test indexer {:?}", test_indexer.compressed_accounts); + let escrow_amount = 100u64; + let lockup_time = 0u64; + perform_escrow_with_event( + &mut context, + &mut test_indexer, + &env, + &payer, + &escrow_amount, + &lockup_time, + ) + .await + .unwrap(); + println!("here"); + assert_escrow( + &mut context, + &test_indexer, + &payer_pubkey, + amount, + escrow_amount, + &lockup_time, + ) + .await; + + println!("withdrawal _----------------------------------------------------------------"); + let withdrawal_amount = 50u64; + perform_withdrawal_with_event( + &mut context, + &mut test_indexer, + &env, + &payer, + &withdrawal_amount, + None, + ) + .await + .unwrap(); + + assert_withdrawal( + &test_indexer, + &payer_pubkey, + withdrawal_amount, + escrow_amount, + ); + + let second_payer = Keypair::new(); + let second_payer_pubkey = second_payer.pubkey(); + println!("second payer pub key {:?}", second_payer_pubkey); + let second_payer_token_balance = 1_000_000_000; + airdrop_lamports(&mut context, &second_payer_pubkey, 1_000_000_000) + .await + .unwrap(); + mint_tokens_helper( + &mut context, + &mut test_indexer, + &merkle_tree_pubkey, + &payer, + &mint, + vec![second_payer_token_balance], + vec![second_payer_pubkey], + ) + .await; + + let escrow_amount = 100u64; + let lockup_time = 100u64; + perform_escrow_with_event( + &mut context, + &mut test_indexer, + &env, + &second_payer, + &escrow_amount, + &lockup_time, + ) + .await + .unwrap(); + + assert_escrow( + &mut context, + &test_indexer, + &second_payer_pubkey, + second_payer_token_balance, + escrow_amount, + &lockup_time, + ) + .await; + + // try withdrawal before lockup time + let withdrawal_amount = 50u64; + let res = perform_withdrawal_failing( + &mut context, + &mut test_indexer, + &env, + &second_payer, + &withdrawal_amount, + None, + ) + .await; + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom(EscrowError::EscrowLocked.into()) + )) + ); + context.warp_to_slot(1000).unwrap(); + // try withdrawal with invalid signer + let res = perform_withdrawal_failing( + &mut context, + &mut test_indexer, + &env, + &second_payer, + &withdrawal_amount, + Some(payer_pubkey), + ) + .await; + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom( + light_compressed_pda::ErrorCode::ProofVerificationFailed.into() + ) + )) + ); + perform_withdrawal_with_event( + &mut context, + &mut test_indexer, + &env, + &second_payer, + &withdrawal_amount, + None, + ) + .await + .unwrap(); + assert_withdrawal( + &test_indexer, + &second_payer_pubkey, + withdrawal_amount, + escrow_amount, + ); +} +pub async fn perform_escrow( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + escrow_amount: &u64, + lock_up_time: &u64, +) -> Instruction { + let input_compressed_token_account_data = test_indexer + .token_compressed_accounts + .iter() + .find(|x| { + println!("searching token account: {:?}", x.token_data); + println!("escrow amount: {:?}", escrow_amount); + println!("payer pub key: {:?}", payer.pubkey()); + return x.token_data.owner == payer.pubkey() && x.token_data.amount >= *escrow_amount; + }) + .expect("no account with enough tokens") + .clone(); + let payer_pubkey = payer.pubkey(); let compressed_input_account_with_context = test_indexer.compressed_accounts[input_compressed_token_account_data.index].clone(); let input_compressed_account_hash = test_indexer.compressed_accounts [input_compressed_token_account_data.index] .compressed_account .hash( - &merkle_tree_pubkey, + &env.merkle_tree_pubkey, &compressed_input_account_with_context.leaf_index, ) .unwrap(); - let (root_indices, proof) = test_indexer - .create_proof_for_compressed_accounts(&[input_compressed_account_hash], &mut context) + let rpc_result = test_indexer + .create_proof_for_compressed_accounts(Some(&[input_compressed_account_hash]), None, context) .await; - let escrow_amount = 100u64; let create_ix_inputs = CreateEscrowInstructionInputs { input_token_data: &vec![input_compressed_token_account_data.token_data], - lock_up_time: 0, + lock_up_time: *lock_up_time, signer: &payer_pubkey, - input_compressed_account_merkle_tree_pubkeys: &[merkle_tree_pubkey], - nullifier_array_pubkeys: &[nullifier_queue_pubkey], - output_compressed_account_merkle_tree_pubkeys: &[merkle_tree_pubkey, merkle_tree_pubkey], + input_compressed_account_merkle_tree_pubkeys: &[env.merkle_tree_pubkey], + nullifier_array_pubkeys: &[env.nullifier_queue_pubkey], + output_compressed_account_merkle_tree_pubkeys: &[ + env.merkle_tree_pubkey, + env.merkle_tree_pubkey, + ], output_compressed_accounts: &Vec::new(), - root_indices: &root_indices, - proof: &proof, + root_indices: &rpc_result.root_indices, + proof: &rpc_result.proof, leaf_indices: &[compressed_input_account_with_context.leaf_index], mint: &input_compressed_token_account_data.token_data.mint, }; - let instruction = create_escrow_instruction(create_ix_inputs.clone(), escrow_amount); - let transaction = Transaction::new_signed_with_payer( + create_escrow_instruction(create_ix_inputs.clone(), *escrow_amount) +} + +pub async fn perform_escrow_with_event( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + escrow_amount: &u64, + lock_up_time: &u64, +) -> Result<(), BanksClientError> { + let instruction = perform_escrow( + context, + test_indexer, + env, + payer, + escrow_amount, + lock_up_time, + ) + .await; + let event = create_and_send_transaction_with_event::( + context, &[instruction], - Some(&payer_pubkey), + &payer.pubkey(), &[&payer], - context.get_new_latest_blockhash().await.unwrap(), - ); - let res = solana_program_test::BanksClient::process_transaction_with_metadata( - &mut context.banks_client, - transaction, + ) + .await? + .unwrap(); + test_indexer.add_compressed_accounts_with_token_data(event); + Ok(()) +} + +pub async fn perform_escrow_failing( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + escrow_amount: &u64, + lock_up_time: &u64, +) -> Result { + let instruction = perform_escrow( + context, + test_indexer, + env, + payer, + escrow_amount, + lock_up_time, ) .await; - test_indexer.add_compressed_accounts_with_token_data( - res.unwrap() - .metadata - .unwrap() - .return_data - .unwrap() - .data - .to_vec(), + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[&payer], + context.get_new_latest_blockhash().await.unwrap(), ); + context + .banks_client + .process_transaction_with_metadata(transaction) + .await +} - let token_data_escrow = test_indexer.token_compressed_accounts[1].token_data.clone(); +pub async fn assert_escrow( + context: &mut ProgramTestContext, + test_indexer: &TestIndexer, + payer_pubkey: &Pubkey, + amount: u64, + escrow_amount: u64, + lock_up_time: &u64, +) { + let token_owner_pda = get_token_owner_pda(&payer_pubkey).0; + let token_data_escrow = test_indexer + .token_compressed_accounts + .iter() + .find(|x| x.token_data.owner == token_owner_pda) + .unwrap() + .token_data + .clone(); assert_eq!(token_data_escrow.amount, escrow_amount); - let cpi_signer = Pubkey::find_program_address( - &[b"escrow".as_ref(), payer_pubkey.as_ref()], - &token_escrow::id(), - ) - .0; - assert_eq!(token_data_escrow.owner, cpi_signer); + assert_eq!(token_data_escrow.owner, token_owner_pda); - let token_data_change_compressed_token_account = - test_indexer.token_compressed_accounts[2].token_data.clone(); + let token_data_change_compressed_token_account = test_indexer.token_compressed_accounts + [test_indexer.token_compressed_accounts.len() - 1] + .token_data + .clone(); assert_eq!( token_data_change_compressed_token_account.amount, amount - escrow_amount ); assert_eq!( token_data_change_compressed_token_account.owner, - payer_pubkey + *payer_pubkey ); - println!("withdrawal _----------------------------------------------------------------"); - let withdrawal_amount = 50u64; + let time_lock_pubkey = get_timelock_pda(&payer_pubkey); + let timelock_account = get_account::(context, time_lock_pubkey).await; + let current_slot = context.banks_client.get_root_slot().await.unwrap(); + assert_eq!(timelock_account.slot, *lock_up_time + current_slot); +} - let escrow_token_data_with_context = test_indexer.token_compressed_accounts[1].clone(); +pub async fn perform_withdrawal( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + withdrawal_amount: &u64, + invalid_signer: Option, +) -> Instruction { + let payer_pubkey = payer.pubkey(); + let token_owner_pda = get_token_owner_pda(&invalid_signer.unwrap_or(payer_pubkey)).0; + let escrow_token_data_with_context = test_indexer + .token_compressed_accounts + .iter() + .find(|x| { + x.token_data.owner == token_owner_pda && x.token_data.amount >= *withdrawal_amount + }) + .expect("no account with enough tokens") + .clone(); let compressed_input_account_with_context = test_indexer.compressed_accounts[escrow_token_data_with_context.index].clone(); let input_compressed_account_hash = test_indexer.compressed_accounts [escrow_token_data_with_context.index] .compressed_account .hash( - &merkle_tree_pubkey, + &env.merkle_tree_pubkey, &compressed_input_account_with_context.leaf_index, ) .unwrap(); - let (root_indices, proof) = test_indexer - .create_proof_for_compressed_accounts(&[input_compressed_account_hash], &mut context) + let rpc_result = test_indexer + .create_proof_for_compressed_accounts(Some(&[input_compressed_account_hash]), None, context) .await; - let escrow_amount = 100u64; let create_ix_inputs = CreateEscrowInstructionInputs { input_token_data: &vec![escrow_token_data_with_context.token_data], lock_up_time: 0, signer: &payer_pubkey, - input_compressed_account_merkle_tree_pubkeys: &[merkle_tree_pubkey], - nullifier_array_pubkeys: &[nullifier_queue_pubkey], - output_compressed_account_merkle_tree_pubkeys: &[merkle_tree_pubkey, merkle_tree_pubkey], + input_compressed_account_merkle_tree_pubkeys: &[env.merkle_tree_pubkey], + nullifier_array_pubkeys: &[env.nullifier_queue_pubkey], + output_compressed_account_merkle_tree_pubkeys: &[ + env.merkle_tree_pubkey, + env.merkle_tree_pubkey, + ], output_compressed_accounts: &Vec::new(), - root_indices: &root_indices, - proof: &proof, + root_indices: &rpc_result.root_indices, + proof: &rpc_result.proof, leaf_indices: &[compressed_input_account_with_context.leaf_index], mint: &escrow_token_data_with_context.token_data.mint, }; - let instruction = create_withdrawal_escrow_instruction(create_ix_inputs, withdrawal_amount); - let transaction = Transaction::new_signed_with_payer( + create_withdrawal_escrow_instruction(create_ix_inputs, *withdrawal_amount) +} + +pub async fn perform_withdrawal_with_event( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + withdrawal_amount: &u64, + invalid_signer: Option, +) -> Result<(), BanksClientError> { + let instruction = perform_withdrawal( + context, + test_indexer, + env, + payer, + withdrawal_amount, + invalid_signer, + ) + .await; + let event = create_and_send_transaction_with_event::( + context, &[instruction], - Some(&payer_pubkey), + &payer.pubkey(), &[&payer], - context.get_new_latest_blockhash().await.unwrap(), - ); - let res = solana_program_test::BanksClient::process_transaction_with_metadata( - &mut context.banks_client, - transaction, + ) + .await? + .unwrap(); + test_indexer.add_compressed_accounts_with_token_data(event); + Ok(()) +} + +pub async fn perform_withdrawal_failing( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + withdrawal_amount: &u64, + invalid_signer: Option, +) -> Result { + let instruction = perform_withdrawal( + context, + test_indexer, + env, + payer, + withdrawal_amount, + invalid_signer, ) .await; - test_indexer.add_compressed_accounts_with_token_data( - res.unwrap() - .metadata - .unwrap() - .return_data - .unwrap() - .data - .to_vec(), + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[&payer], + context.get_new_latest_blockhash().await.unwrap(), ); + context + .banks_client + .process_transaction_with_metadata(transaction) + .await +} +pub fn assert_withdrawal( + test_indexer: &TestIndexer, + payer_pubkey: &Pubkey, + withdrawal_amount: u64, + escrow_amount: u64, +) { + let token_owner_pda = get_token_owner_pda(&payer_pubkey).0; + let token_data_withdrawal = test_indexer + .token_compressed_accounts + .iter() + .any(|x| x.token_data.owner == *payer_pubkey && x.token_data.amount == withdrawal_amount); - let token_data_withdrawal = test_indexer.token_compressed_accounts[3].token_data.clone(); - assert_eq!(token_data_withdrawal.amount, withdrawal_amount); - assert_eq!(token_data_withdrawal.owner, payer_pubkey); - let token_data_escrow_change = test_indexer.token_compressed_accounts[4].token_data.clone(); - assert_eq!( - token_data_escrow_change.amount, + assert!( + token_data_withdrawal, + "Withdrawal compressed account doesn't exist or has incorrect amount {} expected amount", + withdrawal_amount + ); + let token_data_escrow_change = test_indexer.token_compressed_accounts.iter().any(|x| { + x.token_data.owner == token_owner_pda + && x.token_data.amount == escrow_amount - withdrawal_amount + }); + assert!( + token_data_escrow_change, + "Escrow change compressed account doesn't exist or has incorrect amount {} expected amount", escrow_amount - withdrawal_amount ); - assert_eq!(token_data_escrow_change.owner, cpi_signer); } diff --git a/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs b/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs new file mode 100644 index 0000000000..b89722398d --- /dev/null +++ b/examples/token-escrow/programs/token-escrow/tests/test_compressed_pda.rs @@ -0,0 +1,553 @@ +#![cfg(feature = "test-sbf")] + +// 2. escrow tokens with compressed pda +// create test env +// create mint and mint tokens +// escrow compressed tokens - with compressed pda +// release compressed tokens + +// TODO: 3. escrow tokens by decompression with compressed pda +// this design pattern can be used to use compressed accounts with an AMMM +// create test env +// create mint and mint tokens +// decomcompress compressed tokens into program owned token account - with compressed pda +// release compressed tokens + +use anchor_lang::AnchorDeserialize; +use light_compressed_pda::compressed_account::MerkleContext; +use light_compressed_pda::event::PublicTransactionEvent; +use light_hasher::{Hasher, Poseidon}; +use light_test_utils::create_and_send_transaction_with_event; +use light_test_utils::test_env::{setup_test_programs_with_accounts, EnvAccounts}; +use light_test_utils::test_indexer::{create_mint_helper, mint_tokens_helper, TestIndexer}; +use solana_program_test::{ + BanksClientError, BanksTransactionResultWithMetadata, ProgramTestContext, +}; +use solana_sdk::instruction::{Instruction, InstructionError}; +use solana_sdk::signature::Keypair; +use solana_sdk::{signer::Signer, transaction::Transaction}; +use token_escrow::escrow_with_compressed_pda::sdk::{ + create_escrow_instruction, create_withdrawal_instruction, get_token_owner_pda, + CreateCompressedPdaEscrowInstructionInputs, CreateCompressedPdaWithdrawalInstructionInputs, +}; +use token_escrow::{EscrowError, EscrowTimeLock}; + +#[tokio::test] +async fn test_escrow_with_compressed_pda() { + let (mut context, env) = setup_test_programs_with_accounts(Some(vec![( + String::from("token_escrow"), + token_escrow::ID, + )])) + .await; + let payer = context.payer.insecure_clone(); + + let address_merkle_tree_pubkey = env.address_merkle_tree_pubkey; + let test_indexer = TestIndexer::new( + env.merkle_tree_pubkey, + env.nullifier_queue_pubkey, + address_merkle_tree_pubkey, + payer.insecure_clone(), + true, + true, + "../../../../circuit-lib/circuitlib-rs/scripts/prover.sh", + ); + let mint = create_mint_helper(&mut context, &payer).await; + let mut test_indexer = test_indexer.await; + + let amount = 10000u64; + mint_tokens_helper( + &mut context, + &mut test_indexer, + &env.merkle_tree_pubkey, + &payer, + &mint, + vec![amount], + vec![payer.pubkey()], + ) + .await; + + let seed = [1u8; 32]; + let escrow_amount = 100u64; + let lock_up_time = 1000u64; + + perform_escrow_with_event( + &mut test_indexer, + &mut context, + &env, + &payer, + lock_up_time, + escrow_amount, + seed, + ) + .await + .unwrap(); + + let current_slot = context.banks_client.get_root_slot().await.unwrap(); + let lockup_end = lock_up_time + current_slot; + assert_escrow( + &mut test_indexer, + &env, + &payer, + &escrow_amount, + &amount, + &seed, + &lockup_end, + ) + .await; + + println!("withdrawal _----------------------------------------------------------------"); + let withdrawal_amount = escrow_amount; + let new_lock_up_time = 2000u64; + let res = perform_withdrawal_failing( + &mut context, + &mut test_indexer, + &env, + &payer, + lock_up_time, + new_lock_up_time, + withdrawal_amount, + ) + .await; + + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom(EscrowError::EscrowLocked.into()) + )) + ); + context.warp_to_slot(lock_up_time + 1).unwrap(); + + perform_withdrawal_with_event( + &mut context, + &mut test_indexer, + &env, + &payer, + lockup_end, + new_lock_up_time, + withdrawal_amount, + ) + .await + .unwrap(); + + assert_withdrawal( + &mut context, + &mut test_indexer, + &env, + &payer, + &withdrawal_amount, + &escrow_amount, + &seed, + new_lock_up_time, + ) + .await; +} + +pub async fn perform_escrow_failing( + test_indexer: &mut TestIndexer, + context: &mut ProgramTestContext, + env: &EnvAccounts, + payer: &Keypair, + lock_up_time: u64, + escrow_amount: u64, + seed: [u8; 32], +) -> Result { + let (payer_pubkey, instruction) = create_escrow_ix( + payer, + test_indexer, + env, + seed, + context, + lock_up_time, + escrow_amount, + ) + .await; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer_pubkey), + &[&payer], + context.get_new_latest_blockhash().await.unwrap(), + ); + solana_program_test::BanksClient::process_transaction_with_metadata( + &mut context.banks_client, + transaction, + ) + .await +} + +pub async fn perform_escrow_with_event( + test_indexer: &mut TestIndexer, + context: &mut ProgramTestContext, + env: &EnvAccounts, + payer: &Keypair, + lock_up_time: u64, + escrow_amount: u64, + seed: [u8; 32], +) -> Result<(), BanksClientError> { + let (_, instruction) = create_escrow_ix( + payer, + test_indexer, + env, + seed, + context, + lock_up_time, + escrow_amount, + ) + .await; + let event = create_and_send_transaction_with_event::( + context, + &[instruction], + &payer.pubkey(), + &[payer], + ) + .await?; + test_indexer.add_compressed_accounts_with_token_data(event.unwrap()); + Ok(()) +} + +async fn create_escrow_ix( + payer: &Keypair, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + seed: [u8; 32], + context: &mut ProgramTestContext, + lock_up_time: u64, + escrow_amount: u64, +) -> ( + anchor_lang::prelude::Pubkey, + solana_sdk::instruction::Instruction, +) { + let payer_pubkey = payer.pubkey(); + let input_compressed_token_account_data = test_indexer.token_compressed_accounts[0].clone(); + + let compressed_input_account_with_context = + test_indexer.compressed_accounts[input_compressed_token_account_data.index].clone(); + let input_compressed_account_hash = test_indexer.compressed_accounts + [input_compressed_token_account_data.index] + .compressed_account + .hash( + &env.merkle_tree_pubkey, + &compressed_input_account_with_context.leaf_index, + ) + .unwrap(); + + let address = light_compressed_pda::compressed_account::derive_address( + &env.address_merkle_tree_pubkey, + &seed, + ) + .unwrap(); + + let rpc_result = test_indexer + .create_proof_for_compressed_accounts( + Some(&[input_compressed_account_hash]), + Some(&[address]), + context, + ) + .await; + + let new_address_params: light_compressed_pda::NewAddressParams = + light_compressed_pda::NewAddressParams { + seed, + address_merkle_tree_pubkey: env.address_merkle_tree_pubkey, + address_queue_pubkey: env.address_merkle_tree_queue_pubkey, + address_merkle_tree_root_index: rpc_result.address_root_indices[0], + }; + let create_ix_inputs = CreateCompressedPdaEscrowInstructionInputs { + input_token_data: &vec![input_compressed_token_account_data.token_data], + lock_up_time, + signer: &payer_pubkey, + input_compressed_account_merkle_tree_pubkeys: &[env.merkle_tree_pubkey], + nullifier_array_pubkeys: &[env.nullifier_queue_pubkey], + output_compressed_account_merkle_tree_pubkeys: &[ + env.merkle_tree_pubkey, + env.merkle_tree_pubkey, + ], + output_compressed_accounts: &Vec::new(), + root_indices: &rpc_result.root_indices, + proof: &rpc_result.proof, + leaf_indices: &[compressed_input_account_with_context.leaf_index], + mint: &input_compressed_token_account_data.token_data.mint, + new_address_params, + cpi_signature_account: &env.cpi_signature_account_pubkey, + }; + let instruction = create_escrow_instruction(create_ix_inputs.clone(), escrow_amount); + (payer_pubkey, instruction) +} + +pub async fn assert_escrow( + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + escrow_amount: &u64, + amount: &u64, + seed: &[u8; 32], + lock_up_time: &u64, +) { + let payer_pubkey = payer.pubkey(); + let token_owner_pda = get_token_owner_pda(&payer_pubkey).0; + let token_data_escrow = test_indexer + .token_compressed_accounts + .iter() + .find(|x| x.token_data.owner == token_owner_pda) + .unwrap() + .token_data + .clone(); + assert_eq!(token_data_escrow.amount, *escrow_amount); + assert_eq!(token_data_escrow.owner, token_owner_pda); + + let token_data_change_compressed_token_account_exist = + test_indexer.token_compressed_accounts.iter().any(|x| { + x.token_data.owner == payer.pubkey() && x.token_data.amount == amount - escrow_amount + }); + assert!(token_data_change_compressed_token_account_exist); + + let compressed_escrow_pda = test_indexer + .compressed_accounts + .iter() + .find(|x| x.compressed_account.owner == token_escrow::ID) + .unwrap() + .clone(); + let address = light_compressed_pda::compressed_account::derive_address( + &env.address_merkle_tree_pubkey, + &seed, + ) + .unwrap(); + assert_eq!( + compressed_escrow_pda.compressed_account.address.unwrap(), + address + ); + assert_eq!( + compressed_escrow_pda.compressed_account.owner, + token_escrow::ID + ); + let compressed_escrow_pda_deserialized = compressed_escrow_pda + .compressed_account + .data + .as_ref() + .unwrap(); + let compressed_escrow_pda_data = + EscrowTimeLock::deserialize_reader(&mut &compressed_escrow_pda_deserialized.data[..]) + .unwrap(); + println!( + "compressed_escrow_pda_data {:?}", + compressed_escrow_pda_data + ); + assert_eq!(compressed_escrow_pda_data.slot, *lock_up_time); + assert_eq!( + compressed_escrow_pda_deserialized.discriminator, + 1u64.to_le_bytes(), + ); + assert_eq!( + compressed_escrow_pda_deserialized.data_hash, + Poseidon::hash(&compressed_escrow_pda_data.slot.to_le_bytes()).unwrap(), + ); +} +pub async fn perform_withdrawal_with_event( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + old_lock_up_time: u64, + new_lock_up_time: u64, + escrow_amount: u64, +) -> Result<(), BanksClientError> { + let instruction = perform_withdrawal( + context, + test_indexer, + env, + payer, + old_lock_up_time, + new_lock_up_time, + escrow_amount, + ) + .await; + let event = create_and_send_transaction_with_event::( + context, + &[instruction], + &payer.pubkey(), + &[payer], + ) + .await?; + test_indexer.add_compressed_accounts_with_token_data(event.unwrap()); + Ok(()) +} + +pub async fn perform_withdrawal_failing( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + old_lock_up_time: u64, + new_lock_up_time: u64, + escrow_amount: u64, +) -> Result { + let instruction = perform_withdrawal( + context, + test_indexer, + env, + payer, + old_lock_up_time, + new_lock_up_time, + escrow_amount, + ) + .await; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer.pubkey()), + &[&payer], + context.get_new_latest_blockhash().await.unwrap(), + ); + context + .banks_client + .process_transaction_with_metadata(transaction) + .await +} +pub async fn perform_withdrawal( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + old_lock_up_time: u64, + new_lock_up_time: u64, + escrow_amount: u64, +) -> Instruction { + let payer_pubkey = payer.pubkey(); + let compressed_escrow_pda = test_indexer + .compressed_accounts + .iter() + .find(|x| x.compressed_account.owner == token_escrow::ID) + .unwrap() + .clone(); + println!("compressed_escrow_pda {:?}", compressed_escrow_pda); + let token_owner_pda = get_token_owner_pda(&payer_pubkey).0; + let token_escrow = test_indexer + .token_compressed_accounts + .iter() + .find(|x| x.token_data.owner == token_owner_pda) + .unwrap() + .clone(); + let token_escrow_account = test_indexer.compressed_accounts[token_escrow.index].clone(); + let token_escrow_account_hash = token_escrow_account + .compressed_account + .hash(&env.merkle_tree_pubkey, &token_escrow_account.leaf_index) + .unwrap(); + println!("token_data_escrow {:?}", token_escrow); + println!( + "token escrow_account {:?}", + test_indexer.compressed_accounts[token_escrow.index] + ); + let compressed_pda_hash = compressed_escrow_pda + .compressed_account + .hash(&env.merkle_tree_pubkey, &compressed_escrow_pda.leaf_index) + .unwrap(); + + // compressed pda will go first into the proof because in the program + // the compressed pda program executes the transaction + let rpc_result = test_indexer + .create_proof_for_compressed_accounts( + Some(&[compressed_pda_hash, token_escrow_account_hash]), + None, + context, + ) + .await; + + let create_withdrawal_ix_inputs = CreateCompressedPdaWithdrawalInstructionInputs { + input_token_data: &vec![token_escrow.token_data], + signer: &payer_pubkey, + input_compressed_account_merkle_tree_pubkeys: &[env.merkle_tree_pubkey], + nullifier_array_pubkeys: &[env.nullifier_queue_pubkey], + output_compressed_account_merkle_tree_pubkeys: &[ + env.merkle_tree_pubkey, + env.merkle_tree_pubkey, + ], + output_compressed_accounts: &Vec::new(), + root_indices: &rpc_result.root_indices, + proof: &rpc_result.proof, + leaf_indices: &[ + compressed_escrow_pda.leaf_index, + token_escrow_account.leaf_index, + ], + mint: &token_escrow.token_data.mint, + cpi_signature_account: &env.cpi_signature_account_pubkey, + old_lock_up_time, + new_lock_up_time, + address: compressed_escrow_pda.compressed_account.address.unwrap(), + merkle_context: MerkleContext { + leaf_index: compressed_escrow_pda.leaf_index, + merkle_tree_pubkey: env.merkle_tree_pubkey, + nullifier_queue_pubkey: env.nullifier_queue_pubkey, + }, + }; + create_withdrawal_instruction(create_withdrawal_ix_inputs.clone(), escrow_amount) +} + +/// 1. Change escrow compressed account exists +/// 2. Withdrawal token account exists +/// 3. Compressed pda with update lock up time exists +pub async fn assert_withdrawal( + context: &mut ProgramTestContext, + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + withdrawal_amount: &u64, + escrow_amount: &u64, + seed: &[u8; 32], + lock_up_time: u64, +) { + let escrow_change_amount = escrow_amount - withdrawal_amount; + + let payer_pubkey = payer.pubkey(); + let token_owner_pda = get_token_owner_pda(&payer_pubkey).0; + let token_data_escrow = test_indexer.token_compressed_accounts.iter().any(|x| { + x.token_data.owner == token_owner_pda && x.token_data.amount == escrow_change_amount + }); + + assert!( + token_data_escrow, + "change escrow token account does not exist or has incorrect amount", + ); + let withdrawal_account_exits = test_indexer + .token_compressed_accounts + .iter() + .any(|x| x.token_data.owner == payer.pubkey() && x.token_data.amount == *withdrawal_amount); + assert!(withdrawal_account_exits); + + let compressed_escrow_pda = test_indexer + .compressed_accounts + .iter() + .find(|x| x.compressed_account.owner == token_escrow::ID) + .unwrap() + .clone(); + + let address = light_compressed_pda::compressed_account::derive_address( + &env.address_merkle_tree_pubkey, + &seed, + ) + .unwrap(); + assert_eq!( + compressed_escrow_pda.compressed_account.address.unwrap(), + address + ); + assert_eq!( + compressed_escrow_pda.compressed_account.owner, + token_escrow::ID + ); + let compressed_escrow_pda_deserialized = compressed_escrow_pda + .compressed_account + .data + .as_ref() + .unwrap(); + let compressed_escrow_pda_data = + EscrowTimeLock::deserialize_reader(&mut &compressed_escrow_pda_deserialized.data[..]) + .unwrap(); + let current_slot = context.banks_client.get_root_slot().await.unwrap(); + assert_eq!(compressed_escrow_pda_data.slot, lock_up_time + current_slot); + assert_eq!( + compressed_escrow_pda_deserialized.discriminator, + 1u64.to_le_bytes(), + ); + assert_eq!( + compressed_escrow_pda_deserialized.data_hash, + Poseidon::hash(&compressed_escrow_pda_data.slot.to_le_bytes()).unwrap(), + ); +} diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 38cfb84237..06a1f60f1c 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -25,7 +25,7 @@ "test-all:verbose": "vitest run --reporter=verbose", "test-validator": "./../../cli/test_bin/run test-validator", "test:e2e:create-mint": "pnpm test-validator && vitest run tests/e2e/create-mint.test.ts", - "test:e2e:mint-to": "pnpm test-validator && vitest run tests/e2e/mint-to.test.ts", + "test:e2e:mint-to": "pnpm test-validator && vitest run tests/e2e/mint-to.test.ts --reporter=verbose", "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts --reporter=verbose", "test:e2e:compress": "pnpm test-validator && vitest run tests/e2e/compress.test.ts --reporter=verbose", "test:e2e:decompress": "pnpm test-validator && vitest run tests/e2e/decompress.test.ts --reporter=verbose", diff --git a/js/compressed-token/src/idl/light_compressed_token.ts b/js/compressed-token/src/idl/light_compressed_token.ts index 047a01b261..a0e0232567 100644 --- a/js/compressed-token/src/idl/light_compressed_token.ts +++ b/js/compressed-token/src/idl/light_compressed_token.ts @@ -123,6 +123,11 @@ export type LightCompressedToken = { isMut: true; isSigner: false; }, + { + name: 'selfProgram'; + isMut: false; + isSigner: false; + }, ]; args: [ { @@ -211,6 +216,14 @@ export type LightCompressedToken = { name: 'inputs'; type: 'bytes'; }, + { + name: 'cpiContext'; + type: { + option: { + defined: 'CompressedCpiContext'; + }; + }; + }, ]; }, ]; @@ -241,6 +254,46 @@ export type LightCompressedToken = { ]; }; }, + { + name: 'MerkleContext'; + type: { + kind: 'struct'; + fields: [ + { + name: 'merkleTreePubkey'; + type: 'publicKey'; + }, + { + name: 'nullifierQueuePubkey'; + type: 'publicKey'; + }, + { + name: 'leafIndex'; + type: 'u32'; + }, + ]; + }; + }, + { + name: 'PackedMerkleContext'; + type: { + kind: 'struct'; + fields: [ + { + name: 'merkleTreePubkeyIndex'; + type: 'u8'; + }, + { + name: 'nullifierQueuePubkeyIndex'; + type: 'u8'; + }, + { + name: 'leafIndex'; + type: 'u32'; + }, + ]; + }; + }, { name: 'CompressedAccount'; type: { @@ -297,6 +350,32 @@ export type LightCompressedToken = { ]; }; }, + { + name: 'CompressedCpiContext'; + docs: ['To spend multiple compressed']; + type: { + kind: 'struct'; + fields: [ + { + name: 'cpiSignatureAccountIndex'; + docs: [ + 'index of the output state Merkle tree that will be used to store cpi signatures', + 'The transaction will fail if this index is not consistent in your transaction.', + ]; + type: 'u8'; + }, + { + name: 'execute'; + docs: [ + 'The final cpi of your program needs to set execute to true.', + 'Execute compressed transaction will verify the proof and execute the transaction if this is true.', + 'If this is false the transaction will be stored in the cpi signature account.', + ]; + type: 'bool'; + }, + ]; + }; + }, { name: 'PublicTransactionEvent'; type: { @@ -441,6 +520,14 @@ export type LightCompressedToken = { name: 'isCompress'; type: 'bool'; }, + { + name: 'signerSeeds'; + type: { + option: { + vec: 'bytes'; + }; + }; + }, ]; }; }, @@ -967,6 +1054,11 @@ export const IDL: LightCompressedToken = { isMut: true, isSigner: false, }, + { + name: 'selfProgram', + isMut: false, + isSigner: false, + }, ], args: [ { @@ -1055,6 +1147,14 @@ export const IDL: LightCompressedToken = { name: 'inputs', type: 'bytes', }, + { + name: 'cpiContext', + type: { + option: { + defined: 'CompressedCpiContext', + }, + }, + }, ], }, ], @@ -1085,6 +1185,46 @@ export const IDL: LightCompressedToken = { ], }, }, + { + name: 'MerkleContext', + type: { + kind: 'struct', + fields: [ + { + name: 'merkleTreePubkey', + type: 'publicKey', + }, + { + name: 'nullifierQueuePubkey', + type: 'publicKey', + }, + { + name: 'leafIndex', + type: 'u32', + }, + ], + }, + }, + { + name: 'PackedMerkleContext', + type: { + kind: 'struct', + fields: [ + { + name: 'merkleTreePubkeyIndex', + type: 'u8', + }, + { + name: 'nullifierQueuePubkeyIndex', + type: 'u8', + }, + { + name: 'leafIndex', + type: 'u32', + }, + ], + }, + }, { name: 'CompressedAccount', type: { @@ -1141,6 +1281,32 @@ export const IDL: LightCompressedToken = { ], }, }, + { + name: 'CompressedCpiContext', + docs: ['To spend multiple compressed'], + type: { + kind: 'struct', + fields: [ + { + name: 'cpiSignatureAccountIndex', + docs: [ + 'index of the output state Merkle tree that will be used to store cpi signatures', + 'The transaction will fail if this index is not consistent in your transaction.', + ], + type: 'u8', + }, + { + name: 'execute', + docs: [ + 'The final cpi of your program needs to set execute to true.', + 'Execute compressed transaction will verify the proof and execute the transaction if this is true.', + 'If this is false the transaction will be stored in the cpi signature account.', + ], + type: 'bool', + }, + ], + }, + }, { name: 'PublicTransactionEvent', type: { @@ -1285,6 +1451,14 @@ export const IDL: LightCompressedToken = { name: 'isCompress', type: 'bool', }, + { + name: 'signerSeeds', + type: { + option: { + vec: 'bytes', + }, + }, + }, ], }, }, diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 09689423b1..e088c0cb50 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -347,10 +347,7 @@ export class CompressedTokenProgram { /** @internal */ static get deriveCpiAuthorityPda(): PublicKey { const [address, _] = PublicKey.findProgramAddressSync( - [ - CPI_AUTHORITY_SEED, - defaultStaticAccountsStruct().accountCompressionProgram.toBuffer(), - ], + [CPI_AUTHORITY_SEED], this.programId, ); return address; @@ -421,7 +418,6 @@ export class CompressedTokenProgram { const amounts = toArray(amount).map(amount => bn(amount)); const toPubkeys = toArray(toPubkey); - const ix = await this.program.methods .mintTo(toPubkeys, amounts) .accounts({ @@ -438,9 +434,9 @@ export class CompressedTokenProgram { systemKeys.accountCompressionAuthority, accountCompressionProgram: systemKeys.accountCompressionProgram, merkleTree, + selfProgram: this.programId, }) .instruction(); - return ix; } @@ -504,7 +500,7 @@ export class CompressedTokenProgram { } = defaultStaticAccountsStruct(); const instruction = await this.program.methods - .transfer(encodedData) + .transfer(encodedData, null) .accounts({ feePayer: payer!, authority: currentOwner!, @@ -588,7 +584,7 @@ export class CompressedTokenProgram { ); const instruction = await this.program.methods - .transfer(encodedData) + .transfer(encodedData, null) .accounts({ feePayer: payer, authority: owner, @@ -670,7 +666,7 @@ export class CompressedTokenProgram { } = defaultStaticAccountsStruct(); const instruction = await this.program.methods - .transfer(encodedData) + .transfer(encodedData, null) .accounts({ feePayer: payer, authority: currentOwner, diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 060dfee4b2..9382d387c3 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -31,7 +31,7 @@ "test-all": "vitest run", "test:unit:all": "EXCLUDE_E2E=true vitest run", "test-validator": "./../../cli/test_bin/run test-validator", - "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts", + "test:e2e:transfer": "pnpm test-validator && vitest run tests/e2e/transfer.test.ts --reporter=verbose", "test:e2e:compress": "pnpm test-validator && vitest run tests/e2e/compress.test.ts", "test:e2e:rpc": "pnpm test-validator && vitest run tests/e2e/rpc.test.ts", "test:e2e:browser": "pnpm playwright test", diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index 1f2ee51369..81e35849d8 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -17,10 +17,7 @@ export const getRegisteredProgramPda = () => export const getAccountCompressionAuthority = () => PublicKey.findProgramAddressSync( - [ - Buffer.from('cpi_authority'), - new PublicKey(accountCompressionProgram).toBytes(), - ], + [Buffer.from('cpi_authority')], new PublicKey( // TODO: can add check to ensure its consistent with the idl '6UqiSPd2mRCTTwkzhcs1M6DGYsqHWd5jiPueX3LwDMXQ', diff --git a/js/stateless.js/src/idls/light_compressed_pda.ts b/js/stateless.js/src/idls/light_compressed_pda.ts index 23056dede4..8c339f3273 100644 --- a/js/stateless.js/src/idls/light_compressed_pda.ts +++ b/js/stateless.js/src/idls/light_compressed_pda.ts @@ -34,6 +34,32 @@ export type LightCompressedPda = { ]; args: []; }, + { + name: 'initCpiSignatureAccount'; + accounts: [ + { + name: 'feePayer'; + isMut: true; + isSigner: true; + }, + { + name: 'cpiSignatureAccount'; + isMut: true; + isSigner: false; + }, + { + name: 'systemProgram'; + isMut: false; + isSigner: false; + }, + { + name: 'associatedMerkleTree'; + isMut: false; + isSigner: false; + }, + ]; + args: []; + }, { name: 'executeCompressedTransaction'; docs: [ @@ -69,7 +95,7 @@ export type LightCompressedPda = { }, { name: 'cpiSignatureAccount'; - isMut: false; + isMut: true; isSigner: false; isOptional: true; }, @@ -103,10 +129,15 @@ export type LightCompressedPda = { name: 'inputs'; type: 'bytes'; }, + { + name: 'cpiContext'; + type: { + option: { + defined: 'CompressedCpiContext'; + }; + }; + }, ]; - returns: { - defined: 'event::PublicTransactionEvent'; - }; }, ]; accounts: [ @@ -120,8 +151,12 @@ export type LightCompressedPda = { kind: 'struct'; fields: [ { - name: 'slot'; - type: 'u64'; + name: 'associatedMerkleTree'; + type: 'publicKey'; + }, + { + name: 'execute'; + type: 'bool'; }, { name: 'signatures'; @@ -169,6 +204,46 @@ export type LightCompressedPda = { ]; }; }, + { + name: 'MerkleContext'; + type: { + kind: 'struct'; + fields: [ + { + name: 'merkleTreePubkey'; + type: 'publicKey'; + }, + { + name: 'nullifierQueuePubkey'; + type: 'publicKey'; + }, + { + name: 'leafIndex'; + type: 'u32'; + }, + ]; + }; + }, + { + name: 'PackedMerkleContext'; + type: { + kind: 'struct'; + fields: [ + { + name: 'merkleTreePubkeyIndex'; + type: 'u8'; + }, + { + name: 'nullifierQueuePubkeyIndex'; + type: 'u8'; + }, + { + name: 'leafIndex'; + type: 'u32'; + }, + ]; + }; + }, { name: 'CompressedAccount'; type: { @@ -225,6 +300,32 @@ export type LightCompressedPda = { ]; }; }, + { + name: 'CompressedCpiContext'; + docs: ['To spend multiple compressed']; + type: { + kind: 'struct'; + fields: [ + { + name: 'cpiSignatureAccountIndex'; + docs: [ + 'index of the output state Merkle tree that will be used to store cpi signatures', + 'The transaction will fail if this index is not consistent in your transaction.', + ]; + type: 'u8'; + }, + { + name: 'execute'; + docs: [ + 'The final cpi of your program needs to set execute to true.', + 'Execute compressed transaction will verify the proof and execute the transaction if this is true.', + 'If this is false the transaction will be stored in the cpi signature account.', + ]; + type: 'bool'; + }, + ]; + }; + }, { name: 'PublicTransactionEvent'; type: { @@ -369,6 +470,14 @@ export type LightCompressedPda = { name: 'isCompress'; type: 'bool'; }, + { + name: 'signerSeeds'; + type: { + option: { + vec: 'bytes'; + }; + }; + }, ]; }; }, @@ -602,6 +711,26 @@ export type LightCompressedPda = { name: 'DelegateUndefined'; msg: 'DelegateUndefined while delegated amount is defined'; }, + { + code: 6030; + name: 'CpiSignatureAccountUndefined'; + msg: 'CpiSignatureAccountUndefined'; + }, + { + code: 6031; + name: 'WriteAccessCheckFailed'; + msg: 'WriteAccessCheckFailed'; + }, + { + code: 6032; + name: 'InvokingProgramNotProvided'; + msg: 'InvokingProgramNotProvided'; + }, + { + code: 6033; + name: 'SignerSeedsNotProvided'; + msg: 'SignerSeedsNotProvided'; + }, ]; }; @@ -641,6 +770,32 @@ export const IDL: LightCompressedPda = { ], args: [], }, + { + name: 'initCpiSignatureAccount', + accounts: [ + { + name: 'feePayer', + isMut: true, + isSigner: true, + }, + { + name: 'cpiSignatureAccount', + isMut: true, + isSigner: false, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'associatedMerkleTree', + isMut: false, + isSigner: false, + }, + ], + args: [], + }, { name: 'executeCompressedTransaction', docs: [ @@ -676,7 +831,7 @@ export const IDL: LightCompressedPda = { }, { name: 'cpiSignatureAccount', - isMut: false, + isMut: true, isSigner: false, isOptional: true, }, @@ -710,10 +865,15 @@ export const IDL: LightCompressedPda = { name: 'inputs', type: 'bytes', }, + { + name: 'cpiContext', + type: { + option: { + defined: 'CompressedCpiContext', + }, + }, + }, ], - returns: { - defined: 'event::PublicTransactionEvent', - }, }, ], accounts: [ @@ -727,8 +887,12 @@ export const IDL: LightCompressedPda = { kind: 'struct', fields: [ { - name: 'slot', - type: 'u64', + name: 'associatedMerkleTree', + type: 'publicKey', + }, + { + name: 'execute', + type: 'bool', }, { name: 'signatures', @@ -776,6 +940,46 @@ export const IDL: LightCompressedPda = { ], }, }, + { + name: 'MerkleContext', + type: { + kind: 'struct', + fields: [ + { + name: 'merkleTreePubkey', + type: 'publicKey', + }, + { + name: 'nullifierQueuePubkey', + type: 'publicKey', + }, + { + name: 'leafIndex', + type: 'u32', + }, + ], + }, + }, + { + name: 'PackedMerkleContext', + type: { + kind: 'struct', + fields: [ + { + name: 'merkleTreePubkeyIndex', + type: 'u8', + }, + { + name: 'nullifierQueuePubkeyIndex', + type: 'u8', + }, + { + name: 'leafIndex', + type: 'u32', + }, + ], + }, + }, { name: 'CompressedAccount', type: { @@ -832,6 +1036,32 @@ export const IDL: LightCompressedPda = { ], }, }, + { + name: 'CompressedCpiContext', + docs: ['To spend multiple compressed'], + type: { + kind: 'struct', + fields: [ + { + name: 'cpiSignatureAccountIndex', + docs: [ + 'index of the output state Merkle tree that will be used to store cpi signatures', + 'The transaction will fail if this index is not consistent in your transaction.', + ], + type: 'u8', + }, + { + name: 'execute', + docs: [ + 'The final cpi of your program needs to set execute to true.', + 'Execute compressed transaction will verify the proof and execute the transaction if this is true.', + 'If this is false the transaction will be stored in the cpi signature account.', + ], + type: 'bool', + }, + ], + }, + }, { name: 'PublicTransactionEvent', type: { @@ -976,6 +1206,14 @@ export const IDL: LightCompressedPda = { name: 'isCompress', type: 'bool', }, + { + name: 'signerSeeds', + type: { + option: { + vec: 'bytes', + }, + }, + }, ], }, }, @@ -1209,5 +1447,25 @@ export const IDL: LightCompressedPda = { name: 'DelegateUndefined', msg: 'DelegateUndefined while delegated amount is defined', }, + { + code: 6030, + name: 'CpiSignatureAccountUndefined', + msg: 'CpiSignatureAccountUndefined', + }, + { + code: 6031, + name: 'WriteAccessCheckFailed', + msg: 'WriteAccessCheckFailed', + }, + { + code: 6032, + name: 'InvokingProgramNotProvided', + msg: 'InvokingProgramNotProvided', + }, + { + code: 6033, + name: 'SignerSeedsNotProvided', + msg: 'SignerSeedsNotProvided', + }, ], }; diff --git a/js/stateless.js/src/idls/light_compressed_token.ts b/js/stateless.js/src/idls/light_compressed_token.ts index b790b0031a..a0e0232567 100644 --- a/js/stateless.js/src/idls/light_compressed_token.ts +++ b/js/stateless.js/src/idls/light_compressed_token.ts @@ -123,6 +123,11 @@ export type LightCompressedToken = { isMut: true; isSigner: false; }, + { + name: 'selfProgram'; + isMut: false; + isSigner: false; + }, ]; args: [ { @@ -211,6 +216,14 @@ export type LightCompressedToken = { name: 'inputs'; type: 'bytes'; }, + { + name: 'cpiContext'; + type: { + option: { + defined: 'CompressedCpiContext'; + }; + }; + }, ]; }, ]; @@ -241,6 +254,46 @@ export type LightCompressedToken = { ]; }; }, + { + name: 'MerkleContext'; + type: { + kind: 'struct'; + fields: [ + { + name: 'merkleTreePubkey'; + type: 'publicKey'; + }, + { + name: 'nullifierQueuePubkey'; + type: 'publicKey'; + }, + { + name: 'leafIndex'; + type: 'u32'; + }, + ]; + }; + }, + { + name: 'PackedMerkleContext'; + type: { + kind: 'struct'; + fields: [ + { + name: 'merkleTreePubkeyIndex'; + type: 'u8'; + }, + { + name: 'nullifierQueuePubkeyIndex'; + type: 'u8'; + }, + { + name: 'leafIndex'; + type: 'u32'; + }, + ]; + }; + }, { name: 'CompressedAccount'; type: { @@ -297,6 +350,32 @@ export type LightCompressedToken = { ]; }; }, + { + name: 'CompressedCpiContext'; + docs: ['To spend multiple compressed']; + type: { + kind: 'struct'; + fields: [ + { + name: 'cpiSignatureAccountIndex'; + docs: [ + 'index of the output state Merkle tree that will be used to store cpi signatures', + 'The transaction will fail if this index is not consistent in your transaction.', + ]; + type: 'u8'; + }, + { + name: 'execute'; + docs: [ + 'The final cpi of your program needs to set execute to true.', + 'Execute compressed transaction will verify the proof and execute the transaction if this is true.', + 'If this is false the transaction will be stored in the cpi signature account.', + ]; + type: 'bool'; + }, + ]; + }; + }, { name: 'PublicTransactionEvent'; type: { @@ -441,6 +520,14 @@ export type LightCompressedToken = { name: 'isCompress'; type: 'bool'; }, + { + name: 'signerSeeds'; + type: { + option: { + vec: 'bytes'; + }; + }; + }, ]; }; }, @@ -611,12 +698,6 @@ export type LightCompressedToken = { name: 'outputStateMerkleTreeAccountIndices'; type: 'bytes'; }, - { - name: 'pubkeyArray'; - type: { - vec: 'publicKey'; - }; - }, { name: 'isCompress'; type: 'bool'; @@ -973,6 +1054,11 @@ export const IDL: LightCompressedToken = { isMut: true, isSigner: false, }, + { + name: 'selfProgram', + isMut: false, + isSigner: false, + }, ], args: [ { @@ -1061,6 +1147,14 @@ export const IDL: LightCompressedToken = { name: 'inputs', type: 'bytes', }, + { + name: 'cpiContext', + type: { + option: { + defined: 'CompressedCpiContext', + }, + }, + }, ], }, ], @@ -1091,6 +1185,46 @@ export const IDL: LightCompressedToken = { ], }, }, + { + name: 'MerkleContext', + type: { + kind: 'struct', + fields: [ + { + name: 'merkleTreePubkey', + type: 'publicKey', + }, + { + name: 'nullifierQueuePubkey', + type: 'publicKey', + }, + { + name: 'leafIndex', + type: 'u32', + }, + ], + }, + }, + { + name: 'PackedMerkleContext', + type: { + kind: 'struct', + fields: [ + { + name: 'merkleTreePubkeyIndex', + type: 'u8', + }, + { + name: 'nullifierQueuePubkeyIndex', + type: 'u8', + }, + { + name: 'leafIndex', + type: 'u32', + }, + ], + }, + }, { name: 'CompressedAccount', type: { @@ -1147,6 +1281,32 @@ export const IDL: LightCompressedToken = { ], }, }, + { + name: 'CompressedCpiContext', + docs: ['To spend multiple compressed'], + type: { + kind: 'struct', + fields: [ + { + name: 'cpiSignatureAccountIndex', + docs: [ + 'index of the output state Merkle tree that will be used to store cpi signatures', + 'The transaction will fail if this index is not consistent in your transaction.', + ], + type: 'u8', + }, + { + name: 'execute', + docs: [ + 'The final cpi of your program needs to set execute to true.', + 'Execute compressed transaction will verify the proof and execute the transaction if this is true.', + 'If this is false the transaction will be stored in the cpi signature account.', + ], + type: 'bool', + }, + ], + }, + }, { name: 'PublicTransactionEvent', type: { @@ -1291,6 +1451,14 @@ export const IDL: LightCompressedToken = { name: 'isCompress', type: 'bool', }, + { + name: 'signerSeeds', + type: { + option: { + vec: 'bytes', + }, + }, + }, ], }, }, @@ -1461,12 +1629,6 @@ export const IDL: LightCompressedToken = { name: 'outputStateMerkleTreeAccountIndices', type: 'bytes', }, - { - name: 'pubkeyArray', - type: { - vec: 'publicKey', - }, - }, { name: 'isCompress', type: 'bool', diff --git a/js/stateless.js/src/programs/compressed-pda.ts b/js/stateless.js/src/programs/compressed-pda.ts index bd02b6b00f..f4424def34 100644 --- a/js/stateless.js/src/programs/compressed-pda.ts +++ b/js/stateless.js/src/programs/compressed-pda.ts @@ -303,12 +303,13 @@ export class LightSystemProgram { relayFee: null, compressionLamports: null, isCompress: false, + signerSeeds: [], }, ); /// Build anchor instruction const instruction = await this.program.methods - .executeCompressedTransaction(data) + .executeCompressedTransaction(data, null) .accounts({ ...defaultStaticAccountsStruct(), signer: payer, @@ -382,6 +383,7 @@ export class LightSystemProgram { relayFee: null, compressionLamports: lamports, isCompress: true, + signerSeeds: [], }; const data = this.program.coder.types.encode( @@ -391,7 +393,7 @@ export class LightSystemProgram { /// Build anchor instruction const instruction = await this.program.methods - .executeCompressedTransaction(data) + .executeCompressedTransaction(data, null) .accounts({ ...defaultStaticAccountsStruct(), signer: payer, @@ -455,12 +457,13 @@ export class LightSystemProgram { relayFee: null, compressionLamports: lamports, isCompress: false, + signerSeeds: [], }, ); /// Build anchor instruction const instruction = await this.program.methods - .executeCompressedTransaction(data) + .executeCompressedTransaction(data, null) .accounts({ ...defaultStaticAccountsStruct(), signer: payer, diff --git a/js/stateless.js/src/state/types.ts b/js/stateless.js/src/state/types.ts index 9fa7ecacbe..bb4d13dad1 100644 --- a/js/stateless.js/src/state/types.ts +++ b/js/stateless.js/src/state/types.ts @@ -59,6 +59,7 @@ export interface InstructionDataTransfer { compressionLamports: BN | null; // Option isCompress: boolean; // bool newAddressParams: NewAddressParamsPacked[]; // Vec + signerSeeds: number[][]; // Vec> } export interface NewAddressParamsPacked { diff --git a/macros/light/src/lib.rs b/macros/light/src/lib.rs index 02b456b4b7..f15d0796d2 100644 --- a/macros/light/src/lib.rs +++ b/macros/light/src/lib.rs @@ -66,6 +66,5 @@ pub fn heap_neutral(_: TokenStream, input: TokenStream) -> TokenStream { let len = function.block.stmts.len(); function.block.stmts.insert(len - 1, log_post); function.block.stmts.insert(len - 1, cleanup_code); - TokenStream::from(quote! { #function }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbb44550d6..b2da048bc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,10 +485,6 @@ importers: hasher.rs/src/main/wasm-simd: {} - hasher.rs/src/wasm/dist/wasm: {} - - hasher.rs/src/wasm/dist/wasm-simd: {} - js/compressed-token: dependencies: '@coral-xyz/anchor': @@ -8323,7 +8319,7 @@ packages: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.33.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) @@ -8456,7 +8452,7 @@ packages: enhanced-resolve: 5.15.0 eslint: 8.57.0 eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 is-core-module: 2.13.1 @@ -8478,7 +8474,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.6.0)(eslint@8.57.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 @@ -8516,7 +8512,7 @@ packages: debug: 3.2.7 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color dev: true @@ -8550,6 +8546,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.2) + debug: 3.2.7 + eslint: 8.57.0 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@7.6.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -8636,7 +8661,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.6.0)(eslint@8.57.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8646,7 +8671,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.3.2) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -8655,7 +8680,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.6.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8671,7 +8696,7 @@ packages: - supports-color dev: true - /eslint-plugin-import@2.29.1(eslint@8.57.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.6.0)(eslint@8.57.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8681,6 +8706,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: + '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.3.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -8689,7 +8715,7 @@ packages: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.6.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8887,7 +8913,7 @@ packages: '@typescript-eslint/eslint-plugin': 6.7.4(@typescript-eslint/parser@6.21.0)(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 7.2.0(eslint@8.55.0)(typescript@5.3.2) eslint: 8.55.0 - vitest: 0.34.6(playwright@1.43.1) + vitest: 0.34.6(@vitest/browser@0.34.6)(playwright@1.40.1) transitivePeerDependencies: - supports-color - typescript diff --git a/programs/account-compression/src/utils/check_registered_or_signer.rs b/programs/account-compression/src/utils/check_registered_or_signer.rs index d195f28c9a..e8ff6c3a2a 100644 --- a/programs/account-compression/src/utils/check_registered_or_signer.rs +++ b/programs/account-compression/src/utils/check_registered_or_signer.rs @@ -27,11 +27,8 @@ pub fn check_registered_or_signer< ) -> Result<()> { match ctx.accounts.get_registered_program_pda() { Some(account) => { - let derived_address = Pubkey::find_program_address( - &[b"cpi_authority", ctx.program_id.to_bytes().as_ref()], - &account.pubkey, - ) - .0; + let derived_address = + Pubkey::find_program_address(&[b"cpi_authority"], &account.pubkey).0; if ctx.accounts.get_signing_address().key() == derived_address { Ok(()) } else { diff --git a/programs/account-compression/tests/merkle_tree_tests.rs b/programs/account-compression/tests/merkle_tree_tests.rs index aae9f358c9..d65e86fede 100644 --- a/programs/account-compression/tests/merkle_tree_tests.rs +++ b/programs/account-compression/tests/merkle_tree_tests.rs @@ -236,7 +236,6 @@ async fn test_nullify_leaves() { program_test.set_compute_max_units(1_400_000u64); let mut context = program_test.start_with_context().await; - let payer = context.payer.insecure_clone(); let payer_pubkey = context.payer.pubkey(); let nullifier_queue_keypair = Keypair::new(); @@ -385,7 +384,8 @@ pub async fn nullify( change_log_index: u64, leaf_queue_index: u16, element_index: u64, -) -> Result<(), BanksClientError> { +) -> std::result::Result<(), BanksClientError> { + let payer = context.payer.insecure_clone(); let proof: Vec<[u8; 32]> = reference_merkle_tree .get_proof_of_leaf(element_index as usize, false) .unwrap() @@ -404,8 +404,6 @@ pub async fn nullify( nullifier_queue_pubkey, ), ]; - let payer = context.payer.insecure_clone(); - create_and_send_transaction(context, &instructions, &payer.pubkey(), &[&payer]).await?; let merkle_tree = @@ -596,7 +594,6 @@ async fn functional_2_test_insert_into_nullifier_queues( merkle_tree_pubkey: &Pubkey, ) { let payer = context.payer.insecure_clone(); - let elements = vec![[1_u8; 32], [2_u8; 32]]; insert_into_nullifier_queues( &elements, @@ -668,7 +665,6 @@ async fn functional_5_test_insert_into_nullifier_queues( merkle_tree_pubkey: &Pubkey, ) { let payer = context.payer.insecure_clone(); - let element = 3_u32.to_biguint().unwrap(); let elements = vec![bigint_to_be_bytes_array(&element).unwrap()]; insert_into_nullifier_queues( diff --git a/programs/compressed-pda/src/append_state.rs b/programs/compressed-pda/src/append_state.rs index 8df5b8c042..b5ebe0f416 100644 --- a/programs/compressed-pda/src/append_state.rs +++ b/programs/compressed-pda/src/append_state.rs @@ -43,7 +43,12 @@ pub fn insert_output_compressed_accounts_into_state_merkle_tree<'a, 'b, 'c: 'inf } // Address has to be created or a compressed account with this address has to be provided as transaction input. if let Some(address) = inputs.output_compressed_accounts[j].address { - if let Some(position) = addresses.iter().position(|&x| x.unwrap() == address) { + msg!("addresses {:?}", addresses); + if let Some(position) = addresses + .iter() + .filter(|x| x.is_some()) + .position(|&x| x.unwrap() == address) + { addresses.remove(position); } else { msg!("Address {:?}, has not been created and no compressed account with this address was provided as transaction input", address); @@ -82,9 +87,10 @@ pub fn append_leaves_cpi<'a, 'b>( out_merkle_trees_account_infos: Vec>, leaves: Vec<[u8; 32]>, ) -> Result<()> { - let (seed, bump) = get_seeds(program_id, &authority.key())?; + let (_, bump) = + anchor_lang::prelude::Pubkey::find_program_address(&[b"cpi_authority"], program_id); let bump = &[bump]; - let seeds = &[&[b"cpi_authority", seed.as_slice(), bump][..]]; + let seeds = &[&[b"cpi_authority".as_slice(), bump][..]]; let accounts = account_compression::cpi::accounts::AppendLeaves { authority: authority.to_account_info(), @@ -98,12 +104,3 @@ pub fn append_leaves_cpi<'a, 'b>( account_compression::cpi::append_leaves_to_merkle_trees(cpi_ctx, leaves)?; Ok(()) } - -#[inline(never)] -pub fn get_seeds<'a>(program_id: &'a Pubkey, cpi_signer: &'a Pubkey) -> Result<([u8; 32], u8)> { - let seed = account_compression::ID.key().to_bytes(); - let (key, bump) = - Pubkey::find_program_address(&[b"cpi_authority", seed.as_slice()], program_id); - assert_eq!(key, *cpi_signer); - Ok((seed, bump)) -} diff --git a/programs/compressed-pda/src/compressed_account.rs b/programs/compressed-pda/src/compressed_account.rs index 4def60c759..49e8aff785 100644 --- a/programs/compressed-pda/src/compressed_account.rs +++ b/programs/compressed-pda/src/compressed_account.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anchor_lang::prelude::*; use light_hasher::{Hasher, Poseidon}; use light_utils::hash_to_bn254_field_size_le; @@ -10,6 +12,60 @@ pub struct CompressedAccountWithMerkleContext { pub leaf_index: u32, } +// TODO: use in CompressedAccountWithMerkleContext and rename to CompressedAccountAndMerkleContext +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct MerkleContext { + pub merkle_tree_pubkey: Pubkey, + pub nullifier_queue_pubkey: Pubkey, + pub leaf_index: u32, +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedMerkleContext { + pub merkle_tree_pubkey_index: u8, + pub nullifier_queue_pubkey_index: u8, + pub leaf_index: u32, +} + +pub fn pack_merkle_context( + merkle_context: &[MerkleContext], + remaining_accounts: &mut HashMap, +) -> Vec { + let mut merkle_context_packed = merkle_context + .iter() + .map(|x| PackedMerkleContext { + leaf_index: x.leaf_index, + merkle_tree_pubkey_index: 0, // will be assigned later + nullifier_queue_pubkey_index: 0, // will be assigned later + }) + .collect::>(); + let len: usize = remaining_accounts.len(); + for (i, params) in merkle_context.iter().enumerate() { + match remaining_accounts.get(¶ms.merkle_tree_pubkey) { + Some(_) => {} + None => { + remaining_accounts.insert(params.merkle_tree_pubkey, i + len); + } + }; + merkle_context_packed[i].merkle_tree_pubkey_index = + *remaining_accounts.get(¶ms.merkle_tree_pubkey).unwrap() as u8; + } + + let len: usize = remaining_accounts.len(); + for (i, params) in merkle_context.iter().enumerate() { + match remaining_accounts.get(¶ms.nullifier_queue_pubkey) { + Some(_) => {} + None => { + remaining_accounts.insert(params.nullifier_queue_pubkey, i + len); + } + }; + merkle_context_packed[i].nullifier_queue_pubkey_index = *remaining_accounts + .get(¶ms.nullifier_queue_pubkey) + .unwrap() as u8; + } + merkle_context_packed +} + #[derive(Debug, PartialEq, Default, Clone, AnchorSerialize, AnchorDeserialize)] pub struct CompressedAccount { pub owner: Pubkey, diff --git a/programs/compressed-pda/src/compressed_cpi.rs b/programs/compressed-pda/src/compressed_cpi.rs new file mode 100644 index 0000000000..1a23b59d12 --- /dev/null +++ b/programs/compressed-pda/src/compressed_cpi.rs @@ -0,0 +1,99 @@ +use account_compression::StateMerkleTreeAccount; +use anchor_lang::prelude::*; + +use crate::{InstructionDataTransfer, TransferInstruction}; +use aligned_sized::aligned_sized; + +#[derive(Accounts)] +pub struct InitializeCpiSignatureAccount<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + #[account(zero)] + pub cpi_signature_account: Account<'info, CpiSignatureAccount>, + pub system_program: Program<'info, System>, + pub associated_merkle_tree: AccountLoader<'info, StateMerkleTreeAccount>, +} +// Security: +// - checking the slot is not enough there can be multiple transactions in the same slot +// - the CpiSignatureAccount must be derived from the first Merkle tree account as the current transaction +// - to check that all data in the CpiSignature account is from the same transaction we compare the proof bytes +// - I need to guaratee that all the data in the cpi signature account is from the same transaction +// - if we just overwrite the data in the account if the proof is different we cannot be sure because the program could be malicious +// - wouldn't the same proofs be enough, if you overwrite something then I discard everything that is in the account -> these utxos will not be spent +// - do I need to check ownership before or after? before we need to check who invoked the program +// - we need a transaction hash that hashes the complete instruction data, this will be a pain to produce offchain Sha256(proof, input_account_hashes, output_account_hashes, relay_fee, compression_lamports) +// - the last tx passes the hash and tries to recalculate the hash +/// collects invocations without proofs +/// invocations are collected and processed when an invocation with a proof is received +#[aligned_sized(anchor)] +// #[account] +#[derive(Debug, PartialEq, Default)] +#[account] +pub struct CpiSignatureAccount { + pub associated_merkle_tree: Pubkey, + pub execute: bool, + pub signatures: Vec, +} + +impl CpiSignatureAccount { + pub fn init(&mut self, associated_merkle_tree: Pubkey) { + self.associated_merkle_tree = associated_merkle_tree; + self.execute = false; + self.signatures = Vec::new(); + } +} + +pub const CPI_SEED: &[u8] = b"cpi_signature_pda"; + +/// To spend multiple compressed +#[derive(Debug, AnchorSerialize, AnchorDeserialize)] +pub struct CompressedCpiContext { + /// index of the output state Merkle tree that will be used to store cpi signatures + /// The transaction will fail if this index is not consistent in your transaction. + pub cpi_signature_account_index: u8, + /// The final cpi of your program needs to set execute to true. + /// Execute compressed transaction will verify the proof and execute the transaction if this is true. + /// If this is false the transaction will be stored in the cpi signature account. + pub execute: bool, +} + +// TODO: validate security of this approach +pub fn process_cpi_context<'a, 'b, 'c: 'info, 'info>( + cpi_context: CompressedCpiContext, + ctx: &mut Context<'a, 'b, 'c, 'info, TransferInstruction<'info>>, + inputs: &mut InstructionDataTransfer, +) -> Result> { + if !cpi_context.execute { + // TODO: enable for more than invocations by adding an execute tx input, we should have a macro that adds it automatically to a program that wants to activate cpi + // TODO: remove cpi_signature_account and make the Merkle tree accounts bigger + // we should use one of the output Merkle tree accounts as the cpi_signature_account + match ctx.accounts.cpi_signature_account.is_some() { + true => { + if let Some(cpi_signature_account) = &mut ctx.accounts.cpi_signature_account { + msg!("cpi_signature_account detected"); + // Check conditions and modify the signatures + if cpi_signature_account.signatures.is_empty() { + msg!("cpi signatures are empty"); + // cpi signature account should only be used with mutiple compressed accounts owned by different programs + // thus the first invocation execute is assumed to be false + cpi_signature_account.signatures.push(inputs.clone()); + } else if cpi_signature_account.signatures[0].proof.as_ref().unwrap() + == inputs.proof.as_ref().unwrap() + { + cpi_signature_account.signatures.push(inputs.clone()); + } else { + cpi_signature_account.signatures = vec![inputs.clone()]; + } + }; + } + false => { + return err!(crate::ErrorCode::CpiSignatureAccountUndefined); + } + }; + return Ok(Some(true)); + } else if let Some(cpi_signature_account) = &ctx.accounts.cpi_signature_account { + inputs.combine(&cpi_signature_account.signatures); + } + + Ok(None) +} diff --git a/programs/compressed-pda/src/create_address.rs b/programs/compressed-pda/src/create_address.rs index 4131ee3fa4..e15534eb11 100644 --- a/programs/compressed-pda/src/create_address.rs +++ b/programs/compressed-pda/src/create_address.rs @@ -1,10 +1,7 @@ use account_compression::AddressQueueAccount; use anchor_lang::prelude::*; -use crate::{ - append_state::get_seeds, - instructions::{InstructionDataTransfer, TransferInstruction}, -}; +use crate::instructions::{InstructionDataTransfer, TransferInstruction}; pub fn insert_addresses_into_address_merkle_tree_queue<'a, 'b, 'c: 'info, 'info>( inputs: &'a InstructionDataTransfer, @@ -57,9 +54,10 @@ pub fn insert_addresses_cpi<'a, 'b>( address_merkle_tree_account_infos: Vec>, addresses: Vec<[u8; 32]>, ) -> Result<()> { - let (seed, bump) = get_seeds(program_id, &authority.key())?; + let (_, bump) = + anchor_lang::prelude::Pubkey::find_program_address(&[b"cpi_authority"], program_id); let bump = &[bump]; - let seeds = &[&[b"cpi_authority", seed.as_slice(), bump][..]]; + let seeds = &[&[b"cpi_authority".as_slice(), bump][..]]; let accounts = account_compression::cpi::accounts::InsertAddresses { authority: authority.to_account_info(), registered_program_pda: Some(registered_program_pda.to_account_info()), diff --git a/programs/compressed-pda/src/event.rs b/programs/compressed-pda/src/event.rs index 288fc23cb3..d8c8d6e48d 100644 --- a/programs/compressed-pda/src/event.rs +++ b/programs/compressed-pda/src/event.rs @@ -10,7 +10,7 @@ use crate::{ InstructionDataTransfer, TransferInstruction, }; -#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq)] pub struct PublicTransactionEvent { pub input_compressed_account_hashes: Vec<[u8; 32]>, pub output_compressed_account_hashes: Vec<[u8; 32]>, diff --git a/programs/compressed-pda/src/instructions.rs b/programs/compressed-pda/src/instructions.rs index b711674af1..86c14cba6c 100644 --- a/programs/compressed-pda/src/instructions.rs +++ b/programs/compressed-pda/src/instructions.rs @@ -1,11 +1,7 @@ -use std::borrow::Borrow; - -use account_compression::program::AccountCompression; -use anchor_lang::prelude::*; - use crate::{ append_state::insert_output_compressed_accounts_into_state_merkle_tree, compressed_account::{derive_address, CompressedAccount, CompressedAccountWithMerkleContext}, + compressed_cpi::{process_cpi_context, CompressedCpiContext, CpiSignatureAccount}, compression_lamports, create_address::insert_addresses_into_address_merkle_tree_queue, event::emit_state_transition_event, @@ -13,15 +9,32 @@ use crate::{ utils::CompressedProof, verify_state::{ fetch_roots, fetch_roots_address_merkle_tree, hash_input_compressed_accounts, signer_check, - sum_check, verify_state_proof, + sum_check, verify_state_proof, write_access_check, }, CompressedSolPda, ErrorCode, }; +use account_compression::program::AccountCompression; +use anchor_lang::prelude::*; +use std::collections::HashMap; pub fn process_execute_compressed_transaction<'a, 'b, 'c: 'info, 'info>( - inputs: &'a InstructionDataTransfer, - ctx: &'a Context<'a, 'b, 'c, 'info, TransferInstruction<'info>>, + inputs: &mut InstructionDataTransfer, + mut ctx: Context<'a, 'b, 'c, 'info, TransferInstruction<'info>>, + cpi_context: Option, ) -> Result<()> { + // signer check --------------------------------------------------- + signer_check(inputs, &ctx)?; + write_access_check( + inputs, + &ctx.accounts.invoking_program, + &ctx.accounts.signer.key(), + )?; + + if let Some(cpi_context) = cpi_context { + if process_cpi_context(cpi_context, &mut ctx, inputs)?.is_some() { + return Ok(()); + } + } // sum check --------------------------------------------------- // the sum of in compressed accounts and compressed accounts must be equal minus the relay fee sum_check( @@ -32,28 +45,14 @@ pub fn process_execute_compressed_transaction<'a, 'b, 'c: 'info, 'info>( &inputs.is_compress, )?; msg!("sum check success"); - // signer check --------------------------------------------------- - signer_check(inputs, ctx)?; - // TODO: if not proof store the instruction in cpi_signature_account and set cpi account slot to current slot, if slot is not current slot override the vector with a new vector - // TODO: add security check that only data from the current transaction stored in cpi account can be used in the current transaction - // TODO: add check that cpi account was derived from a Merkle tree account in the current transaction - // TODO: add check that if compressed account is program owned that it is signed by the program (if an account has data it is program owned, if the program account is set compressed accounts are program owned) - match ctx.accounts.cpi_signature_account.borrow() { - Some(_cpi_signature_account) => { - // needs to check every compressed account and make sure that signatures exist in cpi_signature_account - msg!("cpi_signature check is not implemented"); - err!(ErrorCode::CpiSignerCheckFailed) - } - None => Ok(()), - }?; // compression_lamports --------------------------------------------------- - compression_lamports(inputs, ctx)?; + compression_lamports(inputs, &ctx)?; let mut roots = vec![[0u8; 32]; inputs.input_compressed_accounts_with_merkle_context.len()]; - fetch_roots(inputs, ctx, &mut roots)?; + fetch_roots(inputs, &ctx, &mut roots)?; let mut address_roots = vec![[0u8; 32]; inputs.new_address_params.len()]; // TODO: enable once address merkle tree init is debugged - fetch_roots_address_merkle_tree(inputs, ctx, &mut address_roots)?; + fetch_roots_address_merkle_tree(inputs, &ctx, &mut address_roots)?; let mut input_compressed_account_hashes = vec![[0u8; 32]; inputs.input_compressed_accounts_with_merkle_context.len()]; @@ -68,15 +67,15 @@ pub fn process_execute_compressed_transaction<'a, 'b, 'c: 'info, 'info>( if !new_addresses.is_empty() { derive_new_addresses( inputs, - ctx, + &ctx, &mut input_compressed_account_addresses, &mut new_addresses, ); - insert_addresses_into_address_merkle_tree_queue(inputs, ctx, &new_addresses)?; + insert_addresses_into_address_merkle_tree_queue(inputs, &ctx, &new_addresses)?; } // TODO: add heap neutral hash_input_compressed_accounts( - ctx, + &ctx, inputs, &mut input_compressed_account_hashes, &mut input_compressed_account_addresses, @@ -102,14 +101,14 @@ pub fn process_execute_compressed_transaction<'a, 'b, 'c: 'info, 'info>( .input_compressed_accounts_with_merkle_context .is_empty() { - insert_nullifiers(inputs, ctx, &input_compressed_account_hashes)?; + insert_nullifiers(inputs, &ctx, &input_compressed_account_hashes)?; } // insert leaves (output compressed account hashes) --------------------------------------------------- if !inputs.output_compressed_accounts.is_empty() { insert_output_compressed_accounts_into_state_merkle_tree( inputs, - ctx, + &ctx, &mut output_leaf_indices, &mut output_compressed_account_hashes, &mut input_compressed_account_addresses, @@ -119,7 +118,7 @@ pub fn process_execute_compressed_transaction<'a, 'b, 'c: 'info, 'info>( // emit state transition event --------------------------------------------------- emit_state_transition_event( inputs, - ctx, + &ctx, &input_compressed_account_hashes, &output_compressed_account_hashes, &output_leaf_indices, @@ -167,10 +166,11 @@ pub struct TransferInstruction<'info> { /// CHECK: this account pub noop_program: UncheckedAccount<'info>, /// CHECK: this account in psp account compression program - #[account(seeds = [b"cpi_authority", account_compression::ID.to_bytes().as_slice()], bump,)] + #[account(seeds = [b"cpi_authority"], bump)] pub account_compression_authority: UncheckedAccount<'info>, /// CHECK: this account in psp account compression program pub account_compression_program: Program<'info, AccountCompression>, + #[account(mut)] pub cpi_signature_account: Option>, pub invoking_program: Option>, #[account(mut)] @@ -180,14 +180,6 @@ pub struct TransferInstruction<'info> { pub system_program: Option>, } -/// collects invocations without proofs -/// invocations are collected and processed when an invocation with a proof is received -#[account] -pub struct CpiSignatureAccount { - pub slot: u64, - pub signatures: Vec, -} - // TODO: add checks for lengths of vectors #[derive(Debug, PartialEq, Default, Clone, AnchorSerialize, AnchorDeserialize)] pub struct InstructionDataTransfer { @@ -201,6 +193,24 @@ pub struct InstructionDataTransfer { pub relay_fee: Option, pub compression_lamports: Option, pub is_compress: bool, + pub signer_seeds: Option>>, +} + +impl InstructionDataTransfer { + pub fn combine(&mut self, other: &[InstructionDataTransfer]) { + for other in other { + self.new_address_params + .extend_from_slice(&other.new_address_params); + self.input_root_indices + .extend_from_slice(&other.input_root_indices); + self.input_compressed_accounts_with_merkle_context + .extend_from_slice(&other.input_compressed_accounts_with_merkle_context); + self.output_compressed_accounts + .extend_from_slice(&other.output_compressed_accounts); + self.output_state_merkle_tree_account_indices + .extend_from_slice(&other.output_state_merkle_tree_account_indices); + } + } } #[derive(Debug, PartialEq, Default, Clone, AnchorSerialize, AnchorDeserialize)] @@ -219,6 +229,49 @@ pub struct NewAddressParams { pub address_merkle_tree_root_index: u16, } +// helper function to pack new address params for instruction data in rust clients +pub fn pack_new_address_params( + new_address_params: &[NewAddressParams], + remaining_accounts: &mut HashMap, +) -> Vec { + let mut new_address_params_packed = new_address_params + .iter() + .map(|x| NewAddressParamsPacked { + seed: x.seed, + address_merkle_tree_root_index: x.address_merkle_tree_root_index, + address_merkle_tree_account_index: 0, // will be assigned later + address_queue_account_index: 0, // will be assigned later + }) + .collect::>(); + let len: usize = remaining_accounts.len(); + for (i, params) in new_address_params.iter().enumerate() { + match remaining_accounts.get(¶ms.address_merkle_tree_pubkey) { + Some(_) => {} + None => { + remaining_accounts.insert(params.address_merkle_tree_pubkey, i + len); + } + }; + new_address_params_packed[i].address_merkle_tree_account_index = *remaining_accounts + .get(¶ms.address_merkle_tree_pubkey) + .unwrap() + as u8; + } + + let len: usize = remaining_accounts.len(); + for (i, params) in new_address_params.iter().enumerate() { + match remaining_accounts.get(¶ms.address_queue_pubkey) { + Some(_) => {} + None => { + remaining_accounts.insert(params.address_queue_pubkey, i + len); + } + }; + new_address_params_packed[i].address_queue_account_index = *remaining_accounts + .get(¶ms.address_queue_pubkey) + .unwrap() as u8; + } + new_address_params_packed +} + impl InstructionDataTransfer { /// Checks that the lengths of the vectors are consistent with each other. /// Note that this function does not check the inputs themselves just plausible of the lengths. @@ -227,18 +280,20 @@ impl InstructionDataTransfer { pub fn check_input_lengths(&self) -> Result<()> { if self.input_root_indices.len() != self.input_compressed_accounts_with_merkle_context.len() { - msg!("input_root_indices.len() {} != {} input_compressed_accounts_with_merkle_context.len()", + msg!("input_root_indices.len() {} != {} input_compressed_accounts_with_merkle_context.len()", self.input_root_indices.len(), self.input_compressed_accounts_with_merkle_context.len() ); + msg!("self {:?}", self); return Err(ErrorCode::LengthMismatch.into()); } if self.output_compressed_accounts.len() != self.output_state_merkle_tree_account_indices.len() { - msg!("output_compressed_accounts.len() {} != {} output_state_merkle_tree_account_indices.len()", + msg!("output_compressed_accounts.len() {} != {} output_state_merkle_tree_account_indices.len()", self.output_compressed_accounts.len(), self.output_state_merkle_tree_account_indices.len() ); + msg!("self {:?}", self); return Err(ErrorCode::LengthMismatch.into()); } @@ -279,3 +334,63 @@ impl InstructionDataTransfer { // output_compressed_accounts, // }) // } + +// test combine instruction data transfer +#[test] +fn test_combine_instruction_data_transfer() { + let mut instruction_data_transfer = InstructionDataTransfer { + proof: Some(CompressedProof { + a: [0; 32], + b: [0; 64], + c: [0; 32], + }), + new_address_params: vec![NewAddressParamsPacked::default()], + input_root_indices: vec![1], + input_compressed_accounts_with_merkle_context: vec![ + CompressedAccountWithMerkleContext::default(), + ], + output_compressed_accounts: vec![CompressedAccount::default()], + output_state_merkle_tree_account_indices: vec![1], + relay_fee: Some(1), + compression_lamports: Some(1), + is_compress: true, + signer_seeds: None, + }; + let other = InstructionDataTransfer { + proof: Some(CompressedProof { + a: [0; 32], + b: [0; 64], + c: [0; 32], + }), + new_address_params: vec![NewAddressParamsPacked::default()], + input_root_indices: vec![1], + input_compressed_accounts_with_merkle_context: vec![ + CompressedAccountWithMerkleContext::default(), + ], + output_compressed_accounts: vec![CompressedAccount::default()], + output_state_merkle_tree_account_indices: vec![1], + relay_fee: Some(1), + compression_lamports: Some(1), + is_compress: true, + signer_seeds: None, + }; + instruction_data_transfer.combine(&[other]); + assert_eq!(instruction_data_transfer.new_address_params.len(), 2); + assert_eq!(instruction_data_transfer.input_root_indices.len(), 2); + assert_eq!( + instruction_data_transfer + .input_compressed_accounts_with_merkle_context + .len(), + 2 + ); + assert_eq!( + instruction_data_transfer.output_compressed_accounts.len(), + 2 + ); + assert_eq!( + instruction_data_transfer + .output_state_merkle_tree_account_indices + .len(), + 2 + ); +} diff --git a/programs/compressed-pda/src/lib.rs b/programs/compressed-pda/src/lib.rs index 2999a23178..829fcdb37f 100644 --- a/programs/compressed-pda/src/lib.rs +++ b/programs/compressed-pda/src/lib.rs @@ -7,6 +7,8 @@ pub mod utils; pub use instructions::*; pub use sol_compression::*; pub mod compressed_account; +pub mod compressed_cpi; +pub use compressed_cpi::*; pub mod create_address; pub mod nullify_state; pub mod sdk; @@ -78,6 +80,14 @@ pub enum ErrorCode { LengthMismatch, #[msg("DelegateUndefined while delegated amount is defined")] DelegateUndefined, + #[msg("CpiSignatureAccountUndefined")] + CpiSignatureAccountUndefined, + #[msg("WriteAccessCheckFailed")] + WriteAccessCheckFailed, + #[msg("InvokingProgramNotProvided")] + InvokingProgramNotProvided, + #[msg("SignerSeedsNotProvided")] + SignerSeedsNotProvided, } // // TODO(vadorovsky): Come up with some less glass chewy way of reusing @@ -102,18 +112,39 @@ pub mod light_compressed_pda { Ok(()) } + pub fn init_cpi_signature_account(ctx: Context) -> Result<()> { + // check that merkle tree is initialized + let merkle_tree_account = ctx.accounts.associated_merkle_tree.load()?; + merkle_tree_account.load_merkle_tree()?; + ctx.accounts + .cpi_signature_account + .init(ctx.accounts.associated_merkle_tree.key()); + msg!( + "initialized cpi signature account pubkey {:?}", + ctx.accounts.cpi_signature_account.key() + ); + Ok(()) + } + /// This function can be used to transfer sol and execute any other compressed transaction. /// Instruction data is not optimized for space. /// This method can be called by cpi so that instruction data can be compressed with a custom algorithm. pub fn execute_compressed_transaction<'a, 'b, 'c: 'info, 'info>( ctx: Context<'a, 'b, 'c, 'info, TransferInstruction<'info>>, inputs: Vec, + cpi_context: Option, ) -> Result<()> { - msg!("execute_compressed_transaction"); - let inputs: InstructionDataTransfer = + // TODO: remove manual deserialization + let mut inputs: InstructionDataTransfer = InstructionDataTransfer::deserialize(&mut inputs.as_slice())?; inputs.check_input_lengths()?; - process_execute_compressed_transaction(&inputs, &ctx) + match process_execute_compressed_transaction(&mut inputs, ctx, cpi_context) { + Ok(_) => Ok(()), + Err(e) => { + msg!("inputs: {:?}", inputs); + Err(e) + } + } } // /// This function can be used to transfer sol and execute any other compressed transaction. diff --git a/programs/compressed-pda/src/nullify_state.rs b/programs/compressed-pda/src/nullify_state.rs index 2217ff64c1..fc5f8b7d75 100644 --- a/programs/compressed-pda/src/nullify_state.rs +++ b/programs/compressed-pda/src/nullify_state.rs @@ -2,10 +2,7 @@ use account_compression::NullifierQueueAccount; use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; use light_macros::heap_neutral; -use crate::{ - append_state::get_seeds, - instructions::{InstructionDataTransfer, TransferInstruction}, -}; +use crate::instructions::{InstructionDataTransfer, TransferInstruction}; /// 1. Checks that the nullifier queue account is associated with a state Merkle tree account. /// 2. Inserts nullifiers into the queue. @@ -67,9 +64,10 @@ pub fn insert_nullifiers_cpi<'a, 'b>( merkle_tree_account_infos: Vec>, nullifiers: Vec<[u8; 32]>, ) -> Result<()> { - let (seed, bump) = get_seeds(program_id, &authority.key())?; + let (_, bump) = + anchor_lang::prelude::Pubkey::find_program_address(&[b"cpi_authority"], program_id); let bump = &[bump]; - let seeds = &[&[b"cpi_authority", seed.as_slice(), bump][..]]; + let seeds = &[&[b"cpi_authority".as_slice(), bump][..]]; let accounts = account_compression::cpi::accounts::InsertIntoNullifierQueues { authority: authority.to_account_info(), diff --git a/programs/compressed-pda/src/sdk.rs b/programs/compressed-pda/src/sdk.rs index b1c93645e7..7c398b59cb 100644 --- a/programs/compressed-pda/src/sdk.rs +++ b/programs/compressed-pda/src/sdk.rs @@ -133,13 +133,17 @@ pub fn create_execute_compressed_instruction( new_address_params: new_address_params_packed, compression_lamports, is_compress, + signer_seeds: None, }; let mut inputs = Vec::new(); InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); - let instruction_data = crate::instruction::ExecuteCompressedTransaction { inputs }; + let instruction_data = crate::instruction::ExecuteCompressedTransaction { + inputs, + cpi_context: None, + }; let compressed_sol_pda = compression_lamports.map(|_| get_compressed_sol_pda()); @@ -150,11 +154,11 @@ pub fn create_execute_compressed_instruction( noop_program: account_compression::state::change_log_event::NOOP_PROGRAM_ID, account_compression_program: account_compression::ID, account_compression_authority: get_cpi_authority_pda(&crate::ID), - cpi_signature_account: None, invoking_program: None, compressed_sol_pda, compression_recipient, system_program: Some(solana_sdk::system_program::ID), + cpi_signature_account: None, }; Instruction { program_id: crate::ID, diff --git a/programs/compressed-pda/src/sol_compression.rs b/programs/compressed-pda/src/sol_compression.rs index 9c162785a3..7723416e4e 100644 --- a/programs/compressed-pda/src/sol_compression.rs +++ b/programs/compressed-pda/src/sol_compression.rs @@ -4,7 +4,7 @@ use anchor_lang::{ solana_program::{account_info::AccountInfo, pubkey::Pubkey}, }; -use crate::{append_state::get_seeds, InstructionDataTransfer, TransferInstruction}; +use crate::{InstructionDataTransfer, TransferInstruction}; #[account] #[aligned_sized(anchor)] @@ -94,10 +94,10 @@ pub fn transfer_lamports<'info>( msg!("to lamports: {}", to.lamports()); let instruction = anchor_lang::solana_program::system_instruction::transfer(from.key, to.key, lamports); - let (seed, bump) = get_seeds(&crate::ID, &authority.key())?; + let (_, bump) = + anchor_lang::prelude::Pubkey::find_program_address(&[b"cpi_authority"], &crate::ID); let bump = &[bump]; - let seeds = &[&[b"cpi_authority", seed.as_slice(), bump][..]]; - + let seeds = &[&[b"cpi_authority".as_slice(), bump][..]]; anchor_lang::solana_program::program::invoke_signed( &instruction, &[authority.clone(), from.clone(), to.clone()], diff --git a/programs/compressed-pda/src/utils.rs b/programs/compressed-pda/src/utils.rs index 29cf0a29d7..84cdc53e39 100644 --- a/programs/compressed-pda/src/utils.rs +++ b/programs/compressed-pda/src/utils.rs @@ -11,14 +11,7 @@ pub fn get_registered_program_pda(program_id: &Pubkey) -> Pubkey { } pub fn get_cpi_authority_pda(program_id: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[ - b"cpi_authority", - account_compression::ID.to_bytes().as_slice(), - ], - program_id, - ) - .0 + Pubkey::find_program_address(&[b"cpi_authority"], program_id).0 } #[derive(Debug, Clone, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] diff --git a/programs/compressed-pda/src/verify_state.rs b/programs/compressed-pda/src/verify_state.rs index d6c4dad0b6..4bbd82879d 100644 --- a/programs/compressed-pda/src/verify_state.rs +++ b/programs/compressed-pda/src/verify_state.rs @@ -82,6 +82,7 @@ pub fn hash_input_compressed_accounts<'a, 'b, 'c: 'info, 'info>( Some(address) => addresses[j - none_counter] = Some(*address), None => { none_counter += 1; + // TODO: debug // Vec::remove(addresses, j); } }; @@ -172,17 +173,18 @@ pub fn signer_check( // - The drawback is that the pda signer is the owner of the compressed account which is confusing if compressed_accounts.compressed_account.data.is_some() { let invoking_program_id = ctx.accounts.invoking_program.as_ref().unwrap().key(); - let signer = Pubkey::find_program_address( - &[b"cpi_authority"], + let signer = Pubkey::create_program_address( + &inputs.signer_seeds.as_ref().unwrap().iter().map(|x|x.as_slice()).collect::>()[..], &invoking_program_id, - ) - .0; - if signer != ctx.accounts.signer.key() - && invoking_program_id != compressed_accounts.compressed_account.owner + ).map_err(ProgramError::from)?; + if signer == ctx.accounts.signer.key() + && invoking_program_id == compressed_accounts.compressed_account.owner { + Ok(()) + } else { msg!( "program signer check failed derived cpi signer {} != signer {}", - compressed_accounts.compressed_account.owner, + signer, ctx.accounts.signer.key() ); msg!( @@ -191,24 +193,88 @@ pub fn signer_check( invoking_program_id ); err!(ErrorCode::SignerCheckFailed) - } else { - Ok(()) + } - } else if compressed_accounts.compressed_account.owner != ctx.accounts.signer.key() + } else if compressed_accounts.compressed_account.owner == ctx.accounts.signer.key() { + Ok(()) + } else { msg!( "signer check failed compressed account owner {} != signer {}", compressed_accounts.compressed_account.owner, ctx.accounts.signer.key() ); err!(ErrorCode::SignerCheckFailed) - } else { - Ok(()) } })?; Ok(()) } +/// Checks the write access for output compressed accounts. +/// Only program owned output accounts can hold data. +/// Every output account that holds data has to be owned by the invoking_program. +/// For every account that has data, the owner has to be the invoking_program. +// #[heap_neutral] //TODO: investigate why owned becomes mint when heap_neutral is used +pub fn write_access_check( + inputs: &InstructionDataTransfer, + invoking_program_id: &Option, + signer: &Pubkey, +) -> Result<()> { + // is triggered if one output account has data + let output_account_with_data = inputs + .output_compressed_accounts + .iter() + .filter(|compressed_account| compressed_account.data.is_some()) + .collect::>(); + if !output_account_with_data.is_empty() { + match invoking_program_id { + Some(invoking_program_id) => { + let seeds = match inputs.signer_seeds.as_ref() { + Some(seeds) => seeds.iter().map(|x| x.as_slice()).collect::>(), + None => { + msg!("signer seeds not provided but trying to create compressed output account with data"); + return err!(crate::ErrorCode::SignerSeedsNotProvided); + } + }; + let derived_signer = + Pubkey::create_program_address(&seeds[..], &invoking_program_id.key()) + .map_err(ProgramError::from)?; + if derived_signer != *signer { + msg!( + "Signer/Program cannot write into an account it doesn't own. Write access check failed derived cpi signer {} != signer {}", + signer, + signer + ); + msg!("seeds: {:?}", seeds); + return err!(crate::ErrorCode::SignerCheckFailed); + } + + output_account_with_data.iter().try_for_each(|compressed_account| { + if compressed_account.owner == invoking_program_id.key() { + Ok(()) + } else { + msg!( + "Signer/Program cannot write into an account it doesn't own. Write access check failed compressed account owner {} != invoking_program_id {}", + compressed_account.owner, + invoking_program_id.key() + ); + + msg!("compressed_account: {:?}", compressed_account); + err!(crate::ErrorCode::WriteAccessCheckFailed) + } + })?; + Ok(()) + } + None => { + msg!("invoking program id not provided but trying to create compressed output account with data"); + err!(crate::ErrorCode::InvokingProgramNotProvided) + } + } + } else { + Ok(()) + } +} + #[heap_neutral] pub fn verify_state_proof( roots: &[[u8; 32]], @@ -226,7 +292,6 @@ pub fn verify_state_proof( compressed_proof, ) } else if !addresses.is_empty() { - msg!("create address verification currently not checked"); verify_create_addresses_zkp(address_roots, addresses, compressed_proof) } else { verify_merkle_proof_zkp(roots, leaves, compressed_proof) diff --git a/programs/compressed-pda/tests/test.rs b/programs/compressed-pda/tests/test.rs index 05229c964e..eb75cfc3e7 100644 --- a/programs/compressed-pda/tests/test.rs +++ b/programs/compressed-pda/tests/test.rs @@ -30,8 +30,9 @@ use light_compressed_pda::{ }; use light_indexed_merkle_tree::array::IndexedArray; use light_test_utils::{ - create_and_send_transaction, create_and_send_transaction_with_event, get_hash_set, - test_env::{setup_test_programs_with_accounts, EnvWithAccounts}, + assert_custom_error_or_program_error, create_and_send_transaction, + create_and_send_transaction_with_event, get_hash_set, + test_env::{setup_test_programs_with_accounts, EnvAccounts}, AccountZeroCopy, }; use num_bigint::{BigInt, BigUint, ToBigUint}; @@ -52,19 +53,18 @@ use tokio::fs::write as async_write; // TODO: use lazy_static to spawn the server once async fn init_mock_indexer( - env: &EnvWithAccounts, + payer: &Keypair, + env: &EnvAccounts, inclusion: bool, non_inclusion: bool, - combined: bool, ) -> MockIndexer { MockIndexer::new( env.merkle_tree_pubkey, env.nullifier_queue_pubkey, env.address_merkle_tree_pubkey, - env.context.payer.insecure_clone(), + payer.insecure_clone(), inclusion, non_inclusion, - combined, ) .await } @@ -76,11 +76,10 @@ async fn init_mock_indexer( /// 4. should succeed: in compressed account inserted in (1.) and valid zkp #[tokio::test] async fn test_execute_compressed_transaction() { - let env: EnvWithAccounts = setup_test_programs_with_accounts(None).await; + let (mut context, env) = setup_test_programs_with_accounts(None).await; - let mut mock_indexer = init_mock_indexer(&env, true, false, false).await; - let mut context = env.context; let payer = context.payer.insecure_clone(); + let mut mock_indexer = init_mock_indexer(&payer, &env, true, false).await; let payer_pubkey = payer.pubkey(); @@ -290,12 +289,10 @@ async fn test_execute_compressed_transaction() { /// testing: (input accounts, new addresses) (1, 1), (1, 2), (2, 1), (2, 2) #[tokio::test] async fn test_with_address() { - let env: EnvWithAccounts = setup_test_programs_with_accounts(None).await; - - let mut mock_indexer = init_mock_indexer(&env, true, true, false).await; + let (mut context, env) = setup_test_programs_with_accounts(None).await; - let mut context = env.context; let payer = context.payer.insecure_clone(); + let mut mock_indexer = init_mock_indexer(&payer, &env, true, true).await; let payer_pubkey = payer.pubkey(); let merkle_tree_pubkey = env.merkle_tree_pubkey; let nullifier_queue_pubkey = env.nullifier_queue_pubkey; @@ -340,13 +337,7 @@ async fn test_with_address() { ) .await .unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::InvalidAddress.into()) - )) - ); + assert_custom_error_or_program_error(res, ErrorCode::InvalidAddress.into()).unwrap(); let event = create_addresses( &mut context, @@ -456,6 +447,7 @@ async fn test_with_address() { TransactionError::InstructionError(0, InstructionError::Custom(6002)) )) )); + println!("test 2in -------------------------"); let address_seed_3 = [3u8; 32]; @@ -654,8 +646,7 @@ pub async fn create_addresses( #[tokio::test] async fn test_with_compression() { - let env: EnvWithAccounts = setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, env) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); @@ -670,7 +661,6 @@ async fn test_with_compression() { payer.insecure_clone(), true, false, - false, ); let instruction_data = light_compressed_pda::instruction::InitCompressSolPda {}; let accounts = light_compressed_pda::accounts::InitializeCompressedSolPda { @@ -729,13 +719,7 @@ async fn test_with_compression() { .await .unwrap(); // should fail because of insufficient input funds - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::ComputeOutputSumFailed.into()) - )) - ); + assert_custom_error_or_program_error(res, ErrorCode::ComputeOutputSumFailed.into()).unwrap(); let instruction = create_execute_compressed_instruction( &payer_pubkey, &Vec::new(), @@ -765,13 +749,7 @@ async fn test_with_compression() { .await .unwrap(); // should fail because of insufficient decompress amount funds - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::ComputeOutputSumFailed.into()) - )) - ); + assert_custom_error_or_program_error(res, ErrorCode::ComputeOutputSumFailed.into()).unwrap(); let instruction = create_execute_compressed_instruction( &payer_pubkey, @@ -877,14 +855,7 @@ async fn test_with_compression() { .await .unwrap(); // should fail because of insufficient output funds - - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::SumCheckFailed.into()) - )) - ); + assert_custom_error_or_program_error(res, ErrorCode::SumCheckFailed.into()).unwrap(); let instruction = create_execute_compressed_instruction( &payer_pubkey, @@ -941,19 +912,15 @@ async fn test_with_compression() { #[tokio::test] async fn regenerate_accounts() { let output_dir = "../../cli/accounts/"; - let env = setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, env) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); - // cannot move this into setup_test_programs_with_accounts because solana program test does not like deve dependencies that import the program itself let compressed_sol_pda = get_compressed_sol_pda(); + let instruction_data = light_compressed_pda::instruction::InitCompressSolPda {}; let accounts = light_compressed_pda::accounts::InitializeCompressedSolPda { fee_payer: payer.pubkey(), compressed_sol_pda, system_program: anchor_lang::solana_program::system_program::ID, }; - use anchor_lang::ToAccountMetas; - let instruction_data = light_compressed_pda::instruction::InitCompressSolPda {}; - let instruction = Instruction { program_id: light_compressed_pda::ID, accounts: accounts.to_account_metas(Some(true)), @@ -1007,6 +974,8 @@ pub struct MockIndexer { pub address_merkle_tree: light_indexed_merkle_tree::reference::IndexedMerkleTree, pub indexing_array: IndexedArray, + pub path: &'static str, + pub proof_types: Vec, } impl MockIndexer { @@ -1017,7 +986,6 @@ impl MockIndexer { payer: Keypair, inclusion: bool, non_inclusion: bool, - combined: bool, ) -> Self { let mut vec_proof_types = vec![]; if inclusion { @@ -1026,19 +994,12 @@ impl MockIndexer { if non_inclusion { vec_proof_types.push(ProofType::NonInclusion); } - if combined { - vec_proof_types.push(ProofType::Combined); - } if vec_proof_types.is_empty() { panic!("At least one proof type must be selected"); } - - spawn_gnark_server( - "../../circuit-lib/circuitlib-rs/scripts/prover.sh", - true, - vec_proof_types.as_slice(), - ) - .await; + let path = "../../circuit-lib/circuitlib-rs/scripts/prover.sh"; + let proof_types = vec_proof_types; + spawn_gnark_server(path, true, proof_types.as_slice()).await; let merkle_tree = light_merkle_tree_reference::MerkleTree::::new( STATE_MERKLE_TREE_HEIGHT as usize, @@ -1072,6 +1033,8 @@ impl MockIndexer { merkle_tree, address_merkle_tree, indexing_array: indexed_array, + path, + proof_types, } } @@ -1122,28 +1085,36 @@ impl MockIndexer { panic!("At least one of compressed_accounts or new_addresses must be provided") } }; - - let response_result = client - .post(&format!("{}{}", SERVER_ADDRESS, path)) - .header("Content-Type", "text/plain; charset=utf-8") - .body(json_payload) - .send() - .await - .expect("Failed to execute request."); - assert!(response_result.status().is_success()); - let body = response_result.text().await.unwrap(); - let proof_json = deserialize_gnark_proof_json(&body).unwrap(); - let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); - let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); - ProofRpcResult { - root_indices, - address_root_indices, - proof: CompressedProof { - a: proof_a, - b: proof_b, - c: proof_c, - }, + let mut retries = 5; + while retries > 0 { + if retries < 3 { + spawn_gnark_server(self.path, true, self.proof_types.as_slice()).await; + } + let response_result = client + .post(&format!("{}{}", SERVER_ADDRESS, path)) + .header("Content-Type", "text/plain; charset=utf-8") + .body(json_payload.clone()) + .send() + .await + .expect("Failed to execute request."); + if response_result.status().is_success() { + let body = response_result.text().await.unwrap(); + let proof_json = deserialize_gnark_proof_json(&body).unwrap(); + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); + let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); + return ProofRpcResult { + root_indices, + address_root_indices, + proof: CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + }, + }; + } + retries -= 1; } + panic!("Failed to get proof from server"); } async fn process_inclusion_proofs( diff --git a/programs/compressed-token/src/lib.rs b/programs/compressed-token/src/lib.rs index 8dcad115d9..725cb069f4 100644 --- a/programs/compressed-token/src/lib.rs +++ b/programs/compressed-token/src/lib.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use light_compressed_pda::compressed_cpi::CompressedCpiContext; pub mod process_mint; pub mod process_transfer; @@ -46,8 +47,9 @@ pub mod light_compressed_token { pub fn transfer<'info>( ctx: Context<'_, '_, '_, 'info, TransferInstruction<'info>>, inputs: Vec, + cpi_context: Option, ) -> Result<()> { - process_transfer::process_transfer(ctx, inputs) + process_transfer::process_transfer(ctx, inputs, cpi_context) } // TODO: implement update mint, freeze compressed_account, thaw compressed_account diff --git a/programs/compressed-token/src/process_mint.rs b/programs/compressed-token/src/process_mint.rs index c5b4e5b386..cfd146a594 100644 --- a/programs/compressed-token/src/process_mint.rs +++ b/programs/compressed-token/src/process_mint.rs @@ -36,7 +36,7 @@ pub struct CreateMintInstruction<'info> { pub mint_authority_pda: AccountInfo<'info>, pub token_program: Program<'info, Token>, /// CHECK: TODO - #[account(seeds = [b"cpi_authority", account_compression::ID.to_bytes().as_slice()], bump)] + #[account(seeds = [b"cpi_authority"], bump)] pub cpi_authority_pda: AccountInfo<'info>, } @@ -110,20 +110,6 @@ pub fn cpi_execute_compressed_transaction_mint_to<'info>( ctx: &Context<'_, '_, '_, 'info, MintToInstruction<'info>>, output_compressed_accounts: &[CompressedAccount], ) -> Result<()> { - let inputs_struct = InstructionDataTransfer { - relay_fee: None, - input_compressed_accounts_with_merkle_context: Vec::new(), - output_compressed_accounts: output_compressed_accounts.to_vec(), - output_state_merkle_tree_account_indices: vec![0u8; output_compressed_accounts.len()], - input_root_indices: Vec::new(), - proof: None, - new_address_params: Vec::new(), - compression_lamports: None, - is_compress: false, - }; - - let mut inputs = Vec::new(); - InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); let authority_bytes = ctx.accounts.authority.key().to_bytes(); let mint_bytes = ctx.accounts.mint.key().to_bytes(); let seeds = [ @@ -139,6 +125,21 @@ pub fn cpi_execute_compressed_transaction_mint_to<'info>( mint_bytes.as_slice(), bump, ]; + let inputs_struct = InstructionDataTransfer { + relay_fee: None, + input_compressed_accounts_with_merkle_context: Vec::new(), + output_compressed_accounts: output_compressed_accounts.to_vec(), + output_state_merkle_tree_account_indices: vec![0u8; output_compressed_accounts.len()], + input_root_indices: Vec::new(), + proof: None, + new_address_params: Vec::new(), + compression_lamports: None, + is_compress: false, + signer_seeds: Some(seeds.iter().map(|seed| seed.to_vec()).collect()), + }; + + let mut inputs = Vec::new(); + InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); let signer_seeds = &[&seeds[..]]; let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { @@ -147,11 +148,11 @@ pub fn cpi_execute_compressed_transaction_mint_to<'info>( noop_program: ctx.accounts.noop_program.to_account_info(), account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), account_compression_program: ctx.accounts.account_compression_program.to_account_info(), - cpi_signature_account: None, - invoking_program: None, + invoking_program: Some(ctx.accounts.self_program.to_account_info()), compressed_sol_pda: None, compression_recipient: None, system_program: None, + cpi_signature_account: None, }; let mut cpi_ctx = CpiContext::new_with_signer( ctx.accounts.compressed_pda_program.to_account_info(), @@ -160,7 +161,7 @@ pub fn cpi_execute_compressed_transaction_mint_to<'info>( ); cpi_ctx.remaining_accounts = vec![ctx.accounts.merkle_tree.to_account_info()]; - light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs)?; + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, None)?; Ok(()) } @@ -188,6 +189,7 @@ pub fn mint_spl_to_pool_pda<'info>( mint_bytes.as_slice(), bump, ]; + let signer_seeds = &[&seeds[..]]; let cpi_accounts = anchor_spl::token::MintTo { authority: ctx.accounts.mint_authority_pda.to_account_info(), @@ -228,7 +230,7 @@ pub struct MintToInstruction<'info> { /// CHECK: this account pub noop_program: UncheckedAccount<'info>, /// CHECK: this account in psp account compression program - #[account(mut, seeds = [b"cpi_authority", account_compression::ID.to_bytes().as_slice()], bump, seeds::program = light_compressed_pda::ID,)] + #[account(mut, seeds = [b"cpi_authority"], bump, seeds::program = light_compressed_pda::ID,)] pub account_compression_authority: UncheckedAccount<'info>, /// CHECK: this account in psp account compression program pub account_compression_program: @@ -236,6 +238,7 @@ pub struct MintToInstruction<'info> { /// CHECK: this account will be checked by psp compressed pda program #[account(mut)] pub merkle_tree: UncheckedAccount<'info>, + pub self_program: Program<'info, crate::program::LightCompressedToken>, } pub fn get_token_authority_pda(signer: &Pubkey, mint: &Pubkey) -> Pubkey { @@ -324,6 +327,7 @@ pub mod mint_sdk { ), account_compression_program: account_compression::ID, merkle_tree: *merkle_tree, + self_program: crate::ID, }; Instruction { diff --git a/programs/compressed-token/src/process_transfer.rs b/programs/compressed-token/src/process_transfer.rs index 062332e3e0..faa2285395 100644 --- a/programs/compressed-token/src/process_transfer.rs +++ b/programs/compressed-token/src/process_transfer.rs @@ -4,6 +4,7 @@ use light_compressed_pda::{ compressed_account::{ CompressedAccount, CompressedAccountData, CompressedAccountWithMerkleContext, }, + compressed_cpi::CompressedCpiContext, utils::CompressedProof, InstructionDataTransfer as LightCompressedPdaInstructionDataTransfer, }; @@ -25,6 +26,7 @@ use crate::{spl_compression::process_compression, ErrorCode}; pub fn process_transfer<'a, 'b, 'c, 'info: 'b + 'c>( ctx: Context<'a, 'b, 'c, 'info, TransferInstruction<'info>>, inputs: Vec, + cpi_context: Option, ) -> Result<()> { let inputs: CompressedTokenInstructionDataTransfer = CompressedTokenInstructionDataTransfer::deserialize(&mut inputs.as_slice())?; @@ -84,6 +86,7 @@ pub fn process_transfer<'a, 'b, 'c, 'info: 'b + 'c>( &output_compressed_accounts, inputs.output_state_merkle_tree_account_indices, inputs.proof, + cpi_context, )?; Ok(()) } @@ -117,7 +120,22 @@ pub fn cpi_execute_compressed_transaction_transfer<'info>( output_compressed_accounts: &[CompressedAccount], output_state_merkle_tree_account_indices: Vec, proof: Option, + cpi_context: Option, ) -> Result<()> { + let (_, bump) = get_cpi_authority_pda(); + let bump = &[bump]; + let seeds: [&[u8]; 2] = [b"cpi_authority".as_slice(), bump]; + + let signer_seeds = &[&seeds[..]]; + let cpi_signature_account = match cpi_context.as_ref() { + Some(cpi_context) => { + let cpi_signature_account = ctx.remaining_accounts + [cpi_context.cpi_signature_account_index as usize] + .to_account_info(); + Some(cpi_signature_account) + } + None => None, + }; let inputs_struct = LightCompressedPdaInstructionDataTransfer { relay_fee: None, input_compressed_accounts_with_merkle_context, @@ -128,28 +146,23 @@ pub fn cpi_execute_compressed_transaction_transfer<'info>( new_address_params: Vec::new(), compression_lamports: None, is_compress: false, + signer_seeds: Some(seeds.iter().map(|seed| seed.to_vec()).collect()), }; let mut inputs = Vec::new(); LightCompressedPdaInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); - let (_, bump) = get_cpi_authority_pda(); - let bump = &[bump]; - let id = account_compression::ID.to_bytes(); - let seeds = [b"cpi_authority".as_slice(), id.as_slice(), bump]; - - let signer_seeds = &[&seeds[..]]; let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { signer: ctx.accounts.cpi_authority_pda.to_account_info(), registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), noop_program: ctx.accounts.noop_program.to_account_info(), account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), account_compression_program: ctx.accounts.account_compression_program.to_account_info(), - cpi_signature_account: None, invoking_program: Some(ctx.accounts.self_program.to_account_info()), compressed_sol_pda: None, compression_recipient: None, system_program: None, + cpi_signature_account, }; let mut cpi_ctx = CpiContext::new_with_signer( ctx.accounts.compressed_pda_program.to_account_info(), @@ -158,7 +171,7 @@ pub fn cpi_execute_compressed_transaction_transfer<'info>( ); cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); - light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs)?; + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, cpi_context)?; Ok(()) } @@ -210,7 +223,7 @@ pub struct TransferInstruction<'info> { pub authority: Signer<'info>, // This is the cpi signer /// CHECK: that mint authority is derived from signer - #[account(seeds = [b"cpi_authority", account_compression::ID.to_bytes().as_slice()], bump,)] + #[account(seeds = [b"cpi_authority"], bump,)] pub cpi_authority_pda: UncheckedAccount<'info>, pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, /// CHECK: this account @@ -218,7 +231,7 @@ pub struct TransferInstruction<'info> { /// CHECK: this account pub noop_program: UncheckedAccount<'info>, /// CHECK: this account in psp account compression program - #[account(seeds = [b"cpi_authority", account_compression::ID.to_bytes().as_slice()], bump, seeds::program = light_compressed_pda::ID,)] + #[account(seeds = [b"cpi_authority"], bump, seeds::program = light_compressed_pda::ID,)] pub account_compression_authority: UncheckedAccount<'info>, /// CHECK: this account in psp account compression program pub account_compression_program: @@ -438,13 +451,7 @@ impl DataHasher for TokenData { } pub fn get_cpi_authority_pda() -> (Pubkey, u8) { - Pubkey::find_program_address( - &[ - b"cpi_authority", - account_compression::ID.key().to_bytes().as_slice(), - ], - &crate::ID, - ) + Pubkey::find_program_address(&[b"cpi_authority"], &crate::ID) } #[cfg(not(target_os = "solana"))] @@ -501,12 +508,15 @@ pub mod transfer_sdk { is_compress, compression_amount, ); - let inputs = inputs_struct - .try_to_vec() - .map_err(|_| TransferSdkError::CreateTransferInstructionFailed)?; + let remaining_accounts = to_account_metas(remaining_accounts); + let mut inputs = Vec::new(); + CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); let (cpi_authority_pda, _) = crate::get_cpi_authority_pda(); - let instruction_data = crate::instruction::Transfer { inputs }; + let instruction_data = crate::instruction::Transfer { + inputs, + cpi_context: None, + }; let accounts = crate::accounts::TransferInstruction { fee_payer: *fee_payer, @@ -550,28 +560,41 @@ pub mod transfer_sdk { owner: &Pubkey, is_compress: bool, compression_amount: Option, - ) -> Result<(Vec, CompressedTokenInstructionDataTransfer), TransferSdkError> { + ) -> Result< + ( + HashMap, + CompressedTokenInstructionDataTransfer, + ), + TransferSdkError, + > { for token_data in input_token_data { // convenience signer check to throw a meaningful error if token_data.owner != *owner { + println!( + "owner: {:?}, token_data.owner: {:?}", + owner, token_data.owner + ); return Err(TransferSdkError::SignerCheckFailed); } } - - Ok(create_inputs_and_remaining_accounts( - input_compressed_account_merkle_tree_pubkeys, - leaf_indices, - input_token_data, - nullifier_array_pubkeys, - output_compressed_account_merkle_tree_pubkeys, - owner_if_delegate_is_signer, - output_compressed_accounts, - root_indices, - proof, - mint, - is_compress, - compression_amount, - )) + let (remaining_accounts, compressed_accounts_ix_data) = + create_inputs_and_remaining_accounts( + input_compressed_account_merkle_tree_pubkeys, + leaf_indices, + input_token_data, + nullifier_array_pubkeys, + output_compressed_account_merkle_tree_pubkeys, + owner_if_delegate_is_signer, + output_compressed_accounts, + root_indices, + proof, + mint, + is_compress, + compression_amount, + // token_pool_pda, + // decompress_token_account, + ); + Ok((remaining_accounts, compressed_accounts_ix_data)) } #[allow(clippy::too_many_arguments)] @@ -588,7 +611,10 @@ pub mod transfer_sdk { mint: Pubkey, is_compress: bool, compression_amount: Option, - ) -> (Vec, CompressedTokenInstructionDataTransfer) { + ) -> ( + HashMap, + CompressedTokenInstructionDataTransfer, + ) { let mut remaining_accounts = HashMap::::new(); if let Some(owner_if_delegate_is_signer) = owner_if_delegate_is_signer { remaining_accounts.insert(owner_if_delegate_is_signer, 0); @@ -660,17 +686,6 @@ pub mod transfer_sdk { *remaining_accounts.get(mt).unwrap() as u8; } - let mut remaining_accounts = remaining_accounts - .iter() - .map(|(k, i)| (AccountMeta::new(*k, false), *i)) - .collect::>(); - // hash maps are not sorted so we need to sort manually and collect into a vector again - remaining_accounts.sort_by(|a, b| a.1.cmp(&b.1)); - let remaining_accounts = remaining_accounts - .iter() - .map(|(k, _)| k.clone()) - .collect::>(); - let inputs_struct = CompressedTokenInstructionDataTransfer { output_compressed_accounts: output_compressed_accounts.to_vec(), root_indices: root_indices.to_vec(), @@ -686,4 +701,18 @@ pub mod transfer_sdk { (remaining_accounts, inputs_struct) } + + pub fn to_account_metas(remaining_accounts: HashMap) -> Vec { + let mut remaining_accounts = remaining_accounts + .iter() + .map(|(k, i)| (AccountMeta::new(*k, false), *i)) + .collect::>(); + // hash maps are not sorted so we need to sort manually and collect into a vector again + remaining_accounts.sort_by(|a, b| a.1.cmp(&b.1)); + let remaining_accounts = remaining_accounts + .iter() + .map(|(k, _)| k.clone()) + .collect::>(); + remaining_accounts + } } diff --git a/programs/compressed-token/src/spl_compression.rs b/programs/compressed-token/src/spl_compression.rs index d6a711d19e..b737f267c9 100644 --- a/programs/compressed-token/src/spl_compression.rs +++ b/programs/compressed-token/src/spl_compression.rs @@ -1,6 +1,5 @@ use anchor_lang::{prelude::*, solana_program::account_info::AccountInfo}; use anchor_spl::token::Transfer; -use light_compressed_pda::append_state::get_seeds; use crate::{CompressedTokenInstructionDataTransfer, TransferInstruction}; @@ -86,10 +85,10 @@ pub fn transfer<'info>( token_program: &AccountInfo<'info>, amount: u64, ) -> Result<()> { - let (seed, bump) = get_seeds(&crate::ID, &authority.key())?; + let (_, bump) = + anchor_lang::prelude::Pubkey::find_program_address(&[b"cpi_authority"], &crate::ID); let bump = &[bump]; - let seeds = &[&[b"cpi_authority", seed.as_slice(), bump][..]]; - + let seeds = &[&[b"cpi_authority".as_slice(), bump][..]]; let accounts = Transfer { from: from.to_account_info(), to: to.to_account_info(), diff --git a/programs/compressed-token/tests/test.rs b/programs/compressed-token/tests/test.rs index 24db41ed51..ec4c2bdef6 100644 --- a/programs/compressed-token/tests/test.rs +++ b/programs/compressed-token/tests/test.rs @@ -26,8 +26,8 @@ use light_compressed_token::{ }; use light_hasher::Poseidon; use light_test_utils::{ - airdrop_lamports, create_account_instruction, create_and_send_transaction, - create_and_send_transaction_with_event, get_hash_set, + airdrop_lamports, assert_custom_error_or_program_error, create_account_instruction, + create_and_send_transaction, create_and_send_transaction_with_event, get_hash_set, test_env::setup_test_programs_with_accounts, AccountZeroCopy, }; use num_bigint::BigInt; @@ -37,10 +37,7 @@ use solana_program_test::{ BanksClientError, BanksTransactionResultWithMetadata, ProgramTestContext, }; use solana_sdk::{ - instruction::{Instruction, InstructionError}, - pubkey::Pubkey, - signature::Keypair, - signer::Signer, + instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction, }; use spl_token::instruction::initialize_mint; @@ -130,9 +127,7 @@ async fn assert_create_mint( #[tokio::test] async fn test_create_mint() { - let env: light_test_utils::test_env::EnvWithAccounts = - setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, _) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); let rent = context @@ -173,9 +168,7 @@ async fn create_mint_helper(context: &mut ProgramTestContext, payer: &Keypair) - #[tokio::test] async fn test_mint_to() { - let env: light_test_utils::test_env::EnvWithAccounts = - setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, env) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); let merkle_tree_pubkey = env.merkle_tree_pubkey; @@ -227,9 +220,7 @@ async fn test_mint_to() { #[tokio::test] async fn test_transfer() { - let env: light_test_utils::test_env::EnvWithAccounts = - setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, env) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); let merkle_tree_pubkey = env.merkle_tree_pubkey; @@ -370,9 +361,7 @@ async fn test_transfer() { #[tokio::test] async fn test_decompression() { - let env: light_test_utils::test_env::EnvWithAccounts = - setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, env) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); let merkle_tree_pubkey = env.merkle_tree_pubkey; @@ -563,9 +552,7 @@ async fn test_decompression() { /// 5. Invalid delegated amount #[tokio::test] async fn test_invalid_inputs() { - let env: light_test_utils::test_env::EnvWithAccounts = - setup_test_programs_with_accounts(None).await; - let mut context = env.context; + let (mut context, env) = setup_test_programs_with_accounts(None).await; let payer = context.payer.insecure_clone(); let payer_pubkey = payer.pubkey(); let merkle_tree_pubkey = env.merkle_tree_pubkey; @@ -656,14 +643,7 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::ComputeOutputSumFailed.into()) - )) - ); - + assert_custom_error_or_program_error(res, ErrorCode::ComputeOutputSumFailed.into()).unwrap(); let transfer_recipient_out_compressed_account_0 = TokenTransferOutputData { amount: 1000 - 1, owner: transfer_recipient_keypair.pubkey(), @@ -683,13 +663,8 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::SumCheckFailed.into()) - )) - ); + + assert_custom_error_or_program_error(res, ErrorCode::SumCheckFailed.into()).unwrap(); let zero_amount = TokenTransferOutputData { amount: 0, @@ -710,13 +685,8 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::SumCheckFailed.into()) - )) - ); + assert_custom_error_or_program_error(res, ErrorCode::SumCheckFailed.into()).unwrap(); + let double_amount = TokenTransferOutputData { amount: input_compressed_account_token_data.amount, owner: transfer_recipient_keypair.pubkey(), @@ -736,13 +706,8 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::ComputeOutputSumFailed.into()) - )) - ); + + assert_custom_error_or_program_error(res, ErrorCode::ComputeOutputSumFailed.into()).unwrap(); let invalid_lamports_amount = TokenTransferOutputData { amount: 1000, @@ -764,16 +729,11 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom( - light_compressed_pda::ErrorCode::ComputeOutputSumFailed.into() - ) - )) - ); + assert_custom_error_or_program_error( + res, + light_compressed_pda::ErrorCode::ComputeOutputSumFailed.into(), + ) + .unwrap(); let mut input_compressed_account_token_data_invalid_amount = mock_indexer.token_compressed_accounts[0].token_data; @@ -816,14 +776,8 @@ async fn test_invalid_inputs() { ) .await .unwrap(); + assert_custom_error_or_program_error(res, ErrorCode::ComputeOutputSumFailed.into()).unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::ComputeOutputSumFailed.into()) - )) - ); let mut input_compressed_account_token_data = mock_indexer.token_compressed_accounts[0].token_data; input_compressed_account_token_data.delegate = Some(Pubkey::new_unique()); @@ -852,17 +806,11 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom( - light_compressed_pda::ErrorCode::ProofVerificationFailed.into() - ) - )) - ); - + assert_custom_error_or_program_error( + res, + light_compressed_pda::ErrorCode::ProofVerificationFailed.into(), + ) + .unwrap(); let input_compressed_accounts = vec![mock_indexer.compressed_accounts [mock_indexer.token_compressed_accounts[0].index] .clone()]; @@ -879,17 +827,11 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom( - light_compressed_pda::ErrorCode::ProofVerificationFailed.into() - ) - )) - ); - + assert_custom_error_or_program_error( + res, + light_compressed_pda::ErrorCode::ProofVerificationFailed.into(), + ) + .unwrap(); let mut input_compressed_account_token_data = mock_indexer.token_compressed_accounts[0].token_data; input_compressed_account_token_data.is_native = Some(0); @@ -918,15 +860,11 @@ async fn test_invalid_inputs() { .await .unwrap(); - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom( - light_compressed_pda::ErrorCode::ProofVerificationFailed.into() - ) - )) - ); + assert_custom_error_or_program_error( + res, + light_compressed_pda::ErrorCode::ProofVerificationFailed.into(), + ) + .unwrap(); let mut input_compressed_account_token_data = mock_indexer.token_compressed_accounts[0].token_data; @@ -955,14 +893,7 @@ async fn test_invalid_inputs() { ) .await .unwrap(); - - assert_eq!( - res.result, - Err(solana_sdk::transaction::TransactionError::InstructionError( - 0, - InstructionError::Custom(ErrorCode::DelegateUndefined.into()) - )) - ); + assert_custom_error_or_program_error(res, ErrorCode::DelegateUndefined.into()).unwrap(); } #[allow(clippy::too_many_arguments)] @@ -1023,6 +954,7 @@ async fn create_transfer_out_utxo_test( ) .await } + pub async fn create_token_account( context: &mut ProgramTestContext, mint: &Pubkey, diff --git a/programs/registry/tests/tests.rs b/programs/registry/tests/tests.rs index 75b9034a91..3d42e7c3c0 100644 --- a/programs/registry/tests/tests.rs +++ b/programs/registry/tests/tests.rs @@ -17,9 +17,9 @@ use solana_sdk::{ }; pub async fn setup_test_programs_with_accounts() -> ProgramTestContext { let mut context = setup_test_programs(None).await; + let payer = context.payer.insecure_clone(); let cpi_authority_pda = get_cpi_authority_pda(); let authority_pda = get_governance_authority_pda(); - let payer = context.payer.insecure_clone(); let instruction = create_initialize_governance_authority_instruction(payer.pubkey(), payer.pubkey()); create_and_send_transaction(&mut context, &[instruction], &payer.pubkey(), &[&payer]) diff --git a/test-programs/program-owned-account-test/Cargo.toml b/test-programs/program-owned-account-test/Cargo.toml new file mode 100644 index 0000000000..a2f34f87e1 --- /dev/null +++ b/test-programs/program-owned-account-test/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "program-owned-account-test" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "program_owned_account_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +test-sbf = [] +custom-heap = [] +default = ["custom-heap"] + +[dependencies] +anchor-lang ={ version="0.29.0" } +light-compressed-token = { path = "../../programs/compressed-token" , features = ["cpi"]} +light-compressed-pda = { path = "../../programs/compressed-pda" , features = ["cpi"]} +account-compression = { path = "../../programs/account-compression" , features = ["cpi"] } +light-hasher = {path = "../../merkle-tree/hasher"} +light-utils = {path = "../../utils"} + +[target.'cfg(not(target_os = "solana"))'.dependencies] +solana-sdk = "1.17.4" + + +[dev-dependencies] +solana-program-test = "1.17.4" +light-test-utils = { version = "0.1.0", path = "../../test-utils", default-features= true, features = ["test_indexer"] } +reqwest = "0.11.26" +tokio = "1.36.0" +light-circuitlib-rs = {path = "../../circuit-lib/circuitlib-rs"} +num-bigint = "0.4.4" +num-traits = "0.2.18" +spl-token = "3.5.0" +anchor-spl = "0.29.0" diff --git a/test-programs/program-owned-account-test/Xargo.toml b/test-programs/program-owned-account-test/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/test-programs/program-owned-account-test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/test-programs/program-owned-account-test/src/create_pda.rs b/test-programs/program-owned-account-test/src/create_pda.rs new file mode 100644 index 0000000000..79257cf61c --- /dev/null +++ b/test-programs/program-owned-account-test/src/create_pda.rs @@ -0,0 +1,234 @@ +use anchor_lang::prelude::*; +use light_compressed_pda::{ + compressed_account::{derive_address, CompressedAccount, CompressedAccountData}, + compressed_cpi::CompressedCpiContext, + utils::CompressedProof, + InstructionDataTransfer, NewAddressParamsPacked, +}; +use light_hasher::{errors::HasherError, DataHasher, Hasher, Poseidon}; + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub enum CreatePdaMode { + ProgramIsSigner, + ProgramIsNotSigner, + InvalidSignerSeeds, +} +/// create compressed pda data +/// transfer tokens +/// execute complete transaction +pub fn process_create_pda<'info>( + ctx: Context<'_, '_, '_, 'info, CreateCompressedPda<'info>>, + data: [u8; 31], + proof: Option, + new_address_params: NewAddressParamsPacked, + owner_program: Pubkey, + cpi_context: CompressedCpiContext, + is_program_signer: CreatePdaMode, + bump: u8, +) -> Result<()> { + let compressed_pda = + create_compressed_pda_data(data, &ctx, &new_address_params, &owner_program)?; + + match is_program_signer { + CreatePdaMode::ProgramIsNotSigner => { + cpi_compressed_pda_transfer_as_non_program( + &ctx, + proof, + new_address_params, + compressed_pda, + cpi_context, + )?; + } + CreatePdaMode::ProgramIsSigner => { + cpi_compressed_pda_transfer_as_program( + &ctx, + proof, + new_address_params, + compressed_pda, + cpi_context, + bump, + b"cpi_signer".as_slice(), + )?; + } + CreatePdaMode::InvalidSignerSeeds => { + cpi_compressed_pda_transfer_as_program( + &ctx, + proof, + new_address_params, + compressed_pda, + cpi_context, + bump, + b"cpi_signer1".as_slice(), + )?; + } + } + Ok(()) +} + +fn cpi_compressed_pda_transfer_as_non_program<'info>( + ctx: &Context<'_, '_, '_, 'info, CreateCompressedPda<'info>>, + proof: Option, + new_address_params: NewAddressParamsPacked, + compressed_pda: CompressedAccount, + cpi_context: CompressedCpiContext, +) -> Result<()> { + msg!("cpi_compressed_pda_transfer_as_non_program"); + let inputs_struct = InstructionDataTransfer { + relay_fee: None, + input_compressed_accounts_with_merkle_context: Vec::new(), + output_compressed_accounts: vec![compressed_pda], + input_root_indices: Vec::new(), + output_state_merkle_tree_account_indices: vec![0], + proof, + new_address_params: vec![new_address_params], + compression_lamports: None, + is_compress: false, + signer_seeds: None, + }; + + let mut inputs = Vec::new(); + InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { + signer: ctx.accounts.signer.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + invoking_program: Some(ctx.accounts.self_program.to_account_info()), + compressed_sol_pda: None, + compression_recipient: None, + system_program: None, + cpi_signature_account: None, + }; + let mut cpi_ctx = CpiContext::new( + ctx.accounts.compressed_pda_program.to_account_info(), + cpi_accounts, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} + +fn cpi_compressed_pda_transfer_as_program<'info>( + ctx: &Context<'_, '_, '_, 'info, CreateCompressedPda<'info>>, + proof: Option, + new_address_params: NewAddressParamsPacked, + compressed_pda: CompressedAccount, + cpi_context: CompressedCpiContext, + bump: u8, + signer_seed: &[u8], +) -> Result<()> { + let local_bump = Pubkey::find_program_address(&[signer_seed], &crate::ID).1; + let seeds: [&[u8]; 2] = [signer_seed, &[local_bump]]; + let inputs_struct = InstructionDataTransfer { + relay_fee: None, + input_compressed_accounts_with_merkle_context: Vec::new(), + output_compressed_accounts: vec![compressed_pda], + input_root_indices: Vec::new(), + output_state_merkle_tree_account_indices: vec![0], + proof, + new_address_params: vec![new_address_params], + compression_lamports: None, + is_compress: false, + signer_seeds: Some(seeds.iter().map(|seed| seed.to_vec()).collect()), + }; + // defining seeds again so that the cpi doesn't fail we want to test the check in the compressed pda program + let seeds: [&[u8]; 2] = [b"cpi_signer".as_slice(), &[bump]]; + let mut inputs = Vec::new(); + InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { + signer: ctx.accounts.cpi_signer.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + invoking_program: Some(ctx.accounts.self_program.to_account_info()), + compressed_sol_pda: None, + compression_recipient: None, + system_program: None, + cpi_signature_account: None, + }; + + let signer_seeds: [&[&[u8]]; 1] = [&seeds[..]]; + + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_pda_program.to_account_info(), + cpi_accounts, + &signer_seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} + +fn create_compressed_pda_data( + data: [u8; 31], + ctx: &Context<'_, '_, '_, '_, CreateCompressedPda<'_>>, + new_address_params: &NewAddressParamsPacked, + owner_program: &Pubkey, +) -> Result { + let timelock_compressed_pda = RegisteredUser { + user_pubkey: *ctx.accounts.signer.key, + data, + }; + let compressed_account_data = CompressedAccountData { + discriminator: 1u64.to_le_bytes(), + data: timelock_compressed_pda.try_to_vec().unwrap(), + data_hash: timelock_compressed_pda.hash().map_err(ProgramError::from)?, + }; + let derive_address = derive_address( + &ctx.remaining_accounts[new_address_params.address_merkle_tree_account_index as usize] + .key(), + &new_address_params.seed, + ) + .map_err(|_| ProgramError::InvalidArgument)?; + Ok(CompressedAccount { + owner: *owner_program, // should be crate::ID, test provides an invalid owner + lamports: 0, + address: Some(derive_address), + data: Some(compressed_account_data), + }) +} + +#[derive(AnchorDeserialize, AnchorSerialize, Debug, Clone)] +pub struct RegisteredUser { + pub user_pubkey: Pubkey, + pub data: [u8; 31], +} + +impl light_hasher::DataHasher for RegisteredUser { + fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { + let truncated_user_pubkey = + light_utils::hash_to_bn254_field_size_le(&self.user_pubkey.to_bytes()) + .unwrap() + .0; + + Poseidon::hashv(&[truncated_user_pubkey.as_slice(), self.data.as_slice()]) + } +} + +#[derive(Accounts)] +pub struct CreateCompressedPda<'info> { + #[account(mut)] + pub signer: Signer<'info>, + pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, + pub account_compression_program: + Program<'info, account_compression::program::AccountCompression>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub compressed_token_cpi_authority_pda: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: + pub noop_program: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::ProgramOwnedAccountTest>, + /// CHECK: + pub cpi_signer: AccountInfo<'info>, +} diff --git a/test-programs/program-owned-account-test/src/invalidate_not_owned_account.rs b/test-programs/program-owned-account-test/src/invalidate_not_owned_account.rs new file mode 100644 index 0000000000..f2896cb891 --- /dev/null +++ b/test-programs/program-owned-account-test/src/invalidate_not_owned_account.rs @@ -0,0 +1,81 @@ +use anchor_lang::prelude::*; +use light_compressed_pda::{ + compressed_account::CompressedAccountWithMerkleContext, compressed_cpi::CompressedCpiContext, + utils::CompressedProof, InstructionDataTransfer, +}; + +/// create compressed pda data +/// transfer tokens +/// execute complete transaction +pub fn process_invalidate_not_owned_compressed_account<'info>( + ctx: Context<'_, '_, '_, 'info, InvalidateNotOwnedCompressedAccount<'info>>, + compressed_account: CompressedAccountWithMerkleContext, + proof: Option, + root_indices: Vec, + bump: u8, +) -> Result<()> { + let seeds: [&[u8]; 2] = [b"cpi_signer".as_slice(), &[bump]]; + let inputs_struct = InstructionDataTransfer { + relay_fee: None, + input_compressed_accounts_with_merkle_context: vec![compressed_account], + output_compressed_accounts: Vec::new(), + input_root_indices: root_indices, + output_state_merkle_tree_account_indices: Vec::new(), + proof, + new_address_params: Vec::new(), + compression_lamports: None, + is_compress: false, + signer_seeds: Some(seeds.iter().map(|seed| seed.to_vec()).collect()), + }; + let cpi_context = CompressedCpiContext { + execute: true, + cpi_signature_account_index: (ctx.remaining_accounts.len() - 1) as u8, + }; + let mut inputs = Vec::new(); + InstructionDataTransfer::serialize(&inputs_struct, &mut inputs).unwrap(); + + let cpi_accounts = light_compressed_pda::cpi::accounts::TransferInstruction { + signer: ctx.accounts.cpi_signer.to_account_info(), + registered_program_pda: ctx.accounts.registered_program_pda.to_account_info(), + noop_program: ctx.accounts.noop_program.to_account_info(), + account_compression_authority: ctx.accounts.account_compression_authority.to_account_info(), + account_compression_program: ctx.accounts.account_compression_program.to_account_info(), + invoking_program: Some(ctx.accounts.self_program.to_account_info()), + compressed_sol_pda: None, + compression_recipient: None, + system_program: None, + cpi_signature_account: None, + }; + let signer_seeds: [&[&[u8]]; 1] = [&seeds[..]]; + + let mut cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compressed_pda_program.to_account_info(), + cpi_accounts, + &signer_seeds, + ); + + cpi_ctx.remaining_accounts = ctx.remaining_accounts.to_vec(); + + light_compressed_pda::cpi::execute_compressed_transaction(cpi_ctx, inputs, Some(cpi_context))?; + Ok(()) +} + +#[derive(Accounts)] +pub struct InvalidateNotOwnedCompressedAccount<'info> { + #[account(mut)] + pub signer: Signer<'info>, + pub compressed_pda_program: Program<'info, light_compressed_pda::program::LightCompressedPda>, + pub account_compression_program: + Program<'info, account_compression::program::AccountCompression>, + /// CHECK: + pub account_compression_authority: AccountInfo<'info>, + /// CHECK: + pub compressed_token_cpi_authority_pda: AccountInfo<'info>, + /// CHECK: + pub registered_program_pda: AccountInfo<'info>, + /// CHECK: + pub noop_program: AccountInfo<'info>, + pub self_program: Program<'info, crate::program::ProgramOwnedAccountTest>, + /// CHECK: + pub cpi_signer: AccountInfo<'info>, +} diff --git a/test-programs/program-owned-account-test/src/lib.rs b/test-programs/program-owned-account-test/src/lib.rs new file mode 100644 index 0000000000..514b02ba7f --- /dev/null +++ b/test-programs/program-owned-account-test/src/lib.rs @@ -0,0 +1,60 @@ +#![allow(clippy::too_many_arguments)] +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey; +use light_compressed_pda::utils::CompressedProof; +pub mod create_pda; +pub use create_pda::*; +pub mod sdk; +use light_compressed_pda::compressed_cpi::CompressedCpiContext; +use light_compressed_pda::NewAddressParamsPacked; +pub mod invalidate_not_owned_account; +pub use invalidate_not_owned_account::*; +use light_compressed_pda::compressed_account::CompressedAccountWithMerkleContext; + +declare_id!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); + +#[program] +pub mod program_owned_account_test { + + use self::invalidate_not_owned_account::process_invalidate_not_owned_compressed_account; + + use super::*; + + pub fn create_compressed_pda<'info>( + ctx: Context<'_, '_, '_, 'info, CreateCompressedPda<'info>>, + data: [u8; 31], + proof: Option, + new_address_parameters: NewAddressParamsPacked, + cpi_context: CompressedCpiContext, + owner_program: Pubkey, + signer_is_program: CreatePdaMode, + bump: u8, + ) -> Result<()> { + process_create_pda( + ctx, + data, + proof, + new_address_parameters, + owner_program, + cpi_context, + signer_is_program, + bump, + ) + } + + pub fn invalidate_not_owned_account<'info>( + ctx: Context<'_, '_, '_, 'info, InvalidateNotOwnedCompressedAccount<'info>>, + compressed_account: CompressedAccountWithMerkleContext, + proof: Option, + root_indices: Vec, + bump: u8, + ) -> Result<()> { + process_invalidate_not_owned_compressed_account( + ctx, + compressed_account, + proof, + root_indices, + bump, + ) + } +} diff --git a/test-programs/program-owned-account-test/src/sdk.rs b/test-programs/program-owned-account-test/src/sdk.rs new file mode 100644 index 0000000000..04ea85cbd8 --- /dev/null +++ b/test-programs/program-owned-account-test/src/sdk.rs @@ -0,0 +1,145 @@ +#![cfg(not(target_os = "solana"))] + +use std::collections::HashMap; + +use account_compression::{Pubkey, NOOP_PROGRAM_ID}; +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressed_pda::{ + compressed_account::CompressedAccountWithMerkleContext, compressed_cpi::CompressedCpiContext, + pack_new_address_params, utils::CompressedProof, NewAddressParams, +}; +use light_compressed_token::transfer_sdk::to_account_metas; +use solana_sdk::instruction::Instruction; + +use crate::CreatePdaMode; + +#[derive(Debug, Clone)] +pub struct CreateCompressedPdaInstructionInputs<'a> { + pub data: [u8; 31], + pub signer: &'a Pubkey, + pub output_compressed_account_merkle_tree_pubkey: &'a Pubkey, + pub proof: &'a CompressedProof, + pub new_address_params: NewAddressParams, + pub cpi_signature_account: &'a Pubkey, + pub owner_program: &'a Pubkey, + pub signer_is_program: CreatePdaMode, +} + +pub fn create_pda_instruction(input_params: CreateCompressedPdaInstructionInputs) -> Instruction { + let (cpi_signer, bump) = + Pubkey::find_program_address(&[b"cpi_signer".as_slice()], &crate::id()); + let mut remaining_accounts = HashMap::new(); + remaining_accounts.insert( + *input_params.output_compressed_account_merkle_tree_pubkey, + 0, + ); + let new_address_params = + pack_new_address_params(&[input_params.new_address_params], &mut remaining_accounts); + let cpi_signature_account_index: u8 = + match remaining_accounts.get(input_params.cpi_signature_account) { + Some(entry) => (*entry).try_into().unwrap(), + None => { + remaining_accounts.insert( + *input_params.cpi_signature_account, + remaining_accounts.len(), + ); + (remaining_accounts.len() - 1) as u8 + } + }; + + let cpi_context = CompressedCpiContext { + execute: true, + cpi_signature_account_index, + }; + let instruction_data = crate::instruction::CreateCompressedPda { + data: input_params.data, + proof: Some(input_params.proof.clone()), + new_address_parameters: new_address_params[0].clone(), + owner_program: *input_params.owner_program, + cpi_context, + bump, + signer_is_program: input_params.signer_is_program, + }; + + let registered_program_pda = Pubkey::find_program_address( + &[light_compressed_pda::ID.to_bytes().as_slice()], + &account_compression::ID, + ) + .0; + let compressed_token_cpi_authority_pda = light_compressed_token::get_cpi_authority_pda().0; + let account_compression_authority = + light_compressed_pda::utils::get_cpi_authority_pda(&light_compressed_pda::ID); + + let accounts = crate::accounts::CreateCompressedPda { + signer: *input_params.signer, + noop_program: NOOP_PROGRAM_ID, + compressed_pda_program: light_compressed_pda::ID, + account_compression_program: account_compression::ID, + registered_program_pda, + compressed_token_cpi_authority_pda, + account_compression_authority, + self_program: crate::ID, + cpi_signer, + }; + let remaining_accounts = to_account_metas(remaining_accounts); + + Instruction { + program_id: crate::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + + data: instruction_data.data(), + } +} + +#[derive(Debug, Clone)] +pub struct InvalidateNotOwnedCompressedAccountInstructionInputs<'a> { + pub signer: &'a Pubkey, + pub root_indices: &'a [u16], + pub proof: &'a CompressedProof, + pub input_merkle_tree_pubkey: &'a Pubkey, + pub compressed_account: &'a CompressedAccountWithMerkleContext, +} +pub fn create_invalidate_not_owned_account_instruction( + input_params: InvalidateNotOwnedCompressedAccountInstructionInputs, +) -> Instruction { + let (cpi_signer, bump) = + Pubkey::find_program_address(&[b"cpi_signer".as_slice()], &crate::id()); + let mut remaining_accounts = HashMap::new(); + remaining_accounts.insert(*input_params.input_merkle_tree_pubkey, 0); + + let instruction_data = crate::instruction::InvalidateNotOwnedAccount { + proof: Some(input_params.proof.clone()), + compressed_account: input_params.compressed_account.clone(), + bump, + root_indices: input_params.root_indices.to_vec(), + }; + + let registered_program_pda = Pubkey::find_program_address( + &[light_compressed_pda::ID.to_bytes().as_slice()], + &account_compression::ID, + ) + .0; + let compressed_token_cpi_authority_pda = light_compressed_token::get_cpi_authority_pda().0; + let account_compression_authority = + light_compressed_pda::utils::get_cpi_authority_pda(&light_compressed_pda::ID); + + let accounts = crate::accounts::InvalidateNotOwnedCompressedAccount { + signer: *input_params.signer, + noop_program: NOOP_PROGRAM_ID, + compressed_pda_program: light_compressed_pda::ID, + account_compression_program: account_compression::ID, + registered_program_pda, + compressed_token_cpi_authority_pda, + account_compression_authority, + self_program: crate::ID, + cpi_signer, + }; + let remaining_accounts = to_account_metas(remaining_accounts); + + Instruction { + program_id: crate::ID, + accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), + + data: instruction_data.data(), + } +} diff --git a/test-programs/program-owned-account-test/tests/test.rs b/test-programs/program-owned-account-test/tests/test.rs new file mode 100644 index 0000000000..89fd68e64e --- /dev/null +++ b/test-programs/program-owned-account-test/tests/test.rs @@ -0,0 +1,349 @@ +#![cfg(feature = "test-sbf")] + +use account_compression::Pubkey; +use anchor_lang::AnchorDeserialize; +use light_compressed_pda::compressed_account::CompressedAccountWithMerkleContext; +use light_hasher::{Hasher, Poseidon}; +use light_test_utils::create_and_send_transaction_with_event; +use light_test_utils::test_env::{setup_test_programs_with_accounts, EnvAccounts}; +use light_test_utils::test_indexer::{create_mint_helper, mint_tokens_helper, TestIndexer}; +use light_utils::hash_to_bn254_field_size_le; +use program_owned_account_test::sdk::{ + create_invalidate_not_owned_account_instruction, create_pda_instruction, + CreateCompressedPdaInstructionInputs, InvalidateNotOwnedCompressedAccountInstructionInputs, +}; +use program_owned_account_test::{self, RegisteredUser}; +use program_owned_account_test::{CreatePdaMode, ID}; +use solana_program_test::{ + BanksClientError, BanksTransactionResultWithMetadata, ProgramTestContext, +}; +use solana_sdk::instruction::InstructionError; +use solana_sdk::signature::Keypair; +use solana_sdk::{signer::Signer, transaction::Transaction}; + +#[tokio::test] +async fn test_create_pda() { + let (mut context, env) = setup_test_programs_with_accounts(Some(vec![( + String::from("program_owned_account_test"), + program_owned_account_test::ID, + )])) + .await; + let payer = context.payer.insecure_clone(); + let payer_pubkey = payer.pubkey(); + println!("payer_pubkey {:?}", payer_pubkey); + + let address_merkle_tree_pubkey = env.address_merkle_tree_pubkey; + let test_indexer = TestIndexer::new( + env.merkle_tree_pubkey, + env.nullifier_queue_pubkey, + address_merkle_tree_pubkey, + payer.insecure_clone(), + true, + true, + "../../circuit-lib/circuitlib-rs/scripts/prover.sh", + ); + + let mut test_indexer = test_indexer.await; + + let seed = [1u8; 32]; + let data = [2u8; 31]; + + perform_create_pda_with_event( + &mut test_indexer, + &mut context, + &env, + &payer, + seed, + &data, + &ID, + CreatePdaMode::ProgramIsSigner, + ) + .await + .unwrap(); + + assert_created_pda(&mut test_indexer, &env, &payer, &seed, &data).await; + + let seed = [2u8; 32]; + let data = [3u8; 31]; + let invalid_owner_program = Pubkey::new_unique(); + let res = perform_create_pda_failing( + &mut test_indexer, + &mut context, + &env, + &payer, + seed, + &data, + &invalid_owner_program, + CreatePdaMode::ProgramIsSigner, + ) + .await; + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom( + light_compressed_pda::ErrorCode::WriteAccessCheckFailed.into() + ) + )) + ); + let res = perform_create_pda_failing( + &mut test_indexer, + &mut context, + &env, + &payer, + seed, + &data, + &invalid_owner_program, + CreatePdaMode::ProgramIsNotSigner, + ) + .await; + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom( + light_compressed_pda::ErrorCode::SignerSeedsNotProvided.into() + ) + )) + ); + let res = perform_create_pda_failing( + &mut test_indexer, + &mut context, + &env, + &payer, + seed, + &data, + &ID, + CreatePdaMode::InvalidSignerSeeds, + ) + .await; + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom(light_compressed_pda::ErrorCode::SignerCheckFailed.into()) + )) + ); + let mint = create_mint_helper(&mut context, &payer).await; + + let amount = 10000u64; + mint_tokens_helper( + &mut context, + &mut test_indexer, + &env.merkle_tree_pubkey, + &payer, + &mint, + vec![amount], + vec![payer.pubkey()], + ) + .await; + let compressed_token_account = test_indexer + .compressed_accounts + .iter() + .find(|x| x.compressed_account.owner == light_compressed_token::ID) + .unwrap() + .clone(); + let res = perform_invalidate_not_owned_compressed_account( + &mut test_indexer, + &mut context, + &env, + &payer, + &compressed_token_account, + ) + .await; + assert_eq!( + res.unwrap().result, + Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom(light_compressed_pda::ErrorCode::SignerCheckFailed.into()) + )) + ); +} + +pub async fn perform_create_pda_failing( + test_indexer: &mut TestIndexer, + context: &mut ProgramTestContext, + env: &EnvAccounts, + payer: &Keypair, + seed: [u8; 32], + data: &[u8; 31], + owner_program: &Pubkey, + signer_is_program: CreatePdaMode, +) -> Result { + let payer_pubkey = payer.pubkey(); + let instruction = perform_create_pda( + env, + seed, + test_indexer, + context, + data, + payer_pubkey, + owner_program, + signer_is_program, + ) + .await; + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer_pubkey), + &[&payer], + context.get_new_latest_blockhash().await.unwrap(), + ); + solana_program_test::BanksClient::process_transaction_with_metadata( + &mut context.banks_client, + transaction, + ) + .await +} +pub async fn perform_create_pda_with_event( + test_indexer: &mut TestIndexer, + context: &mut ProgramTestContext, + env: &EnvAccounts, + payer: &Keypair, + seed: [u8; 32], + data: &[u8; 31], + owner_program: &Pubkey, + signer_is_program: CreatePdaMode, +) -> Result<(), BanksClientError> { + let payer_pubkey = payer.pubkey(); + let instruction = perform_create_pda( + env, + seed, + test_indexer, + context, + data, + payer_pubkey, + owner_program, + signer_is_program, + ) + .await; + let event = + create_and_send_transaction_with_event(context, &[instruction], &payer_pubkey, &[payer]) + .await?; + test_indexer.add_compressed_accounts_with_token_data(event.unwrap()); + Ok(()) +} + +async fn perform_create_pda( + env: &EnvAccounts, + seed: [u8; 32], + test_indexer: &mut TestIndexer, + context: &mut ProgramTestContext, + data: &[u8; 31], + payer_pubkey: Pubkey, + owner_program: &Pubkey, + signer_is_program: CreatePdaMode, +) -> solana_sdk::instruction::Instruction { + let address = light_compressed_pda::compressed_account::derive_address( + &env.address_merkle_tree_pubkey, + &seed, + ) + .unwrap(); + + let rpc_result = test_indexer + .create_proof_for_compressed_accounts(None, Some(&[address]), context) + .await; + + let new_address_params: light_compressed_pda::NewAddressParams = + light_compressed_pda::NewAddressParams { + seed, + address_merkle_tree_pubkey: env.address_merkle_tree_pubkey, + address_queue_pubkey: env.address_merkle_tree_queue_pubkey, + address_merkle_tree_root_index: rpc_result.address_root_indices[0], + }; + let create_ix_inputs = CreateCompressedPdaInstructionInputs { + data: *data, + signer: &payer_pubkey, + output_compressed_account_merkle_tree_pubkey: &env.merkle_tree_pubkey, + proof: &rpc_result.proof, + new_address_params, + cpi_signature_account: &env.cpi_signature_account_pubkey, + owner_program, + signer_is_program, + }; + let instruction = create_pda_instruction(create_ix_inputs.clone()); + instruction +} + +pub async fn assert_created_pda( + test_indexer: &mut TestIndexer, + env: &EnvAccounts, + payer: &Keypair, + seed: &[u8; 32], + data: &[u8; 31], +) { + let compressed_escrow_pda = test_indexer + .compressed_accounts + .iter() + .find(|x| x.compressed_account.owner == ID) + .unwrap() + .clone(); + let address = light_compressed_pda::compressed_account::derive_address( + &env.address_merkle_tree_pubkey, + &seed, + ) + .unwrap(); + assert_eq!( + compressed_escrow_pda.compressed_account.address.unwrap(), + address + ); + assert_eq!(compressed_escrow_pda.compressed_account.owner, ID); + let compressed_escrow_pda_deserialized = compressed_escrow_pda + .compressed_account + .data + .as_ref() + .unwrap(); + let compressed_escrow_pda_data = + RegisteredUser::deserialize_reader(&mut &compressed_escrow_pda_deserialized.data[..]) + .unwrap(); + assert_eq!(compressed_escrow_pda_data.user_pubkey, payer.pubkey()); + assert_eq!(compressed_escrow_pda_data.data, *data); + + assert_eq!( + compressed_escrow_pda_deserialized.discriminator, + 1u64.to_le_bytes(), + ); + let truncated_user_pubkey = + hash_to_bn254_field_size_le(&compressed_escrow_pda_data.user_pubkey.to_bytes()) + .unwrap() + .0; + assert_eq!( + compressed_escrow_pda_deserialized.data_hash, + Poseidon::hashv(&[truncated_user_pubkey.as_slice(), data.as_slice()]).unwrap(), + ); +} + +pub async fn perform_invalidate_not_owned_compressed_account( + test_indexer: &mut TestIndexer, + context: &mut ProgramTestContext, + env: &EnvAccounts, + payer: &Keypair, + compressed_account: &CompressedAccountWithMerkleContext, +) -> Result { + let payer_pubkey = payer.pubkey(); + let hash = compressed_account + .compressed_account + .hash(&env.merkle_tree_pubkey, &compressed_account.leaf_index) + .unwrap(); + let rpc_result = test_indexer + .create_proof_for_compressed_accounts(Some(&[hash]), None, context) + .await; + let create_ix_inputs = InvalidateNotOwnedCompressedAccountInstructionInputs { + signer: &payer_pubkey, + input_merkle_tree_pubkey: &env.merkle_tree_pubkey, + root_indices: &rpc_result.root_indices, + proof: &rpc_result.proof, + compressed_account: compressed_account, + }; + let instruction = create_invalidate_not_owned_account_instruction(create_ix_inputs.clone()); + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&payer_pubkey), + &[&payer], + context.get_new_latest_blockhash().await.unwrap(), + ); + solana_program_test::BanksClient::process_transaction_with_metadata( + &mut context.banks_client, + transaction, + ) + .await +} diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index 9ce66fa553..ba0de1b20a 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -31,6 +31,6 @@ reqwest = "0.11.26" light-hasher = { version = "0.1.0", path = "../merkle-tree/hasher" } light-merkle-tree-reference = { version = "0.1.0", path = "../merkle-tree/reference" } anchor-spl = "0.29.0" - +light-indexed-merkle-tree = {path = "../merkle-tree/indexed/"} [dev-dependencies] rand = "0.8" diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 8b9b3faf92..5d7e66d71e 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -10,7 +10,7 @@ use num_traits::{Bounded, CheckedAdd, CheckedSub, Unsigned}; use solana_program_test::{BanksClientError, ProgramTestContext}; use solana_sdk::{ account::Account, - instruction::Instruction, + instruction::{Instruction, InstructionError}, signature::{Keypair, Signature}, signer::Signer, transaction::Transaction, @@ -193,7 +193,6 @@ where T::try_from_slice(inner_instruction.instruction.data.as_slice()).ok() }) }); - // If transaction was successful, execute it. if let Some(Ok(())) = simulation_result.result { context @@ -218,3 +217,30 @@ pub fn create_account_instruction( }; system_instruction::create_account(payer, &keypair.pubkey(), rent, size as u64, id) } + +/// Asserts that the given `BanksTransactionResultWithMetadata` is an error with a custom error code +/// or a program error. +/// Unfortunately BanksTransactionResultWithMetadata does not reliably expose the custom error code, so +/// we allow program error as well. +// TODO: add generic that parses the error code from the result +pub fn assert_custom_error_or_program_error( + res: solana_program_test::BanksTransactionResultWithMetadata, + error_code: u32, +) -> Result<(), solana_sdk::transaction::TransactionError> { + if !(res.result + == Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::Custom(error_code), + )) + || res.result + == Err(solana_sdk::transaction::TransactionError::InstructionError( + 0, + InstructionError::ProgramFailedToComplete, + ))) + { + println!("result {:?}", res.result); + println!("error_code {:?}", error_code); + return Err(res.result.unwrap_err()); + } + Ok(()) +} diff --git a/test-utils/src/test_env.rs b/test-utils/src/test_env.rs index 46f2d4fcc1..e7e09b812b 100644 --- a/test-utils/src/test_env.rs +++ b/test-utils/src/test_env.rs @@ -5,6 +5,8 @@ use account_compression::{ nullifier_queue_sdk::create_initialize_nullifier_queue_instruction, state::AddressMerkleTreeAccount, GroupAuthority, RegisteredProgram, }; +#[cfg(feature = "test_indexer")] +use anchor_lang::ToAccountMetas; #[cfg(feature = "light_program")] use anchor_lang::{system_program, InstructionData}; use light_macros::pubkey; @@ -63,8 +65,7 @@ pub async fn setup_test_programs( program_test.start_with_context().await } -pub struct EnvWithAccounts { - pub context: ProgramTestContext, +pub struct EnvAccounts { pub merkle_tree_pubkey: Pubkey, pub nullifier_queue_pubkey: Pubkey, pub governance_authority: Keypair, @@ -73,6 +74,7 @@ pub struct EnvWithAccounts { pub registered_program_pda: Pubkey, pub address_merkle_tree_pubkey: Pubkey, pub address_merkle_tree_queue_pubkey: Pubkey, + pub cpi_signature_account_pubkey: Pubkey, } // Hardcoded keypairs for deterministic pubkeys for testing @@ -109,22 +111,29 @@ pub const ADDRESS_MERKLE_TREE_QUEUE_TEST_KEYPAIR: [u8; 64] = [ 184, 217, 99, ]; +pub const SIGNATURE_CPI_TEST_KEYPAIR: [u8; 64] = [ + 189, 58, 29, 111, 77, 118, 218, 228, 64, 122, 227, 119, 148, 83, 245, 92, 107, 168, 153, 61, + 221, 100, 243, 106, 228, 231, 147, 200, 195, 156, 14, 10, 162, 100, 133, 197, 231, 125, 178, + 71, 33, 62, 223, 145, 136, 210, 160, 96, 75, 148, 143, 30, 41, 89, 205, 141, 248, 204, 48, 157, + 195, 216, 81, 204, +]; + /// Setup test programs with accounts /// deploys: /// 1. light program /// 2. account_compression program -/// 3. psp_compressed_token program -/// 4. psp_compressed_pda program +/// 3. light_compressed_token program +/// 4. light_compressed_pda program /// /// Sets up the following accounts: /// 5. creates and initializes governance authority /// 6. creates and initializes group authority -/// 7. registers the psp_compressed_pda program with the group authority +/// 7. registers the light_compressed_pda program with the group authority /// 8. initializes Merkle tree owned by #[cfg(feature = "light_program")] pub async fn setup_test_programs_with_accounts( additional_programs: Option>, -) -> EnvWithAccounts { +) -> (ProgramTestContext, EnvAccounts) { use crate::airdrop_lamports; let mut context = setup_test_programs(additional_programs).await; @@ -238,18 +247,23 @@ pub async fn setup_test_programs_with_accounts( &address_merkle_tree_keypair.pubkey(), ) .await; - - EnvWithAccounts { + let cpi_signature_keypair = Keypair::from_bytes(&SIGNATURE_CPI_TEST_KEYPAIR).unwrap(); + #[cfg(feature = "test_indexer")] + init_cpi_signature_account(&mut context, &merkle_tree_pubkey, &cpi_signature_keypair).await; + ( context, - merkle_tree_pubkey, - nullifier_queue_pubkey, - group_pda, - governance_authority: payer, - governance_authority_pda: authority_pda.0, - registered_program_pda, - address_merkle_tree_pubkey: address_merkle_tree_keypair.pubkey(), - address_merkle_tree_queue_pubkey: address_merkle_tree_queue_keypair.pubkey(), - } + EnvAccounts { + merkle_tree_pubkey, + nullifier_queue_pubkey, + group_pda, + governance_authority: payer, + governance_authority_pda: authority_pda.0, + registered_program_pda, + address_merkle_tree_pubkey: address_merkle_tree_keypair.pubkey(), + address_merkle_tree_queue_pubkey: address_merkle_tree_queue_keypair.pubkey(), + cpi_signature_account_pubkey: cpi_signature_keypair.pubkey(), + }, + ) } #[cfg(feature = "light_program")] @@ -401,3 +415,46 @@ pub async fn create_and_initialize_address_merkle_tree( ); context.banks_client.process_transaction(transaction).await } + +#[cfg(feature = "test_indexer")] +pub async fn init_cpi_signature_account( + context: &mut ProgramTestContext, + merkle_tree_pubkey: &Pubkey, + cpi_account_keypair: &Keypair, +) -> Pubkey { + let payer = context.payer.insecure_clone(); + let account_size: usize = 20 * 1024 + 8; + let account_create_ix = create_account_instruction( + &context.payer.pubkey(), + account_size, + context + .banks_client + .get_rent() + .await + .unwrap() + .minimum_balance(account_size), + &light_compressed_pda::ID, + Some(cpi_account_keypair), + ); + let data = light_compressed_pda::instruction::InitCpiSignatureAccount {}; + let accounts = light_compressed_pda::accounts::InitializeCpiSignatureAccount { + fee_payer: payer.insecure_clone().pubkey(), + cpi_signature_account: cpi_account_keypair.pubkey(), + system_program: system_program::ID, + associated_merkle_tree: *merkle_tree_pubkey, + }; + let instruction = Instruction { + program_id: light_compressed_pda::ID, + accounts: accounts.to_account_metas(Some(true)), + data: data.data(), + }; + create_and_send_transaction( + context, + &[account_create_ix, instruction], + &payer.pubkey(), + &[&payer, &cpi_account_keypair], + ) + .await + .unwrap(); + cpi_account_keypair.pubkey() +} diff --git a/test-utils/src/test_indexer.rs b/test-utils/src/test_indexer.rs index 9d66a4e77a..bfb04e078d 100644 --- a/test-utils/src/test_indexer.rs +++ b/test-utils/src/test_indexer.rs @@ -1,20 +1,26 @@ #![cfg(feature = "test_indexer")] use crate::{ - create_account_instruction, create_and_send_transaction, get_hash_set, AccountZeroCopy, + create_account_instruction, create_and_send_transaction, + create_and_send_transaction_with_event, get_hash_set, AccountZeroCopy, }; use account_compression::{ utils::constants::{STATE_MERKLE_TREE_CANOPY_DEPTH, STATE_MERKLE_TREE_HEIGHT}, - StateMerkleTreeAccount, + AddressMerkleTreeAccount, StateMerkleTreeAccount, }; use anchor_lang::AnchorDeserialize; use light_circuitlib_rs::{ gnark::{ - constants::{INCLUSION_PATH, SERVER_ADDRESS}, + combined_json_formatter::CombinedJsonStruct, + constants::{COMBINED_PATH, INCLUSION_PATH, NON_INCLUSION_PATH, SERVER_ADDRESS}, helpers::{spawn_gnark_server, ProofType}, inclusion_json_formatter::InclusionJsonStruct, + non_inclusion_json_formatter::NonInclusionJsonStruct, proof_helpers::{compress_proof, deserialize_gnark_proof_json, proof_from_json_struct}, }, inclusion::merkle_inclusion_proof_inputs::{InclusionMerkleProofInputs, InclusionProofInputs}, + non_inclusion::merkle_non_inclusion_proof_inputs::{ + get_non_inclusion_proof_inputs, NonInclusionProofInputs, + }, }; use light_compressed_pda::{ compressed_account::CompressedAccountWithMerkleContext, event::PublicTransactionEvent, @@ -26,18 +32,24 @@ use light_compressed_token::{ TokenData, }; use light_hasher::Poseidon; -use num_bigint::BigInt; +use light_indexed_merkle_tree::array::IndexedArray; +use num_bigint::{BigInt, BigUint}; use num_traits::ops::bytes::FromBytes; +use num_traits::Num; use reqwest::Client; use solana_program_test::ProgramTestContext; -use solana_sdk::{ - instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, - transaction::Transaction, -}; +use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer}; use spl_token::instruction::initialize_mint; +#[derive(Debug)] +pub struct ProofRpcResult { + pub proof: CompressedProof, + pub root_indices: Vec, + pub address_root_indices: Vec, +} #[derive(Debug)] pub struct TestIndexer { + pub address_merkle_tree_pubkey: Pubkey, pub merkle_tree_pubkey: Pubkey, pub nullifier_queue_pubkey: Pubkey, pub payer: Keypair, @@ -47,6 +59,11 @@ pub struct TestIndexer { pub token_nullified_compressed_accounts: Vec, pub events: Vec, pub merkle_tree: light_merkle_tree_reference::MerkleTree, + pub address_merkle_tree: + light_indexed_merkle_tree::reference::IndexedMerkleTree, + pub indexing_array: IndexedArray, + pub path: String, + pub proof_types: Vec, } #[derive(Debug, Clone)] @@ -59,74 +76,235 @@ impl TestIndexer { pub async fn new( merkle_tree_pubkey: Pubkey, nullifier_queue_pubkey: Pubkey, + address_merkle_tree_pubkey: Pubkey, payer: Keypair, + inclusion: bool, + non_inclusion: bool, + gnark_bin_path: &str, ) -> Self { - // TODO: add path to gnark bin as parameter - // we should have a release and download the binary to target - spawn_gnark_server( - // correct path so that the examples can be run - "../../../../circuit-lib/circuitlib-rs/scripts/prover.sh", - true, - &[ProofType::Inclusion], - ) - .await; + let mut vec_proof_types = vec![]; + if inclusion { + vec_proof_types.push(ProofType::Inclusion); + } + if non_inclusion { + vec_proof_types.push(ProofType::NonInclusion); + } + if vec_proof_types.is_empty() { + panic!("At least one proof type must be selected"); + } + + // correct path so that the examples can be run: + // "../../../../circuit-lib/circuitlib-rs/scripts/prover.sh", + spawn_gnark_server(gnark_bin_path, true, vec_proof_types.as_slice()).await; - let merkle_tree = light_merkle_tree_reference::MerkleTree::::new( + let merkle_tree = light_merkle_tree_reference::MerkleTree::::new( STATE_MERKLE_TREE_HEIGHT as usize, STATE_MERKLE_TREE_CANOPY_DEPTH as usize, ); + let mut address_merkle_tree = light_indexed_merkle_tree::reference::IndexedMerkleTree::new( + STATE_MERKLE_TREE_HEIGHT as usize, + STATE_MERKLE_TREE_CANOPY_DEPTH as usize, + ) + .unwrap(); + let mut indexed_array = IndexedArray::::default(); + + let init_value = BigUint::from_str_radix( + &"21888242871839275222246405745257275088548364400416034343698204186575808495616", + 10, + ) + .unwrap(); + address_merkle_tree + .append(&init_value, &mut indexed_array) + .unwrap(); Self { merkle_tree_pubkey, nullifier_queue_pubkey, + address_merkle_tree_pubkey, payer, compressed_accounts: vec![], nullified_compressed_accounts: vec![], events: vec![], + merkle_tree, + address_merkle_tree, + indexing_array: indexed_array, token_compressed_accounts: vec![], token_nullified_compressed_accounts: vec![], - merkle_tree, + path: String::from(gnark_bin_path), + proof_types: vec_proof_types, } } + /* + pub async fn create_proof_for_compressed_accounts( + &mut self, + compressed_accounts: &[[u8; 32]], + context: &mut ProgramTestContext, + ) -> (Vec, CompressedProof) { + let client = Client::new(); + + let mut inclusion_proofs = Vec::::new(); + for compressed_account in compressed_accounts.iter() { + let leaf_index = self.merkle_tree.get_leaf_index(compressed_account).unwrap(); + let proof = self + .merkle_tree + .get_proof_of_leaf(leaf_index, true) + .unwrap(); + inclusion_proofs.push(InclusionMerkleProofInputs { + roots: BigInt::from_be_bytes(self.merkle_tree.root().as_slice()), + leaves: BigInt::from_be_bytes(compressed_account), + in_path_indices: BigInt::from_be_bytes(leaf_index.to_be_bytes().as_slice()), // leaf_index as u32, + in_path_elements: proof.iter().map(|x| BigInt::from_be_bytes(x)).collect(), + }); + } + + let inclusion_proof_inputs = InclusionProofInputs(inclusion_proofs.as_slice()); + let json_payload = + InclusionJsonStruct::from_inclusion_proof_inputs(&inclusion_proof_inputs).to_string(); + + let response_result = client + .post(&format!("{}{}", SERVER_ADDRESS, INCLUSION_PATH)) + .header("Content-Type", "text/plain; charset=utf-8") + .body(json_payload) + .send() + .await + .expect("Failed to execute request."); + assert!(response_result.status().is_success()); + let body = response_result.text().await.unwrap(); + let proof_json = deserialize_gnark_proof_json(&body).unwrap(); + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); + let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); + + let merkle_tree_account = + AccountZeroCopy::::new(context, self.merkle_tree_pubkey).await; + let merkle_tree = merkle_tree_account + .deserialized() + .copy_merkle_tree() + .unwrap(); + assert_eq!( + self.merkle_tree.root(), + merkle_tree.root().unwrap(), + "Local Merkle tree root is not equal to latest onchain root" + ); + + let root_indices: Vec = + vec![merkle_tree.current_root_index as u16; compressed_accounts.len()]; + ( + root_indices, + CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + }, + ) + } + */ pub async fn create_proof_for_compressed_accounts( &mut self, - compressed_accounts: &[[u8; 32]], + compressed_accounts: Option<&[[u8; 32]]>, + new_addresses: Option<&[[u8; 32]]>, context: &mut ProgramTestContext, - ) -> (Vec, CompressedProof) { + ) -> ProofRpcResult { + println!("compressed_accounts {:?}", compressed_accounts); + println!("new_addresses {:?}", new_addresses); + println!("self.merkle_tree.root() {:?}", self.merkle_tree.root()); let client = Client::new(); + let (root_indices, address_root_indices, json_payload, path) = + match (compressed_accounts, new_addresses) { + (Some(accounts), None) => { + let (payload, indices) = self.process_inclusion_proofs(accounts, context).await; + (indices, Vec::new(), payload.to_string(), INCLUSION_PATH) + } + (None, Some(addresses)) => { + let (payload, indices) = + self.process_non_inclusion_proofs(addresses, context).await; + ( + Vec::::new(), + indices, + payload.to_string(), + NON_INCLUSION_PATH, + ) + } + (Some(accounts), Some(addresses)) => { + let (inclusion_payload, inclusion_indices) = + self.process_inclusion_proofs(accounts, context).await; + let (non_inclusion_payload, non_inclusion_indices) = + self.process_non_inclusion_proofs(addresses, context).await; - let mut inclusion_proofs = Vec::::new(); - for compressed_account in compressed_accounts.iter() { - let leaf_index = self.merkle_tree.get_leaf_index(compressed_account).unwrap(); + let combined_payload = CombinedJsonStruct { + inclusion: inclusion_payload, + nonInclusion: non_inclusion_payload, + } + .to_string(); + ( + inclusion_indices, + non_inclusion_indices, + combined_payload, + COMBINED_PATH, + ) + } + _ => { + panic!("At least one of compressed_accounts or new_addresses must be provided") + } + }; + + let mut retries = 5; + while retries > 0 { + if retries < 3 { + spawn_gnark_server(self.path.as_str(), true, self.proof_types.as_slice()).await; + } + let response_result = client + .post(&format!("{}{}", SERVER_ADDRESS, path)) + .header("Content-Type", "text/plain; charset=utf-8") + .body(json_payload.clone()) + .send() + .await + .expect("Failed to execute request."); + if response_result.status().is_success() { + let body = response_result.text().await.unwrap(); + let proof_json = deserialize_gnark_proof_json(&body).unwrap(); + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); + let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); + return ProofRpcResult { + root_indices, + address_root_indices, + proof: CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + }, + }; + } + retries -= 1; + } + panic!("Failed to get proof from server"); + } + + async fn process_inclusion_proofs( + &self, + accounts: &[[u8; 32]], + context: &mut ProgramTestContext, + ) -> (InclusionJsonStruct, Vec) { + let mut inclusion_proofs = Vec::new(); + + for account in accounts.iter() { + let leaf_index = self.merkle_tree.get_leaf_index(account).unwrap(); let proof = self .merkle_tree .get_proof_of_leaf(leaf_index, true) .unwrap(); inclusion_proofs.push(InclusionMerkleProofInputs { roots: BigInt::from_be_bytes(self.merkle_tree.root().as_slice()), - leaves: BigInt::from_be_bytes(compressed_account), - in_path_indices: BigInt::from_be_bytes(leaf_index.to_be_bytes().as_slice()), // leaf_index as u32, + leaves: BigInt::from_be_bytes(account), + in_path_indices: BigInt::from_be_bytes(leaf_index.to_be_bytes().as_slice()), in_path_elements: proof.iter().map(|x| BigInt::from_be_bytes(x)).collect(), }); } let inclusion_proof_inputs = InclusionProofInputs(inclusion_proofs.as_slice()); - let json_payload = - InclusionJsonStruct::from_inclusion_proof_inputs(&inclusion_proof_inputs).to_string(); - - let response_result = client - .post(&format!("{}{}", SERVER_ADDRESS, INCLUSION_PATH)) - .header("Content-Type", "text/plain; charset=utf-8") - .body(json_payload) - .send() - .await - .expect("Failed to execute request."); - assert!(response_result.status().is_success()); - let body = response_result.text().await.unwrap(); - let proof_json = deserialize_gnark_proof_json(&body).unwrap(); - let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); - let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); + + let inclusion_proof_inputs_json = + InclusionJsonStruct::from_inclusion_proof_inputs(&inclusion_proof_inputs); let merkle_tree_account = AccountZeroCopy::::new(context, self.merkle_tree_pubkey).await; @@ -137,19 +315,47 @@ impl TestIndexer { assert_eq!( self.merkle_tree.root(), merkle_tree.root().unwrap(), - "Local Merkle tree root is not equal to latest onchain root" + "Merkle tree root mismatch" ); - let root_indices: Vec = - vec![merkle_tree.current_root_index as u16; compressed_accounts.len()]; - ( - root_indices, - CompressedProof { - a: proof_a, - b: proof_b, - c: proof_c, - }, + let root_indices = vec![merkle_tree.current_root_index as u16; accounts.len()]; + + (inclusion_proof_inputs_json, root_indices) + } + + async fn process_non_inclusion_proofs( + &self, + addresses: &[[u8; 32]], + context: &mut ProgramTestContext, + ) -> (NonInclusionJsonStruct, Vec) { + let mut non_inclusion_proofs = Vec::new(); + + for address in addresses.iter() { + let proof_inputs = get_non_inclusion_proof_inputs( + address, + &self.address_merkle_tree, + &self.indexing_array, + ); + non_inclusion_proofs.push(proof_inputs); + } + + let non_inclusion_proof_inputs = NonInclusionProofInputs(non_inclusion_proofs.as_slice()); + let non_inclusion_proof_inputs_json = + NonInclusionJsonStruct::from_non_inclusion_proof_inputs(&non_inclusion_proof_inputs); + + let merkle_tree_account = AccountZeroCopy::::new( + context, + self.address_merkle_tree_pubkey, ) + .await; + let address_merkle_tree = merkle_tree_account + .deserialized() + .copy_merkle_tree() + .unwrap(); + let address_root_indices = + vec![address_merkle_tree.current_root_index as u16; addresses.len()]; + + (non_inclusion_proof_inputs_json, address_root_indices) } /// deserializes an event @@ -167,18 +373,34 @@ impl TestIndexer { event: PublicTransactionEvent, ) -> Vec { for compressed_account in event.input_compressed_accounts.iter() { - self.compressed_accounts - .retain(|x| x.compressed_account != compressed_account.compressed_account); + let index = self + .compressed_accounts + .iter() + .position(|x| x.compressed_account == compressed_account.compressed_account) + .expect("compressed_account not found"); + self.compressed_accounts.remove(index); + let token_compressed_account_element = self + .token_compressed_accounts + .iter() + .find(|x| x.index == index); + if token_compressed_account_element.is_some() { + let token_compressed_account_element = + token_compressed_account_element.unwrap().clone(); + self.token_compressed_accounts.remove(index); + self.token_nullified_compressed_accounts + .push(token_compressed_account_element); + } // TODO: nullify compressed_account in Merkle tree, not implemented yet self.nullified_compressed_accounts .push(compressed_account.clone()); - if let Some((index, _)) = self + let index = self .compressed_accounts .iter() - .enumerate() - .find(|&(_, acc)| acc == compressed_account) - { - let token_compressed_account_element = self.token_compressed_accounts.remove(index); + .position(|x| x == compressed_account); + if let Some(index) = index { + let token_compressed_account_element = + self.token_compressed_accounts[index].clone(); + self.token_compressed_accounts.remove(index); self.token_nullified_compressed_accounts .push(token_compressed_account_element); } @@ -212,9 +434,7 @@ impl TestIndexer { /// adds the input_compressed_accounts to the nullified_compressed_accounts /// deserialiazes token data from the output_compressed_accounts /// adds the token_compressed_accounts to the token_compressed_accounts - pub fn add_compressed_accounts_with_token_data(&mut self, event_bytes: Vec) { - let event_bytes = event_bytes.clone(); - let event = PublicTransactionEvent::deserialize(&mut event_bytes.as_slice()).unwrap(); + pub fn add_compressed_accounts_with_token_data(&mut self, event: PublicTransactionEvent) { let indices = self.add_event_and_compressed_accounts(event); for index in indices.iter() { let data = self.compressed_accounts[*index] @@ -222,11 +442,16 @@ impl TestIndexer { .data .as_ref() .unwrap(); - let token_data = TokenData::deserialize(&mut data.data.as_slice()).unwrap(); - self.token_compressed_accounts.push(TokenDataWithContext { - index: *index, - token_data, - }); + let token_data = TokenData::deserialize(&mut data.data.as_slice()); + match token_data { + Ok(token_data) => { + self.token_compressed_accounts.push(TokenDataWithContext { + index: *index, + token_data, + }); + } + Err(_) => {} + } } } @@ -395,25 +620,14 @@ pub async fn mint_tokens_helper( amounts, recipients, ); - let transaction = Transaction::new_signed_with_payer( + let event = create_and_send_transaction_with_event::( + context, &[instruction], - Some(&mint_authority.pubkey()), + &payer_pubkey, &[&mint_authority], - context.get_new_latest_blockhash().await.unwrap(), - ); - let res = solana_program_test::BanksClient::process_transaction_with_metadata( - &mut context.banks_client, - transaction, ) - .await; - - test_indexer.add_compressed_accounts_with_token_data( - res.unwrap() - .metadata - .unwrap() - .return_data - .unwrap() - .data - .to_vec(), - ); + .await + .unwrap() + .unwrap(); + test_indexer.add_compressed_accounts_with_token_data(event); }