From 39d570cd39e27fa3fa8f39ca9895c66b2ed697e5 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 23 Oct 2025 07:01:59 -0400 Subject: [PATCH 1/4] patch to compile wip add borsh_compat compressed_proof add new_address_owner to instructiondata trait add derive_compressed_address remaining new_address_owner impl add csdk-anchor-test program lint add address_owner trait impl add sdk libs - wip add transfer_interface, transfer_ctoken, transfer_spl_to_ctoken, transfer_ctoken_to_spl, signed, instructions rename consistently transfer_x rename file transfer_decompressed to transfer_ctoken add todos add create_ctoken_account_signed, rename create_compressible_token_account to create_compressible_token_account_instruction add create_associated_ctoken_account add inline comment to copyLocalProgramBinaries.sh remove new_address_owner add pack and unpack for tokendata first pass, compressible helpers for light-sdk wip compiles lint compressAccountsIdempotent csdk works, adjust test asserts to account is_none ctoken add signer flags for decompress_full_ctoken_accounts_with_indices wip stash: removing ctoken from compression all tests working add auto-comp, clean up tests rm dependency on patch fmt lint lint refactor rm wip clean fmt clean clean clean rm macro clean clean dedupe derivation methods clean fmt revert copyLocalProgramBinaries.sh diff add csdk_anchor_test binary to ci fix indexer fix doctest fix cli ci build target fix cli build clean address nits fix cli cache fix cache clean fix csdk anchor test program build add pkg.json csdk rebuild fix syntax fix nx rm panics fix ci fix build sdk-anchor-test try fix bmt keccak spawn_prover fix fix lint fix clock sysvar add test feature to account-compression revert profiler refactor csdk-anchor-test program lib.rs split tests fmt revert cli script reset close_for_compress_and_close to main fmt try revert build account-compression with test flag fmt fix workflow to ensure we build account-compression with test feature fix sdk test nav try sdk-tests.yml with hyphen rm idl build csdk anchor test artifact wip reuse ctoken_types move ctoken to light-compressed-token-sdk clean move pack to compressed-token-sdk clean clean clean clean clean clean wip - add macro lint clean clean fmt clean, rename to sdk-compressible-test --- .github/actions/setup-and-build/action.yml | 3 +- .github/workflows/cli-v1.yml | 10 + .github/workflows/sdk-tests.yml | 4 +- Cargo.lock | 174 ++- Cargo.toml | 5 + cli/src/commands/token-balance/index.ts | 4 +- forester/Cargo.toml | 1 + forester/tests/e2e_test.rs | 2 +- .../batched_state_async_indexer_test.rs | 2 +- pnpm-lock.yaml | 2 + program-libs/batched-merkle-tree/Cargo.toml | 9 +- program-libs/compressible/src/config.rs | 25 + .../account-compression-test/Cargo.toml | 2 +- .../tests/ctoken/compress_and_close.rs | 2 +- .../tests/ctoken/create.rs | 10 +- .../tests/ctoken/functional.rs | 2 +- .../tests/ctoken/shared.rs | 4 +- .../tests/mint/functional.rs | 4 +- .../tests/transfer2/spl_ctoken.rs | 2 +- .../compressed-token-test/tests/v1.rs | 512 +++--- program-tests/registry-test/tests/tests.rs | 1 + program-tests/system-cpi-test/Cargo.toml | 1 + program-tests/system-cpi-test/tests/test.rs | 2 +- .../utils/src/assert_mint_to_compressed.rs | 9 +- program-tests/utils/src/assert_token_tx.rs | 2 +- program-tests/utils/src/assert_transfer2.rs | 22 +- program-tests/utils/src/conversions.rs | 31 +- program-tests/utils/src/e2e_test_env.rs | 2 +- program-tests/utils/src/spl.rs | 2 +- .../program/src/shared/token_input.rs | 2 +- sdk-libs/client/Cargo.toml | 3 + sdk-libs/client/src/constants.rs | 25 + sdk-libs/client/src/indexer/types.rs | 28 +- sdk-libs/client/src/rpc/lut.rs | 37 + sdk-libs/client/src/rpc/mod.rs | 3 + sdk-libs/compressed-token-sdk/Cargo.toml | 1 + sdk-libs/compressed-token-sdk/src/account2.rs | 160 +- sdk-libs/compressed-token-sdk/src/ctoken.rs | 50 + sdk-libs/compressed-token-sdk/src/error.rs | 12 + .../src/instructions/compress_and_close.rs | 68 +- .../create_associated_token_account.rs | 90 ++ .../create_compressed_mint/instruction.rs | 5 +- .../create_compressed_mint/mod.rs | 44 +- .../create_token_account/instruction.rs | 57 +- .../src/instructions/decompress_full.rs | 21 +- .../src/instructions/mod.rs | 20 +- .../src/instructions/transfer_ctoken.rs | 68 + .../src/instructions/transfer_interface.rs | 539 +++++++ .../update_compressed_mint/account_metas.rs | 1 - sdk-libs/compressed-token-sdk/src/lib.rs | 8 + sdk-libs/compressed-token-sdk/src/pack.rs | 329 ++++ .../compressed-token-sdk/src/token_pool.rs | 15 + sdk-libs/compressed-token-sdk/src/utils.rs | 48 +- .../compressed-token-sdk/tests/pack_test.rs | 121 ++ .../src/instruction/update_compressed_mint.rs | 2 +- sdk-libs/compressed-token-types/src/lib.rs | 2 - .../compressed-token-types/src/token_data.rs | 25 - sdk-libs/compressible-client/Cargo.toml | 23 + .../src/get_compressible_account.rs | 162 ++ sdk-libs/compressible-client/src/lib.rs | 393 +++++ sdk-libs/program-test/Cargo.toml | 6 +- sdk-libs/program-test/src/compressible.rs | 133 ++ .../program-test/src/indexer/extensions.rs | 2 +- .../program-test/src/indexer/test_indexer.rs | 6 +- .../src/program_test/compressible_setup.rs | 151 ++ .../program-test/src/program_test/config.rs | 4 + .../src/program_test/extensions.rs | 2 +- .../src/program_test/light_program_test.rs | 25 + sdk-libs/program-test/src/program_test/mod.rs | 3 + .../program-test/src/program_test/test_rpc.rs | 3 + sdk-libs/program-test/src/utils/mod.rs | 3 + sdk-libs/program-test/src/utils/simulation.rs | 36 + sdk-libs/sdk/Cargo.toml | 8 +- sdk-libs/sdk/src/account.rs | 11 + sdk-libs/sdk/src/address.rs | 14 + sdk-libs/sdk/src/compressible/close.rs | 40 + .../sdk/src/compressible/compress_account.rs | 115 ++ .../compressible/compress_account_on_init.rs | 102 ++ .../sdk/src/compressible/compression_info.rs | 121 ++ sdk-libs/sdk/src/compressible/config.rs | 476 ++++++ .../src/compressible/decompress_idempotent.rs | 141 ++ sdk-libs/sdk/src/compressible/mod.rs | 28 + sdk-libs/sdk/src/lib.rs | 10 +- sdk-libs/sdk/src/proof.rs | 88 ++ sdk-libs/sdk/src/token.rs | 59 - sdk-libs/token-client/Cargo.toml | 1 + .../create_compressible_token_account.rs | 3 +- .../src/actions/ctoken_transfer.rs | 23 +- .../src/actions/transfer2/ctoken_to_spl.rs | 8 +- .../src/actions/transfer2/spl_to_ctoken.rs | 27 +- .../src/instructions/create_spl_mint.rs | 9 +- .../src/instructions/mint_action.rs | 9 +- .../src/instructions/mint_to_compressed.rs | 9 +- sdk-libs/token-client/src/lib.rs | 2 + sdk-tests/client-test/Cargo.toml | 1 + sdk-tests/client-test/tests/light_client.rs | 6 +- .../client-test/tests/light_program_test.rs | 6 +- .../tests/user_record_tests.rs | 280 ++++ sdk-tests/sdk-compressible-test/Anchor.toml | 19 + sdk-tests/sdk-compressible-test/Cargo.toml | 57 + sdk-tests/sdk-compressible-test/Xargo.toml | 2 + sdk-tests/sdk-compressible-test/package.json | 11 + .../sdk-compressible-test/src/constants.rs | 3 + sdk-tests/sdk-compressible-test/src/errors.rs | 91 ++ .../src/instruction_accounts.rs | 222 +++ .../compress_accounts_idempotent.rs | 137 ++ .../src/instructions/create_game_session.rs | 76 + .../instructions/create_placeholder_record.rs | 67 + .../src/instructions/create_record.rs | 67 + .../create_user_record_and_game_session.rs | 205 +++ .../decompress_accounts_idempotent.rs | 426 +++++ .../initialize_compression_config.rs | 28 + .../src/instructions/mod.rs | 10 + .../instructions/update_compression_config.rs | 25 + .../src/instructions/update_game_session.rs | 21 + .../src/instructions/update_record.rs | 18 + sdk-tests/sdk-compressible-test/src/lib.rs | 178 +++ sdk-tests/sdk-compressible-test/src/seeds.rs | 219 +++ sdk-tests/sdk-compressible-test/src/state.rs | 522 +++++++ .../tests/game_session_tests.rs | 232 +++ .../sdk-compressible-test/tests/helpers.rs | 336 ++++ .../tests/idempotency_tests.rs | 141 ++ .../tests/multi_account_tests.rs | 1389 +++++++++++++++++ .../tests/placeholder_tests.rs | 536 +++++++ .../tests/user_record_tests.rs | 280 ++++ ...s_create_ctoken_with_compress_to_pubkey.rs | 6 +- .../src/process_four_transfer2.rs | 2 - 127 files changed, 9739 insertions(+), 771 deletions(-) create mode 100644 sdk-libs/client/src/rpc/lut.rs create mode 100644 sdk-libs/compressed-token-sdk/src/ctoken.rs create mode 100644 sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs create mode 100644 sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs create mode 100644 sdk-libs/compressed-token-sdk/src/pack.rs create mode 100644 sdk-libs/compressed-token-sdk/tests/pack_test.rs delete mode 100644 sdk-libs/compressed-token-types/src/token_data.rs create mode 100644 sdk-libs/compressible-client/Cargo.toml create mode 100644 sdk-libs/compressible-client/src/get_compressible_account.rs create mode 100644 sdk-libs/compressible-client/src/lib.rs create mode 100644 sdk-libs/program-test/src/program_test/compressible_setup.rs create mode 100644 sdk-libs/program-test/src/utils/simulation.rs create mode 100644 sdk-libs/sdk/src/compressible/close.rs create mode 100644 sdk-libs/sdk/src/compressible/compress_account.rs create mode 100644 sdk-libs/sdk/src/compressible/compress_account_on_init.rs create mode 100644 sdk-libs/sdk/src/compressible/compression_info.rs create mode 100644 sdk-libs/sdk/src/compressible/config.rs create mode 100644 sdk-libs/sdk/src/compressible/decompress_idempotent.rs create mode 100644 sdk-libs/sdk/src/compressible/mod.rs create mode 100644 sdk-libs/sdk/src/proof.rs delete mode 100644 sdk-libs/sdk/src/token.rs create mode 100644 sdk-tests/csdk-anchor-test/tests/user_record_tests.rs create mode 100644 sdk-tests/sdk-compressible-test/Anchor.toml create mode 100644 sdk-tests/sdk-compressible-test/Cargo.toml create mode 100644 sdk-tests/sdk-compressible-test/Xargo.toml create mode 100644 sdk-tests/sdk-compressible-test/package.json create mode 100644 sdk-tests/sdk-compressible-test/src/constants.rs create mode 100644 sdk-tests/sdk-compressible-test/src/errors.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instruction_accounts.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_record.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/mod.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs create mode 100644 sdk-tests/sdk-compressible-test/src/instructions/update_record.rs create mode 100644 sdk-tests/sdk-compressible-test/src/lib.rs create mode 100644 sdk-tests/sdk-compressible-test/src/seeds.rs create mode 100644 sdk-tests/sdk-compressible-test/src/state.rs create mode 100644 sdk-tests/sdk-compressible-test/tests/game_session_tests.rs create mode 100644 sdk-tests/sdk-compressible-test/tests/helpers.rs create mode 100644 sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs create mode 100644 sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs create mode 100644 sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs create mode 100644 sdk-tests/sdk-compressible-test/tests/user_record_tests.rs diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index 38477abab2..1d0113cc02 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -196,7 +196,8 @@ runs: path: | target/deploy/create_address_test_program.so target/deploy/sdk_anchor_test.so - key: ${{ runner.os }}-program-tests-${{ hashFiles('program-tests/**/Cargo.toml', 'program-tests/**/Cargo.lock', 'program-tests/**/*.rs', 'test-programs/**/Cargo.toml', 'test-programs/**/*.rs') }} + target/deploy/sdk-compressible-test.so + key: ${{ runner.os }}-program-tests-${{ hashFiles('program-tests/**/Cargo.toml', 'program-tests/**/Cargo.lock', 'program-tests/**/*.rs', 'test-programs/**/Cargo.toml', 'test-programs/**/*.rs', 'sdk-tests/**/Cargo.toml', 'sdk-tests/**/*.rs') }} restore-keys: | ${{ runner.os }}-program-tests- diff --git a/.github/workflows/cli-v1.yml b/.github/workflows/cli-v1.yml index 8290ee90f2..f10695b31a 100644 --- a/.github/workflows/cli-v1.yml +++ b/.github/workflows/cli-v1.yml @@ -49,6 +49,16 @@ jobs: skip-components: "redis,disk-cleanup,go" cache-key: "js" + - name: Build stateless.js with V1 + run: | + cd js/stateless.js + pnpm build:v1 + + - name: Build compressed-token with V1 + run: | + cd js/compressed-token + pnpm build:v1 + - name: Build CLI run: | npx nx build @lightprotocol/zk-compression-cli diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 09fd05e183..8f19fe0e6c 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -48,9 +48,9 @@ jobs: matrix: include: - program: native - sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo test-sbf -p client-test"]' + sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo-test-sbf -p client-test"]' - program: anchor & pinocchio - sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]' + sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]' - program: token test sub-tests: '["cargo-test-sbf -p sdk-token-test"]' - program: sdk-libs diff --git a/Cargo.lock b/Cargo.lock index bdae838091..2d1ea15d24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "ark-bn254 0.5.0", "ark-ff 0.5.0", "light-account-checks", @@ -294,7 +294,7 @@ version = "2.0.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "light-compressed-account", "light-ctoken-types", "light-hasher", @@ -410,6 +410,22 @@ dependencies = [ "spl-token-metadata-interface 0.6.0", ] +[[package]] +name = "anchor-spl" +version = "0.31.1" +source = "git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9#d8a2b3d99d61ef900d1f6cdaabcef14eb9af6279" +dependencies = [ + "anchor-lang", + "mpl-token-metadata", + "spl-associated-token-account 6.0.0", + "spl-memo", + "spl-pod", + "spl-token 7.0.0", + "spl-token-2022 6.0.0", + "spl-token-group-interface 0.5.0", + "spl-token-metadata-interface 0.6.0", +] + [[package]] name = "anchor-syn" version = "0.31.1" @@ -1363,6 +1379,7 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressed-token", + "light-compressed-token-sdk", "light-hasher", "light-indexed-array", "light-macros", @@ -1436,7 +1453,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "forester-utils", "light-batched-merkle-tree", "light-client", @@ -3523,6 +3540,7 @@ dependencies = [ "bytemuck", "lazy_static", "light-compressed-account", + "light-compressed-token-sdk", "light-concurrent-merkle-tree", "light-event", "light-hasher", @@ -3546,11 +3564,13 @@ dependencies = [ "solana-hash 2.3.0", "solana-instruction", "solana-keypair", + "solana-message", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", + "solana-signer", "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", @@ -3636,6 +3656,7 @@ dependencies = [ "light-compressed-account", "light-compressed-token", "light-compressed-token-types", + "light-compressible", "light-ctoken-types", "light-macros", "light-program-profiler", @@ -3693,6 +3714,20 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "light-compressible-client" +version = "0.13.1" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-client", + "light-sdk", + "solana-account", + "solana-instruction", + "solana-pubkey 2.4.0", + "thiserror 2.0.17", +] + [[package]] name = "light-concurrent-merkle-tree" version = "4.0.1" @@ -3931,6 +3966,7 @@ dependencies = [ "light-compressed-token", "light-compressed-token-sdk", "light-compressible", + "light-compressible-client", "light-concurrent-merkle-tree", "light-ctoken-types", "light-event", @@ -4022,10 +4058,12 @@ name = "light-sdk" version = "0.16.0" dependencies = [ "anchor-lang", + "bincode", "borsh 0.10.4", "light-account-checks", "light-compressed-account", "light-concurrent-merkle-tree", + "light-ctoken-types", "light-hasher", "light-macros", "light-sdk-macros", @@ -4033,11 +4071,15 @@ dependencies = [ "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", + "solana-clock", "solana-cpi", "solana-instruction", + "solana-loader-v3-interface", "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", + "solana-system-interface 1.0.0", + "solana-sysvar", "thiserror 2.0.17", ] @@ -4151,7 +4193,7 @@ version = "1.2.1" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.13.1", "create-address-test-program", "forester-utils", @@ -4199,6 +4241,7 @@ dependencies = [ "light-compressed-account", "light-compressed-token-sdk", "light-compressed-token-types", + "light-compressible", "light-ctoken-types", "light-sdk", "light-zero-copy", @@ -4448,6 +4491,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "mpl-token-metadata" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046f0779684ec348e2759661361c8798d79021707b1392cb49f3b5eb911340ff" +dependencies = [ + "borsh 0.10.4", + "num-derive 0.3.3", + "num-traits", + "solana-program", + "thiserror 1.0.69", +] + [[package]] name = "multer" version = "2.1.0" @@ -4586,6 +4642,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -6013,6 +6080,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "sdk-compressible-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", + "bincode", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressed-token-sdk", + "light-compressed-token-types", + "light-compressible", + "light-compressible-client", + "light-ctoken-types", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-types", + "light-test-utils", + "light-token-client", + "solana-account", + "solana-instruction", + "solana-keypair", + "solana-logger", + "solana-program", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signature", + "solana-signer", + "tokio", +] + [[package]] name = "sdk-native-test" version = "1.0.0" @@ -6069,7 +6170,7 @@ name = "sdk-token-test" version = "1.0.0" dependencies = [ "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrayvec", "light-batched-merkle-tree", "light-client", @@ -7805,7 +7906,7 @@ dependencies = [ "log", "memoffset", "num-bigint 0.4.6", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -8089,7 +8190,7 @@ dependencies = [ "dialoguer", "hidapi", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot", "qstring", @@ -9125,7 +9226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" dependencies = [ "bincode", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -9151,7 +9252,7 @@ dependencies = [ "agave-feature-set", "bincode", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -9184,7 +9285,7 @@ checksum = "70cea14481d8efede6b115a2581f27bc7c6fdfba0752c20398456c3ac1245fc4" dependencies = [ "agave-feature-set", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -9208,7 +9309,7 @@ dependencies = [ "itertools 0.12.1", "js-sys", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -9237,7 +9338,7 @@ checksum = "579752ad6ea2a671995f13c763bf28288c3c895cb857a518cc4ebab93c9a8dde" dependencies = [ "agave-feature-set", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -9260,7 +9361,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "itertools 0.12.1", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -9303,7 +9404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-associated-token-account-client", @@ -9319,7 +9420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-associated-token-account-client", @@ -9455,7 +9556,7 @@ dependencies = [ "borsh 1.5.7", "bytemuck", "bytemuck_derive", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-msg 2.2.1", @@ -9472,7 +9573,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" dependencies = [ - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-program-error-derive 0.4.1", @@ -9485,7 +9586,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdebc8b42553070b75aa5106f071fef2eb798c64a7ec63375da4b1f058688c6" dependencies = [ - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-msg 2.2.1", @@ -9525,7 +9626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -9547,7 +9648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1408e961215688715d5a1063cbdcf982de225c45f99c82b4f7d7e1dd22b998d7" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -9570,7 +9671,7 @@ checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9585,7 +9686,7 @@ checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-account-info", @@ -9613,7 +9714,7 @@ checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9641,7 +9742,7 @@ checksum = "9048b26b0df0290f929ff91317c83db28b3ef99af2b3493dd35baa146774924c" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9668,7 +9769,7 @@ source = "git+https://github.com/Lightprotocol/token-2022?rev=06d12f50a06db25d73 dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9696,7 +9797,7 @@ checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-account-info", @@ -9864,7 +9965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-instruction", @@ -9883,7 +9984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5597b4cd76f85ce7cd206045b7dc22da8c25516573d42d267c8d1fd128db5129" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-instruction", @@ -9902,7 +10003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-borsh", "solana-decode-error", @@ -9923,7 +10024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "304d6e06f0de0c13a621464b1fd5d4b1bebf60d15ca71a44d3839958e0da16ee" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-borsh", "solana-decode-error", @@ -9945,7 +10046,7 @@ checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9970,7 +10071,7 @@ checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9994,7 +10095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -10012,7 +10113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d417eb548214fa822d93f84444024b4e57c13ed6719d4dcc68eec24fb481e9f5" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -10172,13 +10273,14 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "create-address-test-program", "light-account-checks", "light-batched-merkle-tree", "light-client", "light-compressed-account", "light-compressed-token", + "light-compressed-token-sdk", "light-hasher", "light-merkle-tree-metadata", "light-program-test", @@ -10201,7 +10303,7 @@ version = "0.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "create-address-test-program", "light-account-checks", "light-batched-merkle-tree", diff --git a/Cargo.toml b/Cargo.toml index 8dd688f5a4..8d2ed244ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "sdk-libs/sdk-types", "sdk-libs/photon-api", "sdk-libs/program-test", + "sdk-libs/compressible-client", "xtask", "program-tests/account-compression-test", "program-tests/batched-merkle-tree-test", @@ -53,6 +54,7 @@ members = [ "sdk-tests/sdk-native-test", "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", + "sdk-tests/sdk-compressible-test", "forester-utils", "forester", "sparse-merkle-tree", @@ -91,6 +93,7 @@ solana-transaction-context = "2.3" solana-frozen-abi = "2.3" solana-frozen-abi-macro = "2.3" solana-msg = { version = "2.2" } +solana-message = "2.2" solana-zk-token-sdk = "2.3" solana-logger = "2.3" solana-bn254 = "2.2" @@ -102,6 +105,7 @@ solana-transaction-error = { version = "2.2" } solana-hash = { version = "2.3" } solana-clock = { version = "2.2" } solana-signature = { version = "2.3" } +solana-loader-v3-interface = { version = "5.0" } solana-commitment-config = { version = "2.2" } solana-account = { version = "2.2" } solana-epoch-info = { version = "2.2" } @@ -180,6 +184,7 @@ light-sdk-macros = { path = "sdk-libs/macros", version = "0.16.0" } light-sdk-types = { path = "sdk-libs/sdk-types", version = "0.16.0", default-features = false } light-compressed-account = { path = "program-libs/compressed-account", version = "0.6.2", default-features = false } light-compressible = { path = "program-libs/compressible", version = "0.1.0" } +light-compressible-client = { path = "sdk-libs/compressible-client", version = "0.13.1" } light-ctoken-types = { path = "program-libs/ctoken-types", version = "0.1.0" } light-account-checks = { path = "program-libs/account-checks", version = "0.5.1", default-features = false } light-verifier = { path = "program-libs/verifier", version = "5.0.0" } diff --git a/cli/src/commands/token-balance/index.ts b/cli/src/commands/token-balance/index.ts index f1f4b8859c..19471f1c61 100644 --- a/cli/src/commands/token-balance/index.ts +++ b/cli/src/commands/token-balance/index.ts @@ -44,7 +44,7 @@ class TokenBalanceCommand extends Command { return; } - const compressedTokenAccounts = tokenAccounts.items.filter((acc) => + const compressedTokenAccounts = tokenAccounts.items.filter((acc: any) => acc.parsed.mint.equals(refMint), ); @@ -56,7 +56,7 @@ class TokenBalanceCommand extends Command { let totalBalance = BigInt(0); - compressedTokenAccounts.forEach((account) => { + compressedTokenAccounts.forEach((account: any) => { const amount = account.parsed.amount; totalBalance += BigInt(amount.toString()); }); diff --git a/forester/Cargo.toml b/forester/Cargo.toml index fa8a495042..714d2f2e70 100644 --- a/forester/Cargo.toml +++ b/forester/Cargo.toml @@ -79,5 +79,6 @@ light-batched-merkle-tree = { workspace = true, features = ["test-only"] } light-token-client = { workspace = true } dotenvy = "0.15" light-compressed-token = { workspace = true } +light-compressed-token-sdk = { workspace = true } rand = { workspace = true } create-address-test-program = { workspace = true } diff --git a/forester/tests/e2e_test.rs b/forester/tests/e2e_test.rs index d15fd0cedb..dd51f28efb 100644 --- a/forester/tests/e2e_test.rs +++ b/forester/tests/e2e_test.rs @@ -35,11 +35,11 @@ use light_compressed_token::process_transfer::{ transfer_sdk::{create_transfer_instruction, to_account_metas}, TokenTransferOutputData, }; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_ctoken_types::state::TokenDataVersion; use light_hasher::Poseidon; use light_program_test::accounts::test_accounts::TestAccounts; use light_prover_client::prover::spawn_prover; -use light_sdk::token::TokenDataWithMerkleContext; use light_test_utils::{ conversions::sdk_to_program_token_data, get_concurrent_merkle_tree, get_indexed_merkle_tree, pack::pack_new_address_params_assigned, spl::create_mint_helper_with_keypair, diff --git a/forester/tests/legacy/batched_state_async_indexer_test.rs b/forester/tests/legacy/batched_state_async_indexer_test.rs index 935d7e6492..2fd28955bb 100644 --- a/forester/tests/legacy/batched_state_async_indexer_test.rs +++ b/forester/tests/legacy/batched_state_async_indexer_test.rs @@ -23,13 +23,13 @@ use light_compressed_account::{ use light_compressed_token::process_transfer::{ transfer_sdk::create_transfer_instruction, TokenTransferOutputData, }; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_program_test::accounts::test_accounts::TestAccounts; use light_prover_client::prover::spawn_prover; use light_registry::{ protocol_config::state::{ProtocolConfig, ProtocolConfigPda}, utils::get_protocol_config_pda_address, }; -use light_sdk::token::TokenDataWithMerkleContext; use light_test_utils::{ conversions::sdk_to_program_token_data, spl::create_mint_helper_with_keypair, system_program::create_invoke_instruction, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e3203bf38..3335823363 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -442,6 +442,8 @@ importers: programs: {} + sdk-tests/sdk-compressible-test: {} + sdk-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': diff --git a/program-libs/batched-merkle-tree/Cargo.toml b/program-libs/batched-merkle-tree/Cargo.toml index 5208858c75..5fdce5de90 100644 --- a/program-libs/batched-merkle-tree/Cargo.toml +++ b/program-libs/batched-merkle-tree/Cargo.toml @@ -17,10 +17,12 @@ solana = [ "solana-msg", "light-zero-copy/solana", "light-hasher/solana", + "light-hasher/keccak", "light-account-checks/solana", "light-bloom-filter/solana", "light-macros/solana", "light-compressed-account/solana", + "light-compressed-account/keccak", "light-merkle-tree-metadata/solana", ] pinocchio = [ @@ -31,6 +33,7 @@ pinocchio = [ "light-bloom-filter/pinocchio", "light-macros/pinocchio", "light-compressed-account/pinocchio", + "light-compressed-account/keccak", "light-merkle-tree-metadata/pinocchio", ] @@ -43,7 +46,7 @@ solana-sysvar = { workspace = true, optional = true } solana-msg = { workspace = true, optional = true } solana-account-info = { workspace = true, optional = true } light-zero-copy = { workspace = true, features = ["std"] } -light-hasher = { workspace = true, features = ["poseidon"] } +light-hasher = { workspace = true, features = ["poseidon", "keccak"] } light-bloom-filter = { workspace = true } light-verifier = { workspace = true } thiserror = { workspace = true } @@ -51,14 +54,14 @@ light-merkle-tree-metadata = { workspace = true } borsh = { workspace = true } zerocopy = { workspace = true } pinocchio = { workspace = true, optional = true } -light-compressed-account = { workspace = true, features = ["std"] } +light-compressed-account = { workspace = true, features = ["std", "keccak"] } light-macros = { workspace = true } [dev-dependencies] rand = { workspace = true } light-merkle-tree-reference = { workspace = true } light-account-checks = { workspace = true, features = ["test-only"] } -light-compressed-account = { workspace = true, features = ["new-unique"] } +light-compressed-account = { workspace = true, features = ["new-unique", "keccak"] } light-hasher = { workspace = true, features = ["keccak"] } [lints.rust.unexpected_cfgs] diff --git a/program-libs/compressible/src/config.rs b/program-libs/compressible/src/config.rs index 347e4dfbf8..09536e951f 100644 --- a/program-libs/compressible/src/config.rs +++ b/program-libs/compressible/src/config.rs @@ -258,4 +258,29 @@ impl CompressibleConfig { pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) { Self::derive_pda(program_id, 0) } + + pub fn derive_compression_authority_pda(program_id: &Pubkey, version: u16) -> (Pubkey, u8) { + let seeds = Self::get_compression_authority_seeds(version); + let seeds_refs: [&[u8]; 2] = [seeds[0].as_slice(), seeds[1].as_slice()]; + Pubkey::find_program_address(&seeds_refs, program_id) + } + + pub fn derive_rent_sponsor_pda(program_id: &Pubkey, version: u16) -> (Pubkey, u8) { + let seeds = Self::get_rent_sponsor_seeds(version); + let seeds_refs: [&[u8]; 2] = [seeds[0].as_slice(), seeds[1].as_slice()]; + Pubkey::find_program_address(&seeds_refs, program_id) + } + + /// Derives the default ctoken compression authority PDA (version = 1) + pub fn ctoken_v1_compression_authority_pda() -> Pubkey { + Self::derive_compression_authority_pda( + &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"), + 1, + ) + .0 + } + /// Derives the default ctoken rent sponsor PDA (version = 1) + pub fn ctoken_v1_rent_sponsor_pda() -> Pubkey { + Self::derive_rent_sponsor_pda(&pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"), 1).0 + } } diff --git a/program-tests/account-compression-test/Cargo.toml b/program-tests/account-compression-test/Cargo.toml index 5c5fb01c91..d59b6e4461 100644 --- a/program-tests/account-compression-test/Cargo.toml +++ b/program-tests/account-compression-test/Cargo.toml @@ -31,7 +31,7 @@ light-prover-client = { workspace = true, features = ["devenv"] } num-bigint = { workspace = true } anchor-spl = { workspace = true } anchor-lang = { workspace = true } -account-compression = { workspace = true } +account-compression = { workspace = true, features = ["test"] } light-hasher = { workspace = true, features = ["poseidon"] } light-hash-set = { workspace = true } light-concurrent-merkle-tree = { workspace = true } diff --git a/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs b/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs index 9c882ed62d..b41ccaa264 100644 --- a/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs +++ b/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs @@ -440,7 +440,7 @@ async fn test_compressible_account_with_custom_rent_payer_close_with_compression // Initialize compressible token account let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/ctoken/create.rs b/program-tests/compressed-token-test/tests/ctoken/create.rs index dbdeade9b5..4ea62aeba8 100644 --- a/program-tests/compressed-token-test/tests/ctoken/create.rs +++ b/program-tests/compressed-token-test/tests/ctoken/create.rs @@ -9,7 +9,7 @@ use solana_sdk::instruction::Instruction; use super::shared::*; #[tokio::test] -async fn test_create_compressible_token_account() { +async fn test_create_compressible_token_account_instruction() { let mut context = setup_account_test().await.unwrap(); let payer_pubkey = context.payer.pubkey(); @@ -227,7 +227,7 @@ async fn test_create_compressible_token_account_failing() { let token_account_pubkey = Keypair::new(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey.pubkey(), mint_pubkey: context.mint_pubkey, @@ -364,7 +364,7 @@ async fn test_create_compressible_token_account_failing() { }; let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, @@ -412,7 +412,7 @@ async fn test_create_compressible_token_account_failing() { .unwrap(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: context.token_account_keypair.pubkey(), mint_pubkey: context.mint_pubkey, @@ -452,7 +452,7 @@ async fn test_create_compressible_token_account_failing() { let wrong_account_type = context.rpc.test_accounts.protocol.governance_authority_pda; let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: context.token_account_keypair.pubkey(), mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/ctoken/functional.rs b/program-tests/compressed-token-test/tests/ctoken/functional.rs index e1ce56d148..222ff42845 100644 --- a/program-tests/compressed-token-test/tests/ctoken/functional.rs +++ b/program-tests/compressed-token-test/tests/ctoken/functional.rs @@ -123,7 +123,7 @@ async fn test_compressible_account_with_compression_authority_lifecycle() { // Initialize compressible token account let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/ctoken/shared.rs b/program-tests/compressed-token-test/tests/ctoken/shared.rs index 0822e9bcf7..c54f568295 100644 --- a/program-tests/compressed-token-test/tests/ctoken/shared.rs +++ b/program-tests/compressed-token-test/tests/ctoken/shared.rs @@ -91,7 +91,7 @@ pub async fn create_and_assert_token_account( let token_account_pubkey = context.token_account_keypair.pubkey(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, @@ -143,7 +143,7 @@ pub async fn create_and_assert_token_account_fails( let token_account_pubkey = context.token_account_keypair.pubkey(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/mint/functional.rs b/program-tests/compressed-token-test/tests/mint/functional.rs index 1c0b6b6dd5..51b95818a1 100644 --- a/program-tests/compressed-token-test/tests/mint/functional.rs +++ b/program-tests/compressed-token-test/tests/mint/functional.rs @@ -29,7 +29,7 @@ use light_test_utils::{ Rpc, }; use light_token_client::{ - actions::{create_mint, ctoken_transfer, mint_to_compressed, transfer2}, + actions::{create_mint, mint_to_compressed, transfer2, transfer_ctoken}, instructions::transfer2::{ create_decompress_instruction, create_generic_transfer2_instruction, CompressInput, DecompressInput, Transfer2InstructionType, TransferInput, @@ -820,7 +820,7 @@ async fn test_ctoken_transfer() { second_recipient_ata_balance ); // Execute the decompressed transfer - let transfer_result = ctoken_transfer( + let transfer_result = transfer_ctoken( &mut rpc, recipient_ata, // Source account (has 1000 tokens) second_recipient_ata, // Destination account diff --git a/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs b/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs index 859ed58628..3df688aada 100644 --- a/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs +++ b/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs @@ -128,7 +128,7 @@ async fn test_spl_to_ctoken_transfer() { println!("Testing reverse transfer: ctoken to SPL"); // Transfer from recipient's compressed token account back to sender's SPL token account - transfer2::ctoken_to_spl_transfer( + transfer2::transfer_ctoken_to_spl( &mut rpc, associated_token_account, spl_token_account_keypair.pubkey(), diff --git a/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index 287aa2089d..c037652362 100644 --- a/program-tests/compressed-token-test/tests/v1.rs +++ b/program-tests/compressed-token-test/tests/v1.rs @@ -38,6 +38,7 @@ use light_compressed_token::{ spl_compression::check_spl_token_pool_derivation_with_index, ErrorCode, TokenData, }; +use light_compressed_token_sdk::compat::{AccountState, TokenDataWithMerkleContext}; use light_program_test::{ accounts::{test_accounts::TestAccounts, test_keypairs::TestKeypairs}, indexer::{TestIndexer, TestIndexerExtensions}, @@ -45,7 +46,6 @@ use light_program_test::{ LightProgramTest, ProgramTestConfig, }; use light_prover_client::prover::spawn_prover; -use light_sdk::token::{AccountState, TokenDataWithMerkleContext}; use light_system_program::{errors::SystemProgramError, utils::get_sol_pool_pda}; use light_test_utils::{ assert_custom_error_or_program_error, @@ -667,12 +667,11 @@ async fn test_wrapped_sol() { None, ) .await; - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); decompress_test( &payer, &mut rpc, @@ -1379,7 +1378,7 @@ async fn perform_transfer_22_test( for _ in 0..outputs { recipients.push(Pubkey::new_unique()); } - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1595,7 +1594,7 @@ async fn test_mint_to_and_burn_from_all_token_pools() { iterator }; for i in iterator { - let accounts: Vec = test_indexer + let accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) .await .unwrap() @@ -1690,7 +1689,7 @@ async fn test_multiple_decompression() { let mut iterator = vec![0, 1, 2, 3, 4]; iterator.shuffle(rng); for i in iterator { - let accounts: Vec = test_indexer + let accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1742,7 +1741,7 @@ async fn test_multiple_decompression() { // Decompress from all token pools { - let all_accounts: Vec = test_indexer + let all_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1770,7 +1769,7 @@ async fn test_multiple_decompression() { Some(add_token_pool_accounts.clone()), ) .await; - let all_accounts: Vec = test_indexer + let all_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1839,12 +1838,11 @@ async fn test_delegation( .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -1868,12 +1866,11 @@ async fn test_delegation( let recipient = Pubkey::new_unique(); // 2. Transfer partial delegated amount { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -1898,12 +1895,11 @@ async fn test_delegation( } // 3. Transfer full delegated amount { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -1980,12 +1976,11 @@ async fn test_delegation_mixed() { .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -2009,23 +2004,21 @@ async fn test_delegation_mixed() { let recipient = Pubkey::new_unique(); // 2. Transfer partial delegated amount with delegate change account { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let mut input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) - .await - .unwrap() - .into(); + let delegate_input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) + .await + .unwrap() + .into(); input_compressed_accounts .extend_from_slice(&[delegate_input_compressed_accounts[0].clone()]); let delegate_lamports = delegate_input_compressed_accounts[0] @@ -2056,23 +2049,21 @@ async fn test_delegation_mixed() { let recipient = Pubkey::new_unique(); // 3. Transfer partial delegated amount without delegate change account { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let mut input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) - .await - .unwrap() - .into(); + let delegate_input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) + .await + .unwrap() + .into(); input_compressed_accounts .extend_from_slice(&[delegate_input_compressed_accounts[0].clone()]); let delegate_input_amount = input_compressed_accounts @@ -2105,23 +2096,21 @@ async fn test_delegation_mixed() { } // 3. Transfer full delegated amount { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let mut input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) - .await - .unwrap() - .into(); + let delegate_input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) + .await + .unwrap() + .into(); input_compressed_accounts.extend_from_slice(&delegate_input_compressed_accounts); let input_amount = input_compressed_accounts @@ -2218,7 +2207,7 @@ async fn test_approve_failing() { ) .await; - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2506,12 +2495,11 @@ async fn test_revoke(num_inputs: usize, mint_amount: u64, delegated_amount: u64) .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); for input in input_compressed_accounts.iter() { let input_compressed_accounts = vec![input.clone()]; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] @@ -2536,17 +2524,16 @@ async fn test_revoke(num_inputs: usize, mint_amount: u64, delegated_amount: u64) } // 2. Revoke { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items - .iter() - .filter(|x| x.token.delegate.is_some()) - .map(|x| x.clone().into()) - .collect::>(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items + .iter() + .filter(|x| x.token.delegate.is_some()) + .map(|x| x.clone().into()) + .collect::>(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -2632,12 +2619,11 @@ async fn test_revoke_failing() { .await; // Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_amount = 1000u64; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -2659,7 +2645,7 @@ async fn test_revoke_failing() { .await; } - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2855,12 +2841,11 @@ async fn test_burn() { .await; // 1. Burn tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1000u64; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -2883,12 +2868,11 @@ async fn test_burn() { } // 2. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_amount = 1000u64; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -2911,12 +2895,11 @@ async fn test_burn() { } // 3. Burn delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -2944,12 +2927,11 @@ async fn test_burn() { } // 3. Burn all delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -2994,18 +2976,17 @@ async fn test_burn() { ) .await .unwrap(); - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items - .iter() - .filter(|x| x.token.amount != 0) - .map(|x| x.clone().into()) - .collect::>()[0..4] - .to_vec(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items + .iter() + .filter(|x| x.token.amount != 0) + .map(|x| x.clone().into()) + .collect::>()[0..4] + .to_vec(); let burn_amount = input_compressed_accounts .iter() .map(|x| x.token_data.amount) @@ -3046,17 +3027,16 @@ async fn test_burn() { .unwrap(); let slot = rpc.get_slot().await.unwrap(); test_indexer.add_event_and_compressed_accounts(slot, &event); - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items - .iter() - .filter(|x| x.token.amount != 0) - .map(|x| x.clone().into()) - .collect::>(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items + .iter() + .filter(|x| x.token.amount != 0) + .map(|x| x.clone().into()) + .collect::>(); let burn_amount = input_compressed_accounts .iter() .map(|x| x.token_data.amount) @@ -3129,12 +3109,11 @@ async fn failing_tests_burn() { .await; // Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_amount = 1000u64; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3157,12 +3136,11 @@ async fn failing_tests_burn() { } // 1. invalid proof { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3191,12 +3169,11 @@ async fn failing_tests_burn() { } // 2. Signer is delegate but token data has no delegate. { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3228,12 +3205,11 @@ async fn failing_tests_burn() { } // 3. Signer is delegate but token data has no delegate. { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -3266,12 +3242,11 @@ async fn failing_tests_burn() { } // 4. invalid authority (use delegate as authority) { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3303,12 +3278,11 @@ async fn failing_tests_burn() { } // 5. invalid mint { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3341,12 +3315,11 @@ async fn failing_tests_burn() { } // 6. invalid change merkle tree { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3379,12 +3352,11 @@ async fn failing_tests_burn() { } // 6. invalid token pool (not initialized) { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3412,12 +3384,11 @@ async fn failing_tests_burn() { } // 7. invalid token pool (invalid mint) { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3492,12 +3463,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { .await; // 1. Freeze tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let output_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3516,12 +3486,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 2. Thaw tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Frozen) @@ -3544,12 +3513,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 3. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3571,12 +3539,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 4. Freeze delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let output_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3595,12 +3562,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 5. Thaw delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Frozen) @@ -3681,15 +3647,14 @@ async fn test_failing_freeze() { ) .await; - let input_compressed_accounts: Vec = - vec![test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items[0] - .clone() - .into()]; + let input_compressed_accounts: Vec = vec![test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items[0] + .clone() + .into()]; let outputs_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3947,15 +3912,14 @@ async fn test_failing_thaw() { // Freeze tokens { - let input_compressed_accounts: Vec = - vec![test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items[0] - .clone() - .into()]; + let input_compressed_accounts: Vec = vec![test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items[0] + .clone() + .into()]; let output_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3973,12 +3937,11 @@ async fn test_failing_thaw() { .await; } - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Frozen) @@ -4120,12 +4083,11 @@ async fn test_failing_thaw() { } // 4. thaw compressed account which is not frozen { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Initialized) @@ -4253,12 +4215,11 @@ async fn test_failing_decompression() { ) .await .unwrap(); - let input_compressed_account: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_account: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let decompress_amount = amount - 1000; // Test 1: invalid decompress account { @@ -5505,12 +5466,11 @@ async fn test_transfer_with_photon_and_batched_tree() { Pubkey::from_str("24fLJv6tHmsxQg5vDD7XWy85TMhFzJdkqZ9Ta3LtVReU").unwrap(), ]; println!("recipients {:?}", recipients); - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); let equal_amount = (amount * inputs as u64) / outputs as u64; let rest_amount = (amount * inputs as u64) % outputs as u64; let mut output_amounts = vec![equal_amount; outputs - 1]; @@ -5646,16 +5606,14 @@ async fn batch_compress_with_batched_tree() { test_indexer.add_compressed_accounts_with_token_data(slot, &event); for i in 0..num_recipients { - let recipient_compressed_token_accounts: Vec< - light_sdk::token::TokenDataWithMerkleContext, - > = test_indexer + let recipient_compressed_token_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&recipients[i as usize], None, None) .await .unwrap() .into(); assert_eq!(recipient_compressed_token_accounts.len(), 1); let recipient_compressed_token_account = &recipient_compressed_token_accounts[0]; - let expected_token_data = light_sdk::token::TokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint, owner: recipients[i as usize], amount: (i + 1), @@ -5714,16 +5672,14 @@ async fn batch_compress_with_batched_tree() { test_indexer.add_compressed_accounts_with_token_data(slot, &event); for recipient in &recipients { - let recipient_compressed_token_accounts: Vec< - light_sdk::token::TokenDataWithMerkleContext, - > = test_indexer + let recipient_compressed_token_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(recipient, None, None) .await .unwrap() .into(); assert_eq!(recipient_compressed_token_accounts.len(), 1); let recipient_compressed_token_account = &recipient_compressed_token_accounts[0]; - let expected_token_data = light_sdk::token::TokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint, owner: *recipient, amount, diff --git a/program-tests/registry-test/tests/tests.rs b/program-tests/registry-test/tests/tests.rs index 3c20c6fac4..cb2d08f2b7 100644 --- a/program-tests/registry-test/tests/tests.rs +++ b/program-tests/registry-test/tests/tests.rs @@ -191,6 +191,7 @@ async fn test_initialize_protocol_config() { config: ProgramTestConfig::default(), transaction_counter: 0, pre_context: None, + auto_compress_programs: Vec::new(), }; let payer = rpc.get_payer().insecure_clone(); diff --git a/program-tests/system-cpi-test/Cargo.toml b/program-tests/system-cpi-test/Cargo.toml index 5e7c6bf40d..2cf113e912 100644 --- a/program-tests/system-cpi-test/Cargo.toml +++ b/program-tests/system-cpi-test/Cargo.toml @@ -24,6 +24,7 @@ default = ["custom-heap"] anchor-lang = { workspace = true } anchor-spl = { workspace = true } light-compressed-token = { workspace = true, features = ["cpi"] } +light-compressed-token-sdk = { workspace = true } light-system-program-anchor = { workspace = true, features = ["cpi"] } light-registry = { workspace = true, features = ["cpi"] } account-compression = { workspace = true, features = ["cpi"] } diff --git a/program-tests/system-cpi-test/tests/test.rs b/program-tests/system-cpi-test/tests/test.rs index 9f4623ed02..48af7923d3 100644 --- a/program-tests/system-cpi-test/tests/test.rs +++ b/program-tests/system-cpi-test/tests/test.rs @@ -18,6 +18,7 @@ use light_compressed_account::{ TreeType, }; use light_compressed_token::process_transfer::InputTokenDataWithContext; +use light_compressed_token_sdk::compat::{AccountState, TokenDataWithMerkleContext}; use light_hasher::{Hasher, Poseidon}; use light_merkle_tree_metadata::errors::MerkleTreeMetadataError; use light_program_test::{ @@ -28,7 +29,6 @@ use light_program_test::{ ProgramTestConfig, }; use light_registry::account_compression_cpi::sdk::create_batch_update_address_tree_instruction; -use light_sdk::token::{AccountState, TokenDataWithMerkleContext}; use light_system_program::errors::SystemProgramError; use light_test_utils::{ e2e_test_env::init_program_test_env, diff --git a/program-tests/utils/src/assert_mint_to_compressed.rs b/program-tests/utils/src/assert_mint_to_compressed.rs index d777d6088f..52c3bf5fa4 100644 --- a/program-tests/utils/src/assert_mint_to_compressed.rs +++ b/program-tests/utils/src/assert_mint_to_compressed.rs @@ -5,7 +5,7 @@ use light_client::{ rpc::Rpc, }; use light_compressed_token::instructions::create_token_pool::find_token_pool_pda_with_index; -use light_compressed_token_sdk::instructions::derive_compressed_mint_from_spl_mint; +use light_compressed_token_sdk::instructions::derive_cmint_from_spl_mint; use light_ctoken_types::{ instructions::mint_action::Recipient, state::CompressedMint, COMPRESSED_TOKEN_PROGRAM_ID, }; @@ -21,8 +21,7 @@ pub async fn assert_mint_to_compressed( ) -> Vec { // Derive compressed mint address from SPL mint PDA (same as instruction) let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_mint_address = - derive_compressed_mint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); + let compressed_mint_address = derive_cmint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); // Verify each recipient received their tokens let mut all_token_accounts = Vec::new(); let mut total_minted = 0u64; @@ -52,12 +51,12 @@ pub async fn assert_mint_to_compressed( }); // Create expected token data - let expected_token_data = light_sdk::token::TokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint: spl_mint_pda, owner: recipient_pubkey, amount: recipient.amount, delegate: None, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; diff --git a/program-tests/utils/src/assert_token_tx.rs b/program-tests/utils/src/assert_token_tx.rs index f59326447c..d9f774f3d1 100644 --- a/program-tests/utils/src/assert_token_tx.rs +++ b/program-tests/utils/src/assert_token_tx.rs @@ -2,9 +2,9 @@ use anchor_lang::AnchorSerialize; use light_client::{indexer::Indexer, rpc::Rpc}; use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; use light_compressed_token::process_transfer::{get_cpi_authority_pda, TokenTransferOutputData}; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_event::event::PublicTransactionEvent; use light_program_test::indexer::TestIndexerExtensions; -use light_sdk::token::TokenDataWithMerkleContext; use solana_sdk::{program_pack::Pack, pubkey::Pubkey}; use crate::assert_compressed_tx::{ diff --git a/program-tests/utils/src/assert_transfer2.rs b/program-tests/utils/src/assert_transfer2.rs index ea49ec2459..2eab0525c4 100644 --- a/program-tests/utils/src/assert_transfer2.rs +++ b/program-tests/utils/src/assert_transfer2.rs @@ -88,12 +88,12 @@ pub async fn assert_transfer2_with_delegate( }; // Get mint from the source compressed token account - let expected_recipient_token_data = light_sdk::token::TokenData { + let expected_recipient_token_data = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: transfer_input.to, amount: transfer_input.amount, delegate: None, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -157,12 +157,12 @@ pub async fn assert_transfer2_with_delegate( None // No delegate to preserve }; - let expected_change_token = light_sdk::token::TokenData { + let expected_change_token = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: source_owner, amount: change_amount, delegate: expected_delegate, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -216,12 +216,12 @@ pub async fn assert_transfer2_with_delegate( None // Default to None if no authority specified }; - let expected_change_token = light_sdk::token::TokenData { + let expected_change_token = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: source_owner, amount: change_amount, delegate: expected_delegate, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -278,12 +278,12 @@ pub async fn assert_transfer2_with_delegate( .value .items; - let expected_change_token = light_sdk::token::TokenData { + let expected_change_token = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: source_owner, amount: change_amount, delegate: Some(approve_input.delegate), - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -336,12 +336,12 @@ pub async fn assert_transfer2_with_delegate( .map(|accounts| accounts.iter().map(|a| a.token.amount).sum::()) .unwrap_or(0); - let expected_recipient_token_data = light_sdk::token::TokenData { + let expected_recipient_token_data = light_compressed_token_sdk::compat::TokenData { mint: compress_input.mint, owner: compress_input.to, amount: compress_input.amount + compressed_input_amount, delegate: None, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; recipient_accounts.iter().for_each(|account| { @@ -482,7 +482,7 @@ pub async fn assert_transfer2_with_delegate( ); assert_eq!( compressed_account.token.state, - light_sdk::token::AccountState::Initialized, + light_compressed_token_sdk::compat::AccountState::Initialized, "CompressAndClose compressed account should be initialized" ); assert_eq!( diff --git a/program-tests/utils/src/conversions.rs b/program-tests/utils/src/conversions.rs index 00d4dab456..b12d6ed4a7 100644 --- a/program-tests/utils/src/conversions.rs +++ b/program-tests/utils/src/conversions.rs @@ -1,6 +1,5 @@ use light_ctoken_types::state::{CompressedTokenAccountState, TokenData as ProgramTokenData}; -use light_sdk::{self as sdk}; - +// TODO: most of this seems to be legacy and can be replaced with into(). // pub fn sdk_to_program_merkle_context( // sdk_merkle_context: sdk::merkle_context::MerkleContext, // ) -> ProgramMerkleContext { @@ -84,23 +83,31 @@ use light_sdk::{self as sdk}; // } pub fn sdk_to_program_account_state( - sdk_state: sdk::token::AccountState, + sdk_state: light_compressed_token_sdk::compat::AccountState, ) -> CompressedTokenAccountState { match sdk_state { - sdk::token::AccountState::Initialized => CompressedTokenAccountState::Initialized, - sdk::token::AccountState::Frozen => CompressedTokenAccountState::Frozen, + light_compressed_token_sdk::compat::AccountState::Initialized => { + CompressedTokenAccountState::Initialized + } + light_compressed_token_sdk::compat::AccountState::Frozen => { + CompressedTokenAccountState::Frozen + } } } -pub fn program_to_sdk_account_state(program_state: u8) -> sdk::token::AccountState { +pub fn program_to_sdk_account_state( + program_state: u8, +) -> light_compressed_token_sdk::compat::AccountState { match program_state { - 0 => sdk::token::AccountState::Initialized, - 1 => sdk::token::AccountState::Frozen, + 0 => light_compressed_token_sdk::compat::AccountState::Initialized, + 1 => light_compressed_token_sdk::compat::AccountState::Frozen, _ => panic!("program_to_sdk_account_state: invalid account state"), } } -pub fn sdk_to_program_token_data(sdk_token: sdk::token::TokenData) -> ProgramTokenData { +pub fn sdk_to_program_token_data( + sdk_token: light_compressed_token_sdk::compat::TokenData, +) -> ProgramTokenData { ProgramTokenData { mint: sdk_token.mint.into(), owner: sdk_token.owner.into(), @@ -111,8 +118,10 @@ pub fn sdk_to_program_token_data(sdk_token: sdk::token::TokenData) -> ProgramTok } } -pub fn program_to_sdk_token_data(program_token: ProgramTokenData) -> sdk::token::TokenData { - sdk::token::TokenData { +pub fn program_to_sdk_token_data( + program_token: ProgramTokenData, +) -> light_compressed_token_sdk::compat::TokenData { + light_compressed_token_sdk::compat::TokenData { mint: program_token.mint.into(), owner: program_token.owner.into(), amount: program_token.amount, diff --git a/program-tests/utils/src/e2e_test_env.rs b/program-tests/utils/src/e2e_test_env.rs index aca8fc6596..09ea74ed6a 100644 --- a/program-tests/utils/src/e2e_test_env.rs +++ b/program-tests/utils/src/e2e_test_env.rs @@ -117,6 +117,7 @@ use light_compressed_account::{ TreeType, }; use light_compressed_token::process_transfer::transfer_sdk::to_account_metas; +use light_compressed_token_sdk::compat::{AccountState, TokenDataWithMerkleContext}; use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; use light_indexed_merkle_tree::{ array::IndexedArray, reference::IndexedMerkleTree, HIGHEST_ADDRESS_PLUS_ONE, @@ -147,7 +148,6 @@ use light_registry::{ use light_sdk::{ address::NewAddressParamsAssignedPacked, constants::{ADDRESS_MERKLE_TREE_ROOTS, CPI_AUTHORITY_PDA_SEED, STATE_MERKLE_TREE_ROOTS}, - token::{AccountState, TokenDataWithMerkleContext}, }; use light_sparse_merkle_tree::{ changelog::ChangelogEntry, indexed_changelog::IndexedChangelogEntry, SparseMerkleTree, diff --git a/program-tests/utils/src/spl.rs b/program-tests/utils/src/spl.rs index 96eb90265f..6780f1e5f5 100644 --- a/program-tests/utils/src/spl.rs +++ b/program-tests/utils/src/spl.rs @@ -25,10 +25,10 @@ use light_compressed_token::{ process_compress_spl_token_account::sdk::create_compress_spl_token_account_instruction, process_transfer::{transfer_sdk::create_transfer_instruction, TokenTransferOutputData}, }; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_ctoken_types::state::{CompressedTokenAccountState, TokenData}; use light_hasher::Poseidon; use light_program_test::{indexer::TestIndexerExtensions, program_test::TestRpc}; -use light_sdk::token::TokenDataWithMerkleContext; use solana_banks_client::BanksClientError; use solana_sdk::{ instruction::Instruction, diff --git a/programs/compressed-token/program/src/shared/token_input.rs b/programs/compressed-token/program/src/shared/token_input.rs index decd9f7426..f8e4306f32 100644 --- a/programs/compressed-token/program/src/shared/token_input.rs +++ b/programs/compressed-token/program/src/shared/token_input.rs @@ -146,7 +146,7 @@ fn set_input_compressed_account_inner( &input_token_data.merkle_context, *input_token_data.root_index, lamports, - None, // Token accounts don't have addresses + None, )?; Ok(()) } diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index ea89387e43..3f33c3935d 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -27,6 +27,7 @@ solana-clock = { workspace = true } solana-signature = { workspace = true } solana-commitment-config = { workspace = true } solana-account = { workspace = true } +solana-signer = { workspace = true } solana-epoch-info = { workspace = true } solana-keypair = { workspace = true } solana-compute-budget-interface = { workspace = true } @@ -35,6 +36,7 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [ "bytemuck", "bincode", ] } +solana-message = { workspace = true } # Light Protocol dependencies light-merkle-tree-metadata = { workspace = true, features = ["solana"] } @@ -43,6 +45,7 @@ light-indexed-merkle-tree = { workspace = true } light-sdk = { workspace = true } light-hasher = { workspace = true, features = ["poseidon"] } light-compressed-account = { workspace = true, features = ["solana", "poseidon"] } +light-compressed-token-sdk = { workspace = true } light-event = { workspace = true } photon-api = { workspace = true } diff --git a/sdk-libs/client/src/constants.rs b/sdk-libs/client/src/constants.rs index ec1c0432b9..1ad9dfb6e1 100644 --- a/sdk-libs/client/src/constants.rs +++ b/sdk-libs/client/src/constants.rs @@ -19,3 +19,28 @@ pub const STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = /// Used to reduce transaction size by referencing queues via lookup table indices. pub const NULLIFIED_STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = pubkey!("5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP"); + +/// Address lookup table with zk compression related keys. Use to reduce +/// transaction size. +/// +/// Keys include: all protocol pubkeys, default state trees, address trees, and +/// more. +/// +/// Example usage: +/// ```bash +/// +/// # By cloning from mainnet +/// light test-validator --validator-args "\ +/// --clone 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ \ +/// --url https://api.mainnet-beta.solana.com \ +/// --upgradeable-program ~/.config/solana/id.json" +/// +/// # With a local LUT file +/// light test-validator --validator-args "\ +/// --account 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ ./scripts/lut.json \ +/// --url https://api.mainnet-beta.solana.com \ +/// --upgradeable-program ~/.config/solana/id.json" +/// +/// ``` +pub const LIGHT_PROTOCOL_LOOKUP_TABLE_ADDRESS: Pubkey = + pubkey!("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index d88d51d1cf..723d450cf3 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -6,10 +6,10 @@ use light_compressed_account::{ instruction_data::compressed_proof::CompressedProof, TreeType, }; +use light_compressed_token_sdk::compat::{AccountState, TokenData}; use light_indexed_merkle_tree::array::IndexedElement; -use light_sdk::{ - instruction::{PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof}, - token::{AccountState, TokenData}, +use light_sdk::instruction::{ + PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof, }; use num_bigint::BigUint; use solana_pubkey::Pubkey; @@ -813,11 +813,13 @@ impl TryFrom<&photon_api::models::TokenAccountV2> for CompressedTokenAccount { } #[allow(clippy::from_over_into)] -impl Into for CompressedTokenAccount { - fn into(self) -> light_sdk::token::TokenDataWithMerkleContext { +impl Into + for CompressedTokenAccount +{ + fn into(self) -> light_compressed_token_sdk::compat::TokenDataWithMerkleContext { let compressed_account = CompressedAccountWithMerkleContext::from(self.account); - light_sdk::token::TokenDataWithMerkleContext { + light_compressed_token_sdk::compat::TokenDataWithMerkleContext { token_data: self.token, compressed_account, } @@ -825,30 +827,32 @@ impl Into for CompressedTokenAccou } #[allow(clippy::from_over_into)] -impl Into> +impl Into> for super::response::Response> { - fn into(self) -> Vec { + fn into(self) -> Vec { self.value .items .into_iter() .map( - |token_account| light_sdk::token::TokenDataWithMerkleContext { + |token_account| light_compressed_token_sdk::compat::TokenDataWithMerkleContext { token_data: token_account.token, compressed_account: CompressedAccountWithMerkleContext::from( token_account.account.clone(), ), }, ) - .collect::>() + .collect::>() } } -impl TryFrom for CompressedTokenAccount { +impl TryFrom + for CompressedTokenAccount +{ type Error = IndexerError; fn try_from( - token_data_with_context: light_sdk::token::TokenDataWithMerkleContext, + token_data_with_context: light_compressed_token_sdk::compat::TokenDataWithMerkleContext, ) -> Result { let account = CompressedAccount::try_from(token_data_with_context.compressed_account)?; diff --git a/sdk-libs/client/src/rpc/lut.rs b/sdk-libs/client/src/rpc/lut.rs new file mode 100644 index 0000000000..e1adfe9872 --- /dev/null +++ b/sdk-libs/client/src/rpc/lut.rs @@ -0,0 +1,37 @@ +pub use solana_address_lookup_table_interface::{ + error, instruction, program, state::AddressLookupTable, +}; +use solana_message::AddressLookupTableAccount; +use solana_pubkey::Pubkey; +use solana_rpc_client::rpc_client::RpcClient; + +use crate::rpc::errors::RpcError; + +/// Gets a lookup table account state from the network. +/// +/// # Arguments +/// +/// * `client` - The RPC client to use to get the lookup table account state. +/// * `lookup_table_address` - The address of the lookup table account to get. +/// +/// # Returns +/// +/// * `AddressLookupTableAccount` - The lookup table account state. +pub fn load_lookup_table( + client: &RpcClient, + lookup_table_address: &Pubkey, +) -> Result { + let raw_account = client.get_account(lookup_table_address)?; + let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| { + RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}")) + })?; + let address_lookup_table_account = AddressLookupTableAccount { + key: lookup_table_address.to_bytes().into(), + addresses: address_lookup_table + .addresses + .iter() + .map(|p| p.to_bytes().into()) + .collect(), + }; + Ok(address_lookup_table_account) +} diff --git a/sdk-libs/client/src/rpc/mod.rs b/sdk-libs/client/src/rpc/mod.rs index 0b968c26c5..724a71c106 100644 --- a/sdk-libs/client/src/rpc/mod.rs +++ b/sdk-libs/client/src/rpc/mod.rs @@ -11,3 +11,6 @@ pub use client::{LightClient, RetryConfig}; pub use errors::RpcError; pub use rpc_trait::{LightClientConfig, Rpc}; pub mod get_light_state_tree_infos; + +pub mod lut; +pub use lut::load_lookup_table; diff --git a/sdk-libs/compressed-token-sdk/Cargo.toml b/sdk-libs/compressed-token-sdk/Cargo.toml index 1cb9a01765..794c9d2b17 100644 --- a/sdk-libs/compressed-token-sdk/Cargo.toml +++ b/sdk-libs/compressed-token-sdk/Cargo.toml @@ -22,6 +22,7 @@ profile-heap = [ # Light Protocol dependencies light-compressed-token-types = { workspace = true } light-compressed-account = { workspace = true, features = ["std"] } +light-compressible = { workspace = true } light-ctoken-types = { workspace = true } light-sdk = { workspace = true, features = ["v2"] } light-macros = { workspace = true } diff --git a/sdk-libs/compressed-token-sdk/src/account2.rs b/sdk-libs/compressed-token-sdk/src/account2.rs index bbd8b69e2c..dfeeec39cd 100644 --- a/sdk-libs/compressed-token-sdk/src/account2.rs +++ b/sdk-libs/compressed-token-sdk/src/account2.rs @@ -1,22 +1,13 @@ use std::ops::Deref; -use light_compressed_token_types::ValidityProof; use light_ctoken_types::instructions::transfer2::{ Compression, CompressionMode, MultiInputTokenDataWithContext, MultiTokenTransferOutputData, }; use light_program_profiler::profile; use solana_account_info::AccountInfo; -use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -use crate::{ - error::TokenSdkError, - instructions::transfer2::{ - account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config, - Transfer2Inputs, - }, - utils::get_token_account_balance, -}; +use crate::{error::TokenSdkError, utils::get_token_account_balance}; #[derive(Debug, PartialEq, Clone)] pub struct CTokenAccount2 { @@ -410,152 +401,3 @@ impl Deref for CTokenAccount2 { &self.output } } - -#[allow(clippy::too_many_arguments)] -#[profile] -pub fn create_spl_to_ctoken_transfer_instruction( - source_spl_token_account: Pubkey, - to: Pubkey, - amount: u64, - authority: Pubkey, - mint: Pubkey, - payer: Pubkey, - token_pool_pda: Pubkey, - token_pool_pda_bump: u8, -) -> Result { - let packed_accounts = vec![ - // Mint (index 0) - AccountMeta::new_readonly(mint, false), - // Destination token account (index 1) - AccountMeta::new(to, false), - // Authority for compression (index 2) - signer - AccountMeta::new_readonly(authority, true), - // Source SPL token account (index 3) - writable - AccountMeta::new(source_spl_token_account, false), - // Token pool PDA (index 4) - writable - AccountMeta::new(token_pool_pda, false), - // SPL Token program (index 5) - needed for CPI - AccountMeta::new_readonly( - Pubkey::from(light_compressed_token_types::constants::SPL_TOKEN_PROGRAM_ID), - false, - ), - ]; - - let wrap_spl_to_ctoken_account = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::compress_spl( - amount, - 0, // mint - 3, // source or recpient - 2, // authority - 4, // pool_account_index: - 0, // pool_index - token_pool_pda_bump, - )), - delegate_is_set: false, - method_used: true, - }; - - let ctoken_account = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::decompress_ctoken(amount, 0, 1)), - delegate_is_set: false, - method_used: true, - }; - - // Create Transfer2Inputs following the test pattern - let inputs = Transfer2Inputs { - validity_proof: ValidityProof::default(), - transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), - meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( - payer, - packed_accounts, - ), - in_lamports: None, - out_lamports: None, - token_accounts: vec![wrap_spl_to_ctoken_account, ctoken_account], - output_queue: 0, // Decompressed accounts only, no output queue needed - }; - - // Create the actual transfer2 instruction - create_transfer2_instruction(inputs) -} - -#[allow(clippy::too_many_arguments)] -#[profile] -pub fn create_ctoken_to_spl_transfer_instruction( - source_ctoken_account: Pubkey, - destination_spl_token_account: Pubkey, - amount: u64, - authority: Pubkey, - mint: Pubkey, - payer: Pubkey, - token_pool_pda: Pubkey, - token_pool_pda_bump: u8, -) -> Result { - let packed_accounts = vec![ - // Mint (index 0) - AccountMeta::new_readonly(mint, false), - // Source ctoken account (index 1) - writable - AccountMeta::new(source_ctoken_account, false), - // Destination SPL token account (index 2) - writable - AccountMeta::new(destination_spl_token_account, false), - // Authority (index 3) - signer - AccountMeta::new_readonly(authority, true), - // Token pool PDA (index 4) - writable - AccountMeta::new(token_pool_pda, false), - // SPL Token program (index 5) - needed for CPI - AccountMeta::new_readonly( - Pubkey::from(light_compressed_token_types::constants::SPL_TOKEN_PROGRAM_ID), - false, - ), - ]; - - // First operation: compress from ctoken account to pool using compress_spl - let compress_to_pool = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::compress_ctoken( - amount, 0, // mint index - 1, // source ctoken account index - 3, // authority index - )), - delegate_is_set: false, - method_used: true, - }; - - // Second operation: decompress from pool to SPL token account using decompress_spl - let decompress_to_spl = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::decompress_spl( - amount, - 0, // mint index - 2, // destination SPL token account index - 4, // pool_account_index - 0, // pool_index (TODO: make dynamic) - token_pool_pda_bump, - )), - delegate_is_set: false, - method_used: true, - }; - - // Create Transfer2Inputs - let inputs = Transfer2Inputs { - validity_proof: ValidityProof::default(), - transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), - meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( - payer, - packed_accounts, - ), - in_lamports: None, - out_lamports: None, - token_accounts: vec![compress_to_pool, decompress_to_spl], - output_queue: 0, // Decompressed accounts only, no output queue needed - }; - - // Create the actual transfer2 instruction - create_transfer2_instruction(inputs) -} diff --git a/sdk-libs/compressed-token-sdk/src/ctoken.rs b/sdk-libs/compressed-token-sdk/src/ctoken.rs new file mode 100644 index 0000000000..5e02ced318 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/ctoken.rs @@ -0,0 +1,50 @@ +use light_compressed_token_types::POOL_SEED; +use light_compressible::config::CompressibleConfig; +use solana_pubkey::{pubkey, Pubkey}; + +pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); + +/// Returns the program ID for the Compressed Token Program +pub fn id() -> Pubkey { + CTOKEN_PROGRAM_ID +} + +/// Return the cpi authority pda of the Compressed Token Program. +pub fn cpi_authority() -> Pubkey { + CTOKEN_CPI_AUTHORITY +} + +pub fn get_token_pool_address_and_bump(mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[POOL_SEED, mint.as_ref()], &CTOKEN_PROGRAM_ID) +} + +/// Returns the associated ctoken address for a given owner and mint. +pub fn get_associated_ctoken_address(owner: &Pubkey, mint: &Pubkey) -> Pubkey { + get_associated_ctoken_address_and_bump(owner, mint).0 +} + +/// Returns the associated ctoken address and bump for a given owner and mint. +pub fn get_associated_ctoken_address_and_bump(owner: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], + &id(), + ) +} + +pub use crate::instructions::create_compressed_mint::{ + derive_cmint_from_spl_mint, find_spl_mint_address, +}; + +pub fn config_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_config_pda() +} + +pub fn rent_sponsor_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_rent_sponsor_pda() +} + +pub fn compression_authority_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_compression_authority_pda() +} diff --git a/sdk-libs/compressed-token-sdk/src/error.rs b/sdk-libs/compressed-token-sdk/src/error.rs index 3f4206a02a..f067db9542 100644 --- a/sdk-libs/compressed-token-sdk/src/error.rs +++ b/sdk-libs/compressed-token-sdk/src/error.rs @@ -49,6 +49,14 @@ pub enum TokenSdkError { CannotMintWithDecompressedInCpiWrite, #[error("RentAuthorityIsNone")] RentAuthorityIsNone, + #[error("Incomplete SPL bridge config")] + IncompleteSplBridgeConfig, + #[error("SPL bridge config required")] + SplBridgeConfigRequired, + #[error("Use regular SPL transfer")] + UseRegularSplTransfer, + #[error("Cannot determine account type")] + CannotDetermineAccountType, #[error(transparent)] CompressedTokenTypes(#[from] LightTokenSdkTypeError), #[error(transparent)] @@ -97,6 +105,10 @@ impl From for u32 { TokenSdkError::PackedAccountIndexOutOfBounds => 17017, TokenSdkError::CannotMintWithDecompressedInCpiWrite => 17018, TokenSdkError::RentAuthorityIsNone => 17019, + TokenSdkError::SplBridgeConfigRequired => 17020, + TokenSdkError::IncompleteSplBridgeConfig => 17021, + TokenSdkError::UseRegularSplTransfer => 17022, + TokenSdkError::CannotDetermineAccountType => 17023, TokenSdkError::CompressedTokenTypes(e) => e.into(), TokenSdkError::CTokenError(e) => e.into(), TokenSdkError::LightSdkTypesError(e) => e.into(), diff --git a/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs b/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs index e326f2c5ab..0ddee8502a 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs @@ -9,6 +9,7 @@ use light_sdk::{ }; use light_zero_copy::traits::ZeroCopyAt; use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; use solana_instruction::{AccountMeta, Instruction}; use solana_msg::msg; use solana_pubkey::Pubkey; @@ -23,6 +24,7 @@ use crate::{ }, CTokenDefaultAccounts, }, + AccountInfoToCompress, }; /// Struct to hold all the indices needed for CompressAndClose operation @@ -216,6 +218,7 @@ pub fn compress_and_close_ctoken_accounts_with_indices<'info>( i as u8, // Pass the index in the output array idx.destination_index, // destination for user funds )?; + if rent_sponsor_is_signer { packed_account_metas[idx.authority_index as usize].is_signer = true; } else { @@ -365,16 +368,12 @@ pub fn compress_and_close_ctoken_accounts<'info>( rent_sponsor_pubkey.unwrap() }; - // Determine destination based on authority type let destination_pubkey = if with_compression_authority { - // When rent authority closes, everything goes to rent recipient actual_rent_sponsor } else { - // When owner closes, user funds go to owner owner_pubkey }; - // Find indices for all required accounts let indices = find_account_indices( find_index, ctoken_account_info.key, @@ -383,7 +382,6 @@ pub fn compress_and_close_ctoken_accounts<'info>( &authority, &actual_rent_sponsor, &destination_pubkey, - // &output_queue_pubkey, )?; indices_vec.push(indices); } @@ -391,7 +389,6 @@ pub fn compress_and_close_ctoken_accounts<'info>( packed_accounts_vec.push(output_queue); packed_accounts_vec.extend_from_slice(packed_accounts); - // Delegate to the with_indices version compress_and_close_ctoken_accounts_with_indices( fee_payer, with_compression_authority, @@ -401,6 +398,65 @@ pub fn compress_and_close_ctoken_accounts<'info>( ) } +/// Compress and close ctoken accounts, and invoke cpi. +/// +/// Wraps `compress_and_close_ctoken_accounts`, builds the instruction, and +/// calls `invoke_signed` with provided seeds. +/// +/// `remaining_accounts` must include required Light system accounts for +/// `transfer2`, followed by any additional accounts. Post_system accounts are a +/// subset of `remaining_accounts`. +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>( + token_accounts_to_compress: &[AccountInfoToCompress<'info>], + fee_payer: AccountInfo<'info>, + output_queue: AccountInfo<'info>, + compressed_token_rent_sponsor: AccountInfo<'info>, + compressed_token_cpi_authority: AccountInfo<'info>, + cpi_authority: AccountInfo<'info>, + post_system: &[AccountInfo<'info>], + remaining_accounts: &[AccountInfo<'info>], + with_compression_authority: bool, +) -> Result<(), TokenSdkError> { + let mut packed_accounts = Vec::with_capacity(post_system.len() + 4); + packed_accounts.extend_from_slice(post_system); + packed_accounts.push(cpi_authority); + packed_accounts.push(compressed_token_rent_sponsor.clone()); + + let ctoken_infos: Vec<&AccountInfo<'info>> = token_accounts_to_compress + .iter() + .map(|t| t.account_info.as_ref()) + .collect(); + + let instruction = compress_and_close_ctoken_accounts( + *fee_payer.key, + with_compression_authority, + output_queue, + &ctoken_infos, + &packed_accounts, + )?; + // infos + let total_capacity = packed_accounts.len() + remaining_accounts.len() + 1; + let mut account_infos: Vec> = Vec::with_capacity(total_capacity); + account_infos.extend_from_slice(&packed_accounts); + account_infos.push(compressed_token_cpi_authority); + account_infos.extend_from_slice(remaining_accounts); + + let token_seeds_refs: Vec> = token_accounts_to_compress + .iter() + .map(|t| t.signer_seeds.iter().map(|v| v.as_slice()).collect()) + .collect(); + let mut all_signer_seeds: Vec<&[&[u8]]> = Vec::with_capacity(token_seeds_refs.len()); + for seeds in &token_seeds_refs { + all_signer_seeds.push(seeds.as_slice()); + } + + invoke_signed(&instruction, &account_infos, &all_signer_seeds) + .map_err(|e| TokenSdkError::CpiError(e.to_string()))?; + Ok(()) +} + pub struct CompressAndCloseAccounts { pub compressed_token_program: Pubkey, pub cpi_authority_pda: Pubkey, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs index f7e7280654..9b19afe417 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs @@ -7,6 +7,7 @@ use light_ctoken_types::{ }, state::TokenDataVersion, }; +use solana_account_info::AccountInfo; use solana_instruction::Instruction; use solana_pubkey::Pubkey; @@ -401,3 +402,92 @@ fn create_ata2_instruction_unified( + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + compressible_config: AccountInfo<'info>, + rent_sponsor: AccountInfo<'info>, + authority: AccountInfo<'info>, + mint: Pubkey, + bump: u8, + pre_pay_num_epochs: Option, + lamports_per_write: Option, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let inputs = CreateCompressibleAssociatedTokenAccountInputs { + payer: *payer.key, + owner: *authority.key, + mint, + compressible_config: *compressible_config.key, + rent_sponsor: *rent_sponsor.key, + pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(1), + lamports_per_write, + token_account_version: TokenDataVersion::ShaFlat, + }; + + // TODO: switch to wrapper ixn using accounts instead of ixdata. + let ix = create_compressible_associated_token_account_with_bump( + inputs, + *associated_token_account.key, + bump, + )?; + + solana_cpi::invoke( + &ix, + &[ + payer, + associated_token_account, + system_program, + compressible_config, + rent_sponsor, + authority, + ], + ) +} + +/// CPI wrapper to create a compressible c-token associated token account +/// idempotently. +#[allow(clippy::too_many_arguments)] +pub fn create_associated_ctoken_account_idempotent<'info>( + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + compressible_config: AccountInfo<'info>, + rent_sponsor: AccountInfo<'info>, + authority: Pubkey, + mint: Pubkey, + bump: u8, + pre_pay_num_epochs: Option, + lamports_per_write: Option, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let inputs = CreateCompressibleAssociatedTokenAccountInputs { + payer: *payer.key, + owner: authority, + mint, + compressible_config: *compressible_config.key, + rent_sponsor: *rent_sponsor.key, + pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(1), + lamports_per_write, + token_account_version: TokenDataVersion::ShaFlat, + }; + + let ix = create_compressible_associated_token_account_with_bump_and_mode::( + inputs, + *associated_token_account.key, + bump, + )?; + + solana_cpi::invoke( + &ix, + &[ + payer, + associated_token_account, + system_program, + compressible_config, + rent_sponsor, + ], + ) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs index a73a23fbe3..a3706648ef 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs @@ -201,10 +201,7 @@ pub fn derive_compressed_mint_address( ) } -pub fn derive_compressed_mint_from_spl_mint( - mint: &Pubkey, - address_tree_pubkey: &Pubkey, -) -> [u8; 32] { +pub fn derive_cmint_from_spl_mint(mint: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] { light_compressed_account::address::derive_address( &mint.to_bytes(), &address_tree_pubkey.to_bytes(), diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs index 3d2f390bf6..9569f03812 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs @@ -5,45 +5,7 @@ pub use account_metas::{ get_create_compressed_mint_instruction_account_metas, CreateCompressedMintMetaConfig, }; pub use instruction::{ - create_compressed_mint, create_compressed_mint_cpi, derive_compressed_mint_address, - derive_compressed_mint_from_spl_mint, find_spl_mint_address, CreateCompressedMintInputs, - CREATE_COMPRESSED_MINT_DISCRIMINATOR, + create_compressed_mint, create_compressed_mint_cpi, create_compressed_mint_cpi_write, + derive_cmint_from_spl_mint, derive_compressed_mint_address, find_spl_mint_address, + CreateCompressedMintInputs, CREATE_COMPRESSED_MINT_DISCRIMINATOR, }; -use light_account_checks::AccountInfoTrait; -use light_sdk::cpi::CpiSigner; - -#[derive(Clone, Debug)] -pub struct CpiContextWriteAccounts<'a, T: AccountInfoTrait + Clone> { - pub mint_signer: &'a T, - pub light_system_program: &'a T, - pub fee_payer: &'a T, - pub cpi_authority_pda: &'a T, - pub cpi_context: &'a T, - pub cpi_signer: CpiSigner, -} - -impl CpiContextWriteAccounts<'_, T> { - pub fn bump(&self) -> u8 { - self.cpi_signer.bump - } - - pub fn invoking_program(&self) -> [u8; 32] { - self.cpi_signer.program_id - } - - pub fn to_account_infos(&self) -> Vec { - // The 5 accounts expected by create_compressed_mint_cpi_write: - // [mint_signer, light_system_program, fee_payer, cpi_authority_pda, cpi_context] - vec![ - self.mint_signer.clone(), - self.light_system_program.clone(), - self.fee_payer.clone(), - self.cpi_authority_pda.clone(), - self.cpi_context.clone(), - ] - } - - pub fn to_account_info_refs(&self) -> [&T; 3] { - [self.mint_signer, self.fee_payer, self.cpi_context] - } -} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs index 8a7c0c6952..d69989ef4a 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs @@ -5,7 +5,9 @@ use light_ctoken_types::{ extensions::compressible::{CompressToPubkey, CompressibleExtensionInstructionData}, }, state::TokenDataVersion, + CTokenError, }; +use solana_account_info::AccountInfo; use solana_instruction::Instruction; use solana_pubkey::Pubkey; @@ -35,7 +37,7 @@ pub struct CreateCompressibleTokenAccount { pub token_account_version: TokenDataVersion, } -pub fn create_compressible_token_account( +pub fn create_compressible_token_account_instruction( inputs: CreateCompressibleTokenAccount, ) -> Result { // Create the CompressibleExtensionInstructionData @@ -114,3 +116,56 @@ pub fn create_token_account( data, }) } + +/// Create a c-token account with signer seeds. +#[allow(clippy::too_many_arguments)] +pub fn create_ctoken_account_signed<'info>( + program_id: Pubkey, + payer: AccountInfo<'info>, + token_account: AccountInfo<'info>, + mint_account: AccountInfo<'info>, + authority: Pubkey, + signer_seeds: &[&[u8]], + ctoken_rent_sponsor: AccountInfo<'info>, + ctoken_config_account: AccountInfo<'info>, + pre_pay_num_epochs: Option, + lamports_per_write: Option, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let bump = signer_seeds[signer_seeds.len() - 1][0]; + let seeds: Vec> = signer_seeds[..signer_seeds.len() - 1] + .iter() + .map(|seed| seed.to_vec()) + .collect(); + + let params = CreateCompressibleTokenAccount { + payer: *payer.key, + account_pubkey: *token_account.key, + mint_pubkey: *mint_account.key, + owner_pubkey: authority, + compressible_config: *ctoken_config_account.key, + rent_sponsor: *ctoken_rent_sponsor.key, + pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(0), + lamports_per_write, + compress_to_account_pubkey: Some(CompressToPubkey { + bump, + program_id: program_id.to_bytes(), + seeds, + }), + token_account_version: TokenDataVersion::ShaFlat, + }; + let ix = create_compressible_token_account_instruction(params) + .map_err(|_| TokenSdkError::CTokenError(CTokenError::InvalidInstructionData))?; + + // TODO: check whether we need to pass c-token program / system program + solana_cpi::invoke_signed( + &ix, + &[ + payer, + token_account, + mint_account, + ctoken_rent_sponsor, + ctoken_config_account, + ], + &[signer_seeds], + ) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs index 5297a9d6e1..9e0e827234 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs @@ -6,7 +6,6 @@ use light_program_profiler::profile; use light_sdk::{ error::LightSdkError, instruction::{AccountMetasVec, PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig}, - token::TokenData, }; use solana_account_info::AccountInfo; use solana_instruction::{AccountMeta, Instruction}; @@ -14,6 +13,7 @@ use solana_pubkey::Pubkey; use crate::{ account2::CTokenAccount2, + compat::TokenData, error::TokenSdkError, instructions::{ transfer2::{ @@ -59,6 +59,12 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( // Process each set of indices let mut token_accounts = Vec::with_capacity(indices.len()); + // Convert packed_accounts to AccountMetas + // TODO: we may have to add conditional delegate signers for delegate + // support via CPI. + // Build signer flags in O(n) instead of scanning on every meta push + let mut signer_flags = vec![false; packed_accounts.len()]; + for idx in indices.iter() { // Create CTokenAccount2 with the source data // For decompress_full, we don't have an output tree since everything goes to the destination @@ -67,18 +73,23 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( // Set up decompress_full - decompress entire balance to destination ctoken account token_account.decompress_ctoken(idx.source.amount, idx.destination_index)?; token_accounts.push(token_account); + + let owner_idx = idx.source.owner as usize; + if owner_idx >= signer_flags.len() { + return Err(TokenSdkError::InvalidAccountData); + } + signer_flags[owner_idx] = true; } - // Convert packed_accounts to AccountMetas let mut packed_account_metas = Vec::with_capacity(packed_accounts.len()); - for info in packed_accounts.iter() { + + for (i, info) in packed_accounts.iter().enumerate() { packed_account_metas.push(AccountMeta { pubkey: *info.key, - is_signer: info.is_signer, + is_signer: info.is_signer || signer_flags[i], is_writable: info.is_writable, }); } - let (meta_config, transfer_config) = if let Some(cpi_context) = cpi_context_pubkey { let cpi_context_config = CompressedCpiContext { set_context: false, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index b5234076ee..ade4b9ee1c 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -13,6 +13,8 @@ pub mod mint_action; pub mod mint_to_compressed; pub mod transfer; pub mod transfer2; +pub mod transfer_ctoken; +pub mod transfer_interface; pub mod update_compressed_mint; pub mod withdraw_funding_pool; @@ -34,7 +36,8 @@ pub use create_associated_token_account::*; pub use create_compressed_mint::*; pub use create_spl_mint::*; pub use create_token_account::{ - create_compressible_token_account, create_token_account, CreateCompressibleTokenAccount, + create_compressible_token_account_instruction, create_ctoken_account_signed, + create_token_account, CreateCompressibleTokenAccount, }; pub use ctoken_accounts::*; pub use decompress_full::{decompress_full_ctoken_accounts_with_indices, DecompressFullIndices}; @@ -49,18 +52,13 @@ pub use mint_to_compressed::{ create_mint_to_compressed_instruction, get_mint_to_compressed_instruction_account_metas, DecompressedMintConfig, MintToCompressedInputs, MintToCompressedMetaConfig, }; +pub use transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed}; +pub use transfer_interface::{ + create_transfer_ctoken_to_spl_instruction, create_transfer_spl_to_ctoken_instruction, + transfer_interface, transfer_interface_signed, +}; pub use update_compressed_mint::{ update_compressed_mint, update_compressed_mint_cpi, UpdateCompressedMintInputs, UPDATE_COMPRESSED_MINT_DISCRIMINATOR, }; pub use withdraw_funding_pool::withdraw_funding_pool; - -/// Derive token pool information for a given mint -pub fn derive_token_pool(mint: &solana_pubkey::Pubkey, index: u8) -> mint_action::TokenPool { - let (pubkey, bump) = crate::token_pool::find_token_pool_pda_with_index(mint, index); - mint_action::TokenPool { - pubkey, - bump, - index, - } -} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs new file mode 100644 index 0000000000..c5f1b97dac --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs @@ -0,0 +1,68 @@ +use light_sdk_types::C_TOKEN_PROGRAM_ID; +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +/// Create a c-token transfer instruction. +/// +/// # Arguments +/// * `source` - Source token account +/// * `destination` - Destination token account +/// * `amount` - Amount to transfer +/// * `authority` - Authority pubkey +/// +/// # Returns +/// `Instruction` +fn create_transfer_ctoken_instruction( + source: Pubkey, + destination: Pubkey, + amount: u64, + authority: Pubkey, +) -> Instruction { + Instruction { + program_id: Pubkey::from(C_TOKEN_PROGRAM_ID), + accounts: vec![ + AccountMeta::new(source, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + ], + data: { + // TODO: check why we have 2 discriminators + let mut data = vec![3u8]; + data.push(3u8); + data.extend_from_slice(&amount.to_le_bytes()); + data + }, + } +} + +/// Transfer c-tokens +pub fn transfer_ctoken<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let ix = create_transfer_ctoken_instruction(*from.key, *to.key, amount, *authority.key); + + invoke(&ix, &[from.clone(), to.clone(), authority.clone()]) +} + +/// Transfer c-tokens CPI +pub fn transfer_ctoken_signed<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let ix = create_transfer_ctoken_instruction(*from.key, *to.key, amount, *authority.key); + + invoke_signed( + &ix, + &[from.clone(), to.clone(), authority.clone()], + signer_seeds, + ) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs new file mode 100644 index 0000000000..414d5012ff --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -0,0 +1,539 @@ +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_ctoken_types::instructions::transfer2::{Compression, MultiTokenTransferOutputData}; +use light_program_profiler::profile; +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +use super::transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed}; +use crate::{ + account2::CTokenAccount2, + error::TokenSdkError, + instructions::transfer2::{ + account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config, + Transfer2Inputs, + }, + utils::is_ctoken_account, +}; + +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn create_transfer_spl_to_ctoken_instruction( + source_spl_token_account: Pubkey, + to: Pubkey, + amount: u64, + authority: Pubkey, + mint: Pubkey, + payer: Pubkey, + token_pool_pda: Pubkey, + token_pool_pda_bump: u8, + spl_token_program: Pubkey, +) -> Result { + let packed_accounts = vec![ + // Mint (index 0) + AccountMeta::new_readonly(mint, false), + // Destination token account (index 1) + AccountMeta::new(to, false), + // Authority for compression (index 2) - signer + AccountMeta::new_readonly(authority, true), + // Source SPL token account (index 3) - writable + AccountMeta::new(source_spl_token_account, false), + // Token pool PDA (index 4) - writable + AccountMeta::new(token_pool_pda, false), + // SPL Token program (index 5) - needed for CPI + AccountMeta::new_readonly(spl_token_program, false), + ]; + + let wrap_spl_to_ctoken_account = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::compress_spl( + amount, + 0, // mint + 3, // source or recpient + 2, // authority + 4, // pool_account_index: + 0, // pool_index + token_pool_pda_bump, + )), + delegate_is_set: false, + method_used: true, + }; + + let ctoken_account = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::decompress_ctoken(amount, 0, 1)), + delegate_is_set: false, + method_used: true, + }; + + let inputs = Transfer2Inputs { + validity_proof: ValidityProof::new(None), + transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), + meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( + payer, + packed_accounts, + ), + in_lamports: None, + out_lamports: None, + token_accounts: vec![wrap_spl_to_ctoken_account, ctoken_account], + output_queue: 0, // Decompressed accounts only, no output queue needed + }; + + create_transfer2_instruction(inputs) +} + +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn create_transfer_ctoken_to_spl_instruction( + source_ctoken_account: Pubkey, + destination_spl_token_account: Pubkey, + amount: u64, + authority: Pubkey, + mint: Pubkey, + payer: Pubkey, + token_pool_pda: Pubkey, + token_pool_pda_bump: u8, + spl_token_program: Pubkey, +) -> Result { + let packed_accounts = vec![ + // Mint (index 0) + AccountMeta::new_readonly(mint, false), + // Source ctoken account (index 1) - writable + AccountMeta::new(source_ctoken_account, false), + // Destination SPL token account (index 2) - writable + AccountMeta::new(destination_spl_token_account, false), + // Authority (index 3) - signer + AccountMeta::new_readonly(authority, true), + // Token pool PDA (index 4) - writable + AccountMeta::new(token_pool_pda, false), + // SPL Token program (index 5) - needed for CPI + AccountMeta::new_readonly(spl_token_program, false), + ]; + + // First operation: compress from ctoken account to pool using compress_spl + let compress_to_pool = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::compress_ctoken( + amount, 0, // mint index + 1, // source ctoken account index + 3, // authority index + )), + delegate_is_set: false, + method_used: true, + }; + + // Second operation: decompress from pool to SPL token account using decompress_spl + let decompress_to_spl = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::decompress_spl( + amount, + 0, // mint index + 2, // destination SPL token account index + 4, // pool_account_index + 0, // pool_index (TODO: make dynamic) + token_pool_pda_bump, + )), + delegate_is_set: false, + method_used: true, + }; + + let inputs = Transfer2Inputs { + validity_proof: ValidityProof::new(None), + transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), + meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( + payer, + packed_accounts, + ), + in_lamports: None, + out_lamports: None, + token_accounts: vec![compress_to_pool, decompress_to_spl], + output_queue: 0, // Decompressed accounts only, no output queue needed + }; + + create_transfer2_instruction(inputs) +} + +/// Transfer SPL tokens to compressed tokens +#[allow(clippy::too_many_arguments)] +pub fn transfer_spl_to_ctoken<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_spl_token_account: AccountInfo<'info>, + destination_ctoken_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let instruction = create_transfer_spl_to_ctoken_instruction( + *source_spl_token_account.key, + *destination_ctoken_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // let mut account_infos = remaining_accounts.to_vec(); + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_ctoken_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_spl_token_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke(&instruction, &account_infos)?; + Ok(()) +} + +// TODO: must test this. +/// Transfer SPL tokens to compressed tokens via CPI signer +#[allow(clippy::too_many_arguments)] +pub fn transfer_spl_to_ctoken_signed<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_spl_token_account: AccountInfo<'info>, + destination_ctoken_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let instruction = create_transfer_spl_to_ctoken_instruction( + *source_spl_token_account.key, + *destination_ctoken_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_ctoken_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_spl_token_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke_signed(&instruction, &account_infos, signer_seeds)?; + Ok(()) +} + +// TODO: TEST. +/// Transfer compressed tokens to SPL tokens +#[allow(clippy::too_many_arguments)] +pub fn transfer_ctoken_to_spl<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_ctoken_account: AccountInfo<'info>, + destination_spl_token_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let instruction = create_transfer_ctoken_to_spl_instruction( + *source_ctoken_account.key, + *destination_spl_token_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_spl_token_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_ctoken_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke(&instruction, &account_infos)?; + Ok(()) +} + +/// Transfer compressed tokens to SPL tokens via CPI signer +#[allow(clippy::too_many_arguments)] +pub fn transfer_ctoken_to_spl_signed<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_ctoken_account: AccountInfo<'info>, + destination_spl_token_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let instruction = create_transfer_ctoken_to_spl_instruction( + *source_ctoken_account.key, + *destination_spl_token_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_spl_token_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_ctoken_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke_signed(&instruction, &account_infos, signer_seeds)?; + Ok(()) +} + +/// Unified transfer interface for ctoken<->ctoken and ctoken<->spl transfers +/// +/// # Arguments +/// * `source_account` - Source token account (can be ctoken or SPL) +/// * `destination_account` - Destination token account (can be ctoken or SPL) +/// * `authority` - Authority for the transfer (must be signer) +/// * `amount` - Amount to transfer +/// * `payer` - Payer for the transaction +/// * `compressed_token_program_authority` - Compressed token program authority +/// * `mint` - Optional mint account (required for SPL<->ctoken transfers) +/// * `spl_token_program` - Optional SPL token program (required for SPL<->ctoken transfers) +/// * `compressed_token_pool_pda` - Optional token pool PDA (required for SPL<->ctoken transfers) +/// * `compressed_token_pool_pda_bump` - Optional bump seed for token pool PDA +/// +/// # Errors +/// * `SplBridgeConfigRequired` - If transferring to/from SPL without required accounts +/// * `UseRegularSplTransfer` - If both source and destination are SPL accounts +/// * `CannotDetermineAccountType` - If account type cannot be determined +#[allow(clippy::too_many_arguments)] +pub fn transfer_interface<'info>( + source_account: &AccountInfo<'info>, + destination_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + payer: &AccountInfo<'info>, + compressed_token_program_authority: &AccountInfo<'info>, + mint: Option<&AccountInfo<'info>>, + spl_token_program: Option<&AccountInfo<'info>>, + compressed_token_pool_pda: Option<&AccountInfo<'info>>, + compressed_token_pool_pda_bump: Option, +) -> Result<(), ProgramError> { + let source_is_ctoken = + is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?; + let dest_is_ctoken = + is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?; + + match (source_is_ctoken, dest_is_ctoken) { + (true, true) => transfer_ctoken(source_account, destination_account, authority, amount), + + (true, false) => { + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_ctoken_to_spl( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + ) + } + + (false, true) => { + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_spl_to_ctoken( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + ) + } + + // spl -> spl: Not supported + (false, false) => Err(ProgramError::Custom( + TokenSdkError::UseRegularSplTransfer.into(), + )), + } +} + +/// Unified transfer interface with CPI +#[allow(clippy::too_many_arguments)] +pub fn transfer_interface_signed<'info>( + source_account: &AccountInfo<'info>, + destination_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + payer: &AccountInfo<'info>, + compressed_token_program_authority: &AccountInfo<'info>, + mint: Option<&AccountInfo<'info>>, + spl_token_program: Option<&AccountInfo<'info>>, + compressed_token_pool_pda: Option<&AccountInfo<'info>>, + compressed_token_pool_pda_bump: Option, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + // Determine account types + let source_is_ctoken = + is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?; + let dest_is_ctoken = + is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?; + + match (source_is_ctoken, dest_is_ctoken) { + // ctoken -> ctoken: Direct transfer (bridge accounts not needed) + (true, true) => transfer_ctoken_signed( + source_account, + destination_account, + authority, + amount, + signer_seeds, + ), + + // ctoken -> spl: Requires bridge accounts + (true, false) => { + // Validate all required accounts are provided + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_ctoken_to_spl_signed( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + signer_seeds, + ) + } + + // spl -> ctoken: Requires bridge accounts + (false, true) => { + // Validate all required accounts are provided + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_spl_to_ctoken_signed( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + signer_seeds, + ) + } + + // spl -> spl: Not supported + (false, false) => Err(ProgramError::Custom( + TokenSdkError::UseRegularSplTransfer.into(), + )), + } +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs b/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs index 8c574b1c12..480e42a421 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs @@ -15,7 +15,6 @@ pub struct UpdateCompressedMintMetaConfig { } /// Generates account metas for the update compressed mint instruction -/// Following the same pattern as other compressed token instructions pub fn get_update_compressed_mint_instruction_account_metas( config: UpdateCompressedMintMetaConfig, ) -> Vec { diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 45ac38becd..c38cdd1255 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -1,7 +1,9 @@ pub mod account; pub mod account2; +pub mod ctoken; pub mod error; pub mod instructions; +pub mod pack; pub mod token_metadata_ui; pub mod token_pool; pub mod utils; @@ -11,4 +13,10 @@ pub mod utils; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +// Re-export pub use light_compressed_token_types::*; +pub use pack::{compat, Pack, Unpack}; +pub use utils::{ + account_meta_from_account_info, is_ctoken_account, AccountInfoToCompress, + PackedCompressedTokenDataWithContext, +}; diff --git a/sdk-libs/compressed-token-sdk/src/pack.rs b/sdk-libs/compressed-token-sdk/src/pack.rs new file mode 100644 index 0000000000..60eeab2330 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/pack.rs @@ -0,0 +1,329 @@ +//! Pack implementation for TokenData types for c-tokens. +use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +pub use light_ctoken_types::state::TokenData; +use light_ctoken_types::state::TokenDataVersion; +use light_sdk::{ + instruction::PackedAccounts, + light_hasher::{sha256::Sha256BE, HasherError}, +}; +use solana_account_info::AccountInfo; +use solana_program_error::ProgramError; + +use crate::{AnchorDeserialize, AnchorSerialize}; + +// We define the traits here to circumvent the orphan rule. +pub trait Pack { + type Packed; + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; +} +pub trait Unpack { + type Unpacked; + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result; +} + +impl Pack for TokenData { + type Packed = light_ctoken_types::instructions::transfer2::MultiTokenTransferOutputData; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + Self::Packed { + owner: remaining_accounts.insert_or_get(self.owner.to_bytes().into()), + mint: remaining_accounts.insert_or_get_read_only(self.mint.to_bytes().into()), + amount: self.amount, + has_delegate: self.delegate.is_some(), + delegate: if let Some(delegate) = self.delegate { + remaining_accounts.insert_or_get(delegate.to_bytes().into()) + } else { + 0 + }, + version: TokenDataVersion::ShaFlat as u8, + } + } +} + +impl Unpack for TokenData { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +/// Solana-compatible token types using `solana_pubkey::Pubkey` +pub mod compat { + use solana_pubkey::Pubkey; + + use super::*; + + #[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] + #[repr(u8)] + pub enum AccountState { + #[default] + Initialized = 0, + Frozen = 1, + } + + impl From for light_ctoken_types::state::CompressedTokenAccountState { + fn from(state: AccountState) -> Self { + match state { + AccountState::Initialized => { + light_ctoken_types::state::CompressedTokenAccountState::Initialized + } + AccountState::Frozen => { + light_ctoken_types::state::CompressedTokenAccountState::Frozen + } + } + } + } + + impl TryFrom for AccountState { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(AccountState::Initialized), + 1 => Ok(AccountState::Frozen), + _ => Err(ProgramError::InvalidAccountData), + } + } + } + + /// TokenData using standard Solana pubkeys. + /// + /// For zero-copy operations, use [`TokenData`](crate::types::TokenData) from the crate root. + #[derive(Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Clone, Default)] + pub struct TokenData { + /// The mint associated with this account + pub mint: Pubkey, + /// The owner of this account + pub owner: Pubkey, + /// The amount of tokens this account holds + pub amount: u64, + /// Optional delegate authorized to transfer tokens + pub delegate: Option, + /// The account's state + pub state: AccountState, + /// Placeholder for TokenExtension tlv data (unimplemented) + pub tlv: Option>, + } + + impl TokenData { + /// TokenDataVersion 3 + /// CompressedAccount Discriminator [0,0,0,0,0,0,0,4] + #[inline(always)] + pub fn hash_sha_flat(&self) -> Result<[u8; 32], HasherError> { + use light_sdk::light_hasher::Hasher; + let bytes = self.try_to_vec().map_err(|_| HasherError::BorshError)?; + Sha256BE::hash(bytes.as_slice()) + } + } + + /// TokenData with merkle context for verification + #[derive(Debug, Clone, PartialEq)] + pub struct TokenDataWithMerkleContext { + pub token_data: TokenData, + pub compressed_account: CompressedAccountWithMerkleContext, + } + + impl TokenDataWithMerkleContext { + /// Only works for sha flat hash + pub fn hash(&self) -> Result<[u8; 32], HasherError> { + if let Some(data) = self.compressed_account.compressed_account.data.as_ref() { + match data.discriminator { + [0, 0, 0, 0, 0, 0, 0, 4] => self.token_data.hash_sha_flat(), + _ => Err(HasherError::EmptyInput), + } + } else { + Err(HasherError::EmptyInput) + } + } + } + + impl From for crate::pack::TokenData { + fn from(data: TokenData) -> Self { + use light_ctoken_types::state::CompressedTokenAccountState; + + Self { + mint: data.mint.to_bytes().into(), + owner: data.owner.to_bytes().into(), + amount: data.amount, + delegate: data.delegate.map(|d| d.to_bytes().into()), + state: match data.state { + AccountState::Initialized => CompressedTokenAccountState::Initialized as u8, + AccountState::Frozen => CompressedTokenAccountState::Frozen as u8, + }, + tlv: data.tlv, + } + } + } + + impl From for TokenData { + fn from(data: crate::pack::TokenData) -> Self { + Self { + mint: Pubkey::new_from_array(data.mint.to_bytes()), + owner: Pubkey::new_from_array(data.owner.to_bytes()), + amount: data.amount, + delegate: data.delegate.map(|d| Pubkey::new_from_array(d.to_bytes())), + state: AccountState::try_from(data.state).unwrap_or(AccountState::Initialized), + tlv: data.tlv, + } + } + } + + impl Pack for TokenData { + type Packed = InputTokenDataCompressible; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + InputTokenDataCompressible { + owner: remaining_accounts.insert_or_get(self.owner), + mint: remaining_accounts.insert_or_get_read_only(self.mint), + amount: self.amount, + has_delegate: self.delegate.is_some(), + delegate: if let Some(delegate) = self.delegate { + remaining_accounts.insert_or_get(delegate) + } else { + 0 + }, + version: TokenDataVersion::ShaFlat as u8, + } + } + } + + impl Unpack for TokenData { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + + impl Unpack for InputTokenDataCompressible { + type Unpacked = TokenData; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenData { + owner: *remaining_accounts + .get(self.owner as usize) + .ok_or(ProgramError::InvalidAccountData)? + .key, + amount: self.amount, + delegate: if self.has_delegate { + Some( + *remaining_accounts + .get(self.delegate as usize) + .ok_or(ProgramError::InvalidAccountData)? + .key, + ) + } else { + None + }, + mint: *remaining_accounts + .get(self.mint as usize) + .ok_or(ProgramError::InvalidAccountData)? + .key, + state: AccountState::Initialized, + tlv: None, + }) + } + } + + /// Wrapper for token data with variant information + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] + pub struct TokenDataWithVariant { + pub variant: V, + pub token_data: TokenData, + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] + pub struct PackedCTokenDataWithVariant { + pub variant: V, + pub token_data: InputTokenDataCompressible, + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] + pub struct CTokenDataWithVariant { + pub variant: V, + pub token_data: TokenData, + } + + impl Pack for CTokenDataWithVariant + where + V: AnchorSerialize + Clone + std::fmt::Debug, + { + type Packed = PackedCTokenDataWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedCTokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.pack(remaining_accounts), + } + } + } + + impl Unpack for CTokenDataWithVariant + where + V: Clone, + { + type Unpacked = TokenDataWithVariant; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.unpack(remaining_accounts)?, + }) + } + } + + impl Pack for TokenDataWithVariant + where + V: AnchorSerialize + Clone + std::fmt::Debug, + { + type Packed = PackedCTokenDataWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedCTokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.pack(remaining_accounts), + } + } + } + + impl Unpack for PackedCTokenDataWithVariant + where + V: Clone, + { + type Unpacked = TokenDataWithVariant; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.unpack(remaining_accounts)?, + }) + } + } + + // TODO: remove aliases in separate PR + pub type InputTokenDataCompressible = + light_ctoken_types::instructions::transfer2::MultiTokenTransferOutputData; + pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; + pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; + pub type CTokenData = CTokenDataWithVariant; + pub type PackedCTokenData = PackedCTokenDataWithVariant; +} diff --git a/sdk-libs/compressed-token-sdk/src/token_pool.rs b/sdk-libs/compressed-token-sdk/src/token_pool.rs index 605706100d..e354eaf268 100644 --- a/sdk-libs/compressed-token-sdk/src/token_pool.rs +++ b/sdk-libs/compressed-token-sdk/src/token_pool.rs @@ -2,10 +2,14 @@ use light_compressed_token_types::constants::POOL_SEED; use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID; use solana_pubkey::Pubkey; +use crate::instructions::mint_action::TokenPool; + +/// Derive the token pool pda for a given mint pub fn get_token_pool_pda(mint: &Pubkey) -> Pubkey { get_token_pool_pda_with_index(mint, 0) } +/// Find the token pool pda for a given mint and index pub fn find_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> (Pubkey, u8) { let seeds = &[POOL_SEED, mint.as_ref(), &[token_pool_index]]; let seeds = if token_pool_index == 0 { @@ -16,6 +20,17 @@ pub fn find_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> (P Pubkey::find_program_address(seeds, &Pubkey::from(COMPRESSED_TOKEN_PROGRAM_ID)) } +/// Get the token pool pda for a given mint and index pub fn get_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> Pubkey { find_token_pool_pda_with_index(mint, token_pool_index).0 } + +/// Derive token pool information for a given mint +pub fn derive_token_pool(mint: &solana_pubkey::Pubkey, index: u8) -> TokenPool { + let (pubkey, bump) = find_token_pool_pda_with_index(mint, index); + TokenPool { + pubkey, + bump, + index, + } +} diff --git a/sdk-libs/compressed-token-sdk/src/utils.rs b/sdk-libs/compressed-token-sdk/src/utils.rs index b8d3050649..220be9a846 100644 --- a/sdk-libs/compressed-token-sdk/src/utils.rs +++ b/sdk-libs/compressed-token-sdk/src/utils.rs @@ -1,18 +1,60 @@ +use light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext; +use light_sdk_types::C_TOKEN_PROGRAM_ID; use solana_account_info::AccountInfo; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; use spl_pod::bytemuck::pod_from_bytes; use spl_token_2022::pod::PodAccount; -use crate::error::TokenSdkError; +use crate::{error::TokenSdkError, AnchorDeserialize, AnchorSerialize}; -/// Get token account balance from account info pub fn get_token_account_balance(token_account_info: &AccountInfo) -> Result { let token_account_data = token_account_info .try_borrow_data() .map_err(|_| TokenSdkError::AccountBorrowFailed)?; - // Use zero-copy PodAccount to access the token account let pod_account = pod_from_bytes::(&token_account_data) .map_err(|_| TokenSdkError::InvalidAccountData)?; Ok(pod_account.amount.into()) } + +pub fn is_ctoken_account(account_info: &AccountInfo) -> Result { + let ctoken_program_id = Pubkey::from(C_TOKEN_PROGRAM_ID); + + if account_info.owner == &ctoken_program_id { + return Ok(true); + } + + let token_22 = spl_token_2022::ID; + let spl_token = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + + if account_info.owner == &token_22 || account_info.owner == &spl_token { + return Ok(false); + } + + Err(TokenSdkError::CannotDetermineAccountType) +} + +pub const CLOSE_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 9; + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedCompressedTokenDataWithContext { + pub mint: u8, + pub source_or_recipient_token_account: u8, + pub multi_input_token_data_with_context: MultiInputTokenDataWithContext, +} + +pub fn account_meta_from_account_info(account_info: &AccountInfo) -> AccountMeta { + AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct AccountInfoToCompress<'info> { + pub account_info: AccountInfo<'info>, + pub signer_seeds: Vec>, +} diff --git a/sdk-libs/compressed-token-sdk/tests/pack_test.rs b/sdk-libs/compressed-token-sdk/tests/pack_test.rs new file mode 100644 index 0000000000..67f7502bc4 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/tests/pack_test.rs @@ -0,0 +1,121 @@ +#[cfg(test)] +mod tests { + + use light_compressed_token_sdk::{ + compat::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + Pack, + }; + use light_sdk::instruction::PackedAccounts; + use solana_pubkey::Pubkey; + + #[test] + fn test_token_data_packing() { + let mut remaining_accounts = PackedAccounts::default(); + + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + + let token_data = TokenData { + owner, + mint, + amount: 1000, + delegate: Some(delegate), + state: Default::default(), + tlv: None, + }; + + // Pack the token data + let packed = token_data.pack(&mut remaining_accounts); + + // Verify the packed data + assert_eq!(packed.owner, 0); // First pubkey gets index 0 + assert_eq!(packed.mint, 1); // Second pubkey gets index 1 + assert_eq!(packed.delegate, 2); // Third pubkey gets index 2 + assert_eq!(packed.amount, 1000); + assert!(packed.has_delegate); + assert_eq!(packed.version, 3); // TokenDataVersion::ShaFlat + + // Verify remaining_accounts contains the pubkeys + let pubkeys = remaining_accounts.packed_pubkeys(); + assert_eq!(pubkeys[0], owner); + assert_eq!(pubkeys[1], mint); + assert_eq!(pubkeys[2], delegate); + } + + #[test] + fn test_token_data_with_variant_packing() { + use anchor_lang::{AnchorDeserialize, AnchorSerialize}; + + #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)] + enum MyVariant { + TypeA = 0, + TypeB = 1, + } + + let mut remaining_accounts = PackedAccounts::default(); + + let token_with_variant = TokenDataWithVariant { + variant: MyVariant::TypeA, + token_data: TokenData { + owner: Pubkey::new_unique(), + mint: Pubkey::new_unique(), + amount: 500, + delegate: None, + state: Default::default(), + tlv: None, + }, + }; + + // Pack the wrapper + let packed: PackedCTokenDataWithVariant = + token_with_variant.pack(&mut remaining_accounts); + + // Verify variant is unchanged + assert!(matches!(packed.variant, MyVariant::TypeA)); + + // Verify token data is packed + assert_eq!(packed.token_data.owner, 0); + assert_eq!(packed.token_data.mint, 1); + assert_eq!(packed.token_data.amount, 500); + assert!(!packed.token_data.has_delegate); + } + + #[test] + fn test_deduplication_in_packing() { + let mut remaining_accounts = PackedAccounts::default(); + + let shared_owner = Pubkey::new_unique(); + let shared_mint = Pubkey::new_unique(); + + let token1 = TokenData { + owner: shared_owner, + mint: shared_mint, + amount: 100, + delegate: None, + state: Default::default(), + tlv: None, + }; + + let token2 = TokenData { + owner: shared_owner, // Same owner + mint: shared_mint, // Same mint + amount: 200, + delegate: None, + state: Default::default(), + tlv: None, + }; + + // Pack both tokens + let packed1 = token1.pack(&mut remaining_accounts); + let packed2 = token2.pack(&mut remaining_accounts); + + // Both should reference the same indices + assert_eq!(packed1.owner, packed2.owner); + assert_eq!(packed1.mint, packed2.mint); + + // Only 2 unique pubkeys should be stored + let pubkeys = remaining_accounts.packed_pubkeys(); + assert_eq!(pubkeys.len(), 2); + } +} diff --git a/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs b/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs index 4716848078..5c0b7347da 100644 --- a/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs +++ b/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs @@ -1,6 +1,6 @@ use crate::{AnchorDeserialize, AnchorSerialize}; -/// Authority types for compressed mint updates, following SPL Token-2022 pattern +/// Authority types for compressed mint updates #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] pub enum CompressedMintAuthorityType { diff --git a/sdk-libs/compressed-token-types/src/lib.rs b/sdk-libs/compressed-token-types/src/lib.rs index ee7e1d7813..16c3c4a72a 100644 --- a/sdk-libs/compressed-token-types/src/lib.rs +++ b/sdk-libs/compressed-token-types/src/lib.rs @@ -2,7 +2,6 @@ pub mod account_infos; pub mod constants; pub mod error; pub mod instruction; -pub mod token_data; // Conditional anchor re-exports #[cfg(feature = "anchor")] @@ -11,4 +10,3 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use constants::*; pub use instruction::*; -pub use token_data::*; diff --git a/sdk-libs/compressed-token-types/src/token_data.rs b/sdk-libs/compressed-token-types/src/token_data.rs deleted file mode 100644 index b126d6582f..0000000000 --- a/sdk-libs/compressed-token-types/src/token_data.rs +++ /dev/null @@ -1,25 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[repr(u8)] -pub enum AccountState { - Initialized, - Frozen, -} - -#[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Clone)] -pub struct TokenData { - /// The mint associated with this account - pub mint: [u8; 32], - /// The owner of this account. - pub owner: [u8; 32], - /// The amount of tokens this account holds. - pub amount: u64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: Option<[u8; 32]>, - /// The account's state - pub state: AccountState, - /// Placeholder for TokenExtension tlv data (unimplemented) - pub tlv: Option>, -} diff --git a/sdk-libs/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml new file mode 100644 index 0000000000..c368d3db35 --- /dev/null +++ b/sdk-libs/compressible-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "light-compressible-client" +version = "0.13.1" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/lightprotocol/light-protocol" +description = "Client instruction builders for Light Protocol compressible accounts" + +[features] +anchor = ["anchor-lang", "light-sdk/anchor"] + +[dependencies] +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-account = { workspace = true } + +light-client = { workspace = true, features = ["v2"] } +light-sdk = { workspace = true, features = ["v2", "cpi-context"] } + +anchor-lang = { workspace = true, features = ["idl-build"], optional = true } +borsh = { workspace = true } + +thiserror = { workspace = true } \ No newline at end of file diff --git a/sdk-libs/compressible-client/src/get_compressible_account.rs b/sdk-libs/compressible-client/src/get_compressible_account.rs new file mode 100644 index 0000000000..cd056e5bbd --- /dev/null +++ b/sdk-libs/compressible-client/src/get_compressible_account.rs @@ -0,0 +1,162 @@ +use light_client::{ + indexer::{Indexer, TreeInfo}, + rpc::{Rpc, RpcError}, +}; +use light_sdk::address::v1::derive_address; +use solana_account::Account; +use solana_pubkey::Pubkey; +use thiserror::Error; + +#[cfg(not(feature = "anchor"))] +use crate::AnchorDeserialize; + +#[derive(Debug, Error)] +pub enum CompressibleAccountError { + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), + + #[error("Indexer error: {0}")] + Indexer(#[from] light_client::indexer::IndexerError), + + #[error("Compressed account has no data")] + NoData, + + #[cfg(feature = "anchor")] + #[error("Anchor deserialization error: {0}")] + AnchorDeserialization(#[from] anchor_lang::error::Error), + + #[error("Borsh deserialization error: {0}")] + BorshDeserialization(#[from] std::io::Error), +} + +#[derive(Debug, Clone)] +pub struct MerkleContext { + pub tree_info: TreeInfo, + pub hash: [u8; 32], + pub leaf_index: u32, + pub prove_by_index: bool, +} + +#[derive(Debug, Clone)] +pub struct AccountInfoInterface { + pub account_info: Account, + pub is_compressed: bool, + pub merkle_context: Option, +} + +/// Get account info with unified interface. +/// +/// If the account is cold, returns additional metadata for loading it to hot +/// state. +pub async fn get_account_info_interface( + address: &Pubkey, + program_id: &Pubkey, + address_tree_info: &TreeInfo, + rpc: &mut R, +) -> Result, CompressibleAccountError> +where + R: Rpc + Indexer, +{ + let (compressed_address, _) = + derive_address(&[&address.to_bytes()], &address_tree_info.tree, program_id); + + // TODO: concurrency + let onchain_account = rpc.get_account(*address).await?; + let compressed_account = rpc + .get_compressed_account(compressed_address, None) + .await? + .value; + + if let Some(onchain) = onchain_account { + let merkle_context = compressed_account.as_ref().map(|ca| MerkleContext { + tree_info: ca.tree_info, + hash: ca.hash, + leaf_index: ca.leaf_index, + prove_by_index: ca.prove_by_index, + }); + + return Ok(Some(AccountInfoInterface { + account_info: onchain, + is_compressed: false, + merkle_context, + })); + } + + if let Some(ca) = compressed_account { + if let Some(data) = ca.data.as_ref() { + if !data.data.is_empty() { + let mut account_data = + Vec::with_capacity(data.discriminator.len() + data.data.len()); + account_data.extend_from_slice(&data.discriminator); + account_data.extend_from_slice(&data.data); + + let account_info = Account { + lamports: ca.lamports, + data: account_data, + owner: ca.owner, + executable: false, + // TODO: consider 0. + rent_epoch: u64::MAX, + }; + + return Ok(Some(AccountInfoInterface { + account_info, + is_compressed: true, + merkle_context: Some(MerkleContext { + tree_info: ca.tree_info, + hash: ca.hash, + leaf_index: ca.leaf_index, + prove_by_index: ca.prove_by_index, + }), + })); + } + } + } + + Ok(None) +} + +#[cfg(feature = "anchor")] +#[allow(clippy::result_large_err)] +pub fn deserialize_account(account: &AccountInfoInterface) -> Result +where + T: anchor_lang::AccountDeserialize, +{ + let data = &account.account_info.data; + T::try_deserialize(&mut &data[..]).map_err(CompressibleAccountError::AnchorDeserialization) +} + +// TODO: add discriminator check. +#[cfg(not(feature = "anchor"))] +#[allow(clippy::result_large_err)] +pub fn deserialize_account(account: &AccountInfoInterface) -> Result +where + T: AnchorDeserialize, +{ + let data = &account.account_info.data; + if data.len() < 8 { + return Err(CompressibleAccountError::BorshDeserialization( + std::io::Error::new(std::io::ErrorKind::InvalidData, "Account data too short"), + )); + } + T::deserialize(&mut &data[8..]).map_err(CompressibleAccountError::BorshDeserialization) +} + +#[cfg(feature = "anchor")] +#[allow(clippy::result_large_err)] +pub async fn get_anchor_account( + address: &Pubkey, + program_id: &Pubkey, + address_tree_info: &TreeInfo, + rpc: &mut R, +) -> Result +where + T: anchor_lang::AccountDeserialize, + R: Rpc + Indexer, +{ + let account_interface = get_account_info_interface(address, program_id, address_tree_info, rpc) + .await? + .ok_or(CompressibleAccountError::NoData)?; + + deserialize_account::(&account_interface) +} diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs new file mode 100644 index 0000000000..7a679d74b1 --- /dev/null +++ b/sdk-libs/compressible-client/src/lib.rs @@ -0,0 +1,393 @@ +pub mod get_compressible_account; + +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +use light_client::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext}; +pub use light_sdk::compressible::config::CompressibleConfig; +use light_sdk::{ + compressible::{compression_info::CompressedAccountData, Pack}, + constants::C_TOKEN_PROGRAM_ID, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + SystemAccountMetaConfig, ValidityProof, + }, +}; +use solana_account::Account; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeCompressionConfigData { + pub compression_delay: u32, + pub rent_sponsor: Pubkey, + pub address_space: Vec, + pub config_bump: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct UpdateCompressionConfigData { + pub new_compression_delay: Option, + pub new_rent_sponsor: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, +} + +/// T is the packed type from calling .pack() on the original type +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct DecompressMultipleAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec>, + pub system_accounts_offset: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CompressAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec, + pub signer_seeds: Vec>>, + pub system_accounts_offset: u8, +} + +/// Instruction builders for compressible accounts +pub mod compressible_instruction { + use super::*; + + /// SHA256("global:initialize_compression_config")[..8] + pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [133, 228, 12, 169, 56, 76, 222, 61]; + /// SHA256("global:update_compression_config")[..8] + pub const UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [135, 215, 243, 81, 163, 146, 33, 70]; + /// SHA256("global:decompress_accounts_idempotent")[..8] + pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [114, 67, 61, 123, 234, 31, 1, 112]; + /// SHA256("global:compress_token_account_ctoken_signer")[..8] + pub const COMPRESS_TOKEN_ACCOUNT_CTOKEN_SIGNER_DISCRIMINATOR: [u8; 8] = + [243, 154, 172, 243, 44, 214, 139, 73]; + /// SHA256("global:compress_accounts_idempotent")[..8] + pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [89, 130, 165, 88, 12, 207, 178, 185]; + + /// Creates an initialize_compression_config instruction + #[allow(clippy::too_many_arguments)] + pub fn initialize_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + payer: &Pubkey, + authority: &Pubkey, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, + config_bump: Option, + ) -> Instruction { + let config_bump = config_bump.unwrap_or(0); + let (config_pda, _) = CompressibleConfig::derive_pda(program_id, config_bump); + + // Get program data account for BPF Loader Upgradeable + let bpf_loader_upgradeable_id = + solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"); + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable_id); + + let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + let accounts = vec![ + AccountMeta::new(*payer, true), // payer + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(program_data_pda, false), // program_data + AccountMeta::new_readonly(*authority, true), // authority + AccountMeta::new_readonly(system_program_id, false), // system_program + ]; + + let instruction_data = InitializeCompressionConfigData { + compression_delay, + rent_sponsor, + address_space, + config_bump, + }; + + // Prepend discriminator to serialized data, following Solana SDK pattern + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Instruction { + program_id: *program_id, + accounts, + data, + } + } + + /// Updates compression config + pub fn update_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + authority: &Pubkey, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Instruction { + let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0); + + let accounts = vec![ + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(*authority, true), // authority + ]; + + let instruction_data = UpdateCompressionConfigData { + new_compression_delay, + new_rent_sponsor, + new_address_space, + new_update_authority, + }; + + // Prepend discriminator to serialized data, following Solana SDK pattern + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + let mut data = Vec::with_capacity(discriminator.len() + serialized_data.len()); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Instruction { + program_id: *program_id, + accounts, + data, + } + } + + /// Builds decompress_accounts_idempotent instruction + #[allow(clippy::too_many_arguments)] + pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + decompressed_account_addresses: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, + ) -> Result> + where + T: Pack + Clone + std::fmt::Debug, + { + let mut remaining_accounts = PackedAccounts::default(); + + let mut has_tokens = false; + let mut has_pdas = false; + for (compressed_account, _) in compressed_accounts.iter() { + if compressed_account.owner == C_TOKEN_PROGRAM_ID.into() { + has_tokens = true; + } else { + has_pdas = true; + } + if has_tokens && has_pdas { + break; + } + } + if !has_tokens && !has_pdas { + return Err("No tokens or PDAs found in compressed accounts".into()); + }; + if decompressed_account_addresses.len() != compressed_accounts.len() { + return Err("PDA accounts and compressed accounts must have the same length".into()); + } + + // pack cpi_context_account if required. + if has_pdas && has_tokens { + let cpi_context_of_first_input = + compressed_accounts[0].0.tree_info.cpi_context.unwrap(); + let system_config = SystemAccountMetaConfig::new_with_cpi_context( + *program_id, + cpi_context_of_first_input, + ); + remaining_accounts.add_system_accounts_v2(system_config)?; + } else { + let system_config = SystemAccountMetaConfig::new(*program_id); + remaining_accounts.add_system_accounts_v2(system_config)?; + } + + // pack output queue + let output_state_tree_index = + remaining_accounts.insert_or_get(output_state_tree_info.queue); + + // pack tree infos + let packed_tree_infos = + validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + let mut accounts = program_account_metas.to_vec(); + + // pack account data + let typed_compressed_accounts: Vec> = compressed_accounts + .iter() + .map(|(compressed_account, data)| { + let queue_index = + remaining_accounts.insert_or_get(compressed_account.tree_info.queue); + // create compressed_account_meta + let compressed_meta = CompressedAccountMetaNoLamportsNoAddress { + tree_info: packed_tree_infos + .state_trees + .as_ref() + .unwrap() + .packed_tree_infos + .iter() + .find(|pti| { + pti.queue_pubkey_index == queue_index + && pti.leaf_index == compressed_account.leaf_index + }) + .copied() + .ok_or( + "Matching PackedStateTreeInfo (queue_pubkey_index + leaf_index) not found", + )?, + output_state_tree_index, + }; + + let packed_data = data.pack(&mut remaining_accounts); + Ok(CompressedAccountData { + meta: compressed_meta, + data: packed_data, + }) + }) + .collect::, Box>>()?; + + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + for account in decompressed_account_addresses { + accounts.push(AccountMeta::new(*account, false)); + } + + let instruction_data = DecompressMultipleAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: typed_compressed_accounts, + system_accounts_offset: system_accounts_offset as u8, + }; + + let serialized_data = instruction_data.try_to_vec()?; + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } + + /// Builds compress_accounts_idempotent instruction for PDAs and token accounts + #[allow(clippy::too_many_arguments)] + pub fn compress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + account_pubkeys: &[Pubkey], + accounts_to_compress: &[Account], + program_account_metas: &[AccountMeta], + signer_seeds: Vec>>, + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, + ) -> Result> { + if account_pubkeys.len() != accounts_to_compress.len() { + return Err("Accounts pubkeys length must match accounts length".into()); + } + println!( + "compress_accounts_idempotent - account_pubkeys: {:?}", + account_pubkeys + ); + // Sanity checks. + if !signer_seeds.is_empty() && signer_seeds.len() != accounts_to_compress.len() { + return Err("Signer seeds length must match accounts length or be empty".into()); + } + for (i, account) in account_pubkeys.iter().enumerate() { + if !signer_seeds.is_empty() { + let seeds = &signer_seeds[i]; + if !seeds.is_empty() { + let derived = Pubkey::create_program_address( + &seeds.iter().map(|v| v.as_slice()).collect::>(), + program_id, + ); + match derived { + Ok(derived_pubkey) => { + if derived_pubkey != *account { + return Err(format!( + "Derived PDA does not match account_to_compress at index {}: expected {}, got {:?}", + i, + account, + derived_pubkey + ).into()); + } + } + Err(e) => { + return Err(format!( + "Failed to derive PDA for account_to_compress at index {}: {}", + i, e + ) + .into()); + } + } + } + } + } + + let mut remaining_accounts = PackedAccounts::default(); + + let system_config = SystemAccountMetaConfig::new(*program_id); + remaining_accounts.add_system_accounts_v2(system_config)?; + + let output_state_tree_index = + remaining_accounts.insert_or_get(output_state_tree_info.queue); + + let packed_tree_infos = + validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + let mut compressed_account_metas_no_lamports_no_address = Vec::new(); + + for packed_tree_info in packed_tree_infos + .state_trees + .as_ref() + .unwrap() + .packed_tree_infos + .iter() + { + compressed_account_metas_no_lamports_no_address.push( + CompressedAccountMetaNoLamportsNoAddress { + tree_info: *packed_tree_info, + output_state_tree_index, + }, + ); + } + + let mut accounts = program_account_metas.to_vec(); + + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + for account in account_pubkeys { + accounts.push(AccountMeta::new(*account, false)); + } + + let instruction_data = CompressAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: compressed_account_metas_no_lamports_no_address, + signer_seeds, + system_accounts_offset: system_accounts_offset as u8, + }; + + let serialized_data = instruction_data.try_to_vec()?; + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } +} + +pub use compressible_instruction as CompressibleInstruction; diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 52ff204f02..a4e0a32279 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [features] default = [] -devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-compressed-token-sdk", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] +devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] v2 = ["light-client/v2"] [dependencies] @@ -20,7 +20,7 @@ light-concurrent-merkle-tree = { workspace = true, optional = true } light-hasher = { workspace = true, features = ["poseidon", "sha256", "keccak", "std"] } light-ctoken-types = { workspace = true, optional = true } light-compressible = { workspace = true, optional = true } -light-compressed-token-sdk = { workspace = true, optional = true } +light-compressed-token-sdk = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor", "poseidon"] } light-batched-merkle-tree = { workspace = true, features = ["test-only"], optional = true } light-event = { workspace = true } @@ -30,6 +30,8 @@ light-prover-client = { workspace = true } light-zero-copy = { workspace = true } litesvm = { workspace = true } spl-token-2022 = { workspace = true } +light-compressible-client = { workspace = true, features = ["anchor"] } + light-registry = { workspace = true, features = ["cpi"], optional = true } light-compressed-token = { workspace = true, features = ["cpi"], optional = true } diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 65a6dceaca..64d4e2e1f4 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -166,3 +166,136 @@ pub async fn claim_and_compress( Ok(()) } + +#[cfg(feature = "devenv")] +pub async fn auto_compress_program_pdas( + rpc: &mut LightProgramTest, + program_id: Pubkey, +) -> Result<(), RpcError> { + use solana_instruction::AccountMeta; + use solana_sdk::signature::Signer; + + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let cfg_acc = rpc + .get_account(config_pda) + .await? + .ok_or_else(|| RpcError::CustomError("compressible config not found".into()))?; + let cfg = CompressibleConfig::deserialize(&mut &cfg_acc.data[..]) + .map_err(|e| RpcError::CustomError(format!("config deserialize: {e:?}")))?; + let rent_sponsor = cfg.rent_sponsor; + let address_tree = cfg.address_space[0]; + + let program_accounts = rpc.context.get_program_accounts(&program_id); + if program_accounts.is_empty() { + return Ok(()); + } + + let output_state_tree_info = rpc + .get_random_state_tree_info() + .map_err(|e| RpcError::CustomError(format!("no state tree: {e:?}")))?; + + let program_metas = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(rent_sponsor, false), + ]; + + const BATCH_SIZE: usize = 5; + let mut chunk = Vec::with_capacity(BATCH_SIZE); + for (pubkey, account) in program_accounts + .into_iter() + .filter(|(_, acc)| acc.lamports > 0 && !acc.data.is_empty()) + { + chunk.push((pubkey, account)); + if chunk.len() == BATCH_SIZE { + try_compress_chunk( + rpc, + &program_id, + &chunk, + &program_metas, + &address_tree, + output_state_tree_info, + ) + .await; + chunk.clear(); + } + } + + if !chunk.is_empty() { + try_compress_chunk( + rpc, + &program_id, + &chunk, + &program_metas, + &address_tree, + output_state_tree_info, + ) + .await; + } + + Ok(()) +} + +#[cfg(feature = "devenv")] +async fn try_compress_chunk( + rpc: &mut LightProgramTest, + program_id: &Pubkey, + chunk: &[(Pubkey, solana_sdk::account::Account)], + program_metas: &[solana_instruction::AccountMeta], + address_tree: &Pubkey, + output_state_tree_info: light_client::indexer::TreeInfo, +) { + use light_client::indexer::Indexer; + use light_compressed_account::address::derive_address; + use light_compressible_client::CompressibleInstruction; + use solana_sdk::signature::Signer; + + let mut pdas = Vec::with_capacity(chunk.len()); + let mut accounts_to_compress = Vec::with_capacity(chunk.len()); + let mut hashes = Vec::with_capacity(chunk.len()); + for (pda, acc) in chunk.iter() { + let addr = derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &program_id.to_bytes(), + ); + if let Ok(resp) = rpc.get_compressed_account(addr, None).await { + if let Some(cacc) = resp.value { + pdas.push(*pda); + accounts_to_compress.push(acc.clone()); + hashes.push(cacc.hash); + } + } + } + if pdas.is_empty() { + return; + } + + let proof_with_context = match rpc.get_validity_proof(hashes, vec![], None).await { + Ok(r) => r.value, + Err(_) => return, + }; + + let signer_seeds: Vec>> = (0..pdas.len()).map(|_| Vec::new()).collect(); + + let ix_res = CompressibleInstruction::compress_accounts_idempotent( + program_id, + &CompressibleInstruction::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &pdas, + &accounts_to_compress, + program_metas, + signer_seeds, + proof_with_context, + output_state_tree_info, + ) + .map_err(|e| e.to_string()); + if let Ok(ix) = ix_res { + let payer = rpc.get_payer().insecure_clone(); + let payer_pubkey = payer.pubkey(); + let _ = rpc + .create_and_send_transaction(&[ix], &payer_pubkey, &[&payer]) + .await; + } +} diff --git a/sdk-libs/program-test/src/indexer/extensions.rs b/sdk-libs/program-test/src/indexer/extensions.rs index 3e802ec66e..13aa49d04d 100644 --- a/sdk-libs/program-test/src/indexer/extensions.rs +++ b/sdk-libs/program-test/src/indexer/extensions.rs @@ -4,8 +4,8 @@ use light_client::indexer::{ AddressMerkleTreeAccounts, MerkleProof, NewAddressProofWithContext, StateMerkleTreeAccounts, }; use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_event::event::PublicTransactionEvent; -use light_sdk::token::TokenDataWithMerkleContext; use solana_sdk::signature::Keypair; use super::{address_tree::AddressMerkleTreeBundle, state_tree::StateMerkleTreeBundle}; diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index b5ae74e3d0..bc5a07e6e6 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -40,6 +40,7 @@ use light_compressed_account::{ tx_hash::create_tx_hash, TreeType, }; +use light_compressed_token_sdk::compat::{TokenData, TokenDataWithMerkleContext}; use light_event::event::PublicTransactionEvent; use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; use light_merkle_tree_reference::MerkleTree; @@ -66,10 +67,7 @@ use light_prover_client::{ }, }, }; -use light_sdk::{ - light_hasher::Hash, - token::{TokenData, TokenDataWithMerkleContext}, -}; +use light_sdk::light_hasher::Hash; use log::info; use num_bigint::{BigInt, BigUint}; use num_traits::FromBytes; diff --git a/sdk-libs/program-test/src/program_test/compressible_setup.rs b/sdk-libs/program-test/src/program_test/compressible_setup.rs new file mode 100644 index 0000000000..ed187d34a5 --- /dev/null +++ b/sdk-libs/program-test/src/program_test/compressible_setup.rs @@ -0,0 +1,151 @@ +//! Test helpers for compressible account operations +//! +//! This module provides common functionality for testing compressible accounts, +//! including mock program data setup and configuration management. + +use light_client::rpc::{Rpc, RpcError}; +use light_compressible_client::CompressibleInstruction; +use solana_sdk::{ + bpf_loader_upgradeable, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +use crate::program_test::TestRpc; + +/// Create mock program data account for testing +/// +/// This creates a minimal program data account structure that mimics +/// what the BPF loader would create for deployed programs. +pub fn create_mock_program_data(authority: Pubkey) -> Vec { + let mut data = vec![0u8; 1024]; + data[0..4].copy_from_slice(&3u32.to_le_bytes()); // Program data discriminator + data[4..12].copy_from_slice(&0u64.to_le_bytes()); // Slot + data[12] = 1; // Option Some(authority) + data[13..45].copy_from_slice(authority.as_ref()); // Authority pubkey + data +} + +/// Setup mock program data account for testing +/// +/// For testing without ledger, LiteSVM does not create program data accounts, +/// so we need to create them manually. This is required for programs that +/// check their upgrade authority. +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The payer keypair (used as authority) +/// * `program_id` - The program ID to create data account for +/// +/// # Returns +/// The pubkey of the created program data account +pub fn setup_mock_program_data( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, +) -> Pubkey { + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::ID); + let mock_data = create_mock_program_data(payer.pubkey()); + let mock_account = solana_sdk::account::Account { + lamports: 1_000_000, + data: mock_data, + owner: bpf_loader_upgradeable::ID, + executable: false, + rent_epoch: 0, + }; + rpc.set_account(program_data_pda, mock_account); + program_data_pda +} + +/// Initialize compression config for a program +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The transaction fee payer +/// * `program_id` - The program to initialize config for +/// * `authority` - The config authority (can be same as payer) +/// * `compression_delay` - Number of slots to wait before compression +/// * `rent_sponsor` - Where to send rent from compressed accounts +/// * `address_space` - List of address trees for this program +/// +/// # Returns +/// `Result` - The transaction signature +#[allow(clippy::too_many_arguments)] +pub async fn initialize_compression_config( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, + authority: &Keypair, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, + discriminator: &[u8], + config_bump: Option, +) -> Result { + if address_space.is_empty() { + return Err(RpcError::CustomError( + "At least one address space must be provided".to_string(), + )); + } + + let instruction = CompressibleInstruction::initialize_compression_config( + program_id, + discriminator, + &payer.pubkey(), + &authority.pubkey(), + compression_delay, + rent_sponsor, + address_space, + config_bump, + ); + + let signers = if payer.pubkey() == authority.pubkey() { + vec![payer] + } else { + vec![payer, authority] + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) + .await +} + +/// Update compression config for a program +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The transaction fee payer +/// * `program_id` - The program to update config for +/// * `authority` - The current config authority +/// * `new_compression_delay` - New compression delay (optional) +/// * `new_rent_sponsor` - New rent recipient (optional) +/// * `new_address_space` - New address space list (optional) +/// * `new_update_authority` - New authority (optional) +/// +/// # Returns +/// `Result` - The transaction signature +#[allow(clippy::too_many_arguments)] +pub async fn update_compression_config( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, + authority: &Keypair, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, + discriminator: &[u8], +) -> Result { + let instruction = CompressibleInstruction::update_compression_config( + program_id, + discriminator, + &authority.pubkey(), + new_compression_delay, + new_rent_sponsor, + new_address_space, + new_update_authority, + ); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, authority]) + .await +} diff --git a/sdk-libs/program-test/src/program_test/config.rs b/sdk-libs/program-test/src/program_test/config.rs index 45fa9a8cbc..662431a453 100644 --- a/sdk-libs/program-test/src/program_test/config.rs +++ b/sdk-libs/program-test/src/program_test/config.rs @@ -21,6 +21,8 @@ pub struct ProgramTestConfig { pub protocol_config: ProtocolConfig, pub with_prover: bool, #[cfg(feature = "devenv")] + pub auto_register_custom_programs_for_pda_compression: bool, + #[cfg(feature = "devenv")] pub skip_register_programs: bool, #[cfg(feature = "devenv")] pub skip_v1_trees: bool, @@ -132,6 +134,8 @@ impl Default for ProgramTestConfig { }, with_prover: true, #[cfg(feature = "devenv")] + auto_register_custom_programs_for_pda_compression: true, + #[cfg(feature = "devenv")] skip_second_v1_tree: false, #[cfg(feature = "devenv")] skip_register_programs: false, diff --git a/sdk-libs/program-test/src/program_test/extensions.rs b/sdk-libs/program-test/src/program_test/extensions.rs index 27c02e2094..b90210db95 100644 --- a/sdk-libs/program-test/src/program_test/extensions.rs +++ b/sdk-libs/program-test/src/program_test/extensions.rs @@ -4,8 +4,8 @@ use light_client::indexer::{ AddressMerkleTreeAccounts, MerkleProof, NewAddressProofWithContext, StateMerkleTreeAccounts, }; use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_event::event::PublicTransactionEvent; -use light_sdk::token::TokenDataWithMerkleContext; use solana_sdk::signature::Keypair; use crate::{ diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 86fe5e824d..67a35fe213 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -33,6 +33,8 @@ pub struct LightProgramTest { pub test_accounts: TestAccounts, pub payer: Keypair, pub transaction_counter: usize, + #[cfg(feature = "devenv")] + pub auto_compress_programs: Vec, } impl LightProgramTest { @@ -77,6 +79,8 @@ impl LightProgramTest { payer, config: config.clone(), transaction_counter: 0, + #[cfg(feature = "devenv")] + auto_compress_programs: Vec::new(), }; let keypairs = TestKeypairs::program_test_default(); @@ -150,6 +154,20 @@ impl LightProgramTest { })?; } } + let (auto_register, additional_programs) = { + let auto = context + .config + .auto_register_custom_programs_for_pda_compression; + let progs = context.config.additional_programs.clone(); + (auto, progs) + }; + if auto_register { + if let Some(programs) = additional_programs { + for (_, pid) in programs.into_iter() { + context.register_auto_compression(pid); + } + } + } // Copy v1 state merkle tree accounts to devnet pubkeys { let tree_account = context @@ -404,6 +422,13 @@ impl LightProgramTest { .ok_or(RpcError::IndexerNotInitialized)?) .clone()) } + + #[cfg(feature = "devenv")] + pub fn register_auto_compression(&mut self, program_id: solana_sdk::pubkey::Pubkey) { + if !self.auto_compress_programs.contains(&program_id) { + self.auto_compress_programs.push(program_id); + } + } } impl MerkleTreeExt for LightProgramTest {} diff --git a/sdk-libs/program-test/src/program_test/mod.rs b/sdk-libs/program-test/src/program_test/mod.rs index c9eee711e3..a6b565f6ae 100644 --- a/sdk-libs/program-test/src/program_test/mod.rs +++ b/sdk-libs/program-test/src/program_test/mod.rs @@ -8,3 +8,6 @@ pub mod test_rpc; pub use light_program_test::LightProgramTest; pub mod indexer; pub use test_rpc::TestRpc; + +pub mod compressible_setup; +pub use compressible_setup::*; diff --git a/sdk-libs/program-test/src/program_test/test_rpc.rs b/sdk-libs/program-test/src/program_test/test_rpc.rs index 49ad31fb02..8660095ab3 100644 --- a/sdk-libs/program-test/src/program_test/test_rpc.rs +++ b/sdk-libs/program-test/src/program_test/test_rpc.rs @@ -157,6 +157,9 @@ impl TestRpc for LightProgramTest { self.context.warp_to_slot(current_slot); let mut store = CompressibleAccountStore::new(); crate::compressible::claim_and_compress(self, &mut store).await?; + for program_id in self.auto_compress_programs.clone() { + crate::compressible::auto_compress_program_pdas(self, program_id).await?; + } Ok(()) } } diff --git a/sdk-libs/program-test/src/utils/mod.rs b/sdk-libs/program-test/src/utils/mod.rs index ccd3e02457..2309bd1055 100644 --- a/sdk-libs/program-test/src/utils/mod.rs +++ b/sdk-libs/program-test/src/utils/mod.rs @@ -6,3 +6,6 @@ pub mod load_accounts; pub mod register_test_forester; pub mod setup_light_programs; pub mod tree_accounts; + +pub mod simulation; +pub use simulation::simulate_cu; diff --git a/sdk-libs/program-test/src/utils/simulation.rs b/sdk-libs/program-test/src/utils/simulation.rs new file mode 100644 index 0000000000..a5af0b331e --- /dev/null +++ b/sdk-libs/program-test/src/utils/simulation.rs @@ -0,0 +1,36 @@ +use solana_sdk::{ + instruction::Instruction, + signature::{Keypair, Signer}, + transaction::{Transaction, VersionedTransaction}, +}; + +use crate::{program_test::LightProgramTest, Rpc}; + +/// Simulate a transaction and return the compute units consumed. +/// +/// This is a test utility function for measuring transaction costs. +pub async fn simulate_cu( + rpc: &mut LightProgramTest, + payer: &Keypair, + instruction: &Instruction, +) -> u64 { + let blockhash = rpc + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash") + .0; + let tx = Transaction::new_signed_with_payer( + std::slice::from_ref(instruction), + Some(&payer.pubkey()), + &[payer], + blockhash, + ); + let simulate_tx = VersionedTransaction::from(tx); + + let simulate_result = rpc + .context + .simulate_transaction(simulate_tx) + .unwrap_or_else(|err| panic!("Transaction simulation failed: {:?}", err)); + + simulate_result.meta.compute_units_consumed +} diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index 6a53f45446..024adcd982 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -25,7 +25,7 @@ poseidon = ["light-hasher/poseidon", "light-compressed-account/poseidon"] keccak = ["light-hasher/keccak", "light-compressed-account/keccak"] sha256 = ["light-hasher/sha256", "light-compressed-account/sha256"] merkle-tree = ["light-concurrent-merkle-tree/solana"] - +anchor-discriminator = ["light-sdk-macros/anchor-discriminator"] [dependencies] solana-pubkey = { workspace = true, features = ["borsh", "sha2", "curve25519"] } @@ -34,12 +34,17 @@ solana-msg = { workspace = true } solana-cpi = { workspace = true } solana-program-error = { workspace = true } solana-instruction = { workspace = true } +solana-clock = { workspace = true } +solana-sysvar = { workspace = true } +solana-system-interface = { workspace = true } +solana-loader-v3-interface = { workspace = true, features = ["serde"] } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } borsh = { workspace = true } thiserror = { workspace = true } +bincode = "1" light-sdk-macros = { workspace = true } light-sdk-types = { workspace = true, features = ["std"] } @@ -49,6 +54,7 @@ light-hasher = { workspace = true, features = ["std"] } light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } light-concurrent-merkle-tree = { workspace = true, optional = true } +light-ctoken-types = { workspace = true } [dev-dependencies] num-bigint = { workspace = true } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 8c49c28982..fc865d689d 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -302,6 +302,17 @@ pub mod __internal { pub fn owner(&self) -> &Pubkey { &self.owner } + /// Get the byte size of the account type. + pub fn size(&self) -> usize + where + A: Size, + { + self.account.size() + } + + pub fn remove_data(&mut self) { + self.should_remove_data = true; + } pub fn in_account_info(&self) -> &Option { &self.account_info.input diff --git a/sdk-libs/sdk/src/address.rs b/sdk-libs/sdk/src/address.rs index c3d89f1105..d7e7f78707 100644 --- a/sdk-libs/sdk/src/address.rs +++ b/sdk-libs/sdk/src/address.rs @@ -134,6 +134,20 @@ pub mod v2 { ) } + /// Derive address from PDA using Pubkey types. + pub fn derive_compressed_address( + account_address: &Pubkey, + address_tree_pubkey: &Pubkey, + program_id: &Pubkey, + ) -> [u8; 32] { + derive_address( + &[account_address.to_bytes().as_ref()], + address_tree_pubkey, + program_id, + ) + .0 + } + /// Derives an address from provided seeds. Returns that address and a singular /// seed. /// diff --git a/sdk-libs/sdk/src/compressible/close.rs b/sdk-libs/sdk/src/compressible/close.rs new file mode 100644 index 0000000000..95b7e85074 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -0,0 +1,40 @@ +use solana_account_info::AccountInfo; + +use crate::error::{LightSdkError, Result}; + +// close native solana account +pub fn close<'info>( + info: &mut AccountInfo<'info>, + sol_destination: AccountInfo<'info>, +) -> Result<()> { + let lamports_to_transfer = info.lamports(); + + let new_destination_lamports = sol_destination + .lamports() + .checked_add(lamports_to_transfer) + .ok_or(LightSdkError::ConstraintViolation)?; + + if info.lamports() != lamports_to_transfer { + return Err(LightSdkError::ConstraintViolation); + } + + { + let mut destination_lamports = sol_destination + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)?; + **destination_lamports = new_destination_lamports; + } + + { + let mut source_lamports = info + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)?; + **source_lamports = 0; + } + + let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + info.assign(&system_program_id); + info.resize(0)?; + + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/compress_account.rs b/sdk-libs/sdk/src/compressible/compress_account.rs new file mode 100644 index 0000000000..d0b8e7dc32 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account.rs @@ -0,0 +1,115 @@ +use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; +use light_hasher::DataHasher; +use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; +use solana_account_info::AccountInfo; +use solana_clock::Clock; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_sysvar::Sysvar; + +use crate::{ + account::sha::LightAccount, + compressible::compression_info::{CompressAs, HasCompressionInfo}, + cpi::v2::CpiAccounts, + error::LightSdkError, + instruction::account_meta::CompressedAccountMeta, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, ProgramError, +}; + +/// Prepare account for compression. +/// +/// # Arguments +/// * `program_id` - The program that owns the account +/// * `account_info` - The account to compress +/// * `account_data` - Mutable reference to the deserialized account data +/// * `compressed_account_meta` - Metadata for the compressed account +/// * `cpi_accounts` - Accounts for CPI to light system program +/// * `compression_delay` - Minimum slots before compression allowed +/// * `address_space` - Address space for validation +#[cfg(feature = "v2")] +pub fn prepare_account_for_compression<'info, A>( + program_id: &Pubkey, + account_info: &AccountInfo<'info>, + account_data: &mut A, + compressed_account_meta: &CompressedAccountMetaNoLamportsNoAddress, + cpi_accounts: &CpiAccounts<'_, 'info>, + compression_delay: &u32, + address_space: &[Pubkey], +) -> std::result::Result +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo + + CompressAs, + A::Output: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + HasCompressionInfo + + Default + + crate::compressible::compression_info::CompressedInitSpace, +{ + use light_compressed_account::address::derive_address; + + let derived_c_pda = derive_address( + &account_info.key.to_bytes(), + &address_space[0].to_bytes(), + &program_id.to_bytes(), + ); + + let meta_with_address = CompressedAccountMeta { + tree_info: compressed_account_meta.tree_info, + address: derived_c_pda, + output_state_tree_index: compressed_account_meta.output_state_tree_index, + }; + + let current_slot = Clock::get()?.slot; + let last_written_slot = account_data.compression_info().last_written_slot(); + + if current_slot < last_written_slot + *compression_delay as u64 { + msg!( + "prepare_account_for_compression failed: Cannot compress yet. {} slots remaining", + (last_written_slot + *compression_delay as u64).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + account_data.compression_info_mut().set_compressed(); + { + let mut data = account_info + .try_borrow_mut_data() + .map_err(|_| LightSdkError::ConstraintViolation)?; + let writer = &mut &mut data[..]; + account_data.serialize(writer).map_err(|e| { + msg!("Failed to serialize account data: {}", e); + LightSdkError::ConstraintViolation + })?; + } + + let owner_program_id = cpi_accounts.self_program_id(); + let mut compressed_account = + LightAccount::::new_empty(&owner_program_id, &meta_with_address)?; + + let compressed_data = match account_data.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), + std::borrow::Cow::Owned(data) => data, + }; + compressed_account.account = compressed_data; + { + use crate::compressible::compression_info::CompressedInitSpace; + let __lp_size = 8 + ::COMPRESSED_INIT_SPACE; + if __lp_size > 800 { + msg!( + "Compressed account would exceed 800-byte limit ({} bytes)", + __lp_size + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + } + + compressed_account.to_account_info() +} diff --git a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs new file mode 100644 index 0000000000..1bf9c1ad89 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs @@ -0,0 +1,102 @@ +use light_compressed_account::instruction_data::{ + data::NewAddressParamsAssignedPacked, with_account_info::CompressedAccountInfo, +}; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_msg::msg; +use solana_pubkey::Pubkey; + +use crate::{ + account::sha::LightAccount, compressible::compression_info::HasCompressionInfo, + cpi::v2::CpiAccounts, error::LightSdkError, light_account_checks::AccountInfoTrait, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, ProgramError, +}; + +/// Prepare a compressed account on init. +/// +/// Does NOT close the PDA, does NOT invoke CPI. +/// +/// # Arguments +/// * `account_info` - The PDA AccountInfo +/// * `account_data` - Mutable reference to deserialized account data +/// * `address` - The address for the compressed account +/// * `new_address_param` - Address parameters for the compressed account +/// * `output_state_tree_index` - Output state tree index +/// * `cpi_accounts` - Accounts for validation +/// * `address_space` - Address space for validation (can contain multiple tree +/// pubkeys) +/// * `with_data` - If true, copies account data to compressed account, if +/// false, creates empty compressed account +/// +/// # Returns +/// CompressedAccountInfo +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "v2")] +pub fn prepare_compressed_account_on_init<'info, A>( + account_info: &AccountInfo<'info>, + account_data: &mut A, + address: [u8; 32], + new_address_param: NewAddressParamsAssignedPacked, + output_state_tree_index: u8, + cpi_accounts: &CpiAccounts<'_, 'info>, + address_space: &[Pubkey], + with_data: bool, +) -> std::result::Result +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + let tree = cpi_accounts + .get_tree_account_info(new_address_param.address_merkle_tree_account_index as usize) + .map_err(|_| { + msg!( + "Failed to get tree account at index {}", + new_address_param.address_merkle_tree_account_index + ); + LightSdkError::ConstraintViolation + })? + .pubkey(); + if !address_space.iter().any(|a| a == &tree) { + msg!("Address tree {} not in allowed address space", tree); + return Err(LightSdkError::ConstraintViolation.into()); + } + *account_data.compression_info_mut_opt() = + Some(super::compression_info::CompressionInfo::new_decompressed()?); + + if with_data { + account_data.compression_info_mut().set_compressed(); + } else { + account_data + .compression_info_mut() + .bump_last_written_slot()?; + } + { + let mut data = account_info + .try_borrow_mut_data() + .map_err(|_| LightSdkError::ConstraintViolation)?; + account_data.serialize(&mut &mut data[..]).map_err(|e| { + msg!("Failed to serialize account data: {}", e); + LightSdkError::ConstraintViolation + })?; + } + + let owner_program_id = cpi_accounts.self_program_id(); + + let mut compressed_account = + LightAccount::::new_init(&owner_program_id, Some(address), output_state_tree_index); + + if with_data { + let mut compressed_data = account_data.clone(); + compressed_data.set_compression_info_none(); + compressed_account.account = compressed_data; + } else { + compressed_account.remove_data(); + } + + compressed_account.to_account_info() +} diff --git a/sdk-libs/sdk/src/compressible/compression_info.rs b/sdk-libs/sdk/src/compressible/compression_info.rs new file mode 100644 index 0000000000..3027385bd7 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -0,0 +1,121 @@ +use std::borrow::Cow; + +use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; +use solana_account_info::AccountInfo; +use solana_clock::Clock; +use solana_sysvar::Sysvar; + +use crate::{instruction::PackedAccounts, AnchorDeserialize, AnchorSerialize}; + +/// Replace 32-byte Pubkeys with 1-byte indices to save space. +/// If your type has no Pubkeys, just return self. +pub trait Pack { + type Packed: AnchorSerialize + Clone + std::fmt::Debug; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; +} + +pub trait Unpack { + type Unpacked; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> Result; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +#[repr(u8)] +pub enum AccountState { + Initialized, + Frozen, +} + +pub trait HasCompressionInfo { + fn compression_info(&self) -> &CompressionInfo; + fn compression_info_mut(&mut self) -> &mut CompressionInfo; + fn compression_info_mut_opt(&mut self) -> &mut Option; + fn set_compression_info_none(&mut self); +} + +/// Account space when compressed. +pub trait CompressedInitSpace { + const COMPRESSED_INIT_SPACE: usize; +} + +/// Override what gets stored when compressing. Return Self or a different type. +pub trait CompressAs { + type Output: crate::AnchorSerialize + + crate::AnchorDeserialize + + crate::LightDiscriminator + + crate::account::Size + + HasCompressionInfo + + Default + + Clone; + + fn compress_as(&self) -> Cow<'_, Self::Output>; +} + +#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)] +pub struct CompressionInfo { + pub last_written_slot: u64, + pub state: CompressionState, +} + +#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize, PartialEq)] +pub enum CompressionState { + #[default] + Uninitialized, + Decompressed, + Compressed, +} + +impl CompressionInfo { + pub fn new_decompressed() -> Result { + Ok(Self { + last_written_slot: Clock::get()?.slot, + state: CompressionState::Decompressed, + }) + } + + pub fn bump_last_written_slot(&mut self) -> Result<(), crate::ProgramError> { + self.last_written_slot = Clock::get()?.slot; + Ok(()) + } + + pub fn set_last_written_slot(&mut self, slot: u64) { + self.last_written_slot = slot; + } + + pub fn last_written_slot(&self) -> u64 { + self.last_written_slot + } + + pub fn set_compressed(&mut self) { + self.state = CompressionState::Compressed; + } + + pub fn is_compressed(&self) -> bool { + self.state == CompressionState::Compressed + } +} + +pub trait Space { + const INIT_SPACE: usize; +} + +impl Space for CompressionInfo { + const INIT_SPACE: usize = 8 + 1; // u64 + state enum (u8) +} + +#[cfg(feature = "anchor")] +impl anchor_lang::Space for CompressionInfo { + const INIT_SPACE: usize = ::INIT_SPACE; +} + +/// Compressed account data used when decompressing. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CompressedAccountData { + pub meta: CompressedAccountMetaNoLamportsNoAddress, + pub data: T, +} diff --git a/sdk-libs/sdk/src/compressible/config.rs b/sdk-libs/sdk/src/compressible/config.rs new file mode 100644 index 0000000000..c5836a8dff --- /dev/null +++ b/sdk-libs/sdk/src/compressible/config.rs @@ -0,0 +1,476 @@ +use std::collections::HashSet; + +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_loader_v3_interface::state::UpgradeableLoaderState; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_system_interface::instruction as system_instruction; +use solana_sysvar::{rent::Rent, Sysvar}; + +use crate::{error::LightSdkError, AnchorDeserialize, AnchorSerialize}; + +pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config"; +pub const MAX_ADDRESS_TREES_PER_SPACE: usize = 1; +const BPF_LOADER_UPGRADEABLE_ID: Pubkey = + Pubkey::from_str_const("BPFLoaderUpgradeab1e11111111111111111111111"); + +// TODO: add rent_authority + rent_func like in ctoken. +/// Global configuration for compressible accounts +#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)] +pub struct CompressibleConfig { + /// Config version for future upgrades + pub version: u8, + /// Number of slots to wait before compression is allowed + pub compression_delay: u32, + /// Authority that can update the config + pub update_authority: Pubkey, + /// Account that receives rent from compressed PDAs + pub rent_sponsor: Pubkey, + /// Config bump seed (0) + pub config_bump: u8, + /// PDA bump seed + pub bump: u8, + /// Address space for compressed accounts (currently 1 address_tree allowed) + pub address_space: Vec, +} + +impl CompressibleConfig { + pub const LEN: usize = 1 + 4 + 32 + 32 + 1 + 4 + (32 * MAX_ADDRESS_TREES_PER_SPACE) + 1; // 107 bytes max + + /// Calculate the exact size needed for a CompressibleConfig with the given + /// number of address spaces + pub fn size_for_address_space(num_address_trees: usize) -> usize { + 1 + 4 + 32 + 32 + 1 + 4 + (32 * num_address_trees) + 1 + } + + /// Derives the config PDA address with config bump + pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) { + Pubkey::find_program_address(&[COMPRESSIBLE_CONFIG_SEED, &[config_bump]], program_id) + } + + /// Derives the default config PDA address (config_bump = 0) + pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) { + Self::derive_pda(program_id, 0) + } + + /// Checks the config account + pub fn validate(&self) -> Result<(), crate::ProgramError> { + if self.version != 1 { + msg!( + "CompressibleConfig validation failed: Unsupported config version: {}", + self.version + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + if self.address_space.len() != 1 { + msg!( + "CompressibleConfig validation failed: Address space must contain exactly 1 pubkey, found: {}", + self.address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + // For now, only allow config_bump = 0 to keep it simple + if self.config_bump != 0 { + msg!( + "CompressibleConfig validation failed: Config bump must be 0 for now, found: {}", + self.config_bump + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + Ok(()) + } + + /// Loads and validates config from account, checking owner and PDA derivation + #[inline(never)] + pub fn load_checked( + account: &AccountInfo, + program_id: &Pubkey, + ) -> Result { + if account.owner != program_id { + msg!( + "CompressibleConfig::load_checked failed: Config account owner mismatch. Expected: {:?}. Found: {:?}.", + program_id, + account.owner + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + let data = account.try_borrow_data()?; + let config = Self::try_from_slice(&data).map_err(|err| { + msg!( + "CompressibleConfig::load_checked failed: Failed to deserialize config data: {:?}", + err + ); + LightSdkError::Borsh + })?; + config.validate()?; + + // CHECK: PDA derivation + let (expected_pda, _) = Self::derive_pda(program_id, config.config_bump); + if expected_pda != *account.key { + msg!( + "CompressibleConfig::load_checked failed: Config account key mismatch. Expected PDA: {:?}. Found: {:?}.", + expected_pda, + account.key + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + Ok(config) + } +} + +/// Creates a new compressible config PDA +/// +/// # Security - Solana Best Practice +/// This function follows the standard Solana pattern where only the program's +/// upgrade authority can create the initial config. This prevents unauthorized +/// parties from hijacking the config system. +/// +/// # Arguments +/// * `config_account` - The config PDA account to initialize +/// * `update_authority` - Authority that can update the config after creation +/// * `rent_sponsor` - Account that receives rent from compressed PDAs +/// * `address_space` - Address space for compressed accounts (currently 1 address_tree allowed) +/// * `compression_delay` - Number of slots to wait before compression +/// * `config_bump` - Config bump seed (must be 0 for now) +/// * `payer` - Account paying for the PDA creation +/// * `system_program` - System program +/// * `program_id` - The program that owns the config +/// +/// # Required Validation (must be done by caller) +/// The caller MUST validate that the signer is the program's upgrade authority +/// by checking against the program data account. This cannot be done in the SDK +/// due to dependency constraints. +/// +/// # Returns +/// * `Ok(())` if config was created successfully +/// * `Err(ProgramError)` if there was an error +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_compression_config_account_info<'info>( + config_account: &AccountInfo<'info>, + update_authority: &AccountInfo<'info>, + rent_sponsor: &Pubkey, + address_space: Vec, + compression_delay: u32, + config_bump: u8, + payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + // CHECK: only 1 address_space + if config_bump != 0 { + msg!("Config bump must be 0 for now, found: {}", config_bump); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: not already initialized + if config_account.data_len() > 0 { + msg!("Config account already initialized"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: only 1 address_space + if address_space.len() != 1 { + msg!( + "Address space must contain exactly 1 pubkey, found: {}", + address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: unique pubkeys in address_space + validate_address_space_no_duplicates(&address_space)?; + + // CHECK: signer + if !update_authority.is_signer { + msg!("Update authority must be signer for initial config creation"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: pda derivation + let (derived_pda, bump) = CompressibleConfig::derive_pda(program_id, config_bump); + if derived_pda != *config_account.key { + msg!("Invalid config PDA"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + let rent = Rent::get().map_err(LightSdkError::from)?; + let account_size = CompressibleConfig::size_for_address_space(address_space.len()); + let rent_lamports = rent.minimum_balance(account_size); + + let seeds = &[COMPRESSIBLE_CONFIG_SEED, &[config_bump], &[bump]]; + let create_account_ix = system_instruction::create_account( + payer.key, + config_account.key, + rent_lamports, + account_size as u64, + program_id, + ); + + invoke_signed( + &create_account_ix, + &[ + payer.clone(), + config_account.clone(), + system_program.clone(), + ], + &[seeds], + ) + .map_err(LightSdkError::from)?; + + let config = CompressibleConfig { + version: 1, + compression_delay, + update_authority: *update_authority.key, + rent_sponsor: *rent_sponsor, + config_bump, + address_space, + bump, + }; + + let mut data = config_account + .try_borrow_mut_data() + .map_err(LightSdkError::from)?; + config + .serialize(&mut &mut data[..]) + .map_err(|_| LightSdkError::Borsh)?; + + Ok(()) +} + +/// Updates an existing compressible config +/// +/// # Arguments +/// * `config_account` - The config PDA account to update +/// * `authority` - Current update authority (must match config) +/// * `new_update_authority` - Optional new update authority +/// * `new_rent_sponsor` - Optional new rent recipient +/// * `new_address_space` - Optional new address space (currently 1 address_tree allowed) +/// * `new_compression_delay` - Optional new compression delay +/// * `owner_program_id` - The program that owns the config +/// +/// # Returns +/// * `Ok(())` if config was updated successfully +/// * `Err(ProgramError)` if there was an error +pub fn process_update_compression_config<'info>( + config_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + new_update_authority: Option<&Pubkey>, + new_rent_sponsor: Option<&Pubkey>, + new_address_space: Option>, + new_compression_delay: Option, + owner_program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + // CHECK: PDA derivation + let mut config = CompressibleConfig::load_checked(config_account, owner_program_id)?; + + // CHECK: signer + if !authority.is_signer { + msg!("Update authority must be signer"); + return Err(LightSdkError::ConstraintViolation.into()); + } + // CHECK: authority + if *authority.key != config.update_authority { + msg!("Invalid update authority"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + if let Some(new_authority) = new_update_authority { + config.update_authority = *new_authority; + } + if let Some(new_recipient) = new_rent_sponsor { + config.rent_sponsor = *new_recipient; + } + if let Some(new_address_space) = new_address_space { + // CHECK: address space length + if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE { + msg!( + "New address space must contain exactly 1 pubkey, found: {}", + new_address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + validate_address_space_no_duplicates(&new_address_space)?; + + validate_address_space_only_adds(&config.address_space, &new_address_space)?; + + config.address_space = new_address_space; + } + if let Some(new_delay) = new_compression_delay { + config.compression_delay = new_delay; + } + + let mut data = config_account.try_borrow_mut_data().map_err(|e| { + msg!("Failed to borrow mut data for config_account: {:?}", e); + LightSdkError::from(e) + })?; + config.serialize(&mut &mut data[..]).map_err(|e| { + msg!("Failed to serialize updated config: {:?}", e); + LightSdkError::Borsh + })?; + + Ok(()) +} + +/// Checks that the signer is the program's upgrade authority +/// +/// # Arguments +/// * `program_id` - The program to check +/// * `program_data_account` - The program's data account (ProgramData) +/// * `authority` - The authority to verify +/// +/// # Returns +/// * `Ok(())` if authority is valid +/// * `Err(LightSdkError)` if authority is invalid or verification fails +pub fn check_program_upgrade_authority( + program_id: &Pubkey, + program_data_account: &AccountInfo, + authority: &AccountInfo, +) -> Result<(), crate::ProgramError> { + // CHECK: program data PDA + let (expected_program_data, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID); + if program_data_account.key != &expected_program_data { + msg!("Invalid program data account"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + let data = program_data_account.try_borrow_data()?; + let program_state: UpgradeableLoaderState = bincode::deserialize(&data).map_err(|_| { + msg!("Failed to deserialize program data account"); + LightSdkError::ConstraintViolation + })?; + + // Extract upgrade authority + let upgrade_authority = match program_state { + UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address, + } => { + match upgrade_authority_address { + Some(auth) => { + // Check for invalid zero authority when authority exists + if auth == Pubkey::default() { + msg!("Invalid state: authority is zero pubkey"); + return Err(LightSdkError::ConstraintViolation.into()); + } + auth + } + None => { + msg!("Program has no upgrade authority"); + return Err(LightSdkError::ConstraintViolation.into()); + } + } + } + _ => { + msg!("Account is not ProgramData, found: {:?}", program_state); + return Err(LightSdkError::ConstraintViolation.into()); + } + }; + + // CHECK: upgrade authority is signer + if !authority.is_signer { + msg!("Authority must be signer"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: upgrade authority is program's upgrade authority + if *authority.key != upgrade_authority { + msg!( + "Signer is not the program's upgrade authority. Signer: {:?}, Expected Authority: {:?}", + authority.key, + upgrade_authority + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + Ok(()) +} + +/// Creates a new compressible config PDA. +/// +/// # Arguments +/// * `config_account` - The config PDA account to initialize +/// * `update_authority` - Must be the program's upgrade authority +/// * `program_data_account` - The program's data account for validation +/// * `rent_sponsor` - Account that receives rent from compressed PDAs +/// * `address_space` - Address spaces for compressed accounts (exactly 1 +/// allowed) +/// * `compression_delay` - Number of slots to wait before compression +/// * `config_bump` - Config bump seed (must be 0 for now) +/// * `payer` - Account paying for the PDA creation +/// * `system_program` - System program +/// * `program_id` - The program that owns the config +/// +/// # Returns +/// * `Ok(())` if config was created successfully +/// * `Err(ProgramError)` if there was an error or authority validation fails +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_compression_config_checked<'info>( + config_account: &AccountInfo<'info>, + update_authority: &AccountInfo<'info>, + program_data_account: &AccountInfo<'info>, + rent_sponsor: &Pubkey, + address_space: Vec, + compression_delay: u32, + config_bump: u8, + payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + msg!( + "create_compression_config_checked program_data_account: {:?}", + program_data_account.key + ); + msg!( + "create_compression_config_checked program_id: {:?}", + program_id + ); + // Verify the signer is the program's upgrade authority + check_program_upgrade_authority(program_id, program_data_account, update_authority)?; + + // Create the config with validated authority + process_initialize_compression_config_account_info( + config_account, + update_authority, + rent_sponsor, + address_space, + compression_delay, + config_bump, + payer, + system_program, + program_id, + ) +} + +/// Validates that address_space contains no duplicate pubkeys +fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> { + let mut seen = HashSet::new(); + for pubkey in address_space { + if !seen.insert(pubkey) { + msg!("Duplicate pubkey found in address_space: {}", pubkey); + return Err(LightSdkError::ConstraintViolation); + } + } + Ok(()) +} + +/// Validates that new_address_space only adds to existing address_space (no removals) +fn validate_address_space_only_adds( + existing_address_space: &[Pubkey], + new_address_space: &[Pubkey], +) -> Result<(), LightSdkError> { + // Check that all existing pubkeys are still present in new address space + for existing_pubkey in existing_address_space { + if !new_address_space.contains(existing_pubkey) { + msg!( + "Cannot remove existing pubkey from address_space: {}", + existing_pubkey + ); + return Err(LightSdkError::ConstraintViolation); + } + } + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs new file mode 100644 index 0000000000..42ef34640d --- /dev/null +++ b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs @@ -0,0 +1,141 @@ +#![allow(clippy::all)] // TODO: Remove. + +use light_compressed_account::address::derive_address; +use light_sdk_types::instruction::account_meta::{ + CompressedAccountMeta, CompressedAccountMetaNoLamportsNoAddress, +}; +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_system_interface::instruction as system_instruction; +use solana_sysvar::{rent::Rent, Sysvar}; + +use crate::{ + account::sha::LightAccount, compressible::compression_info::HasCompressionInfo, + cpi::v2::CpiAccounts, error::LightSdkError, AnchorDeserialize, AnchorSerialize, + LightDiscriminator, +}; + +/// Convert a `CompressedAccountMetaNoLamportsNoAddress` to a +/// `CompressedAccountMeta` by deriving the compressed address from the solana +/// account's pubkey. +pub fn into_compressed_meta_with_address<'info>( + compressed_meta_no_lamports_no_address: &CompressedAccountMetaNoLamportsNoAddress, + solana_account: &AccountInfo<'info>, + address_space: Pubkey, + program_id: &Pubkey, +) -> CompressedAccountMeta { + let derived_c_pda = derive_address( + &solana_account.key.to_bytes(), + &address_space.to_bytes(), + &program_id.to_bytes(), + ); + + let meta_with_address = CompressedAccountMeta { + tree_info: compressed_meta_no_lamports_no_address.tree_info, + address: derived_c_pda, + output_state_tree_index: compressed_meta_no_lamports_no_address.output_state_tree_index, + }; + + meta_with_address +} + +// TODO: consider folding into main fn. +/// Helper to invoke create_account on heap. +#[inline(never)] +fn invoke_create_account_with_heap<'info>( + rent_payer: &AccountInfo<'info>, + solana_account: &AccountInfo<'info>, + rent_minimum_balance: u64, + space: u64, + program_id: &Pubkey, + seeds: &[&[u8]], + system_program: &AccountInfo<'info>, +) -> Result<(), LightSdkError> { + let create_account_ix = system_instruction::create_account( + rent_payer.key, + solana_account.key, + rent_minimum_balance, + space, + program_id, + ); + + invoke_signed( + &create_account_ix, + &[ + rent_payer.clone(), + solana_account.clone(), + system_program.clone(), + ], + &[seeds], + ) + .map_err(|e| LightSdkError::ProgramError(e)) +} + +/// Helper function to decompress a compressed account into a PDA +/// idempotently with seeds. +#[inline(never)] +#[cfg(feature = "v2")] +pub fn prepare_account_for_decompression_idempotent<'a, 'info, T>( + program_id: &Pubkey, + data: T, + compressed_meta: CompressedAccountMeta, + solana_account: &AccountInfo<'info>, + rent_payer: &AccountInfo<'info>, + cpi_accounts: &CpiAccounts<'a, 'info>, + signer_seeds: &[&[u8]], +) -> Result< + Option, + LightSdkError, +> +where + T: Clone + + crate::account::Size + + LightDiscriminator + + Default + + AnchorSerialize + + AnchorDeserialize + + HasCompressionInfo + + 'info, +{ + if !solana_account.data_is_empty() { + msg!("Account already initialized, skipping"); + return Ok(None); + } + let rent = Rent::get().map_err(|err| { + msg!("Failed to get rent: {:?}", err); + LightSdkError::Borsh + })?; + + let light_account = LightAccount::::new_close(program_id, &compressed_meta, data)?; + + let space = T::size(&light_account.account); + let rent_minimum_balance = rent.minimum_balance(space); + + invoke_create_account_with_heap( + rent_payer, + solana_account, + rent_minimum_balance, + space as u64, + &cpi_accounts.self_program_id(), + signer_seeds, + cpi_accounts.system_program()?, + )?; + + let mut decompressed_pda = light_account.account.clone(); + *decompressed_pda.compression_info_mut_opt() = + Some(super::compression_info::CompressionInfo::new_decompressed()?); + + let mut account_data = solana_account.try_borrow_mut_data()?; + let discriminator_len = T::LIGHT_DISCRIMINATOR.len(); + account_data[..discriminator_len].copy_from_slice(&T::LIGHT_DISCRIMINATOR); + decompressed_pda + .serialize(&mut &mut account_data[discriminator_len..]) + .map_err(|err| { + msg!("Failed to serialize decompressed PDA: {:?}", err); + LightSdkError::Borsh + })?; + + Ok(Some(light_account.to_account_info()?)) +} diff --git a/sdk-libs/sdk/src/compressible/mod.rs b/sdk-libs/sdk/src/compressible/mod.rs new file mode 100644 index 0000000000..7c72acbb9f --- /dev/null +++ b/sdk-libs/sdk/src/compressible/mod.rs @@ -0,0 +1,28 @@ +pub mod close; +pub mod compression_info; +pub mod config; + +#[cfg(feature = "v2")] +pub mod compress_account; +#[cfg(feature = "v2")] +pub mod compress_account_on_init; +#[cfg(feature = "v2")] +pub mod decompress_idempotent; +#[cfg(feature = "v2")] +pub use close::close; +#[cfg(feature = "v2")] +pub use compress_account::prepare_account_for_compression; +#[cfg(feature = "v2")] +pub use compress_account_on_init::prepare_compressed_account_on_init; +pub use compression_info::{ + CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Space, Unpack, +}; +pub use config::{ + process_initialize_compression_config_account_info, + process_initialize_compression_config_checked, process_update_compression_config, + CompressibleConfig, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE, +}; +#[cfg(feature = "v2")] +pub use decompress_idempotent::{ + into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, +}; diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index a8586aba90..87606a1dda 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -147,11 +147,13 @@ pub mod error; /// Utilities to build instructions for programs with compressed accounts. pub mod instruction; pub mod legacy; -pub mod token; +pub mod proof; /// Transfer compressed sol between compressed accounts. pub mod transfer; pub mod utils; +pub use proof::borsh_compat; +pub mod compressible; #[cfg(feature = "merkle-tree")] pub mod merkle_tree; @@ -159,6 +161,12 @@ pub mod merkle_tree; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use compressible::{ + process_initialize_compression_config_account_info, + process_initialize_compression_config_checked, process_update_compression_config, CompressAs, + CompressedInitSpace, CompressibleConfig, CompressionInfo, HasCompressionInfo, Pack, Space, + Unpack, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE, +}; pub use light_account_checks::{self, discriminator::Discriminator as LightDiscriminator}; pub use light_hasher; #[cfg(feature = "poseidon")] diff --git a/sdk-libs/sdk/src/proof.rs b/sdk-libs/sdk/src/proof.rs new file mode 100644 index 0000000000..c64deb7bf0 --- /dev/null +++ b/sdk-libs/sdk/src/proof.rs @@ -0,0 +1,88 @@ +// TODO: try removing in separate PR +pub mod borsh_compat { + use crate::{AnchorDeserialize, AnchorSerialize}; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] + pub struct CompressedProof { + pub a: [u8; 32], + pub b: [u8; 64], + pub c: [u8; 32], + } + + impl Default for CompressedProof { + fn default() -> Self { + Self { + a: [0; 32], + b: [0; 64], + c: [0; 32], + } + } + } + + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] + /// Borsh-compatible ValidityProof. Use this in your anchor program unless + /// you have zero-copy instruction data. + pub struct ValidityProof(pub Option); + + impl ValidityProof { + pub fn new(proof: Option) -> Self { + Self(proof) + } + } + + impl From + for CompressedProof + { + fn from( + proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, + ) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From + for light_compressed_account::instruction_data::compressed_proof::CompressedProof + { + fn from(proof: CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From + for ValidityProof + { + fn from( + proof: light_compressed_account::instruction_data::compressed_proof::ValidityProof, + ) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From + for light_compressed_account::instruction_data::compressed_proof::ValidityProof + { + fn from(proof: ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for ValidityProof { + fn from(proof: CompressedProof) -> Self { + Self(Some(proof)) + } + } + + impl From> for ValidityProof { + fn from(proof: Option) -> Self { + Self(proof) + } + } +} diff --git a/sdk-libs/sdk/src/token.rs b/sdk-libs/sdk/src/token.rs deleted file mode 100644 index 14c0cafb7b..0000000000 --- a/sdk-libs/sdk/src/token.rs +++ /dev/null @@ -1,59 +0,0 @@ -use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; -use light_hasher::{sha256::Sha256BE, HasherError}; - -use crate::{AnchorDeserialize, AnchorSerialize, Pubkey}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] -#[repr(u8)] -pub enum AccountState { - #[default] - Initialized, - Frozen, -} -// TODO: extract token data from program into into a separate crate, import it and remove this file. -#[derive(Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Clone, Default)] -pub struct TokenData { - /// The mint associated with this account - pub mint: Pubkey, - /// The owner of this account. - pub owner: Pubkey, - /// The amount of tokens this account holds. - pub amount: u64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: Option, - /// The account's state - pub state: AccountState, - /// Placeholder for TokenExtension tlv data (unimplemented) - pub tlv: Option>, -} - -impl TokenData { - /// TokenDataVersion 3 - /// CompressedAccount Discriminator [0,0,0,0,0,0,0,4] - #[inline(always)] - pub fn hash_sha_flat(&self) -> Result<[u8; 32], HasherError> { - use light_hasher::Hasher; - let bytes = self.try_to_vec().map_err(|_| HasherError::BorshError)?; - Sha256BE::hash(bytes.as_slice()) - } -} -#[derive(Debug, Clone, PartialEq)] -pub struct TokenDataWithMerkleContext { - pub token_data: TokenData, - pub compressed_account: CompressedAccountWithMerkleContext, -} - -impl TokenDataWithMerkleContext { - /// Only works for sha flat hash - pub fn hash(&self) -> Result<[u8; 32], HasherError> { - if let Some(data) = self.compressed_account.compressed_account.data.as_ref() { - match data.discriminator { - [0, 0, 0, 0, 0, 0, 0, 4] => self.token_data.hash_sha_flat(), - _ => Err(HasherError::EmptyInput), - } - } else { - Err(HasherError::EmptyInput) - } - } -} diff --git a/sdk-libs/token-client/Cargo.toml b/sdk-libs/token-client/Cargo.toml index 7790616f5b..815940f3e0 100644 --- a/sdk-libs/token-client/Cargo.toml +++ b/sdk-libs/token-client/Cargo.toml @@ -14,6 +14,7 @@ light-sdk = { workspace = true } light-client = { workspace = true, features = ["v2"] } light-compressed-token-sdk = { workspace = true } light-zero-copy = { workspace = true } +light-compressible = { workspace = true } # Solana dependencies solana-pubkey = { workspace = true, features = ["sha2", "curve25519"] } diff --git a/sdk-libs/token-client/src/actions/create_compressible_token_account.rs b/sdk-libs/token-client/src/actions/create_compressible_token_account.rs index 9ec93b93d5..54f0ce4598 100644 --- a/sdk-libs/token-client/src/actions/create_compressible_token_account.rs +++ b/sdk-libs/token-client/src/actions/create_compressible_token_account.rs @@ -1,6 +1,7 @@ use light_client::rpc::{Rpc, RpcError}; use light_compressed_token_sdk::instructions::{ - create_compressible_token_account as create_instruction, CreateCompressibleTokenAccount, + create_compressible_token_account_instruction as create_instruction, + CreateCompressibleTokenAccount, }; use light_ctoken_types::state::TokenDataVersion; use solana_keypair::Keypair; diff --git a/sdk-libs/token-client/src/actions/ctoken_transfer.rs b/sdk-libs/token-client/src/actions/ctoken_transfer.rs index 9f4a20cb43..38c9b9be54 100644 --- a/sdk-libs/token-client/src/actions/ctoken_transfer.rs +++ b/sdk-libs/token-client/src/actions/ctoken_transfer.rs @@ -5,8 +5,7 @@ use solana_pubkey::Pubkey; use solana_signature::Signature; use solana_signer::Signer; -/// Transfer SPL tokens between decompressed compressed token accounts (accounts with compressible extensions). -/// This performs a regular SPL token transfer on accounts that were decompressed from compressed tokens. +/// Transfer from one c-token account to another. /// /// # Arguments /// * `rpc` - RPC client @@ -18,7 +17,7 @@ use solana_signer::Signer; /// /// # Returns /// `Result` - The transaction signature -pub async fn ctoken_transfer( +pub async fn transfer_ctoken( rpc: &mut R, source: Pubkey, destination: Pubkey, @@ -27,7 +26,7 @@ pub async fn ctoken_transfer( payer: &Keypair, ) -> Result { let transfer_instruction = - create_ctoken_transfer_instruction(source, destination, amount, authority.pubkey())?; + create_transfer_ctoken_instruction(source, destination, amount, authority.pubkey())?; let mut signers = vec![payer]; if authority.pubkey() != payer.pubkey() { @@ -38,9 +37,8 @@ pub async fn ctoken_transfer( .await } -/// Create a decompressed token transfer instruction. -/// This creates an instruction that uses discriminator 3 (CTokenTransfer) to perform -/// SPL token transfers on decompressed compressed token accounts. +// TODO: consume the variant from compressed-token-sdk instead +/// Create a ctoken transfer instruction. /// /// # Arguments /// * `source` - Source token account @@ -51,7 +49,7 @@ pub async fn ctoken_transfer( /// # Returns /// `Result` #[allow(clippy::result_large_err)] -pub fn create_ctoken_transfer_instruction( +pub fn create_transfer_ctoken_instruction( source: Pubkey, destination: Pubkey, amount: u64, @@ -62,12 +60,13 @@ pub fn create_ctoken_transfer_instruction( accounts: vec![ AccountMeta::new(source, false), // Source token account AccountMeta::new(destination, false), // Destination token account - AccountMeta::new(authority, true), // Owner/Authority (signer, writable for lamport transfers) - AccountMeta::new_readonly(Pubkey::default(), false), // System program for CPI transfers + AccountMeta::new(authority, true), + AccountMeta::new_readonly(Pubkey::default(), false), ], data: { - let mut data = vec![3u8]; // CTokenTransfer discriminator - // Add SPL Token Transfer instruction data exactly like SPL does + // CTokenTransfer discriminator + let mut data = vec![3u8]; + // Add SPL Token Transfer instruction data exactly like SPL does data.extend_from_slice(&amount.to_le_bytes()); // Amount as u64 little-endian data }, diff --git a/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs b/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs index 60c149ff66..a8775e12f1 100644 --- a/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs +++ b/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs @@ -3,7 +3,8 @@ use light_client::{ rpc::{Rpc, RpcError}, }; use light_compressed_token_sdk::{ - account2::create_ctoken_to_spl_transfer_instruction, token_pool::find_token_pool_pda_with_index, + instructions::create_transfer_ctoken_to_spl_instruction, + token_pool::find_token_pool_pda_with_index, SPL_TOKEN_PROGRAM_ID, }; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -11,7 +12,7 @@ use solana_signature::Signature; use solana_signer::Signer; /// Transfer tokens from a compressed token account to an SPL token account -pub async fn ctoken_to_spl_transfer( +pub async fn transfer_ctoken_to_spl( rpc: &mut R, source_ctoken_account: Pubkey, destination_spl_token_account: Pubkey, @@ -24,7 +25,7 @@ pub async fn ctoken_to_spl_transfer( let (token_pool_pda, token_pool_pda_bump) = find_token_pool_pda_with_index(&mint, 0); // Create the transfer instruction - let transfer_ix = create_ctoken_to_spl_transfer_instruction( + let transfer_ix = create_transfer_ctoken_to_spl_instruction( source_ctoken_account, destination_spl_token_account, amount, @@ -33,6 +34,7 @@ pub async fn ctoken_to_spl_transfer( payer.pubkey(), token_pool_pda, token_pool_pda_bump, + Pubkey::new_from_array(SPL_TOKEN_PROGRAM_ID), // TODO: make dynamic ) .map_err(|e| RpcError::AssertRpcError(format!("Failed to create instruction: {:?}", e)))?; diff --git a/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs b/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs index 99607ed3aa..890e95b793 100644 --- a/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs +++ b/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs @@ -3,7 +3,8 @@ use light_client::{ rpc::{Rpc, RpcError}, }; use light_compressed_token_sdk::{ - account2::create_spl_to_ctoken_transfer_instruction, token_pool::find_token_pool_pda_with_index, + instructions::create_transfer_spl_to_ctoken_instruction, + token_pool::find_token_pool_pda_with_index, SPL_TOKEN_PROGRAM_ID, }; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -12,21 +13,7 @@ use solana_signer::Signer; use spl_pod::bytemuck::pod_from_bytes; use spl_token_2022::pod::PodAccount; -/// Transfer SPL tokens directly to compressed tokens in a single transaction. -/// -/// This function wraps `create_spl_to_ctoken_transfer_instruction` to provide -/// a convenient action for transferring from SPL token accounts to compressed tokens. -/// -/// # Arguments -/// * `rpc` - RPC client with indexer capabilities -/// * `source_spl_token_account` - The SPL token account to transfer from -/// * `to` - Recipient pubkey for the compressed tokens -/// * `amount` - Amount of tokens to transfer -/// * `authority` - Authority that can spend from the SPL token account -/// * `payer` - Transaction fee payer -/// -/// # Returns -/// `Result` - The transaction signature +/// Transfer SPL tokens to compressed tokens pub async fn spl_to_ctoken_transfer( rpc: &mut R, source_spl_token_account: Pubkey, @@ -35,7 +22,6 @@ pub async fn spl_to_ctoken_transfer( authority: &Keypair, payer: &Keypair, ) -> Result { - // Get mint from SPL token account let token_account_info = rpc .get_account(source_spl_token_account) .await? @@ -46,11 +32,9 @@ pub async fn spl_to_ctoken_transfer( let mint = pod_account.mint; - // Derive token pool PDA let (token_pool_pda, bump) = find_token_pool_pda_with_index(&mint, 0); - // Create the SPL to CToken transfer instruction - let ix = create_spl_to_ctoken_transfer_instruction( + let ix = create_transfer_spl_to_ctoken_instruction( source_spl_token_account, to, amount, @@ -59,16 +43,15 @@ pub async fn spl_to_ctoken_transfer( payer.pubkey(), token_pool_pda, bump, + Pubkey::new_from_array(SPL_TOKEN_PROGRAM_ID), // TODO: make dynamic ) .map_err(|e| RpcError::CustomError(e.to_string()))?; - // Prepare signers let mut signers = vec![payer]; if authority.pubkey() != payer.pubkey() { signers.push(authority); } - // Send transaction rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &signers) .await } diff --git a/sdk-libs/token-client/src/instructions/create_spl_mint.rs b/sdk-libs/token-client/src/instructions/create_spl_mint.rs index 2ad637160d..5ac5a40ce0 100644 --- a/sdk-libs/token-client/src/instructions/create_spl_mint.rs +++ b/sdk-libs/token-client/src/instructions/create_spl_mint.rs @@ -3,9 +3,12 @@ use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; -use light_compressed_token_sdk::instructions::{ - create_spl_mint_instruction as sdk_create_spl_mint_instruction, derive_token_pool, - find_spl_mint_address, CreateSplMintInputs, +use light_compressed_token_sdk::{ + instructions::{ + create_spl_mint_instruction as sdk_create_spl_mint_instruction, find_spl_mint_address, + CreateSplMintInputs, + }, + token_pool::derive_token_pool, }; use light_ctoken_types::{ instructions::mint_action::CompressedMintWithContext, state::CompressedMint, diff --git a/sdk-libs/token-client/src/instructions/mint_action.rs b/sdk-libs/token-client/src/instructions/mint_action.rs index d9b06c2017..6fa6dad77a 100644 --- a/sdk-libs/token-client/src/instructions/mint_action.rs +++ b/sdk-libs/token-client/src/instructions/mint_action.rs @@ -3,9 +3,12 @@ use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; -use light_compressed_token_sdk::instructions::{ - create_mint_action, derive_compressed_mint_address, derive_token_pool, find_spl_mint_address, - mint_action::{MintActionInputs, MintActionType, MintToRecipient}, +use light_compressed_token_sdk::{ + instructions::{ + create_mint_action, derive_compressed_mint_address, find_spl_mint_address, + mint_action::{MintActionInputs, MintActionType, MintToRecipient}, + }, + token_pool::derive_token_pool, }; use light_ctoken_types::{ instructions::{ diff --git a/sdk-libs/token-client/src/instructions/mint_to_compressed.rs b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs index 11fa87ab2d..74b67d73c4 100644 --- a/sdk-libs/token-client/src/instructions/mint_to_compressed.rs +++ b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs @@ -5,10 +5,10 @@ use light_client::{ }; use light_compressed_token_sdk::{ instructions::{ - create_mint_to_compressed_instruction, derive_compressed_mint_from_spl_mint, - derive_token_pool, DecompressedMintConfig, MintToCompressedInputs, + create_mint_to_compressed_instruction, derive_cmint_from_spl_mint, DecompressedMintConfig, + MintToCompressedInputs, }, - token_pool::find_token_pool_pda_with_index, + token_pool::{derive_token_pool, find_token_pool_pda_with_index}, }; use light_ctoken_types::{ instructions::mint_action::{CompressedMintWithContext, Recipient}, @@ -28,8 +28,7 @@ pub async fn mint_to_compressed_instruction( ) -> Result { // Derive compressed mint address from SPL mint PDA let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_mint_address = - derive_compressed_mint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); + let compressed_mint_address = derive_cmint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); // Get the compressed mint account let compressed_mint_account = rpc diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index 8f5d67d6dc..3e6a942795 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -1,2 +1,4 @@ pub mod actions; pub mod instructions; +// re-export +pub use light_compressed_token_sdk::ctoken; diff --git a/sdk-tests/client-test/Cargo.toml b/sdk-tests/client-test/Cargo.toml index 20ab2c3169..86c9cfaa1d 100644 --- a/sdk-tests/client-test/Cargo.toml +++ b/sdk-tests/client-test/Cargo.toml @@ -26,6 +26,7 @@ light-zero-copy = { workspace = true } light-hasher = { workspace = true, features = ["poseidon"] } light-compressed-account = { workspace = true, features = ["std"] } light-compressed-token = { workspace = true } +light-compressed-token-sdk = { workspace = true } light-indexed-array = { workspace = true } light-merkle-tree-reference = { workspace = true } light-macros = { workspace = true } diff --git a/sdk-tests/client-test/tests/light_client.rs b/sdk-tests/client-test/tests/light_client.rs index 7f82532f10..d36fd37e90 100644 --- a/sdk-tests/client-test/tests/light_client.rs +++ b/sdk-tests/client-test/tests/light_client.rs @@ -11,13 +11,11 @@ use light_compressed_account::{hash_to_bn254_field_size_be, TreeType}; use light_compressed_token::mint_sdk::{ create_create_token_pool_instruction, create_mint_to_instruction, }; +use light_compressed_token_sdk::compat::{AccountState, TokenData}; use light_hasher::Poseidon; use light_merkle_tree_reference::{indexed::IndexedMerkleTree, MerkleTree}; use light_program_test::accounts::test_accounts::TestAccounts; -use light_sdk::{ - address::{v1::derive_address, NewAddressParams}, - token::{AccountState, TokenData}, -}; +use light_sdk::address::{v1::derive_address, NewAddressParams}; use light_test_utils::{system_program::create_invoke_instruction, Rpc, RpcError}; use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_keypair::Keypair; diff --git a/sdk-tests/client-test/tests/light_program_test.rs b/sdk-tests/client-test/tests/light_program_test.rs index bf3b49fe6f..c51ee482cf 100644 --- a/sdk-tests/client-test/tests/light_program_test.rs +++ b/sdk-tests/client-test/tests/light_program_test.rs @@ -9,13 +9,11 @@ use light_compressed_account::hash_to_bn254_field_size_be; use light_compressed_token::mint_sdk::{ create_create_token_pool_instruction, create_mint_to_instruction, }; +use light_compressed_token_sdk::compat::{AccountState, TokenData}; use light_program_test::{ accounts::test_accounts::TestAccounts, program_test::LightProgramTest, ProgramTestConfig, }; -use light_sdk::{ - address::{v1::derive_address, NewAddressParams}, - token::{AccountState, TokenData}, -}; +use light_sdk::address::{v1::derive_address, NewAddressParams}; use light_test_utils::{system_program::create_invoke_instruction, RpcError}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, diff --git a/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs new file mode 100644 index 0000000000..2a53f9b1d5 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs @@ -0,0 +1,280 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use sdk_compressible_test::UserRecord; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests +// 1. init compressed, decompress, and compress +// 2. update_record bumps compression info +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(101).unwrap(); + + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; + assert!(result.is_err(), "Compression should fail due to slot delay"); + if let Err(err) = result { + let err_msg = format!("{:?}", err); + assert!( + err_msg.contains("Custom(16001)"), + "Expected error message about slot delay, got: {}", + err_msg + ); + } + rpc.warp_to_slot(200).unwrap(); + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; + assert!(result.is_ok(), "Compression should succeed"); +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(150).unwrap(); + + let accounts = sdk_compressible_test::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = sdk_compressible_test::instruction::UpdateRecord { + name: "Updated User".to_string(), + score: 42, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_ok(), "Update record transaction should succeed"); + + rpc.warp_to_slot(200).unwrap(); + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User record account should exist after update" + ); + + let account_data = user_pda_account.unwrap().data; + let updated_user_record = UserRecord::try_deserialize(&mut &account_data[..]).unwrap(); + + assert_eq!(updated_user_record.name, "Updated User"); + assert_eq!(updated_user_record.score, 42); + assert_eq!(updated_user_record.owner, payer.pubkey()); + + assert_eq!( + updated_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + 150 + ); + assert!(!updated_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +pub async fn compress_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + should_fail: bool, +) -> Result { + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User PDA account should exist before compression" + ); + let account = user_pda_account.unwrap(); + assert!( + account.lamports > 0, + "Account should have lamports before compression" + ); + assert!( + !account.data.is_empty(), + "Account data should not be empty before compression" + ); + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value + .unwrap(); + let compressed_address = compressed_account.address.unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*user_record_pda], + &[account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![sdk_compressible_test::get_userrecord_seeds(&payer.pubkey()).0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + if should_fail { + assert!(result.is_err(), "Compress transaction should fail"); + return result; + } else { + assert!(result.is_ok(), "Compress transaction should succeed"); + } + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); + Ok(result.unwrap()) +} diff --git a/sdk-tests/sdk-compressible-test/Anchor.toml b/sdk-tests/sdk-compressible-test/Anchor.toml new file mode 100644 index 0000000000..7225c40f12 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/Anchor.toml @@ -0,0 +1,19 @@ +[toolchain] + +[features] +resolution = true +skip-lint = false + +[programs.localnet] +sdk_compressible_test = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + diff --git a/sdk-tests/sdk-compressible-test/Cargo.toml b/sdk-tests/sdk-compressible-test/Cargo.toml new file mode 100644 index 0000000000..1cc326d68b --- /dev/null +++ b/sdk-tests/sdk-compressible-test/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "sdk-compressible-test" +version = "0.1.0" +description = "Simple Anchor program template with user records" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "sdk_compressible_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-hasher = { workspace = true, features = ["solana"] } +solana-program = { workspace = true } +light-macros = { workspace = true, features = ["solana"] } +borsh = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } +anchor-lang = { workspace = true, features = ["idl-build"] } +anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } +light-ctoken-types = { workspace = true, features = ["anchor"] } +light-compressed-token-sdk = { workspace = true, features = ["anchor"] } +light-compressed-token-types = { workspace = true, features = ["anchor"] } +light-compressible = { workspace = true, features = ["anchor"] } + +[dev-dependencies] +light-token-client = { workspace = true } +light-program-test = { workspace = true, features = ["v2", "devenv"] } +light-client = { workspace = true, features = ["v2"] } +light-compressible-client = { workspace = true, features = ["anchor"] } +light-test-utils = { workspace = true} +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-logger = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-signature = { workspace = true } +solana-signer = { workspace = true } +solana-keypair = { workspace = true } +solana-account = { workspace = true } +bincode = "1.3" + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] \ No newline at end of file diff --git a/sdk-tests/sdk-compressible-test/Xargo.toml b/sdk-tests/sdk-compressible-test/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/sdk-tests/sdk-compressible-test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/sdk-tests/sdk-compressible-test/package.json b/sdk-tests/sdk-compressible-test/package.json new file mode 100644 index 0000000000..3711173d19 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@lightprotocol/sdk-compressible-test", + "version": "0.1.0", + "license": "Apache-2.0", + "scripts": { + "build": "cargo build-sbf", + "test": "cargo test-sbf -p sdk-compressible-test -- --nocapture" + }, + "nx": {} +} + diff --git a/sdk-tests/sdk-compressible-test/src/constants.rs b/sdk-tests/sdk-compressible-test/src/constants.rs new file mode 100644 index 0000000000..2fcae0aa3a --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/constants.rs @@ -0,0 +1,3 @@ +pub const POOL_VAULT_SEED: &str = "pool_vault"; +pub const USER_RECORD_SEED: &str = "user_record"; +pub const CTOKEN_SIGNER_SEED: &str = "ctoken_signer"; diff --git a/sdk-tests/sdk-compressible-test/src/errors.rs b/sdk-tests/sdk-compressible-test/src/errors.rs new file mode 100644 index 0000000000..78da53fab4 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/errors.rs @@ -0,0 +1,91 @@ +use anchor_lang::prelude::*; + +#[repr(u32)] +pub enum ErrorCode { + InvalidAccountCount, + InvalidRentRecipient, + MintCreationFailed, + MissingCompressedTokenProgram, + MissingCompressedTokenProgramAuthorityPDA, + RentRecipientMismatch, + InvalidAccountDiscriminator, + DerivedTokenAccountMismatch, + MissingAuthority, + MissingCpiContext, +} + +#[automatically_derived] +impl ::core::fmt::Debug for ErrorCode { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str( + f, + match self { + ErrorCode::InvalidAccountCount => "InvalidAccountCount", + ErrorCode::InvalidRentRecipient => "InvalidRentRecipient", + ErrorCode::MintCreationFailed => "MintCreationFailed", + ErrorCode::MissingCompressedTokenProgram => "MissingCompressedTokenProgram", + ErrorCode::MissingCompressedTokenProgramAuthorityPDA => { + "MissingCompressedTokenProgramAuthorityPDA" + } + ErrorCode::RentRecipientMismatch => "RentRecipientMismatch", + ErrorCode::InvalidAccountDiscriminator => "InvalidAccountDiscriminator", + ErrorCode::DerivedTokenAccountMismatch => "DerivedTokenAccountMismatch", + ErrorCode::MissingAuthority => "MissingAuthority", + ErrorCode::MissingCpiContext => "MissingCpiContext", + }, + ) + } +} + +impl std::fmt::Display for ErrorCode { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + ErrorCode::InvalidAccountCount => fmt.write_fmt(format_args!( + "Invalid account count: PDAs and compressed accounts must match", + )), + ErrorCode::InvalidRentRecipient => { + fmt.write_fmt(format_args!("Rent recipient does not match config")) + } + ErrorCode::MintCreationFailed => { + fmt.write_fmt(format_args!("Failed to create compressed mint")) + } + ErrorCode::MissingCompressedTokenProgram => fmt.write_fmt(format_args!( + "Compressed token program account not found in remaining accounts", + )), + ErrorCode::MissingCompressedTokenProgramAuthorityPDA => fmt.write_fmt(format_args!( + "Compressed token program authority PDA account not found in remaining accounts", + )), + ErrorCode::RentRecipientMismatch => { + fmt.write_fmt(format_args!("Rent recipient does not match config")) + } + ErrorCode::InvalidAccountDiscriminator => fmt.write_fmt(format_args!( + "Trying to compress account with invalid discriminator" + )), + ErrorCode::DerivedTokenAccountMismatch => fmt.write_fmt(format_args!( + "Derived token account address must match owner_info.key" + )), + ErrorCode::MissingAuthority => fmt.write_fmt(format_args!( + "Authority account is missing from CPI accounts" + )), + ErrorCode::MissingCpiContext => fmt.write_fmt(format_args!( + "CPI context account is missing from CPI accounts" + )), + } + } +} + +impl From for ProgramError { + fn from(e: ErrorCode) -> Self { + ProgramError::Custom(e as u32) + } +} + +#[repr(u32)] +pub enum CompressibleInstructionError { + InvalidRentRecipient, + CTokenDecompressionNotImplemented, + PdaDecompressionNotImplemented, + TokenCompressionNotImplemented, + PdaCompressionNotImplemented, +} diff --git a/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs b/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs new file mode 100644 index 0000000000..560219358d --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs @@ -0,0 +1,222 @@ +use anchor_lang::prelude::*; + +/// CompressAccountsIdempotent, DecompressAccountsIdempotent, +/// InitializeCompressionConfig, UpdateCompressionConfig accounts are all +/// auto-generated by compressible_instructions macro. +use crate::state::*; + +#[derive(Accounts)] +pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(placeholder_id: u64)] +pub struct CreatePlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + compression_info + owner + string len + name + placeholder_id + space = 8 + 10 + 32 + 4 + 32 + 8, + seeds = [b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + bump, + )] + pub placeholder_record: Account<'info, PlaceholderRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(account_data: AccountCreationData)] +pub struct CreateUserRecordAndGameSession<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + #[account( + init, + payer = user, + // discriminator + option + session_id + player + + // string len + game_type + start_time + end_time(Option) + score + space = 8 + 10 + 8 + 32 + 4 + 32 + 8 + 9 + 8, + seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + + // Compressed mint creation accounts - only token-specific ones needed + /// The mint signer used for PDA derivation + pub mint_signer: Signer<'info>, + + /// The mint authority used for PDA derivation + pub mint_authority: Signer<'info>, + + /// Compressed token program + /// CHECK: Program ID validated using C_TOKEN_PROGRAM_ID constant + pub ctoken_program: UncheckedAccount<'info>, + + /// CHECK: CPI authority of the compressed token program + pub compress_token_program_cpi_authority: UncheckedAccount<'info>, + + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct CreateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + init, + payer = player, + space = 8 + 9 + 8 + 32 + 4 + 32 + 8 + 9 + 8, // discriminator + compression_info + session_id + player + string len + game_type + start_time + end_time(Option) + score + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user.key().as_ref()], + bump, + constraint = user_record.owner == user.key() + )] + pub user_record: Account<'info, UserRecord>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct UpdateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + mut, + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + constraint = game_session.player == player.key() + )] + pub game_session: Account<'info, GameSession>, +} + +#[derive(Accounts)] +pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +// TODO: split into one ix with ctoken and one without. +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: load_checked. + pub config: AccountInfo<'info>, + /// UNCHECKED: Anyone can pay to init PDAs. + #[account(mut)] + pub rent_payer: Signer<'info>, + /// CHECK: Checked in protocol. + #[account(mut)] + pub ctoken_rent_sponsor: UncheckedAccount<'info>, + /// CHECK: Checked in protocol. + pub ctoken_config: UncheckedAccount<'info>, + /// ctoken program (always required in mixed variant) + /// CHECK: Checked by Protocol. + pub ctoken_program: UncheckedAccount<'info>, + /// CPI authority PDA of the compressed token program (always required in mixed variant) + /// CHECK: Checked by Protocol. + pub ctoken_cpi_authority: UncheckedAccount<'info>, + /// CHECK: unchecked. + pub some_mint: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct InitializeCompressionConfig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Config PDA is created and validated by the SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// The program's data account + /// CHECK: Program data account is validated by the SDK + pub program_data: AccountInfo<'info>, + /// The program's upgrade authority (must sign) + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct UpdateCompressionConfig<'info> { + /// CHECK: Config PDA is created and validated by the SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// Must match the update authority stored in config + pub authority: Signer<'info>, +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs new file mode 100644 index 0000000000..1b2a77f7a0 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs @@ -0,0 +1,137 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{compress_account::prepare_account_for_compression, CompressibleConfig}, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, ValidityProof}, + LightDiscriminator, +}; + +/// Auto-generated by compressible_instructions macro. +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, +) -> Result<()> { + let compression_config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + if ctx.accounts.rent_sponsor.key() != compression_config.rent_sponsor { + msg!( + "rent recipient passed: {:?}", + ctx.accounts.rent_sponsor.key() + ); + msg!( + "rent recipient config: {:?}", + compression_config.rent_sponsor + ); + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + let cpi_accounts = CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ); + + let pda_accounts_start = ctx.remaining_accounts.len() - signer_seeds.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + + let mut compressed_pda_infos = Vec::new(); + let mut pda_indices_to_close: Vec = Vec::new(); + + for (i, account_info) in solana_accounts.iter().enumerate() { + if account_info.data_is_empty() { + msg!("No data. Account already compressed or uninitialized. Skipping."); + continue; + } + if account_info.owner == &crate::ID { + let data = account_info.try_borrow_data()?; + let discriminator = &data[0..8]; + let meta = compressed_accounts[i]; + + // TODO: consider CHECKING seeds. + match discriminator { + d if d == UserRecord::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data()?; + let mut account_data = UserRecord::try_deserialize(&mut &data_borrow[..])?; + drop(data_borrow); + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + account_info, + &mut account_data, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + compressed_pda_infos.push(compressed_info); + pda_indices_to_close.push(i); + } + d if d == GameSession::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data()?; + let mut account_data = GameSession::try_deserialize(&mut &data_borrow[..])?; + drop(data_borrow); + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + account_info, + &mut account_data, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + compressed_pda_infos.push(compressed_info); + pda_indices_to_close.push(i); + } + d if d == PlaceholderRecord::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data()?; + let mut account_data = + PlaceholderRecord::try_deserialize(&mut &data_borrow[..])?; + drop(data_borrow); + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + account_info, + &mut account_data, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + compressed_pda_infos.push(compressed_info); + pda_indices_to_close.push(i); + } + _ => { + return Err(ProgramError::from(ErrorCode::InvalidAccountDiscriminator).into()); + } + } + } + } + let has_pdas = !compressed_pda_infos.is_empty(); + if has_pdas { + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_account_infos(&compressed_pda_infos) + .invoke(cpi_accounts)?; + + // Close + for idx in pda_indices_to_close.into_iter() { + let mut info = solana_accounts[idx].clone(); + light_sdk::compressible::close::close(&mut info, ctx.accounts.rent_sponsor.clone()) + .map_err(anchor_lang::prelude::ProgramError::from)?; + } + } + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs new file mode 100644 index 0000000000..e6fcd68e13 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs @@ -0,0 +1,76 @@ +use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn create_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, + session_id: u64, + game_type: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, +) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + // Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Set your account data. + game_session.session_id = session_id; + game_session.player = ctx.accounts.player.key(); + game_session.game_type = game_type; + game_session.start_time = Clock::get()?.unix_timestamp as u64; + game_session.end_time = None; + game_session.score = 0; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // Create CPI accounts. + let player_account_info = ctx.accounts.player.to_account_info(); + let cpi_accounts = CpiAccounts::new( + &player_account_info, + ctx.remaining_accounts, + LIGHT_CPI_SIGNER, + ); + + // Prepare new address params. The cpda takes the address of the + // compressible pda account as seed. + let new_address_params = address_tree_info + .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(0)); + + let game_session_info = game_session.to_account_info(); + let game_session_data_mut = &mut **game_session; + let compressed_info = prepare_compressed_account_on_init::( + &game_session_info, + game_session_data_mut, + compressed_address, + new_address_params, + output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_new_addresses(&[new_address_params]) + .with_account_infos(&[compressed_info]) + .invoke(cpi_accounts)?; + + game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs new file mode 100644 index 0000000000..b8ee6fbe06 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs @@ -0,0 +1,67 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, +) -> Result<()> { + let placeholder_record = &mut ctx.accounts.placeholder_record; + + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + placeholder_record.owner = ctx.accounts.user.key(); + placeholder_record.name = name; + placeholder_record.placeholder_id = placeholder_id; + + // Verify rent recipient matches config + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); + + let new_address_params = address_tree_info.into_new_address_params_assigned_packed( + placeholder_record.key().to_bytes().into(), + Some(0), + ); + + let placeholder_info = placeholder_record.to_account_info(); + let placeholder_data_mut = &mut **placeholder_record; + let compressed_info = prepare_compressed_account_on_init::( + &placeholder_info, + placeholder_data_mut, + compressed_address, + new_address_params, + output_state_tree_index, + &cpi_accounts, + &config.address_space, + false, // with_data = false for empty compressed account + )?; + + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_new_addresses(&[new_address_params]) + .with_account_infos(&[compressed_info]) + .invoke(cpi_accounts)?; + + // Note: PDA is NOT closed in this example (compression_info is set, account remains) + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs new file mode 100644 index 0000000000..eb50dfc649 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs @@ -0,0 +1,67 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn create_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, +) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + // 1. Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + user_record.owner = ctx.accounts.user.key(); + user_record.name = name; + user_record.score = 11; + + // 2. Verify rent recipient matches config + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // 3. Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); + + let new_address_params = address_tree_info + .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); + + let user_record_info = user_record.to_account_info(); + let user_record_data_mut = &mut **user_record; + let compressed_info = prepare_compressed_account_on_init::( + &user_record_info, + user_record_data_mut, + compressed_address, + new_address_params, + output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_new_addresses(&[new_address_params]) + .with_account_infos(&[compressed_info]) + .invoke(cpi_accounts)?; + + // Close the PDA + user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs new file mode 100644 index 0000000000..2d82dc0884 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs @@ -0,0 +1,205 @@ +use anchor_lang::{ + prelude::*, + solana_program::{program::invoke, sysvar::clock::Clock}, +}; +use light_compressed_token_sdk::instructions::{ + create_mint_action_cpi, find_spl_mint_address, MintActionInputs, +}; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, +}; +use light_sdk_types::{ + cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, seeds::*, state::*, LIGHT_CPI_SIGNER}; +pub fn create_user_record_and_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, + account_data: AccountCreationData, + compression_params: CompressionParams, +) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + let game_session = &mut ctx.accounts.game_session; + + // Load your config checked. + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // Set your account data. + user_record.owner = ctx.accounts.user.key(); + user_record.name = account_data.user_name.clone(); + user_record.score = 11; + + game_session.session_id = account_data.session_id; + game_session.player = ctx.accounts.user.key(); + game_session.game_type = account_data.game_type.clone(); + game_session.start_time = Clock::get()?.unix_timestamp as u64; + game_session.end_time = None; + game_session.score = 0; + + // Create CPI accounts from remaining accounts + let cpi_accounts = CpiAccounts::new_with_config( + ctx.accounts.user.as_ref(), + ctx.remaining_accounts, + CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), + ); + let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); + let cpi_context_account = cpi_accounts.cpi_context().unwrap(); + + // Prepare new address params. One per pda account. + let user_new_address_params = compression_params + .user_address_tree_info + .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); + let game_new_address_params = compression_params + .game_address_tree_info + .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); + + let mut all_compressed_infos = Vec::new(); + + // Prepare user record for compression + let user_record_info = user_record.to_account_info(); + let user_record_data_mut = &mut **user_record; + let user_compressed_info = prepare_compressed_account_on_init::( + &user_record_info, + user_record_data_mut, + compression_params.user_compressed_address, + user_new_address_params, + compression_params.user_output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + + all_compressed_infos.push(user_compressed_info); + + // Prepare game session for compression + let game_session_info = game_session.to_account_info(); + let game_session_data_mut = &mut **game_session; + let game_compressed_info = prepare_compressed_account_on_init::( + &game_session_info, + game_session_data_mut, + compression_params.game_compressed_address, + game_new_address_params, + compression_params.game_output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + all_compressed_infos.push(game_compressed_info); + + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority().unwrap(), + cpi_context: cpi_context_account, + cpi_signer: LIGHT_CPI_SIGNER, + }; + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) + .with_new_addresses(&[user_new_address_params, game_new_address_params]) + .with_account_infos(&all_compressed_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(cpi_context_accounts)?; + + // these are custom seeds of the caller program that are used to derive the program owned onchain tokenb account PDA. + // dual use: as owner of the compressed token account. + let mint = find_spl_mint_address(&ctx.accounts.mint_signer.key()).0; + let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); + + let actions = vec![ + light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { + recipients: vec![ + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: token_account_address, // TRY: THE DECOMPRESS TOKEN ACCOUNT ADDRES IS THE OWNER OF ITS COMPRESSIBLED VERSION. + amount: 1000, // Mint the full supply to the user + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer2_seeds(&ctx.accounts.user.key()).1, + amount: 1000, + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer3_seeds(&ctx.accounts.user.key()).1, + amount: 1000, + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer4_seeds( + &ctx.accounts.user.key(), + &ctx.accounts.user.key(), + ) + .1, // user as fee_payer + amount: 1000, + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer5_seeds(&ctx.accounts.user.key(), &mint, 42).1, // Fixed index 42 + amount: 1000, + }, + ], + token_account_version: 3, + }, + ]; + + let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA + let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA + + let mint_action_inputs = MintActionInputs { + compressed_mint_inputs: compression_params.mint_with_context.clone(), + mint_seed: ctx.accounts.mint_signer.key(), + mint_bump: Some(compression_params.mint_bump), + create_mint: true, + authority: ctx.accounts.mint_authority.key(), + payer: ctx.accounts.user.key(), + proof: compression_params.proof.into(), + actions, + input_queue: None, // Not needed for create_mint: true + output_queue, + tokens_out_queue: Some(output_queue), // For MintTo actions + address_tree_pubkey, + token_pool: None, // Not needed for simple compressed mint creation + }; + + let mint_action_instruction = create_mint_action_cpi( + mint_action_inputs, + Some(light_ctoken_types::instructions::mint_action::CpiContext { + address_tree_pubkey: address_tree_pubkey.to_bytes(), + set_context: false, + first_set_context: false, + in_tree_index: 1, // address tree + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 2, + read_only_address_trees: [0; 4], + }), + Some(cpi_context_pubkey), + ) + .unwrap(); + + // Get all account infos needed for the mint action + let mut account_infos = cpi_accounts.to_account_infos(); + account_infos.push( + ctx.accounts + .compress_token_program_cpi_authority + .to_account_info(), + ); + account_infos.push(ctx.accounts.ctoken_program.to_account_info()); + account_infos.push(ctx.accounts.mint_authority.to_account_info()); + account_infos.push(ctx.accounts.mint_signer.to_account_info()); + account_infos.push(ctx.accounts.user.to_account_info()); + + // Invoke the mint action instruction directly + invoke(&mint_action_instruction, &account_infos)?; + + // at the end of the instruction we always clean up all onchain pdas that we compressed + user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; + game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs new file mode 100644 index 0000000000..de7bb34f5f --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs @@ -0,0 +1,426 @@ +// Auto-generated by compressible_instructions macro. +use anchor_lang::prelude::*; +use light_compressed_token_sdk::instructions::create_token_account::create_ctoken_account_signed; +use light_sdk::{ + compressible::{ + decompress_idempotent::{ + into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, + }, + Unpack, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, +}; +use light_sdk_types::cpi_accounts::CpiAccountsConfig; + +use crate::{constants::*, errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; +pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, +) -> Result<()> { + // Helper functions to handle each account type - kept out of main frame + #[inline(never)] + #[allow(clippy::too_many_arguments)] + fn handle_user_record<'b, 'info>( + data: UserRecord, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + solana_accounts: &[AccountInfo<'info>], + i: usize, + address_space: Pubkey, + cpi_accounts: &CpiAccounts<'b, 'info>, + rent_payer: &Signer<'info>, + out: &mut Vec< + light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, + >, + ) -> Result<()> { + let seeds_vec = { + let seeds: &[&[u8]] = &[USER_RECORD_SEED.as_bytes(), (data.owner).as_ref()]; + let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] + }; + let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); + let infos = prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), + &solana_accounts[i], + rent_payer, + cpi_accounts, + seed_refs.as_slice(), + ) + .map_err(ProgramError::from)?; + out.extend(infos); + Ok(()) + } + + #[inline(never)] + #[allow(clippy::too_many_arguments)] + fn handle_game_session<'b, 'info>( + data: GameSession, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + solana_accounts: &[AccountInfo<'info>], + i: usize, + address_space: Pubkey, + cpi_accounts: &CpiAccounts<'b, 'info>, + rent_payer: &Signer<'info>, + out: &mut Vec< + light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, + >, + ) -> Result<()> { + let seed_binding_1 = data.session_id.to_le_bytes(); + let seeds_vec = { + let seeds: &[&[u8]] = &["game_session".as_bytes(), seed_binding_1.as_ref()]; + let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] + }; + let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); + let infos = prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), + &solana_accounts[i], + rent_payer, + cpi_accounts, + seed_refs.as_slice(), + ) + .map_err(ProgramError::from)?; + out.extend(infos); + Ok(()) + } + + #[inline(never)] + #[allow(clippy::too_many_arguments)] + fn handle_placeholder_record<'b, 'info>( + data: PlaceholderRecord, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + solana_accounts: &[AccountInfo<'info>], + i: usize, + address_space: Pubkey, + cpi_accounts: &CpiAccounts<'b, 'info>, + rent_payer: &Signer<'info>, + out: &mut Vec< + light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, + >, + ) -> Result<()> { + let seed_binding_1 = data.placeholder_id.to_le_bytes(); + let seeds_vec = { + let seeds: &[&[u8]] = &["placeholder_record".as_bytes(), seed_binding_1.as_ref()]; + let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] + }; + let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); + let infos = prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), + &solana_accounts[i], + rent_payer, + cpi_accounts, + seed_refs.as_slice(), + ) + .map_err(ProgramError::from)?; + out.extend(infos); + Ok(()) + } + + #[inline(never)] + fn check_account_types(compressed_accounts: &[CompressedAccountData]) -> (bool, bool) { + let (mut has_tokens, mut has_pdas) = (false, false); + for c in compressed_accounts { + match c.data { + CompressedAccountVariant::PackedCTokenData(_) => { + has_tokens = true; + } + _ => has_pdas = true, + } + if has_tokens && has_pdas { + break; + } + } + (has_tokens, has_pdas) + } + /// Helper function to process token decompression - separated to avoid stack overflow + #[inline(never)] + #[allow(clippy::too_many_arguments, clippy::extra_unused_lifetimes)] + fn process_tokens<'a, 'b, 'info>( + accounts: &DecompressAccountsIdempotent<'info>, + remaining_accounts: &[anchor_lang::prelude::AccountInfo<'info>], + fee_payer: &anchor_lang::prelude::AccountInfo<'info>, + ctoken_program: &anchor_lang::prelude::UncheckedAccount<'info>, + ctoken_rent_sponsor: &anchor_lang::prelude::AccountInfo<'info>, + ctoken_cpi_authority: &anchor_lang::prelude::UncheckedAccount<'info>, + ctoken_config: &anchor_lang::prelude::AccountInfo<'info>, + config: &anchor_lang::prelude::AccountInfo<'info>, + ctoken_accounts: Vec<( + light_compressed_token_sdk::compat::PackedCTokenData, + light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + )>, + proof: light_sdk::instruction::ValidityProof, + cpi_accounts: &CpiAccounts<'b, 'info>, + post_system_accounts: &[anchor_lang::prelude::AccountInfo<'info>], + has_pdas: bool, + ) -> Result<()> { + let mut token_decompress_indices: Box< + Vec, + > = Box::new(Vec::with_capacity(ctoken_accounts.len())); + // Collect per-owner signer seed groups; invoke_signed requires one seed group per PDA signer + let mut token_signers_seed_groups: Vec>> = + Vec::with_capacity(ctoken_accounts.len()); + let packed_accounts = post_system_accounts; + use crate::seeds::ctoken_seed_system::{CTokenSeedContext, CTokenSeedProvider}; + let seed_context = CTokenSeedContext { + accounts, + remaining_accounts, + }; + let authority = cpi_accounts + .authority() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; + let cpi_context = cpi_accounts + .cpi_context() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; + + for (token_data, meta) in ctoken_accounts.into_iter() { + let owner_index: u8 = token_data.token_data.owner; + let mint_index: u8 = token_data.token_data.mint; + let mint_info = packed_accounts[mint_index as usize].to_account_info(); + let owner_info = packed_accounts[owner_index as usize].to_account_info(); + let (ctoken_signer_seeds, derived_token_account_address) = + token_data.variant.get_seeds(&seed_context); + { + if derived_token_account_address != *owner_info.key { + msg!( + "derived_token_account_address: {:?}", + derived_token_account_address + ); + msg!("owner_info.key: {:?}", owner_info.key); + return Err(ProgramError::from(ErrorCode::DerivedTokenAccountMismatch).into()); + } + + // Convert Vec> to &[&[&[u8]]] + let seed_refs: Vec<&[u8]> = + ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect(); + let seeds_slice: &[&[u8]] = &seed_refs; + + create_ctoken_account_signed( + crate::ID, + fee_payer.clone().to_account_info(), + owner_info.clone(), + mint_info.clone(), + *authority.clone().to_account_info().key, + seeds_slice, + ctoken_rent_sponsor.clone().to_account_info(), + ctoken_config.to_account_info(), + Some(2), // TODO: make this configurable + None, // TODO: make this configurable + )?; + } + + // Construct MultiInputTokenDataWithContext from token data and meta + let source = + light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext { + owner: token_data.token_data.owner, + amount: token_data.token_data.amount, + has_delegate: token_data.token_data.has_delegate, + delegate: token_data.token_data.delegate, + mint: token_data.token_data.mint, + version: token_data.token_data.version, + merkle_context: meta.tree_info.into(), + root_index: meta.tree_info.root_index, + }; + let decompress_index = + light_compressed_token_sdk::instructions::DecompressFullIndices { + source, + destination_index: owner_index, + }; + token_decompress_indices.push(decompress_index); + token_signers_seed_groups.push(ctoken_signer_seeds); + } + + let ctoken_ix = + light_compressed_token_sdk::instructions::decompress_full_ctoken_accounts_with_indices( + fee_payer.key(), + proof, + if has_pdas { + Some(cpi_context.key()) + } else { + None + }, + &token_decompress_indices, + packed_accounts, + ) + .map_err(anchor_lang::prelude::ProgramError::from)?; + { + let mut all_account_infos = <[_]>::into_vec(Box::new([fee_payer.to_account_info()])); + all_account_infos.extend(ctoken_cpi_authority.to_account_infos()); + all_account_infos.extend(ctoken_program.to_account_infos()); + all_account_infos.extend(ctoken_rent_sponsor.to_account_infos()); + all_account_infos.extend(config.to_account_infos()); + all_account_infos.extend(cpi_accounts.to_account_infos()); + // Build &[&[&[u8]]] where each inner slice is a distinct PDA seed group + let signer_seed_refs: Vec> = token_signers_seed_groups + .iter() + .map(|group| group.iter().map(|s| s.as_slice()).collect()) + .collect(); + let signer_seed_slices: Vec<&[&[u8]]> = + signer_seed_refs.iter().map(|g| g.as_slice()).collect(); + + anchor_lang::solana_program::program::invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + signer_seed_slices.as_slice(), + )?; + } + Ok(()) + } + + let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( + &ctx.accounts.config, + &crate::ID, + )?; + let address_space = compression_config.address_space[0]; + + let (has_tokens, has_pdas) = check_account_types(&compressed_accounts); + if !has_tokens && !has_pdas { + return Ok(()); + } + + // Pre-count for exact alloc. + let (mut token_count, mut pda_count) = (0usize, 0usize); + for c in &compressed_accounts { + match c.data { + CompressedAccountVariant::PackedCTokenData(_) => token_count += 1, + _ => pda_count += 1, + } + } + + let mut ctoken_accounts: Vec<( + light_compressed_token_sdk::compat::PackedCTokenData, + light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + )> = Vec::with_capacity(token_count); + let mut compressed_pda_infos = Vec::with_capacity(pda_count); + + let cpi_accounts = if has_tokens { + CpiAccounts::new_with_config( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), + ) + } else { + CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ) + }; + + let pda_accounts_start = ctx.remaining_accounts.len() - compressed_accounts.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + let post_system_offset = cpi_accounts.system_accounts_end_offset(); + let all_infos = cpi_accounts.account_infos(); + let post_system_accounts = &all_infos[post_system_offset..]; + for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { + let unpacked_data = compressed_data.data.unpack(post_system_accounts)?; + match unpacked_data { + CompressedAccountVariant::UserRecord(data) => { + handle_user_record( + data, + &compressed_data.meta, + solana_accounts, + i, + address_space, + &cpi_accounts, + &ctx.accounts.rent_payer, + &mut compressed_pda_infos, + )?; + } + CompressedAccountVariant::GameSession(data) => { + handle_game_session( + data, + &compressed_data.meta, + solana_accounts, + i, + address_space, + &cpi_accounts, + &ctx.accounts.rent_payer, + &mut compressed_pda_infos, + )?; + } + CompressedAccountVariant::PlaceholderRecord(data) => { + handle_placeholder_record( + data, + &compressed_data.meta, + solana_accounts, + i, + address_space, + &cpi_accounts, + &ctx.accounts.rent_payer, + &mut compressed_pda_infos, + )?; + } + CompressedAccountVariant::PackedCTokenData(data) => { + ctoken_accounts.push((data, compressed_data.meta)); + } + CompressedAccountVariant::PackedUserRecord(_) + | CompressedAccountVariant::PackedGameSession(_) + | CompressedAccountVariant::PackedPlaceholderRecord(_) + | CompressedAccountVariant::CTokenData(_) => { + panic!("internal error: entered unreachable code"); + } + } + } + // return if no uninitialized accounts. + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !ctoken_accounts.is_empty(); + if !has_pdas && !has_tokens { + return Ok(()); + } + let fee_payer = ctx.accounts.fee_payer.as_ref(); + + // init PDAs. + if has_pdas && has_tokens { + let authority = cpi_accounts + .authority() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; + let cpi_context = cpi_accounts + .cpi_context() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; + let system_cpi_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer: LIGHT_CPI_SIGNER, + }; + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_account_infos(&compressed_pda_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(system_cpi_accounts)?; + } else if has_pdas { + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_account_infos(&compressed_pda_infos) + .invoke(cpi_accounts.clone())?; + } + + // init tokens. + if has_tokens { + process_tokens( + ctx.accounts, + ctx.remaining_accounts, + fee_payer, + &ctx.accounts.ctoken_program, + &ctx.accounts.ctoken_rent_sponsor, + &ctx.accounts.ctoken_cpi_authority, + &ctx.accounts.ctoken_config, + &ctx.accounts.config, + ctoken_accounts, + proof, + &cpi_accounts, + post_system_accounts, + has_pdas, + )?; + } + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs new file mode 100644 index 0000000000..ec1f8f046e --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs @@ -0,0 +1,28 @@ +// Auto-generated by compressible_instructions macro. + +use anchor_lang::prelude::*; +use light_sdk::compressible::process_initialize_compression_config_checked; + +use crate::instruction_accounts::*; + +pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, +) -> Result<()> { + process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_sponsor, + address_space, + compression_delay, + 0, // one global config for now, so bump is 0. + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + )?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/mod.rs b/sdk-tests/sdk-compressible-test/src/instructions/mod.rs new file mode 100644 index 0000000000..5c7bd74407 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/mod.rs @@ -0,0 +1,10 @@ +pub mod compress_accounts_idempotent; +pub mod create_game_session; +pub mod create_placeholder_record; +pub mod create_record; +pub mod create_user_record_and_game_session; +pub mod decompress_accounts_idempotent; +pub mod initialize_compression_config; +pub mod update_compression_config; +pub mod update_game_session; +pub mod update_record; diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs new file mode 100644 index 0000000000..c0e96f83f2 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs @@ -0,0 +1,25 @@ +// Auto-generated by compressible_instructions macro. +use anchor_lang::prelude::*; +use light_sdk::compressible::process_update_compression_config; + +use crate::instruction_accounts::*; + +pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, +) -> Result<()> { + process_update_compression_config( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + new_update_authority.as_ref(), + new_rent_sponsor.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + )?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs new file mode 100644 index 0000000000..25b45b4528 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs @@ -0,0 +1,21 @@ +use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; +use light_sdk::compressible::HasCompressionInfo; + +use crate::instruction_accounts::*; +pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, +) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + game_session.score = new_score; + game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); + + // Must manually set compression info + game_session + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs new file mode 100644 index 0000000000..6b8c7cb618 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; +use light_sdk::compressible::HasCompressionInfo; + +use crate::instruction_accounts::*; + +pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + user_record.name = name; + user_record.score = score; + + // 1. Must manually set compression info + user_record + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/lib.rs b/sdk-tests/sdk-compressible-test/src/lib.rs new file mode 100644 index 0000000000..5e160c4931 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/lib.rs @@ -0,0 +1,178 @@ +#![allow(deprecated)] + +use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; +use light_sdk::derive_light_cpi_signer; +use light_sdk_types::CpiSigner; + +pub mod constants; +pub mod errors; +pub mod instruction_accounts; +pub mod instructions; +pub mod seeds; +pub mod state; + +pub use constants::*; +pub use errors::*; +pub use instruction_accounts::*; +// Re-export types needed by Anchor's macro expansion +pub use light_sdk::instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, ValidityProof, +}; +pub use seeds::*; +pub use state::*; + +declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); + +#[program] +pub mod sdk_compressible_test { + use light_sdk::instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, + ValidityProof, + }; + + use super::*; + + pub fn create_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + instructions::create_record::create_record( + ctx, + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + ) + } + + pub fn create_user_record_and_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, + account_data: AccountCreationData, + compression_params: CompressionParams, + ) -> Result<()> { + instructions::create_user_record_and_game_session::create_user_record_and_game_session( + ctx, + account_data, + compression_params, + ) + } + + pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, + ) -> Result<()> { + instructions::update_game_session::update_game_session(ctx, _session_id, new_score) + } + + pub fn create_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, + session_id: u64, + game_type: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + instructions::create_game_session::create_game_session( + ctx, + session_id, + game_type, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + ) + } + + pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, + ) -> Result<()> { + instructions::initialize_compression_config::initialize_compression_config( + ctx, + compression_delay, + rent_sponsor, + address_space, + ) + } + + pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + instructions::update_record::update_record(ctx, name, score) + } + + pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + instructions::create_placeholder_record::create_placeholder_record( + ctx, + placeholder_id, + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + ) + } + + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + instructions::decompress_accounts_idempotent::decompress_accounts_idempotent( + ctx, + proof, + compressed_accounts, + system_accounts_offset, + ) + } + + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, + ) -> Result<()> { + instructions::compress_accounts_idempotent::compress_accounts_idempotent( + ctx, + proof, + compressed_accounts, + signer_seeds, + system_accounts_offset, + ) + } + + pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + instructions::update_compression_config::update_compression_config( + ctx, + new_compression_delay, + new_rent_sponsor, + new_address_space, + new_update_authority, + ) + } +} diff --git a/sdk-tests/sdk-compressible-test/src/seeds.rs b/sdk-tests/sdk-compressible-test/src/seeds.rs new file mode 100644 index 0000000000..414bbd6d5b --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/seeds.rs @@ -0,0 +1,219 @@ +// Auto-generated by macro. Seed getter implementations. + +use anchor_lang::prelude::Pubkey; + +use crate::constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED, USER_RECORD_SEED}; + +pub fn get_userrecord_seeds(owner: &Pubkey) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(2usize + 1); + seed_values.push((USER_RECORD_SEED.as_bytes()).to_vec()); + seed_values.push((owner.as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_gamesession_seeds(session_id: u64) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(2usize + 1); + seed_values.push(("game_session".as_bytes()).to_vec()); + seed_values.push((session_id.to_le_bytes().as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_placeholderrecord_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(2usize + 1); + seed_values.push(("placeholder_record".as_bytes()).to_vec()); + seed_values.push((placeholder_id.to_le_bytes().as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_ctokensigner_seeds(fee_payer: &Pubkey, some_mint: &Pubkey) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(3usize + 1); + seed_values.push((CTOKEN_SIGNER_SEED.as_bytes()).to_vec()); + seed_values.push((fee_payer.as_ref()).to_vec()); + seed_values.push((some_mint.as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_ctoken_signer_seeds<'a>(user: &'a Pubkey, mint: &'a Pubkey) -> (Vec>, Pubkey) { + let mut seeds = vec![ + b"ctoken_signer".to_vec(), + user.to_bytes().to_vec(), + mint.to_bytes().to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctoken_signer2_seeds(user: &Pubkey) -> (Vec>, Pubkey) { + let mut seeds = vec![b"user_vault".to_vec(), user.to_bytes().to_vec()]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctoken_signer3_seeds(user: &Pubkey) -> (Vec>, Pubkey) { + let mut seeds = vec![ + POOL_VAULT_SEED.as_bytes().to_vec(), + user.to_bytes().to_vec(), + b"liquidity".to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctokensigner_authority_seeds() -> (Vec>, Pubkey) { + let mut seeds = vec![b"cpi_authority".to_vec()]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctokensigner2_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub fn get_ctokensigner3_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub fn get_ctoken_signer4_seeds<'a>( + user: &'a Pubkey, + fee_payer: &'a Pubkey, +) -> (Vec>, Pubkey) { + let mut seeds = vec![ + b"multi_account".to_vec(), + user.to_bytes().to_vec(), + fee_payer.to_bytes().to_vec(), + crate::ID.to_bytes().to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctoken_signer5_seeds<'a>( + user: &'a Pubkey, + mint: &'a Pubkey, + index: u64, +) -> (Vec>, Pubkey) { + let mut seeds = vec![ + b"indexed_vault".to_vec(), + user.to_bytes().to_vec(), + mint.to_bytes().to_vec(), + index.to_le_bytes().to_vec(), + b"final".to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctokensigner4_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub fn get_ctokensigner5_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub mod ctoken_seed_system { + use anchor_lang::prelude::{AccountInfo, Pubkey}; + + use super::super::{ + constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED}, + instruction_accounts::DecompressAccountsIdempotent, + state::CTokenAccountVariant, + }; + + pub struct CTokenSeedContext<'a, 'info> { + pub accounts: &'a DecompressAccountsIdempotent<'info>, + pub remaining_accounts: &'a [AccountInfo<'info>], + } + + pub trait CTokenSeedProvider { + fn get_seeds<'a, 'info>( + &self, + ctx: &CTokenSeedContext<'a, 'info>, + ) -> (Vec>, Pubkey); + } + + impl CTokenSeedProvider for CTokenAccountVariant { + fn get_seeds<'a, 'info>( + &self, + ctx: &CTokenSeedContext<'a, 'info>, + ) -> (Vec>, Pubkey) { + match self { + CTokenAccountVariant::CTokenSigner => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seed_2 = ctx.accounts.some_mint.key.to_bytes(); + let seeds: &[&[u8]] = &[CTOKEN_SIGNER_SEED.as_bytes(), &seed_1, &seed_2]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner2 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seeds: &[&[u8]] = &[b"user_vault", &seed_1]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner3 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seeds: &[&[u8]] = &[POOL_VAULT_SEED.as_bytes(), &seed_1, b"liquidity"]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner4 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seed_2 = ctx.accounts.fee_payer.key.to_bytes(); + let program_id_bytes = crate::ID.to_bytes(); + let seeds: &[&[u8]] = &[b"multi_account", &seed_1, &seed_2, &program_id_bytes]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner5 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seed_2 = ctx.accounts.some_mint.key.to_bytes(); + let index_bytes = 42u64.to_le_bytes(); + let seeds: &[&[u8]] = + &[b"indexed_vault", &seed_1, &seed_2, &index_bytes, b"final"]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + } + } + } +} diff --git a/sdk-tests/sdk-compressible-test/src/state.rs b/sdk-tests/sdk-compressible-test/src/state.rs new file mode 100644 index 0000000000..dd3446c331 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/state.rs @@ -0,0 +1,522 @@ +// Only CompressionParams is custom to the caller program. All other structs are +// auto-generated by macro. Compressed account variant, pack, unpack, +// hasCompressionInfo implementions. + +use anchor_lang::prelude::*; +use light_compressed_token_sdk::Pack as _TokenPack; +use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; +use light_sdk::{ + account::Size, + compressible::{ + CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack as SdkPack, + Unpack as SdkUnpack, + }, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + PackedAddressTreeInfo, ValidityProof, + }, + LightDiscriminator, LightHasher, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] +#[repr(u8)] +pub enum CTokenAccountVariant { + CTokenSigner = 0, + CTokenSigner2 = 1, + CTokenSigner3 = 2, + CTokenSigner4 = 3, + CTokenSigner5 = 4, +} + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedAccountVariant { + UserRecord(UserRecord), + PackedUserRecord(PackedUserRecord), + GameSession(GameSession), + PackedGameSession(PackedGameSession), + PlaceholderRecord(PlaceholderRecord), + PackedPlaceholderRecord(PackedPlaceholderRecord), + PackedCTokenData(light_compressed_token_sdk::compat::PackedCTokenData), + CTokenData(light_compressed_token_sdk::compat::CTokenData), +} + +impl Default for CompressedAccountVariant { + fn default() -> Self { + Self::UserRecord(UserRecord::default()) + } +} + +impl LightDiscriminator for CompressedAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; // This won't be used directly + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +impl HasCompressionInfo for CompressedAccountVariant { + fn compression_info(&self) -> &CompressionInfo { + match self { + Self::UserRecord(data) => data.compression_info(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info(), + Self::PlaceholderRecord(data) => data.compression_info(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + match self { + Self::UserRecord(data) => data.compression_info_mut(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info_mut(), + Self::PlaceholderRecord(data) => data.compression_info_mut(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + match self { + Self::UserRecord(data) => data.compression_info_mut_opt(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info_mut_opt(), + Self::PlaceholderRecord(data) => data.compression_info_mut_opt(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } + + fn set_compression_info_none(&mut self) { + match self { + Self::UserRecord(data) => data.set_compression_info_none(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.set_compression_info_none(), + Self::PlaceholderRecord(data) => data.set_compression_info_none(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } +} + +impl Size for CompressedAccountVariant { + fn size(&self) -> usize { + match self { + Self::UserRecord(data) => data.size(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.size(), + Self::PlaceholderRecord(data) => data.size(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } +} + +// Pack implementation for CompressedAccountVariant +// This delegates to the underlying type's Pack implementation +impl SdkPack for CompressedAccountVariant { + type Packed = Self; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + match self { + Self::PackedUserRecord(_) => unreachable!(), + Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), + Self::GameSession(data) => Self::GameSession(data.pack(remaining_accounts)), + Self::PlaceholderRecord(data) => Self::PlaceholderRecord(data.pack(remaining_accounts)), + Self::PackedCTokenData(_) => { + unreachable!() + } + Self::CTokenData(data) => Self::PackedCTokenData(data.pack(remaining_accounts)), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } +} + +// Unpack implementation for CompressedAccountVariant +// This delegates to the underlying type's Unpack implementation +impl SdkUnpack for CompressedAccountVariant { + type Unpacked = Self; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + match self { + Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), + Self::UserRecord(_) => unreachable!(), + Self::GameSession(data) => Ok(Self::GameSession(data.unpack(remaining_accounts)?)), + Self::PlaceholderRecord(data) => { + Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) + } + Self::PackedCTokenData(_data) => Ok(self.clone()), // as-is + Self::CTokenData(_data) => unreachable!(), // as-is + Self::PackedGameSession(_data) => unreachable!(), + Self::PackedPlaceholderRecord(_data) => unreachable!(), + } + } +} + +// Auto-derived via macro. Ix data implemented for Variant. +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct CompressedAccountData { + pub meta: CompressedAccountMetaNoLamportsNoAddress, + pub data: CompressedAccountVariant, +} + +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct UserRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub score: u64, +} + +// Auto-derived via macro. +impl HasCompressionInfo for UserRecord { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl CompressedInitSpace for UserRecord { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} + +impl CompressedInitSpace for GameSession { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} + +impl CompressedInitSpace for PlaceholderRecord { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} + +impl Size for UserRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for UserRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + // Simple case: return owned data with compression_info = None + // We can't return Cow::Borrowed because compression_info must always be None for compressed storage + std::borrow::Cow::Owned(Self { + compression_info: None, // ALWAYS None for compressed storage + owner: self.owner, + name: self.name.clone(), + score: self.score, + }) + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub score: u64, +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for UserRecord { + type Packed = PackedUserRecord; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedUserRecord { + compression_info: None, + owner: remaining_accounts.insert_or_get(self.owner), + name: self.name.clone(), + score: self.score, + } + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for UserRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for PackedUserRecord { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for PackedUserRecord { + type Unpacked = UserRecord; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(UserRecord { + compression_info: None, + owner: *remaining_accounts[self.owner as usize].key, + name: self.name.clone(), + score: self.score, + }) + } +} + +// Your existing account structs must be manually extended: +// 1. Add compression_info field to the struct, with type +// Option. +// 2. add a #[skip] field for the compression_info field. +// 3. Add LightHasher, LightDiscriminator. +// 4. Add #[hash] attribute to ALL fields that can be >31 bytes. (eg Pubkeys, +// Strings) +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, + #[hash] + pub player: Pubkey, + #[max_len(32)] + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +// Auto-derived via macro. +impl HasCompressionInfo for GameSession { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl Size for GameSession { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for GameSession { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + // Custom compression: return owned data with modified fields + std::borrow::Cow::Owned(Self { + compression_info: None, // ALWAYS None for compressed storage + session_id: self.session_id, // KEEP - identifier + player: self.player, // KEEP - identifier + game_type: self.game_type.clone(), // KEEP - core property + start_time: 0, // RESET - clear timing + end_time: None, // RESET - clear timing + score: 0, // RESET - clear progress + }) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for GameSession { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for GameSession { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// PlaceholderRecord - demonstrates empty compressed account creation +// The PDA remains intact while an empty compressed account is created +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct PlaceholderRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub placeholder_id: u64, +} + +impl HasCompressionInfo for PlaceholderRecord { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl Size for PlaceholderRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for PlaceholderRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + compression_info: None, + owner: self.owner, + name: self.name.clone(), + placeholder_id: self.placeholder_id, + }) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for PlaceholderRecord { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for PlaceholderRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedGameSession { + pub compression_info: Option, + pub session_id: u64, + pub player: u8, + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedPlaceholderRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub placeholder_id: u64, +} + +// Add these struct definitions before the program module +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct AccountCreationData { + pub user_name: String, + pub session_id: u64, + pub game_type: String, + // TODO: Add mint metadata fields when implementing mint functionality + pub mint_name: String, + pub mint_symbol: String, + pub mint_uri: String, + pub mint_decimals: u8, + pub mint_supply: u64, + pub mint_update_authority: Option, + pub mint_freeze_authority: Option, + pub additional_metadata: Option>, +} + +/// Information about a token account to compress +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct TokenAccountInfo { + pub user: Pubkey, + pub mint: Pubkey, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct CompressionParams { + pub proof: ValidityProof, + pub user_compressed_address: [u8; 32], + pub user_address_tree_info: PackedAddressTreeInfo, + pub user_output_state_tree_index: u8, + pub game_compressed_address: [u8; 32], + pub game_address_tree_info: PackedAddressTreeInfo, + pub game_output_state_tree_index: u8, + pub mint_bump: u8, + pub mint_with_context: CompressedMintWithContext, +} diff --git a/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs b/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs new file mode 100644 index 0000000000..5f19878457 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs @@ -0,0 +1,232 @@ +use anchor_lang::{AccountDeserialize, AnchorDeserialize, Discriminator, ToAccountMetas}; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::compressible::{CompressAs, CompressibleConfig}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_game_session, ADDRESS_SPACE, RENT_SPONSOR}; + +// Test: create, decompress game session, compress with custom data at +// compression +#[tokio::test] +async fn test_custom_compression_game_session() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let session_id = 42424u64; + let (game_session_pda, _game_bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &program_id, + ); + + create_game_session( + &mut rpc, + &payer, + &program_id, + &config_pda, + &game_session_pda, + session_id, + None, + ) + .await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_game_session( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + &_game_bump, + session_id, + "Battle Royale", + 100, + 0, + ) + .await; + + rpc.warp_to_slot(250).unwrap(); + + compress_game_session_with_custom_data( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + session_id, + ) + .await; +} + +#[allow(clippy::too_many_arguments)] +pub async fn decompress_single_game_session( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + game_session_pda: &Pubkey, + _game_bump: &u8, + session_id: u64, + expected_game_type: &str, + expected_slot: u64, + expected_score: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = + sdk_compressible_test::GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_game_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*game_session_pda], + &[( + c_game_pda, + sdk_compressible_test::CompressedAccountVariant::GameSession(c_game_session), + )], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" + ); + + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + sdk_compressible_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, expected_score); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); +} + +pub async fn compress_game_session_with_custom_data( + rpc: &mut LightProgramTest, + _payer: &Keypair, + _program_id: &Pubkey, + game_session_pda: &Pubkey, + _session_id: u64, +) { + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); + let game_pda_data = game_pda_account.data; + let original_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + + let custom_compressed_data = match original_game_session.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), + std::borrow::Cow::Owned(data) => data, + }; + + assert_eq!( + custom_compressed_data.session_id, original_game_session.session_id, + "Session ID should be kept" + ); + assert_eq!( + custom_compressed_data.player, original_game_session.player, + "Player should be kept" + ); + assert_eq!( + custom_compressed_data.game_type, original_game_session.game_type, + "Game type should be kept" + ); + assert_eq!( + custom_compressed_data.start_time, 0, + "Start time should be RESET to 0" + ); + assert_eq!( + custom_compressed_data.end_time, None, + "End time should be RESET to None" + ); + assert_eq!( + custom_compressed_data.score, 0, + "Score should be RESET to 0" + ); +} diff --git a/sdk-tests/sdk-compressible-test/tests/helpers.rs b/sdk-tests/sdk-compressible-test/tests/helpers.rs new file mode 100644 index 0000000000..847da9ddaf --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/helpers.rs @@ -0,0 +1,336 @@ +// Common test helpers and constants for all test files +#![allow(dead_code)] + +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; +use light_compressible_client::CompressibleInstruction; +use light_macros::pubkey; +use light_program_test::{program_test::LightProgramTest, AddressWithTree, Indexer, Rpc}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use sdk_compressible_test::{CompressedAccountVariant, GameSession, UserRecord}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; +pub const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +pub const CTOKEN_RENT_SPONSOR: Pubkey = pubkey!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); +pub const CTOKEN_RENT_AUTHORITY: Pubkey = pubkey!("8r3QmazwoLHYppYWysXPgUxYJ3Khn7vh3e313jYDcCKy"); + +// Helper functions used across multiple test files + +pub async fn create_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + state_tree_queue: Option, +) { + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let accounts = sdk_compressible_test::accounts::CreateRecord { + user: payer.pubkey(), + user_record: *user_record_pda, + system_program: solana_sdk::system_program::ID, + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + }; + + let compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let address_tree_info = packed_tree_infos.address_trees[0]; + + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreateRecord { + name: "Test User".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + + let user_record = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); +} + +pub async fn decompress_single_user_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + _user_record_bump: &u8, + expected_user_name: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_user_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + + let compressed_account = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert!(compressed_account.data.unwrap().data.is_empty()); + + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA account data len must be > 0 after decompression" + ); + + let user_pda_data = user_pda_account.unwrap().data; + assert_eq!( + &user_pda_data[0..8], + UserRecord::DISCRIMINATOR, + "User account anchor discriminator mismatch" + ); + + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + assert_eq!(decompressed_user_record.name, expected_user_name); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); +} + +pub async fn create_game_session( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + state_tree_queue: Option, +) { + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let accounts = sdk_compressible_test::accounts::CreateGameSession { + player: payer.pubkey(), + game_session: *game_session_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_sponsor: RENT_SPONSOR, + }; + + let compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let address_tree_info = packed_tree_infos.address_trees[0]; + + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreateGameSession { + session_id, + game_type: "Battle Royale".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_game_session = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_game_session.address, Some(compressed_address)); + assert!(compressed_game_session.data.is_some()); + + let buf = compressed_game_session.data.as_ref().unwrap().data.clone(); + + let game_session = GameSession::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(game_session.session_id, session_id); + assert_eq!(game_session.game_type, "Battle Royale"); + assert_eq!(game_session.player, payer.pubkey()); + assert_eq!(game_session.score, 0); + assert!(game_session.compression_info.is_none()); +} diff --git a/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs b/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs new file mode 100644 index 0000000000..6ce75929c1 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs @@ -0,0 +1,141 @@ +use anchor_lang::{AccountDeserialize, AnchorDeserialize, ToAccountMetas}; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::compressible::CompressibleConfig; +use sdk_compressible_test::{CompressedAccountVariant, UserRecord}; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; + +#[tokio::test] +async fn test_double_decompression_attack() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + let c_user_record = + UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA should be decompressed after first operation" + ); + + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_user_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + &program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(&program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + + assert!( + result.is_ok(), + "Second decompression should succeed idempotently" + ); + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + let user_pda_data = user_pda_account.unwrap().data; + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + + assert_eq!(decompressed_user_record.name, "Test User"); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} diff --git a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs new file mode 100644 index 0000000000..291992b3f1 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs @@ -0,0 +1,1389 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use light_client::indexer::CompressedAccount; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::{ + ctoken, + instructions::{create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address}, + pack::compat::CTokenDataWithVariant, +}; +use light_compressed_token_types::CPI_AUTHORITY_PDA; +use light_compressible_client::CompressibleInstruction; +use light_ctoken_types::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::CompressedMintMetadata, +}; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + AddressWithTree, Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use light_sdk_types::C_TOKEN_PROGRAM_ID; +use sdk_compressible_test::{ + get_ctoken_signer2_seeds, get_ctoken_signer3_seeds, get_ctoken_signer4_seeds, + get_ctoken_signer5_seeds, get_ctoken_signer_seeds, CTokenAccountVariant, + CompressedAccountVariant, GameSession, UserRecord, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_game_session, create_record, ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests +// 1. create and decompress two accounts and compress token accounts after +// decompression +// 2. create and decompress accounts with different state trees +#[tokio::test] +async fn test_create_and_decompress_two_accounts() { + let program_id = sdk_compressible_test::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![crate::helpers::ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let combined_user = Keypair::new(); + let fund_user_ix = solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &combined_user.pubkey(), + 1e9 as u64, + ); + let fund_result = rpc + .create_and_send_transaction(&[fund_user_ix], &payer.pubkey(), &[&payer]) + .await; + assert!(fund_result.is_ok(), "Funding combined user should succeed"); + let combined_session_id = 99999u64; + let (combined_user_record_pda, _combined_user_record_bump) = Pubkey::find_program_address( + &[b"user_record", combined_user.pubkey().as_ref()], + &program_id, + ); + let (combined_game_session_pda, _combined_game_bump) = Pubkey::find_program_address( + &[b"game_session", combined_session_id.to_le_bytes().as_ref()], + &program_id, + ); + + let ( + ctoken_account, + _mint_signer, + ctoken_account_2, + ctoken_account_3, + ctoken_account_4, + ctoken_account_5, + ) = create_user_record_and_game_session( + &mut rpc, + &combined_user, + &program_id, + &config_pda, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + ) + .await; + + rpc.warp_to_slot(200).unwrap(); + + let (_, ctoken_account_address) = sdk_compressible_test::get_ctoken_signer_seeds( + &combined_user.pubkey(), + &ctoken_account.token.mint, + ); + + let (_, ctoken_account_address_2) = + sdk_compressible_test::get_ctoken_signer2_seeds(&combined_user.pubkey()); + + let (_, ctoken_account_address_3) = + sdk_compressible_test::get_ctoken_signer3_seeds(&combined_user.pubkey()); + + let (_, ctoken_account_address_4) = sdk_compressible_test::get_ctoken_signer4_seeds( + &combined_user.pubkey(), + &combined_user.pubkey(), + ); + + let (_, ctoken_account_address_5) = sdk_compressible_test::get_ctoken_signer5_seeds( + &combined_user.pubkey(), + &ctoken_account.token.mint, + 42, + ); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let compressed_user_record_address = derive_address( + &combined_user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_game_session_address = derive_address( + &combined_game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let user_record_before_decompression: CompressedAccount = rpc + .get_compressed_account(compressed_user_record_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_session_before_decompression: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value + .unwrap(); + + decompress_multiple_pdas_with_ctoken( + &mut rpc, + &combined_user, + &program_id, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + "Combined User", + "Combined Game", + 200, + ctoken_account.clone(), + ctoken_account_address, + ctoken_account_2.clone(), + ctoken_account_address_2, + ctoken_account_3.clone(), + ctoken_account_address_3, + ctoken_account_4.clone(), + ctoken_account_address_4, + ctoken_account_5.clone(), + ctoken_account_address_5, + ) + .await; + + rpc.warp_to_slot(300).unwrap(); + + compress_token_account_after_decompress( + &mut rpc, + &combined_user, + &program_id, + &config_pda, + ctoken_account_address, + ctoken_account_address_2, + ctoken_account_address_3, + ctoken_account_address_4, + ctoken_account_address_5, + ctoken_account.token.mint, + ctoken_account.token.amount, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + user_record_before_decompression.hash, + game_session_before_decompression.hash, + ) + .await; +} + +#[allow(clippy::too_many_arguments)] +pub async fn create_user_record_and_game_session( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, +) -> ( + light_client::indexer::CompressedTokenAccount, + Pubkey, + light_client::indexer::CompressedTokenAccount, + light_client::indexer::CompressedTokenAccount, + light_client::indexer::CompressedTokenAccount, + light_client::indexer::CompressedTokenAccount, +) { + let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new_with_cpi_context( + *program_id, + state_tree_info.cpi_context.unwrap(), + ); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let decimals = 6u8; + let mint_authority_keypair = Keypair::new(); + let mint_authority = mint_authority_keypair.pubkey(); + let freeze_authority = mint_authority; + let mint_signer = Keypair::new(); + let compressed_mint_address = + derive_compressed_mint_address(&mint_signer.pubkey(), &address_tree_pubkey); + + let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); + let accounts = sdk_compressible_test::accounts::CreateUserRecordAndGameSession { + user: user.pubkey(), + user_record: *user_record_pda, + game_session: *game_session_pda, + mint_signer: mint_signer.pubkey(), + ctoken_program: C_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_sponsor: RENT_SPONSOR, + mint_authority, + compress_token_program_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), + }; + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![ + AddressWithTree { + address: user_compressed_address, + tree: address_tree_pubkey, + }, + AddressWithTree { + address: game_compressed_address, + tree: address_tree_pubkey, + }, + AddressWithTree { + address: compressed_mint_address, + tree: address_tree_pubkey, + }, + ], + None, + ) + .await + .unwrap() + .value; + + let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let user_address_tree_info = packed_tree_infos.address_trees[0]; + let game_address_tree_info = packed_tree_infos.address_trees[1]; + let mint_address_tree_info = packed_tree_infos.address_trees[2]; + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreateUserRecordAndGameSession { + account_data: sdk_compressible_test::AccountCreationData { + user_name: "Combined User".to_string(), + session_id, + game_type: "Combined Game".to_string(), + mint_name: "Test Game Token".to_string(), + mint_symbol: "TGT".to_string(), + mint_uri: "https://example.com/token.json".to_string(), + mint_decimals: 9, + mint_supply: 1_000_000_000, + mint_update_authority: Some(mint_authority), + mint_freeze_authority: Some(freeze_authority), + additional_metadata: None, + }, + compression_params: sdk_compressible_test::CompressionParams { + proof: rpc_result.proof, + user_compressed_address, + user_address_tree_info, + user_output_state_tree_index, + game_compressed_address, + game_address_tree_info, + game_output_state_tree_index, + mint_bump, + mint_with_context: CompressedMintWithContext { + leaf_index: 0, + prove_by_index: false, + root_index: mint_address_tree_info.root_index, + address: compressed_mint_address, + mint: CompressedMintInstructionData { + supply: 0, + decimals, + metadata: CompressedMintMetadata { + version: 3, + mint: spl_mint.into(), + spl_mint_initialized: false, + }, + mint_authority: Some(mint_authority.into()), + freeze_authority: Some(freeze_authority.into()), + extensions: None, + }, + }, + }, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction( + &[instruction], + &user.pubkey(), + &[user, &mint_signer, &mint_authority_keypair], + ) + .await; + + assert!( + result.is_ok(), + "Combined creation transaction should succeed" + ); + + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_none(), + "User record account should not exist after compression" + ); + + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_none(), + "Game session account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_user_record.address, + Some(user_compressed_address) + ); + assert!(compressed_user_record.data.is_some()); + + let user_buf = compressed_user_record.data.unwrap().data; + + let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); + + assert_eq!(user_record.name, "Combined User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, user.pubkey()); + + let compressed_game_session = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_game_session.address, + Some(game_compressed_address) + ); + assert!(compressed_game_session.data.is_some()); + + let game_buf = compressed_game_session.data.unwrap().data; + let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); + assert_eq!(game_session.session_id, session_id); + assert_eq!(game_session.game_type, "Combined Game"); + assert_eq!(game_session.player, user.pubkey()); + assert_eq!(game_session.score, 0); + + let token_account_address = get_ctoken_signer_seeds( + &user.pubkey(), + &find_spl_mint_address(&mint_signer.pubkey()).0, + ) + .1; + + let mint = find_spl_mint_address(&mint_signer.pubkey()).0; + let token_account_address_2 = get_ctoken_signer2_seeds(&user.pubkey()).1; + let token_account_address_3 = get_ctoken_signer3_seeds(&user.pubkey()).1; + let token_account_address_4 = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()).1; + let token_account_address_5 = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42).1; + + let ctoken_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_2 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_3 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_4 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_5 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) + .await + .unwrap() + .value; + + assert!( + !ctoken_accounts.items.is_empty(), + "Should have at least one compressed token account" + ); + assert!( + !ctoken_accounts_2.items.is_empty(), + "Should have at least one compressed token account 2" + ); + assert!( + !ctoken_accounts_3.items.is_empty(), + "Should have at least one compressed token account 3" + ); + assert!( + !ctoken_accounts_4.items.is_empty(), + "Should have at least one compressed token account 4" + ); + assert!( + !ctoken_accounts_5.items.is_empty(), + "Should have at least one compressed token account 5" + ); + + let ctoken_account = ctoken_accounts.items[0].clone(); + let ctoken_account_2 = ctoken_accounts_2.items[0].clone(); + let ctoken_account_3 = ctoken_accounts_3.items[0].clone(); + let ctoken_account_4 = ctoken_accounts_4.items[0].clone(); + let ctoken_account_5 = ctoken_accounts_5.items[0].clone(); + + ( + ctoken_account, + mint_signer.pubkey(), + ctoken_account_2, + ctoken_account_3, + ctoken_account_4, + ctoken_account_5, + ) +} + +#[allow(clippy::too_many_arguments)] +pub async fn decompress_multiple_pdas_with_ctoken( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, + ctoken_account: light_client::indexer::CompressedTokenAccount, + native_token_account: Pubkey, + ctoken_account_2: light_client::indexer::CompressedTokenAccount, + native_token_account_2: Pubkey, + ctoken_account_3: light_client::indexer::CompressedTokenAccount, + native_token_account_3: Pubkey, + ctoken_account_4: light_client::indexer::CompressedTokenAccount, + native_token_account_4: Pubkey, + ctoken_account_5: light_client::indexer::CompressedTokenAccount, + native_token_account_5: Pubkey, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof( + vec![ + c_user_pda.hash, + c_game_pda.hash, + ctoken_account.clone().account.hash, + ctoken_account_2.clone().account.hash, + ctoken_account_3.clone().account.hash, + ctoken_account_4.clone().account.hash, + ctoken_account_5.clone().account.hash, + ], + vec![], + None, + ) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let ctoken_config = ctoken::config_pda(); + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[ + *user_record_pda, + *game_session_pda, + native_token_account, + native_token_account_2, + native_token_account_3, + native_token_account_4, + native_token_account_5, + ], + &[ + ( + c_user_pda.clone(), + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda.clone(), + CompressedAccountVariant::GameSession(c_game_session), + ), + ( + { + let acc = ctoken_account.clone().account; + let _token = ctoken_account.clone().token; + acc + }, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner, + token_data: ctoken_account.clone().token, + }), + ), + ( + ctoken_account_2.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner2, + token_data: ctoken_account_2.clone().token, + }), + ), + ( + ctoken_account_3.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner3, + token_data: ctoken_account_3.clone().token, + }), + ), + ( + ctoken_account_4.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner4, + token_data: ctoken_account_4.clone().token, + }), + ), + ( + ctoken_account_5.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner5, + token_data: ctoken_account_5.clone().token, + }), + ), + ], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config, + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: ctoken_account.token.mint, + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert_eq!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "Game PDA account data len must be 0 before decompression" + ); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA account data len must be > 0 after decompression" + ); + + let user_pda_data = user_pda_account.unwrap().data; + assert_eq!( + &user_pda_data[0..8], + UserRecord::DISCRIMINATOR, + "User account anchor discriminator mismatch" + ); + + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + assert_eq!(decompressed_user_record.name, expected_user_name); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" + ); + + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + sdk_compressible_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, 0); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let token_account_data = rpc + .get_account(native_token_account) + .await + .unwrap() + .unwrap(); + assert!( + !token_account_data.data.is_empty(), + "Token account should have data" + ); + assert_eq!(token_account_data.owner, C_TOKEN_PROGRAM_ID.into()); + + let compressed_user_record_data = rpc + .get_compressed_account(c_user_pda.clone().address.unwrap(), None) + .await + .unwrap() + .value + .unwrap(); + let compressed_game_session_data = rpc + .get_compressed_account(c_game_pda.clone().address.unwrap(), None) + .await + .unwrap() + .value + .unwrap(); + for ctoken in [ + &ctoken_account, + &ctoken_account_2, + &ctoken_account_3, + &ctoken_account_4, + &ctoken_account_5, + ] { + let response = rpc + .get_compressed_account_by_hash(ctoken.clone().account.hash, None) + .await + .unwrap(); + assert!( + response.value.is_none(), + "Compressed token account should have value == None after being closed" + ); + } + + assert!( + compressed_user_record_data.data.unwrap().data.is_empty(), + "Compressed user record should be closed/empty after decompression" + ); + assert!( + compressed_game_session_data.data.unwrap().data.is_empty(), + "Compressed game session should be closed/empty after decompression" + ); +} + +#[allow(clippy::too_many_arguments)] +pub async fn decompress_multiple_pdas( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_account_data = c_game_pda.data.as_ref().unwrap(); + + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*user_record_pda, *game_session_pda], + &[ + ( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda, + CompressedAccountVariant::GameSession(c_game_session), + ), + ], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert_eq!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "Game PDA account data len must be 0 before decompression" + ); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA account data len must be > 0 after decompression" + ); + + let user_pda_data = user_pda_account.unwrap().data; + assert_eq!( + &user_pda_data[0..8], + UserRecord::DISCRIMINATOR, + "User account anchor discriminator mismatch" + ); + + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + assert_eq!(decompressed_user_record.name, expected_user_name); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" + ); + + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + sdk_compressible_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, 0); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert!(c_game_pda.data.is_some()); + assert_eq!(c_game_pda.data.unwrap().data.len(), 0); +} + +#[allow(clippy::too_many_arguments)] +pub async fn compress_token_account_after_decompress( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + token_account_address: Pubkey, + _token_account_address_2: Pubkey, + _token_account_address_3: Pubkey, + _token_account_address_4: Pubkey, + _token_account_address_5: Pubkey, + mint: Pubkey, + amount: u64, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + user_record_hash_before_decompression: [u8; 32], + game_session_hash_before_decompression: [u8; 32], +) { + let token_account_data = rpc.get_account(token_account_address).await.unwrap(); + assert!( + token_account_data.is_some(), + "Token account should exist before compression" + ); + + let account = token_account_data.unwrap(); + + assert!( + account.lamports > 0, + "Token account should have lamports before compression" + ); + assert!( + !account.data.is_empty(), + "Token account should have data before compression" + ); + + let (user_record_seeds, user_record_pubkey) = + sdk_compressible_test::get_userrecord_seeds(&user.pubkey()); + let (game_session_seeds, game_session_pubkey) = + sdk_compressible_test::get_gamesession_seeds(session_id); + let (_, token_account_address) = get_ctoken_signer_seeds(&user.pubkey(), &mint); + + let (_, token_account_address_2) = get_ctoken_signer2_seeds(&user.pubkey()); + let (_, token_account_address_3) = get_ctoken_signer3_seeds(&user.pubkey()); + let (_, token_account_address_4) = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()); + let (_, token_account_address_5) = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42); + let (_token_signer_seeds, _ctoken_1_authority_pda) = + sdk_compressible_test::get_ctokensigner_authority_seeds(); + + let (_token_signer_seeds_2, _ctoken_2_authority_pda) = + sdk_compressible_test::get_ctokensigner2_authority_seeds(); + + let (_token_signer_seeds_3, _ctoken_3_authority_pda) = + sdk_compressible_test::get_ctokensigner3_authority_seeds(); + + let (_token_signer_seeds_4, _ctoken_4_authority_pda) = + sdk_compressible_test::get_ctokensigner4_authority_seeds(); + + let (_token_signer_seeds_5, _ctoken_5_authority_pda) = + sdk_compressible_test::get_ctokensigner5_authority_seeds(); + + let _cpisigner = Pubkey::new_from_array(sdk_compressible_test::LIGHT_CPI_SIGNER.cpi_signer); + + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap().unwrap(); + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); + let _token_account = rpc + .get_account(token_account_address) + .await + .unwrap() + .unwrap(); + let _token_account_2 = rpc + .get_account(token_account_address_2) + .await + .unwrap() + .unwrap(); + let _token_account_3 = rpc + .get_account(token_account_address_3) + .await + .unwrap() + .unwrap(); + let _token_account_4 = rpc + .get_account(token_account_address_4) + .await + .unwrap() + .unwrap(); + let _token_account_5 = rpc + .get_account(token_account_address_5) + .await + .unwrap() + .unwrap(); + + assert_eq!(*user_record_pda, user_record_pubkey); + assert_eq!(*game_session_pda, game_session_pubkey); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let compressed_user_record_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_game_session_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let user_record: CompressedAccount = rpc + .get_compressed_account(compressed_user_record_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_session: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_record_hash = user_record.hash; + let game_session_hash = game_session.hash; + + assert_ne!( + user_record_hash, user_record_hash_before_decompression, + "User record hash NOT_EQUAL before and after compression" + ); + assert_ne!( + game_session_hash, game_session_hash_before_decompression, + "Game session hash NOT_EQUAL before and after compression" + ); + + let proof_with_context = rpc + .get_validity_proof(vec![user_record_hash, game_session_hash], vec![], None) + .await + .unwrap() + .value; + + let random_tree_info = rpc.get_random_state_tree_info().unwrap(); + let instruction = + light_compressible_client::CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[user_record_pubkey, game_session_pubkey], + &[user_record_account, game_session_account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: user.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![user_record_seeds, game_session_seeds], + proof_with_context, + random_tree_info, + ) + .unwrap(); + + for _account in instruction.accounts.iter() {} + + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await; + + assert!( + result.is_ok(), + "PDA compression should succeed: {:?}", + result + ); + + rpc.warp_slot_forward(20000).await.unwrap(); + + let token_account_after = rpc.get_account(token_account_address).await.unwrap(); + assert!( + token_account_after.is_none(), + "Token account should not exist after compression" + ); + let token_account_after_2 = rpc.get_account(token_account_address_2).await.unwrap(); + assert!( + token_account_after_2.is_none(), + "Token account 2 should not exist after compression" + ); + let token_account_after_3 = rpc.get_account(token_account_address_3).await.unwrap(); + assert!( + token_account_after_3.is_none(), + "Token account 3 should not exist after compression" + ); + let token_account_after_4 = rpc.get_account(token_account_address_4).await.unwrap(); + assert!( + token_account_after_4.is_none(), + "Token account 4 should not exist after compression" + ); + let token_account_after_5 = rpc.get_account(token_account_address_5).await.unwrap(); + assert!( + token_account_after_5.is_none(), + "Token account 5 should not exist after compression" + ); + + let ctoken_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_2 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_3 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_4 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_5 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) + .await + .unwrap() + .value; + + assert!( + !ctoken_accounts.items.is_empty(), + "Should have at least one compressed token account after compression" + ); + assert!( + !ctoken_accounts_2.items.is_empty(), + "Should have at least one compressed token account 2 after compression" + ); + assert!( + !ctoken_accounts_3.items.is_empty(), + "Should have at least one compressed token account 3 after compression" + ); + assert!( + !ctoken_accounts_4.items.is_empty(), + "Should have at least one compressed token account 4 after compression" + ); + assert!( + !ctoken_accounts_5.items.is_empty(), + "Should have at least one compressed token account 5 after compression" + ); + + let ctoken = &ctoken_accounts.items[0]; + assert_eq!( + ctoken.token.mint, mint, + "Compressed token should have the same mint" + ); + assert_eq!( + ctoken.token.owner, token_account_address, + "Compressed token owner should be the token account address" + ); + assert_eq!( + ctoken.token.amount, amount, + "Compressed token should have the same amount" + ); + let ctoken2 = &ctoken_accounts_2.items[0]; + assert_eq!( + ctoken2.token.mint, mint, + "Compressed token 2 should have the same mint" + ); + assert_eq!( + ctoken2.token.owner, token_account_address_2, + "Compressed token 2 owner should be the token account address" + ); + assert_eq!( + ctoken2.token.amount, amount, + "Compressed token 2 should have the same amount" + ); + let ctoken3 = &ctoken_accounts_3.items[0]; + assert_eq!( + ctoken3.token.mint, mint, + "Compressed token 3 should have the same mint" + ); + assert_eq!( + ctoken3.token.owner, token_account_address_3, + "Compressed token 3 owner should be the token account address" + ); + assert_eq!( + ctoken3.token.amount, amount, + "Compressed token 3 should have the same amount" + ); + let ctoken4 = &ctoken_accounts_4.items[0]; + assert_eq!( + ctoken4.token.mint, mint, + "Compressed token 4 should have the same mint" + ); + assert_eq!( + ctoken4.token.owner, token_account_address_4, + "Compressed token 4 owner should be the token account address" + ); + assert_eq!( + ctoken4.token.amount, amount, + "Compressed token 4 should have the same amount" + ); + let ctoken5 = &ctoken_accounts_5.items[0]; + assert_eq!( + ctoken5.token.mint, mint, + "Compressed token 5 should have the same mint" + ); + assert_eq!( + ctoken5.token.owner, token_account_address_5, + "Compressed token 5 owner should be the token account address" + ); + assert_eq!( + ctoken5.token.amount, amount, + "Compressed token 5 should have the same amount" + ); + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + let token_account = rpc.get_account(token_account_address).await.unwrap(); + let token_account_3 = rpc.get_account(token_account_address_3).await.unwrap(); + let token_account_4 = rpc.get_account(token_account_address_4).await.unwrap(); + let token_account_5 = rpc.get_account(token_account_address_5).await.unwrap(); + + assert!( + user_record_account.is_none(), + "User record account should be None" + ); + assert!( + game_session_account.is_none(), + "Game session account should be None" + ); + assert!(token_account.is_none(), "Token account should be None"); + assert!( + user_record_account + .map(|a| a.data.is_empty()) + .unwrap_or(true), + "User record account should be empty" + ); + assert!( + game_session_account + .map(|a| a.data.is_empty()) + .unwrap_or(true), + "Game session account should be empty" + ); + assert!( + token_account.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account should be empty" + ); + assert!( + token_account_3.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account 3 should be empty" + ); + assert!( + token_account_4.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account 4 should be empty" + ); + assert!( + token_account_5.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account 5 should be empty" + ); +} + +#[tokio::test] +async fn test_create_and_decompress_accounts_with_different_state_trees() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, _user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + let session_id = 54321u64; + let (game_session_pda, _game_bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &program_id, + ); + + let first_state_tree_info = rpc.get_state_tree_infos()[0]; + let second_state_tree_info = rpc.get_state_tree_infos()[1]; + + create_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + Some(first_state_tree_info.queue), + ) + .await; + + create_game_session( + &mut rpc, + &payer, + &program_id, + &config_pda, + &game_session_pda, + session_id, + Some(second_state_tree_info.queue), + ) + .await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_multiple_pdas( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &game_session_pda, + session_id, + "Test User", + "Battle Royale", + 100, + ) + .await; +} diff --git a/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs b/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs new file mode 100644 index 0000000000..c4c07f8900 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs @@ -0,0 +1,536 @@ +use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas}; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests for the simplest possible compression flows: +// 1. Create empty compressed account (do not compress at init) +// 2. Idempotent double compression +#[tokio::test] +async fn test_create_empty_compressed_account() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let placeholder_id = 54321u64; + let (placeholder_record_pda, placeholder_record_bump) = Pubkey::find_program_address( + &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + &program_id, + ); + + create_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + placeholder_id, + "Test Placeholder", + ) + .await; + + let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_account.is_some(), + "Placeholder PDA should exist after empty compression" + ); + let account = placeholder_pda_account.unwrap(); + assert!( + account.lamports > 0, + "Placeholder PDA should have lamports (not closed)" + ); + assert!( + !account.data.is_empty(), + "Placeholder PDA should have data (not closed)" + ); + + let placeholder_data = account.data; + let decompressed_placeholder_record = + sdk_compressible_test::PlaceholderRecord::try_deserialize(&mut &placeholder_data[..]) + .unwrap(); + assert_eq!(decompressed_placeholder_record.name, "Test Placeholder"); + assert_eq!( + decompressed_placeholder_record.placeholder_id, + placeholder_id + ); + assert_eq!(decompressed_placeholder_record.owner, payer.pubkey()); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_placeholder.address, + Some(compressed_address), + "Compressed account should exist with correct address" + ); + assert!( + compressed_placeholder.data.is_some(), + "Compressed account should have data field" + ); + + let compressed_data = compressed_placeholder.data.unwrap(); + assert_eq!( + compressed_data.data.len(), + 0, + "Compressed account data should be empty" + ); + + rpc.warp_to_slot(200).unwrap(); + + compress_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + &placeholder_record_bump, + placeholder_id, + ) + .await; +} + +#[tokio::test] +async fn test_double_compression_attack() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let placeholder_id = 99999u64; + let (placeholder_record_pda, _placeholder_record_bump) = Pubkey::find_program_address( + &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + &program_id, + ); + + create_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + placeholder_id, + "Double Compression Test", + ) + .await; + + let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_account.is_some(), + "Placeholder PDA should exist before compression" + ); + let account_before = placeholder_pda_account.unwrap(); + assert!( + account_before.lamports > 0, + "Placeholder PDA should have lamports before compression" + ); + assert!( + !account_before.data.is_empty(), + "Placeholder PDA should have data before compression" + ); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder_before = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_placeholder_before.address, + Some(compressed_address), + "Empty compressed account should exist" + ); + assert_eq!( + compressed_placeholder_before + .data + .as_ref() + .unwrap() + .data + .len(), + 0, + "Compressed account should be empty initially" + ); + + rpc.warp_to_slot(200).unwrap(); + + let first_compression_result = compress_placeholder_record_for_double_test( + &mut rpc, + &payer, + &program_id, + &placeholder_record_pda, + placeholder_id, + Some(account_before.clone()), + ) + .await; + assert!( + first_compression_result.is_ok(), + "First compression should succeed: {:?}", + first_compression_result + ); + + let placeholder_pda_after_first = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_after_first.is_none(), + "PDA should not exist after first compression" + ); + + let compressed_placeholder_after_first = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let first_data_len = compressed_placeholder_after_first + .data + .as_ref() + .unwrap() + .data + .len(); + assert!( + first_data_len > 0, + "Compressed account should contain data after first compression" + ); + + let second_compression_result = compress_placeholder_record_for_double_test( + &mut rpc, + &payer, + &program_id, + &placeholder_record_pda, + placeholder_id, + Some(account_before), + ) + .await; + + assert!( + second_compression_result.is_ok(), + "Second compression should succeed idempotently: {:?}", + second_compression_result + ); + + let placeholder_pda_after_second = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_after_second.is_none(), + "PDA should still not exist after second compression" + ); + + let compressed_placeholder_after_second = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_placeholder_after_first.hash, compressed_placeholder_after_second.hash, + "Compressed account hash should be unchanged after second compression" + ); + assert_eq!( + compressed_placeholder_after_first + .data + .as_ref() + .unwrap() + .data, + compressed_placeholder_after_second + .data + .as_ref() + .unwrap() + .data, + "Compressed account data should be unchanged after second compression" + ); +} + +pub async fn create_placeholder_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + placeholder_record_pda: &Pubkey, + placeholder_id: u64, + name: &str, +) { + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let accounts = sdk_compressible_test::accounts::CreatePlaceholderRecord { + user: payer.pubkey(), + placeholder_record: *placeholder_record_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_sponsor: RENT_SPONSOR, + }; + + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_program_test::AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let address_tree_info = packed_tree_infos.address_trees[0]; + + let output_state_tree_index = + remaining_accounts.insert_or_get(rpc.get_random_state_tree_info().unwrap().queue); + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreatePlaceholderRecord { + placeholder_id, + name: name.to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CreatePlaceholderRecord transaction should succeed" + ); +} + +pub async fn compress_placeholder_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + placeholder_record_pda: &Pubkey, + _placeholder_record_bump: &u8, + placeholder_id: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); + + let account = rpc + .get_account(*placeholder_record_pda) + .await + .unwrap() + .unwrap(); + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &[account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CompressPlaceholderRecord transaction should succeed: {:?}", + result + ); + + let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); + + let compressed_placeholder_after = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert!( + compressed_placeholder_after.data.is_some(), + "Compressed account should have data after compression" + ); + + let compressed_data_after = compressed_placeholder_after.data.unwrap(); + + assert!( + !compressed_data_after.data.is_empty(), + "Compressed account should contain the PDA data" + ); +} + +pub async fn compress_placeholder_record_for_double_test( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + placeholder_record_pda: &Pubkey, + placeholder_id: u64, + previous_account: Option, +) -> Result { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let accounts_to_compress = if let Some(account) = previous_account { + vec![account] + } else { + panic!("Previous account should be provided"); + }; + let instruction = + light_compressible_client::CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &accounts_to_compress, + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} diff --git a/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs b/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs new file mode 100644 index 0000000000..857b77568c --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs @@ -0,0 +1,280 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use sdk_compressible_test::UserRecord; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests +// 1. init compressed, decompress, and compress +// 2. update_record bumps compression info +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(101).unwrap(); + + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; + assert!(result.is_err(), "Compression should fail due to slot delay"); + if let Err(err) = result { + let err_msg = format!("{:?}", err); + assert!( + err_msg.contains("Custom(16001)"), + "Expected error message about slot delay, got: {}", + err_msg + ); + } + rpc.warp_to_slot(200).unwrap(); + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; + assert!(result.is_ok(), "Compression should succeed"); +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(150).unwrap(); + + let accounts = sdk_compressible_test::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = sdk_compressible_test::instruction::UpdateRecord { + name: "Updated User".to_string(), + score: 42, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_ok(), "Update record transaction should succeed"); + + rpc.warp_to_slot(200).unwrap(); + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User record account should exist after update" + ); + + let account_data = user_pda_account.unwrap().data; + let updated_user_record = UserRecord::try_deserialize(&mut &account_data[..]).unwrap(); + + assert_eq!(updated_user_record.name, "Updated User"); + assert_eq!(updated_user_record.score, 42); + assert_eq!(updated_user_record.owner, payer.pubkey()); + + assert_eq!( + updated_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + 150 + ); + assert!(!updated_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +pub async fn compress_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + should_fail: bool, +) -> Result { + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User PDA account should exist before compression" + ); + let account = user_pda_account.unwrap(); + assert!( + account.lamports > 0, + "Account should have lamports before compression" + ); + assert!( + !account.data.is_empty(), + "Account data should not be empty before compression" + ); + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value + .unwrap(); + let compressed_address = compressed_account.address.unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*user_record_pda], + &[account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![sdk_compressible_test::get_userrecord_seeds(&payer.pubkey()).0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + if should_fail { + assert!(result.is_err(), "Compress transaction should fail"); + return result; + } else { + assert!(result.is_ok(), "Compress transaction should succeed"); + } + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); + Ok(result.unwrap()) +} diff --git a/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs b/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs index ee57ab658e..2eedefe23d 100644 --- a/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs +++ b/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs @@ -1,6 +1,6 @@ use anchor_lang::{prelude::*, solana_program::program::invoke_signed}; use light_compressed_token_sdk::instructions::create_token_account::{ - create_compressible_token_account, CreateCompressibleTokenAccount, + create_compressible_token_account_instruction, CreateCompressibleTokenAccount, }; use light_ctoken_types::instructions::extensions::compressible::CompressToPubkey; @@ -38,8 +38,8 @@ pub fn process_create_ctoken_with_compress_to_pubkey<'info>( token_account_version: light_ctoken_types::state::TokenDataVersion::ShaFlat, }; - let instruction = - create_compressible_token_account(create_account_inputs).map_err(ProgramError::from)?; + let instruction = create_compressible_token_account_instruction(create_account_inputs) + .map_err(ProgramError::from)?; let seeds = [seeds[0], seeds[1], &[bump]]; diff --git a/sdk-tests/sdk-token-test/src/process_four_transfer2.rs b/sdk-tests/sdk-token-test/src/process_four_transfer2.rs index 58171bdd82..703ace5298 100644 --- a/sdk-tests/sdk-token-test/src/process_four_transfer2.rs +++ b/sdk-tests/sdk-token-test/src/process_four_transfer2.rs @@ -207,13 +207,11 @@ pub fn process_four_transfer2<'info>( ) .map_err(ProgramError::from)?; - msg!("tree_pubkeys {:?}", cpi_accounts.tree_pubkeys()); let tree_accounts = cpi_accounts.tree_accounts().unwrap(); let mut packed_accounts = Vec::with_capacity(tree_accounts.len()); for account_info in tree_accounts { packed_accounts.push(account_meta_from_account_info(account_info)); } - msg!("packed_accounts {:?}", packed_accounts); let inputs = Transfer2Inputs { validity_proof: proof, From e3cba07946c6803e41dc8c33ef1f0b2b909ba176 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 14 Nov 2025 23:56:55 -0500 Subject: [PATCH 2/4] cargo lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2d1ea15d24..96e69b955e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "Inflector" From bcf5798bd77b2cb5f8c0d098db2f13b777db55ad Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 15 Nov 2025 00:12:57 -0500 Subject: [PATCH 3/4] default auto compress false --- sdk-libs/program-test/src/program_test/config.rs | 2 +- sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk-libs/program-test/src/program_test/config.rs b/sdk-libs/program-test/src/program_test/config.rs index 662431a453..50eccb7e1a 100644 --- a/sdk-libs/program-test/src/program_test/config.rs +++ b/sdk-libs/program-test/src/program_test/config.rs @@ -134,7 +134,7 @@ impl Default for ProgramTestConfig { }, with_prover: true, #[cfg(feature = "devenv")] - auto_register_custom_programs_for_pda_compression: true, + auto_register_custom_programs_for_pda_compression: false, #[cfg(feature = "devenv")] skip_second_v1_tree: false, #[cfg(feature = "devenv")] diff --git a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs index 291992b3f1..0df69a2458 100644 --- a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs +++ b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs @@ -48,6 +48,7 @@ async fn test_create_and_decompress_two_accounts() { let mut config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); config = config.with_light_protocol_events(); + config.auto_register_custom_programs_for_pda_compression = true; let mut rpc = LightProgramTest::new(config).await.unwrap(); From d9f47966cf15f5d056467718475abe8ba4f5710f Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 15 Nov 2025 00:39:08 -0500 Subject: [PATCH 4/4] wip --- sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs index 0df69a2458..291992b3f1 100644 --- a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs +++ b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs @@ -48,7 +48,6 @@ async fn test_create_and_decompress_two_accounts() { let mut config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); config = config.with_light_protocol_events(); - config.auto_register_custom_programs_for_pda_compression = true; let mut rpc = LightProgramTest::new(config).await.unwrap();