From f334ec5ef9db3a994214f429d06f3af25af6e8cf Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 23 Oct 2025 07:01:59 -0400 Subject: [PATCH 01/28] 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 --- Cargo.lock | 169 +- Cargo.toml | 3 + cli/scripts/copyLocalProgramBinaries.sh | 14 +- program-libs/compressed-account/Cargo.toml | 6 +- .../src/instruction_data/compressed_proof.rs | 93 + .../src/instruction_data/traits.rs | 5 +- .../src/instruction_data/zero_copy_set.rs | 9 +- program-libs/compressible/Cargo.toml | 6 +- .../compressible/src/compression_info.rs | 4 +- program-libs/ctoken-types/Cargo.toml | 6 +- .../src/state/compressed_token/hash.rs | 3 +- .../src/state/compressed_token/token_data.rs | 3 +- .../src/state/ctoken/zero_copy.rs | 11 +- .../src/state/mint/compressed_mint.rs | 3 +- .../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 +- program-tests/registry-test/tests/tests.rs | 1 + .../program/src/ctoken_transfer.rs | 7 + .../program/src/shared/token_input.rs | 17 + .../compression/ctoken/compress_and_close.rs | 83 +- .../program/src/transfer2/compression/mod.rs | 9 + .../program/src/transfer2/processor.rs | 3 + sdk-libs/client/Cargo.toml | 4 + sdk-libs/client/src/constants.rs | 25 + sdk-libs/client/src/indexer/types.rs | 51 +- sdk-libs/client/src/rpc/lut.rs | 37 + sdk-libs/client/src/rpc/mod.rs | 3 + sdk-libs/compressed-token-sdk/src/account2.rs | 160 +- sdk-libs/compressed-token-sdk/src/error.rs | 12 + .../src/instructions/compress_and_close.rs | 77 +- .../create_associated_token_account.rs | 89 + .../create_compressed_mint/instruction.rs | 11 + .../create_compressed_mint/mod.rs | 68 +- .../create_token_account/instruction.rs | 56 +- .../src/instructions/decompress_full.rs | 18 +- .../src/instructions/mod.rs | 11 +- .../src/instructions/transfer_ctoken.rs | 70 + .../src/instructions/transfer_interface.rs | 563 ++++ sdk-libs/compressed-token-sdk/src/lib.rs | 1 + sdk-libs/compressed-token-sdk/src/utils.rs | 48 +- sdk-libs/compressible-client/Cargo.toml | 27 + .../src/get_compressible_account.rs | 164 + sdk-libs/compressible-client/src/lib.rs | 464 +++ .../compressible-client/tests/pack_test.rs | 120 + sdk-libs/macros/src/compress_as.rs | 206 ++ sdk-libs/macros/src/compressible.rs | 85 + sdk-libs/macros/src/compressible_derive.rs | 354 +++ sdk-libs/macros/src/lib.rs | 116 + sdk-libs/program-test/Cargo.toml | 2 + sdk-libs/program-test/src/compressible.rs | 144 + .../src/program_test/compressible_setup.rs | 159 + .../program-test/src/program_test/config.rs | 5 + .../src/program_test/light_program_test.rs | 26 + sdk-libs/program-test/src/program_test/mod.rs | 3 + .../program-test/src/program_test/test_rpc.rs | 4 + sdk-libs/program-test/src/utils/mod.rs | 3 + sdk-libs/program-test/src/utils/simulation.rs | 36 + sdk-libs/sdk/Cargo.toml | 7 +- sdk-libs/sdk/src/account.rs | 15 +- sdk-libs/sdk/src/address.rs | 14 + sdk-libs/sdk/src/compressible/close.rs | 29 + .../sdk/src/compressible/compress_account.rs | 115 + .../compressible/compress_account_on_init.rs | 102 + .../sdk/src/compressible/compression_info.rs | 125 + sdk-libs/sdk/src/compressible/config.rs | 482 +++ .../src/compressible/decompress_idempotent.rs | 141 + sdk-libs/sdk/src/compressible/mod.rs | 28 + sdk-libs/sdk/src/cpi/invoke.rs | 2 + sdk-libs/sdk/src/lib.rs | 7 + sdk-libs/sdk/src/token.rs | 180 +- .../create_compressible_token_account.rs | 3 +- .../src/actions/ctoken_transfer.rs | 12 +- .../src/actions/transfer2/ctoken_to_spl.rs | 8 +- .../src/actions/transfer2/spl_to_ctoken.rs | 8 +- sdk-libs/token-client/src/lib.rs | 168 + sdk-tests/csdk-anchor-test/Cargo.toml | 57 + sdk-tests/csdk-anchor-test/Xargo.toml | 2 + sdk-tests/csdk-anchor-test/src/lib.rs | 2225 +++++++++++++ sdk-tests/csdk-anchor-test/tests/test.rs | 2784 +++++++++++++++++ ...s_create_ctoken_with_compress_to_pubkey.rs | 6 +- 84 files changed, 9861 insertions(+), 360 deletions(-) create mode 100644 sdk-libs/client/src/rpc/lut.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/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/compressible-client/tests/pack_test.rs create mode 100644 sdk-libs/macros/src/compress_as.rs create mode 100644 sdk-libs/macros/src/compressible.rs create mode 100644 sdk-libs/macros/src/compressible_derive.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-tests/csdk-anchor-test/Cargo.toml create mode 100644 sdk-tests/csdk-anchor-test/Xargo.toml create mode 100644 sdk-tests/csdk-anchor-test/src/lib.rs create mode 100644 sdk-tests/csdk-anchor-test/tests/test.rs diff --git a/Cargo.lock b/Cargo.lock index bdae838091..6c34f8b5a9 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" @@ -1436,7 +1452,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", @@ -1648,6 +1664,40 @@ dependencies = [ "subtle", ] +[[package]] +name = "csdk-anchor-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 = "ctr" version = "0.9.2" @@ -3516,6 +3566,7 @@ dependencies = [ name = "light-client" version = "0.16.0" dependencies = [ + "anchor-lang", "async-trait", "base64 0.13.1", "borsh 0.10.4", @@ -3546,11 +3597,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", @@ -3693,6 +3746,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 +3998,7 @@ dependencies = [ "light-compressed-token", "light-compressed-token-sdk", "light-compressible", + "light-compressible-client", "light-concurrent-merkle-tree", "light-ctoken-types", "light-event", @@ -4022,6 +4090,7 @@ name = "light-sdk" version = "0.16.0" dependencies = [ "anchor-lang", + "bincode", "borsh 0.10.4", "light-account-checks", "light-compressed-account", @@ -4033,11 +4102,15 @@ dependencies = [ "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", + "solana-clock", "solana-cpi", "solana-instruction", "solana-msg 2.2.1", + "solana-program", "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 +4224,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", @@ -4448,6 +4521,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 +4672,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" @@ -6069,7 +6166,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 +7902,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 +8186,7 @@ dependencies = [ "dialoguer", "hidapi", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot", "qstring", @@ -9125,7 +9222,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 +9248,7 @@ dependencies = [ "agave-feature-set", "bincode", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -9184,7 +9281,7 @@ checksum = "70cea14481d8efede6b115a2581f27bc7c6fdfba0752c20398456c3ac1245fc4" dependencies = [ "agave-feature-set", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -9208,7 +9305,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 +9334,7 @@ checksum = "579752ad6ea2a671995f13c763bf28288c3c895cb857a518cc4ebab93c9a8dde" dependencies = [ "agave-feature-set", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -9260,7 +9357,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 +9400,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 +9416,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 +9552,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 +9569,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 +9582,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 +9622,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 +9644,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 +9667,7 @@ checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9585,7 +9682,7 @@ checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-account-info", @@ -9613,7 +9710,7 @@ checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9641,7 +9738,7 @@ checksum = "9048b26b0df0290f929ff91317c83db28b3ef99af2b3493dd35baa146774924c" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9668,7 +9765,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 +9793,7 @@ checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-account-info", @@ -9864,7 +9961,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 +9980,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 +9999,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 +10020,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 +10042,7 @@ checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9970,7 +10067,7 @@ checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9994,7 +10091,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 +10109,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,7 +10269,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)", "create-address-test-program", "light-account-checks", "light-batched-merkle-tree", @@ -10201,7 +10298,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..d0224bdb97 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/csdk-anchor-test", "forester-utils", "forester", "sparse-merkle-tree", @@ -180,6 +182,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/scripts/copyLocalProgramBinaries.sh b/cli/scripts/copyLocalProgramBinaries.sh index deddd04e8a..afddb155e9 100755 --- a/cli/scripts/copyLocalProgramBinaries.sh +++ b/cli/scripts/copyLocalProgramBinaries.sh @@ -11,6 +11,18 @@ fi keys="account_compression light_system_program_pinocchio light_compressed_token light_registry" for key in $keys do - cp "$root_dir/target/deploy/$key.so" "$out_dir"/"$key".so + # cli build process deletes target/deploy contents, so fall back to + # sbf-solana-solana + src_deploy="$root_dir/target/deploy/$key.so" + src_sbf_release="$root_dir/target/sbf-solana-solana/release/$key.so" + + if [ -f "$src_deploy" ]; then + cp "$src_deploy" "$out_dir/$key.so" + elif [ -f "$src_sbf_release" ]; then + cp "$src_sbf_release" "$out_dir/$key.so" + else + echo "Error: $key.so not found in $src_deploy or $src_sbf_release" >&2 + exit 1 + fi done cp "$root_dir"/third-party/solana-program-library/spl_noop.so "$out_dir"/spl_noop.so diff --git a/program-libs/compressed-account/Cargo.toml b/program-libs/compressed-account/Cargo.toml index 693fba0f53..a5f93db3f6 100644 --- a/program-libs/compressed-account/Cargo.toml +++ b/program-libs/compressed-account/Cargo.toml @@ -15,8 +15,8 @@ anchor = ["anchor-lang", "std"] pinocchio = ["dep:pinocchio"] bytemuck-des = ["bytemuck"] new-unique = ["dep:solana-pubkey"] -profile-program = [] -profile-heap = ["dep:light-heap"] +profile-program = ["dep:light-program-profiler"] +profile-heap = ["dep:light-heap", "dep:light-program-profiler"] poseidon = ["dep:light-poseidon", "light-hasher/poseidon"] keccak = ["light-hasher/keccak"] sha256 = ["light-hasher/sha256"] @@ -36,7 +36,7 @@ anchor-lang = { workspace = true, optional = true } bytemuck = { workspace = true, optional = true, features = ["derive"] } borsh = { workspace = true, default-features = false } solana-pubkey = { workspace = true, optional = true } -light-program-profiler = { workspace = true } +light-program-profiler = { workspace = true, optional = true } light-heap = { workspace = true, optional = true } tinyvec = { workspace = true } diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index de6c7c6a15..e6310fb3cd 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -141,3 +141,96 @@ impl Into> for ValidityProof { self.0 } } + +// Borsh compatible validity proof. Use this in your anchor program unless you +// have zero-copy instruction data. Convert to zero-copy by calling `let proof = +// compression_params.proof.into();`. +// + +pub mod borsh_compat { + #[cfg_attr( + all(feature = "std", feature = "anchor"), + derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize) + )] + #[cfg_attr( + not(feature = "anchor"), + derive(borsh::BorshDeserialize, borsh::BorshSerialize) + )] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + 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], + } + } + } + + #[cfg_attr( + all(feature = "std", feature = "anchor"), + derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize) + )] + #[cfg_attr( + not(feature = "anchor"), + derive(borsh::BorshDeserialize, borsh::BorshSerialize) + )] + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] + pub struct ValidityProof(pub Option); + + impl ValidityProof { + pub fn new(proof: Option) -> Self { + Self(proof) + } + } + + impl From for CompressedProof { + fn from(proof: super::CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From for super::CompressedProof { + fn from(proof: CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From for ValidityProof { + fn from(proof: super::ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for super::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/program-libs/compressed-account/src/instruction_data/traits.rs b/program-libs/compressed-account/src/instruction_data/traits.rs index f5e8a95733..8da9e48248 100644 --- a/program-libs/compressed-account/src/instruction_data/traits.rs +++ b/program-libs/compressed-account/src/instruction_data/traits.rs @@ -5,6 +5,7 @@ use anchor_lang::AnchorSerialize; #[allow(unused_imports)] #[cfg(not(all(feature = "std", feature = "anchor")))] use borsh::BorshSerialize as AnchorSerialize; +#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use tinyvec::ArrayVec; use zerocopy::Ref; @@ -22,7 +23,7 @@ pub trait InstructionDiscriminator { pub trait LightInstructionData: InstructionDiscriminator + AnchorSerialize { #[cfg(feature = "alloc")] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] fn data(&self) -> Result, CompressedAccountError> { let inputs = AnchorSerialize::try_to_vec(self) .map_err(|_| CompressedAccountError::InvalidArgument)?; @@ -32,7 +33,7 @@ pub trait LightInstructionData: InstructionDiscriminator + AnchorSerialize { Ok(data) } - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] fn data_array(&self) -> Result, CompressedAccountError> { let mut data = ArrayVec::new(); // Add discriminator diff --git a/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs b/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs index b5d382a271..60facf1f67 100644 --- a/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs +++ b/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs @@ -1,3 +1,4 @@ +#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::traits::ZeroCopyAt; use zerocopy::little_endian::U16; @@ -13,7 +14,7 @@ use crate::{ }; impl ZOutputCompressedAccountWithPackedContextMut<'_> { - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] #[inline] pub fn set( &mut self, @@ -49,7 +50,7 @@ impl ZOutputCompressedAccountWithPackedContextMut<'_> { impl ZInAccountMut<'_> { #[inline] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn set_z( &mut self, discriminator: [u8; 8], @@ -83,7 +84,7 @@ impl ZInAccountMut<'_> { } #[inline] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn set( &mut self, discriminator: [u8; 8], @@ -159,7 +160,7 @@ impl ZInstructionDataInvokeCpiWithReadOnlyMut<'_> { impl ZNewAddressParamsAssignedPackedMut<'_> { #[inline] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn set( &mut self, seed: [u8; 32], diff --git a/program-libs/compressible/Cargo.toml b/program-libs/compressible/Cargo.toml index c644202214..5c8d69822f 100644 --- a/program-libs/compressible/Cargo.toml +++ b/program-libs/compressible/Cargo.toml @@ -8,8 +8,8 @@ default = ["pinocchio", "solana"] solana = ["dep:solana-program-error", "light-compressed-account/solana", "solana-sysvar", "solana-msg"] anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressed-account/std"] pinocchio = ["light-compressed-account/pinocchio"] -profile-program = [] -profile-heap = ["dep:light-heap"] +profile-program = ["dep:light-program-profiler"] +profile-heap = ["dep:light-heap", "dep:light-program-profiler"] [dependencies] thiserror = { workspace = true } @@ -26,7 +26,7 @@ bytemuck = { workspace = true, features = ["derive"] } borsh = { workspace = true } solana-pubkey = { workspace = true, features = ["std", "sha2", "curve25519", "borsh", "bytemuck"] } pinocchio-pubkey = { workspace = true } -light-program-profiler = { workspace = true } +light-program-profiler = { workspace = true, optional = true } light-heap = { workspace = true, optional = true } light-account-checks = { workspace= true } light-compressed-account = { workspace= true } diff --git a/program-libs/compressible/src/compression_info.rs b/program-libs/compressible/src/compression_info.rs index 5371065817..1007be970e 100644 --- a/program-libs/compressible/src/compression_info.rs +++ b/program-libs/compressible/src/compression_info.rs @@ -1,6 +1,5 @@ use aligned_sized::aligned_sized; use bytemuck::{Pod, Zeroable}; -use light_program_profiler::profile; use light_zero_copy::{ZeroCopy, ZeroCopyMut}; use pinocchio::pubkey::Pubkey; use zerocopy::U64; @@ -62,7 +61,7 @@ macro_rules! impl_is_compressible { /// available_balance = current_lamports - last_lamports /// (we can never claim more lamports than rent is due) /// remaining_balance = available_balance - rent_due - #[profile] + pub fn is_compressible( &self, bytes: u64, @@ -88,7 +87,6 @@ macro_rules! impl_is_compressible { /// Returns 0 if no top-up is needed (account is well-funded). /// Returns write_top_up + rent_deficit if account is compressible. /// Returns write_top_up if account needs more funding but isn't compressible yet. - #[profile] pub fn calculate_top_up_lamports( &self, num_bytes: u64, diff --git a/program-libs/ctoken-types/Cargo.toml b/program-libs/ctoken-types/Cargo.toml index a4955d7b1d..597a2a9c29 100644 --- a/program-libs/ctoken-types/Cargo.toml +++ b/program-libs/ctoken-types/Cargo.toml @@ -7,8 +7,8 @@ edition = { workspace = true } anchor = ["light-compressed-account/anchor", "dep:anchor-lang", "light-compressible/anchor"] solana = ["dep:solana-program-error", "dep:solana-sysvar", "solana-msg"] default = [] -profile-program = [] -profile-heap = ["dep:light-heap"] +profile-program = ["dep:light-program-profiler"] +profile-heap = ["dep:light-heap", "dep:light-program-profiler"] poseidon = ["light-hasher/poseidon"] [dependencies] @@ -31,7 +31,7 @@ solana-sysvar = { workspace = true, optional = true } spl-pod = { workspace = true } spl-token-2022 = { workspace = true } solana-msg = { workspace = true, optional = true } -light-program-profiler = { workspace = true } +light-program-profiler = { workspace = true, optional = true } light-heap = { workspace = true, optional = true } light-compressible = {workspace = true } pinocchio-pubkey = {workspace = true} diff --git a/program-libs/ctoken-types/src/state/compressed_token/hash.rs b/program-libs/ctoken-types/src/state/compressed_token/hash.rs index 13d8979873..fe045ce626 100644 --- a/program-libs/ctoken-types/src/state/compressed_token/hash.rs +++ b/program-libs/ctoken-types/src/state/compressed_token/hash.rs @@ -1,6 +1,7 @@ use borsh::BorshSerialize; use light_compressed_account::hash_to_bn254_field_size_be; use light_hasher::{errors::HasherError, sha256::Sha256BE, Hasher, Poseidon}; +#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use super::TokenData; @@ -81,7 +82,7 @@ impl TokenData { impl TokenData { /// TokenDataVersion 3 /// CompressedAccount Discriminator [0,0,0,0,0,0,0,4] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] #[inline(always)] pub fn hash_sha_flat(&self) -> Result<[u8; 32], HasherError> { let bytes = self.try_to_vec().map_err(|_| HasherError::BorshError)?; diff --git a/program-libs/ctoken-types/src/state/compressed_token/token_data.rs b/program-libs/ctoken-types/src/state/compressed_token/token_data.rs index 05d365b35f..03957ab3e9 100644 --- a/program-libs/ctoken-types/src/state/compressed_token/token_data.rs +++ b/program-libs/ctoken-types/src/state/compressed_token/token_data.rs @@ -1,4 +1,5 @@ use light_compressed_account::Pubkey; +#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::{num_trait::ZeroCopyNumTrait, ZeroCopy, ZeroCopyMut}; @@ -55,7 +56,7 @@ impl TokenData { impl ZTokenDataMut<'_> { /// Set all fields of the TokenData struct at once #[inline] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn set( &mut self, mint: Pubkey, diff --git a/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs b/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs index a708c83844..ee4c0b84b5 100644 --- a/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs +++ b/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; use light_compressed_account::Pubkey; +#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::{ errors::ZeroCopyError, @@ -144,7 +145,7 @@ impl<'a> ZeroCopyAt<'a> for CTokenMeta { impl<'a> ZeroCopyAtMut<'a> for CTokenMeta { type ZeroCopyAtMut = ZCompressedTokenMetaMut<'a>; - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] #[inline(always)] fn zero_copy_at_mut( bytes: &'a mut [u8], @@ -411,7 +412,7 @@ impl DerefMut for ZCompressedTokenMut<'_> { impl<'a> ZeroCopyAt<'a> for CToken { type ZeroCopyAt = ZCToken<'a>; - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] fn zero_copy_at(bytes: &'a [u8]) -> Result<(Self::ZeroCopyAt, &'a [u8]), ZeroCopyError> { let (__meta, bytes) = >::zero_copy_at(bytes)?; let (extensions, bytes) = if !bytes.is_empty() { @@ -436,7 +437,7 @@ impl<'a> ZeroCopyAt<'a> for CToken { impl CToken { /// Zero-copy deserialization with initialization check. /// Returns an error if the account is not initialized (byte 108 must be 1). - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn zero_copy_at_checked( bytes: &[u8], ) -> Result<(ZCToken<'_>, &[u8]), crate::error::CTokenError> { @@ -456,7 +457,7 @@ impl CToken { /// Mutable zero-copy deserialization with initialization check. /// Returns an error if the account is not initialized (byte 108 must be 1). - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn zero_copy_at_mut_checked( bytes: &mut [u8], ) -> Result<(ZCompressedTokenMut<'_>, &mut [u8]), crate::error::CTokenError> { @@ -477,7 +478,7 @@ impl CToken { impl<'a> ZeroCopyAtMut<'a> for CToken { type ZeroCopyAtMut = ZCompressedTokenMut<'a>; - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] #[inline(always)] fn zero_copy_at_mut( bytes: &'a mut [u8], diff --git a/program-libs/ctoken-types/src/state/mint/compressed_mint.rs b/program-libs/ctoken-types/src/state/mint/compressed_mint.rs index bc3e5e7ae5..0c38b07937 100644 --- a/program-libs/ctoken-types/src/state/mint/compressed_mint.rs +++ b/program-libs/ctoken-types/src/state/mint/compressed_mint.rs @@ -1,6 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_compressed_account::Pubkey; use light_hasher::{sha256::Sha256BE, Hasher}; +#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::{ZeroCopy, ZeroCopyMut}; #[cfg(feature = "solana")] @@ -70,7 +71,7 @@ impl CompressedMint { impl ZCompressedMintMut<'_> { /// Set all fields of the CompressedMint struct at once #[inline] - #[profile] + #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] pub fn set( &mut self, ix_data: &>::ZeroCopyAt, 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/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/programs/compressed-token/program/src/ctoken_transfer.rs b/programs/compressed-token/program/src/ctoken_transfer.rs index 58e1fabcf7..0d93a9d4c5 100644 --- a/programs/compressed-token/program/src/ctoken_transfer.rs +++ b/programs/compressed-token/program/src/ctoken_transfer.rs @@ -27,8 +27,12 @@ pub fn process_ctoken_transfer<'a>( return Err(ProgramError::NotEnoughAccountKeys); } + msg!("processing transfer"); + msg!("accounts: {:?}", accounts); + process_transfer(accounts, instruction_data) .map_err(|e| ProgramError::Custom(u64::from(e) as u32))?; + msg!("transfer processed"); calculate_and_execute_top_up_transfers(accounts) } @@ -41,6 +45,9 @@ fn calculate_and_execute_top_up_transfers( // Initialize transfers array with account references, amounts will be updated let account0 = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; let account1 = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; + msg!("accounts: {:?}", accounts); + msg!("account0: {:?}", account0.key()); + msg!("account1: {:?}", account1.key()); let mut transfers = [ Transfer { account: account0, diff --git a/programs/compressed-token/program/src/shared/token_input.rs b/programs/compressed-token/program/src/shared/token_input.rs index decd9f7426..0b1af32f16 100644 --- a/programs/compressed-token/program/src/shared/token_input.rs +++ b/programs/compressed-token/program/src/shared/token_input.rs @@ -93,6 +93,21 @@ fn set_input_compressed_account_inner( ProgramError::Custom(AccountError::NotEnoughAccountKeys.into()) })?; + anchor_lang::solana_program::msg!("DEBUG token_input - mint_account: {:?}", mint_account.key()); + anchor_lang::solana_program::msg!( + "DEBUG token_input - owner_account: {:?}", + owner_account.key() + ); + anchor_lang::solana_program::msg!("DEBUG token_input - amount: {:?}", input_token_data.amount); + anchor_lang::solana_program::msg!( + "DEBUG token_input - has_delegate: {:?}", + input_token_data.has_delegate + ); + anchor_lang::solana_program::msg!( + "DEBUG token_input - leaf_index: {:?}", + input_token_data.merkle_context.leaf_index + ); + let data_hash = { match token_version { TokenDataVersion::ShaFlat => { @@ -140,6 +155,8 @@ fn set_input_compressed_account_inner( } }; + anchor_lang::solana_program::msg!("DEBUG token_input - computed data_hash: {:?}", data_hash); + input_compressed_account.set_z( token_version.discriminator(), data_hash, diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs index 78abf4ff8b..3e4fc68e06 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs @@ -13,7 +13,8 @@ use spl_pod::solana_msg::msg; use super::inputs::CompressAndCloseInputs; use crate::{ close_token_account::{ - accounts::CloseTokenAccountAccounts, processor::validate_token_account_for_close_transfer2, + accounts::CloseTokenAccountAccounts, + processor::{close_token_account, validate_token_account_for_close_transfer2}, }, transfer2::accounts::Transfer2Accounts, }; @@ -168,7 +169,7 @@ fn validate_compressed_token_account( /// Close ctoken accounts after compress and close operations pub fn close_for_compress_and_close( compressions: &[ZCompression<'_>], - _validated_accounts: &Transfer2Accounts, + validated_accounts: &Transfer2Accounts, ) -> Result<(), ProgramError> { // Track used compressed account indices for CompressAndClose to prevent duplicate outputs let mut used_compressed_account_indices = [0u8; 32]; // 256 bits @@ -178,48 +179,44 @@ pub fn close_for_compress_and_close( .iter() .filter(|c| c.mode == ZCompressionMode::CompressAndClose) { - // Check for duplicate compressed account indices in CompressAndClose operations - let compressed_idx = compression.get_compressed_token_account_index()?; - if let Some(mut bit) = used_bits.get_mut(compressed_idx as usize) { - if *bit { - msg!( - "Duplicate compressed account index {} in CompressAndClose operations", - compressed_idx - ); - return Err(ErrorCode::CompressAndCloseDuplicateOutput.into()); - } - *bit = true; - } else { - msg!("Compressed account index {} out of bounds", compressed_idx); - return Err(ProgramError::InvalidInstructionData); - } + let token_account_info = validated_accounts.packed_accounts.get_u8( + compression.source_or_recipient, + "CompressAndClose: source_or_recipient", + )?; + let destination = validated_accounts.packed_accounts.get_u8( + compression.get_destination_index()?, + "CompressAndClose: destination", + )?; + let rent_sponsor = validated_accounts.packed_accounts.get_u8( + compression.get_rent_sponsor_index()?, + "CompressAndClose: rent_sponsor", + )?; - #[cfg(target_os = "solana")] - { - let validated_accounts = _validated_accounts; - let token_account_info = validated_accounts.packed_accounts.get_u8( - compression.source_or_recipient, - "CompressAndClose: source_or_recipient", - )?; - let destination = validated_accounts.packed_accounts.get_u8( - compression.get_destination_index()?, - "CompressAndClose: destination", - )?; - let rent_sponsor = validated_accounts.packed_accounts.get_u8( - compression.get_rent_sponsor_index()?, - "CompressAndClose: rent_sponsor", - )?; - let authority = validated_accounts - .packed_accounts - .get_u8(compression.authority, "CompressAndClose: authority")?; - use crate::close_token_account::processor::close_token_account; - close_token_account(&CloseTokenAccountAccounts { - token_account: token_account_info, - destination, - authority, - rent_sponsor: Some(rent_sponsor), - })?; - } + let authority = validated_accounts + .packed_accounts + .get_u8(compression.authority, "CompressAndClose: authority")?; + msg!( + "Closing ctoken account: {}", + solana_pubkey::Pubkey::new_from_array(*token_account_info.key()) + ); + msg!( + " destination: {}", + solana_pubkey::Pubkey::new_from_array(*destination.key()) + ); + msg!( + " authority: {}", + solana_pubkey::Pubkey::new_from_array(*authority.key()) + ); + msg!( + " rent_sponsor: {}", + solana_pubkey::Pubkey::new_from_array(*rent_sponsor.key()) + ); + close_token_account(&CloseTokenAccountAccounts { + token_account: token_account_info, + destination, + authority, + rent_sponsor: Some(rent_sponsor), + })?; } Ok(()) } diff --git a/programs/compressed-token/program/src/transfer2/compression/mod.rs b/programs/compressed-token/program/src/transfer2/compression/mod.rs index 31430354d7..2869b8734f 100644 --- a/programs/compressed-token/program/src/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/transfer2/compression/mod.rs @@ -37,6 +37,7 @@ pub fn process_token_compression( packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, cpi_authority: &AccountInfo, ) -> Result<(), ProgramError> { + msg!("processing token compression"); if let Some(compressions) = inputs.compressions.as_ref() { // Array to accumulate transfer amounts by account index (max 40 packed accounts) let mut transfer_map = [0u64; 40]; @@ -124,6 +125,14 @@ pub fn process_token_compression( .collect::, ProgramError>>()?; if !transfers.is_empty() { + msg!("Top-up payer: {:?}", fee_payer.key()); + for transfer in &transfers { + msg!( + "Top-up recipient: {} ({} lamports)", + solana_pubkey::Pubkey::new_from_array(*transfer.account.key()), + transfer.amount + ); + } multi_transfer_lamports(fee_payer, &transfers).map_err(convert_program_error)? } } diff --git a/programs/compressed-token/program/src/transfer2/processor.rs b/programs/compressed-token/program/src/transfer2/processor.rs index d64e70f798..99520f5ab8 100644 --- a/programs/compressed-token/program/src/transfer2/processor.rs +++ b/programs/compressed-token/program/src/transfer2/processor.rs @@ -202,6 +202,7 @@ fn process_with_system_program_cpi( .map_err(|e| ProgramError::Custom(e as u32 + 6000))?; if let Some(system_accounts) = validated_accounts.system.as_ref() { + msg!("processing token compression with system program"); // Process token compressions/decompressions/close_and_compress process_token_compression( system_accounts.fee_payer, @@ -225,11 +226,13 @@ fn process_with_system_program_cpi( false, )?; + msg!("closing ctoken accounts"); // Close ctoken accounts at the end of the instruction. if let Some(compressions) = inputs.compressions.as_ref() { close_for_compress_and_close(compressions.as_slice(), validated_accounts)?; } } else if let Some(system_accounts) = validated_accounts.write_to_cpi_context_system.as_ref() { + msg!("processing token compression with system program and write to cpi context"); // CPI context write mode expects exactly 4 accounts: // 0 - light-system-program - skip // 1 - fee_payer diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index ea89387e43..0e14530484 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,9 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [ "bytemuck", "bincode", ] } +solana-message = "2.2" +# TODO: check if we can move. +anchor-lang = { workspace = true, features = ["idl-build"], optional = true } # Light Protocol dependencies light-merkle-tree-metadata = { workspace = true, features = ["solana"] } 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..43fa71fae8 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -13,7 +13,6 @@ use light_sdk::{ }; use num_bigint::BigUint; use solana_pubkey::Pubkey; -use tracing::warn; use super::{ base58::{decode_base58_option_to_pubkey, decode_base58_to_fixed_array}, @@ -529,16 +528,30 @@ impl TryFrom for CompressedAccount { .hash() .map_err(|_| IndexerError::InvalidResponseData)?; // Breaks light-program-test - let tree_info = QUEUE_TREE_MAPPING.get( - &Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()) - .to_string(), - ); - let cpi_context = if let Some(tree_info) = tree_info { - tree_info.cpi_context - } else { - warn!("Cpi context not found in queue tree mapping"); - None - }; + // let tree_info = QUEUE_TREE_MAPPING.get( + // &Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()) + // .to_string(), + // ); + + let tree_pubkey = + Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()); + let tree_info = QUEUE_TREE_MAPPING + .get(&tree_pubkey.to_string()) + .ok_or_else(|| { + println!( + "ERROR: No tree_info found for tree pubkey: {:?}", + tree_pubkey.to_string() + ); + IndexerError::InvalidResponseData + })?; + + if tree_info.cpi_context.is_none() { + panic!( + "Cpi context not found in queue tree mapping for tree pubkey: {:?}", + tree_pubkey.to_string() + ); + } + Ok(CompressedAccount { address: account.compressed_account.address, data: account.compressed_account.data, @@ -546,10 +559,10 @@ impl TryFrom for CompressedAccount { lamports: account.compressed_account.lamports, leaf_index: account.merkle_context.leaf_index, tree_info: TreeInfo { - tree: Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()), + tree: tree_pubkey, queue: Pubkey::new_from_array(account.merkle_context.queue_pubkey.to_bytes()), tree_type: account.merkle_context.tree_type, - cpi_context, + cpi_context: tree_info.cpi_context, next_tree_info: None, }, owner: Pubkey::new_from_array(account.compressed_account.owner.to_bytes()), @@ -620,6 +633,18 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { .map(|ctx| NextTreeInfo::try_from(ctx.as_ref())) .transpose()?, }; + // TODO: check if the above handles it fine. + // let tree_pubkey = + // Pubkey::new_from_array(decode_base58_to_fixed_array(&account.merkle_context.tree)?); + // let tree_info = QUEUE_TREE_MAPPING + // .get(&tree_pubkey.to_string()) + // .ok_or_else(|| { + // println!( + // "ERROR: No tree_info found for tree pubkey: {}", + // account.merkle_context.tree + // ); + // IndexerError::InvalidResponseData + // })?; Ok(CompressedAccount { owner, 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..bfb9210295 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 use lut::load_lookup_table; +pub mod lut; 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/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..41ce6cf049 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 @@ -93,6 +95,7 @@ pub fn pack_for_compress_and_close( false, ), owner_index, // User funds go to owner + // recipient_index, // User funds go to rent sponsor (destination) ) }; Ok(CompressAndCloseIndices { @@ -216,6 +219,9 @@ 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 )?; + // Ensure destination (rent sponsor) is writable to receive lamports + // TODO: Checked remove + packed_account_metas[idx.destination_index as usize].is_writable = true; if rent_sponsor_is_signer { packed_account_metas[idx.authority_index as usize].is_signer = true; } else { @@ -365,14 +371,8 @@ 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 - }; + // Destination for lamports on close is ALWAYS the rent sponsor + let destination_pubkey = actual_rent_sponsor; // Find indices for all required accounts let indices = find_account_indices( @@ -401,6 +401,67 @@ 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_recipient: 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_recipient.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, + ) + .map_err(|_| TokenSdkError::InvalidAccountData)?; + + // 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..120dacb009 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,91 @@ fn create_ata2_instruction_unified( + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + compressible_config: AccountInfo<'info>, + rent_recipient: 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_recipient.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( + inputs, + *associated_token_account.key, + bump, + )?; + + solana_cpi::invoke( + &ix, + &[ + payer, + associated_token_account, + system_program, + compressible_config, + rent_recipient, + 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..65aff76ec4 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 @@ -218,3 +218,14 @@ pub fn find_spl_mint_address(mint_seed: &Pubkey) -> (Pubkey, u8) { &Pubkey::new_from_array(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), ) } + +/// DEPRECATED: Use derive_compressed_mint_address instead +/// Derives the compressed mint address from the mint seed and address tree +pub fn derive_ctoken_mint_address(mint_seed: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] { + derive_compressed_mint_address(mint_seed, address_tree_pubkey) +} + +/// Alias for find_spl_mint_address +pub fn find_mint_address(signer: &Pubkey) -> (Pubkey, u8) { + find_spl_mint_address(signer) +} 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..6d8fb47836 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 @@ -6,44 +6,42 @@ pub use account_metas::{ }; 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, + derive_compressed_mint_from_spl_mint, derive_ctoken_mint_address, find_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, -} +// #[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 - } +// 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 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_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] - } -} +// 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..a41a68deeb 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,55 @@ 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))?; + + 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..91d5f068b1 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs @@ -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,22 @@ 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() { + 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..a10fa08c11 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,6 +52,12 @@ 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}; +// TODO: export the others too. +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, 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..35d62108b0 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs @@ -0,0 +1,70 @@ +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 decompressed token transfer instruction. This creates an +/// instruction that uses discriminator 3 (DecompressedTransfer) to perform SPL +/// token transfers on decompressed compressed token accounts. +/// +/// # 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: { + let mut data = vec![3u8]; // DecompressedTransfer discriminator + data.push(3u8); // SPL Transfer discriminator + data.extend_from_slice(&amount.to_le_bytes()); + data + }, + } +} + +/// Transfer decompressed ctokens +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); + + // Return Result directly, as is best practice for CPI helpers in native Solana programs. + invoke(&ix, &[from.clone(), to.clone(), authority.clone()]) +} + +/// Transfer decompressed ctokens with signer seeds +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..159670e8a9 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -0,0 +1,563 @@ +use light_compressed_account::instruction_data::compressed_proof::borsh_compat::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, + }; + + // Create Transfer2Inputs following the test + let inputs = Transfer2Inputs { + validity_proof: ValidityProof::new(None).into(), + 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).into(), + 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 +/// +/// This function creates the instruction and immediately invokes it. +/// Similar to SPL Token's transfer wrapper functions. +#[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![ + authority.clone(), + 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. +/// +/// This function creates the instruction and invokes it with the provided +/// signer seeds. +#[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(|_| TokenSdkError::MethodUsed)?; + + let account_infos = vec![ + payer.clone(), + 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) + .map_err(|_| TokenSdkError::MethodUsed)?; + Ok(()) +} + +// TODO: TEST. +/// Transfer compressed tokens to SPL tokens +/// +/// This function creates the instruction and invokes it. +#[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![ + authority.clone(), + 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. +/// +/// This function creates the instruction and invokes it with the provided +/// signer seeds. +#[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.clone(), + 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 that automatically handles both ctoken<->ctoken and ctoken<->spl transfers +/// +/// This function inspects the source and destination accounts to determine the transfer type +/// and validates that the correct optional parameters are provided. +/// +/// # 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> { + // 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(source_account, destination_account, authority, amount), + + // 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( + 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 -> 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( + 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 signer seeds for CPI +/// +/// Same as `transfer_interface` but uses invoke_signed for CPI calls +#[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/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 45ac38becd..535a493d89 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -12,3 +12,4 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use light_compressed_token_types::*; +pub use utils::*; 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/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml new file mode 100644 index 0000000000..7132e73142 --- /dev/null +++ b/sdk-libs/compressible-client/Cargo.toml @@ -0,0 +1,27 @@ +[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 dependencies +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-account = { workspace = true } + +# Light Protocol dependencies +light-client = { workspace = true, features = ["v2"] } +light-sdk = { workspace = true, features = ["v2", "cpi-context"] } + +# Conditional dependencies +anchor-lang = { workspace = true, features = ["idl-build"], optional = true } +borsh = { workspace = true } + +# External dependencies +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..de30448235 --- /dev/null +++ b/sdk-libs/compressible-client/src/get_compressible_account.rs @@ -0,0 +1,164 @@ +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 from either compressed or onchain storage. +/// Matches TypeScript getAccountInfoInterface behavior. +/// +/// Returns account info with compression state and merkle context. +/// Fetches both onchain and compressed in parallel for optimal performance. +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); + + let onchain_result = rpc.get_account(*address).await; + let compressed_result = rpc.get_compressed_account(compressed_address, None).await; + + let onchain_account = onchain_result.ok().flatten(); + let compressed_account = compressed_result.ok().and_then(|r| r.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)] +/// Deserialize account from AccountInfoInterface using Anchor (includes discriminator). +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) +} + +#[cfg(not(feature = "anchor"))] +#[allow(clippy::result_large_err)] +/// Deserialize account from AccountInfoInterface using borsh (skips 8-byte discriminator). +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")] +/// Fetch and deserialize a compressible account using 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..6cd0b0e70a --- /dev/null +++ b/sdk-libs/compressible-client/src/lib.rs @@ -0,0 +1,464 @@ +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; + +/// Generic instruction data for initialize config +/// Note: Real programs should use their specific instruction format +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeCompressionConfigData { + pub compression_delay: u32, + pub rent_recipient: Pubkey, + pub address_space: Vec, + pub config_bump: u8, +} + +/// Generic instruction data for update config +/// Note: Real programs should use their specific instruction format +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct UpdateCompressionConfigData { + pub new_compression_delay: Option, + pub new_rent_recipient: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, +} + +/// Instruction data structure for decompress_accounts_idempotent +/// This matches the exact format expected by Anchor programs +/// T is the packed type (result of 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, +} + +/// Instruction data structure for compress_accounts_idempotent +/// This matches the exact format expected by Anchor programs +#[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, following Solana SDK patterns +/// These are generic builders that work with any program implementing the compressible pattern +pub struct CompressibleInstruction; + +impl CompressibleInstruction { + pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [133, 228, 12, 169, 56, 76, 222, 61]; + pub const UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [135, 215, 243, 81, 163, 146, 33, 70]; + /// Hardcoded discriminator for the standardized decompress_accounts_idempotent instruction + /// This is calculated as SHA256("global:decompress_accounts_idempotent")[..8] (Anchor format) + pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [114, 67, 61, 123, 234, 31, 1, 112]; + /// Hardcoded discriminator for compress_token_account_ctoken_signer instruction + /// This is calculated as SHA256("global:compress_token_account_ctoken_signer")[..8] (Anchor format) + pub const COMPRESS_TOKEN_ACCOUNT_CTOKEN_SIGNER_DISCRIMINATOR: [u8; 8] = + [243, 154, 172, 243, 44, 214, 139, 73]; + /// Hardcoded discriminator for the standardized compress_accounts_idempotent instruction + /// This is calculated as SHA256("global:compress_accounts_idempotent")[..8] (Anchor format) + pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [89, 130, 165, 88, 12, 207, 178, 185]; + + /// Creates an initialize_compression_config instruction + /// + /// Following Solana SDK patterns like system_instruction::transfer() + /// Returns Instruction directly - errors surface at execution time + /// + /// # Arguments + /// * `program_id` - The program ID + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `payer` - The payer account + /// * `authority` - The authority account + /// * `compression_delay` - The compression delay + /// * `rent_recipient` - The rent recipient + /// * `address_space` - The address space + /// * `config_bump` - The config bump + #[allow(clippy::too_many_arguments)] + pub fn initialize_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + payer: &Pubkey, + authority: &Pubkey, + compression_delay: u32, + rent_recipient: 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_recipient, + 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, + } + } + + /// Creates an update config instruction + /// + /// Following Solana SDK patterns - returns Instruction directly + /// + /// # Arguments + /// * `program_id` - The program ID + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `authority` - The authority account + /// * `new_compression_delay` - Optional new compression delay + /// * `new_rent_recipient` - Optional new rent recipient + /// * `new_address_space` - Optional new address space + /// * `new_update_authority` - Optional new update authority + pub fn update_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + authority: &Pubkey, + new_compression_delay: Option, + new_rent_recipient: 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_recipient, + 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, + } + } + + /// Build a `decompress_accounts_idempotent` instruction for any program's compressed account variant. + /// + /// # Arguments + /// * `program_id` - Target program + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `decompressed_account_addresses` - addresses of the accounts to decompress into + /// * `compressed_accounts` - Compressed accounts with their data (which implements Pack trait) + /// * `program_account_metas` - Additional accounts required for seed derivation (e.g., amm_config, token_mints) + /// * `validity_proof_with_context` - Validity proof with context + /// * `output_state_tree_info` - Output state tree info + /// + /// Returns `Ok(Instruction)` or error. + #[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(); + + // check if pdas/tokens + 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 all tree infos + let packed_tree_infos = + validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + // Add remaining program accounts + // accounts.extend(remaining_program_accounts); + let mut accounts = program_account_metas.to_vec(); + + // Pack all account data using the Pack trait. This converts types with + // Pubkeys to their packed versions with u8 indices. PDAs must implement + // pack trait. Tokens have a standard implementation. + 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, + }; + // Pack data. Is standardized for TokenData and user-implemented for other types. + let packed_data = data.pack(&mut remaining_accounts); + Ok(CompressedAccountData { + meta: compressed_meta, + data: packed_data, + }) + }) + .collect::, Box>>()?; + + // add all packed systemaccounts to anchor metas. + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + // decompressed account addresses must be the last metas. + 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, + }; + + // Serialize instruction data with discriminator + 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, + }) + } + + /// Build a `compress_accounts_idempotent` instruction for compressing multiple accounts (PDAs and token accounts). + /// + /// # Arguments + /// * `program_id` - Target program + /// * `discriminator` - The instruction discriminator bytes (flexible length) + /// * `account_pubkeys` - Accounts to compress (PDAs and token accounts) + /// * `accounts_to_compress` - Account data to compress + /// * `program_account_metas` - Program-specific accounts (assembled from Anchor accounts struct) + /// * `signer_seeds` - Signer seeds for each account (empty vec if no seeds needed) + /// * `validity_proof_with_context` - Validity proof with context + /// * `output_state_tree_info` - Output state tree info + /// + /// Returns `Ok(Instruction)` or error. + #[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()); + } + + // Sanity check for better error messages. + 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, + }, + ); + } + + // Use program-provided account metas (from Anchor accounts struct) + let mut accounts = program_account_metas.to_vec(); + + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + // Accounts to compress must be at the end. + 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, + }) + } +} + +/// Generic instruction data for decompress multiple PDAs +// Re-export for easy access following Solana SDK patterns +pub use CompressibleInstruction as compressible_instruction; diff --git a/sdk-libs/compressible-client/tests/pack_test.rs b/sdk-libs/compressible-client/tests/pack_test.rs new file mode 100644 index 0000000000..88d9c330b6 --- /dev/null +++ b/sdk-libs/compressible-client/tests/pack_test.rs @@ -0,0 +1,120 @@ +#[cfg(test)] +mod tests { + use light_sdk::{ + compressible::Pack, + instruction::PackedAccounts, + token::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + }; + 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, 2); + + // 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/macros/src/compress_as.rs b/sdk-libs/macros/src/compress_as.rs new file mode 100644 index 0000000000..2e0e82e5f9 --- /dev/null +++ b/sdk-libs/macros/src/compress_as.rs @@ -0,0 +1,206 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Expr, Ident, ItemStruct, Result, Token, +}; + +/// Parse the compress_as attribute content +struct CompressAsFields { + fields: Punctuated, +} + +struct CompressAsField { + name: Ident, + value: Expr, +} + +impl Parse for CompressAsField { + fn parse(input: ParseStream) -> Result { + let name: Ident = input.parse()?; + input.parse::()?; + let value: Expr = input.parse()?; + Ok(CompressAsField { name, value }) + } +} + +impl Parse for CompressAsFields { + fn parse(input: ParseStream) -> Result { + Ok(CompressAsFields { + fields: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates CompressAs trait implementation for a struct with optional compress_as attribute +pub fn derive_compress_as(input: ItemStruct) -> Result { + let struct_name = &input.ident; + + // Find the compress_as attribute (optional) + let compress_as_attr = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("compress_as")); + + // Parse the attribute content if it exists + let compress_as_fields = if let Some(attr) = compress_as_attr { + Some(attr.parse_args::()?) + } else { + None + }; + + // Get all struct fields + let struct_fields = match &input.fields { + syn::Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + &input, + "CompressAs derive only supports structs with named fields", + )); + } + }; + + // Create field assignments for the compress_as method + let field_assignments = struct_fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + + // ALWAYS set compression_info to None - this is required for compressed storage + if field_name == "compression_info" { + return quote! { #field_name: None }; + } + + // Check if this field is overridden in the compress_as attribute + let override_field = compress_as_fields + .as_ref() + .and_then(|fields| fields.fields.iter().find(|f| f.name == *field_name)); + + if let Some(override_field) = override_field { + let override_value = &override_field.value; + quote! { #field_name: #override_value } + } else { + // Keep the original value - determine how to clone/copy based on field type + let field_type = &field.ty; + if is_copy_type(field_type) { + quote! { #field_name: self.#field_name } + } else { + quote! { #field_name: self.#field_name.clone() } + } + } + }); + + // Determine if we need custom compression (any fields specified in compress_as attribute) + let has_custom_fields = compress_as_fields.is_some(); + + let compress_as_impl = if has_custom_fields { + // Custom compression - return Cow::Owned with modified fields + quote! { + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + #(#field_assignments,)* + }) + } + } + } else { + // Simple case - return Cow::Owned with compression_info = None + // We can't return Cow::Borrowed because compression_info must be None + quote! { + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + #(#field_assignments,)* + }) + } + } + }; + + // Generate HasCompressionInfo implementation (automatically included with Compressible) + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::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; + } + } + }; + + let expanded = quote! { + impl light_sdk::compressible::CompressAs for #struct_name { + type Output = Self; + + #compress_as_impl + } + + impl light_sdk::Size for #struct_name { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + + // Automatically derive HasCompressionInfo when using Compressible + #has_compression_info_impl + }; + + Ok(expanded) +} + +/// Determines if a type is likely to be Copy (simple heuristic) +fn is_copy_type(ty: &syn::Type) -> bool { + match ty { + syn::Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + let type_name = segment.ident.to_string(); + matches!( + type_name.as_str(), + "u8" | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "bool" + | "char" + | "Pubkey" + ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) + } else { + false + } + } + _ => false, + } +} + +/// Check if Option where T is Copy +fn has_copy_inner_type(args: &syn::PathArguments) -> bool { + match args { + syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(ty) = arg { + is_copy_type(ty) + } else { + false + } + }), + _ => false, + } +} diff --git a/sdk-libs/macros/src/compressible.rs b/sdk-libs/macros/src/compressible.rs new file mode 100644 index 0000000000..37cf02cbb0 --- /dev/null +++ b/sdk-libs/macros/src/compressible.rs @@ -0,0 +1,85 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::Result; + +// TODO: remove or add. +// /// Parse a comma-separated list of identifiers +// #[derive(Clone)] +// enum CompressibleType { +// Regular(Ident), +// } + +// struct CompressibleTypeList { +// types: Punctuated, +// } + +// impl Parse for CompressibleType { +// fn parse(input: ParseStream) -> Result { +// let ident: Ident = input.parse()?; +// Ok(CompressibleType::Regular(ident)) +// } +// } + +// impl Parse for CompressibleTypeList { +// fn parse(input: ParseStream) -> Result { +// Ok(CompressibleTypeList { +// types: Punctuated::parse_terminated(input)?, +// }) +// } +// } +/// Generates HasCompressionInfo trait implementation for a struct with compression_info field +pub fn derive_has_compression_info(input: syn::ItemStruct) -> Result { + let struct_name = input.ident.clone(); + + // Find the compression_info field + let compression_info_field = match &input.fields { + syn::Fields::Named(fields) => fields.named.iter().find(|field| { + field + .ident + .as_ref() + .map(|ident| ident == "compression_info") + .unwrap_or(false) + }), + _ => { + return Err(syn::Error::new_spanned( + &struct_name, + "HasCompressionInfo can only be derived for structs with named fields", + )) + } + }; + + let _compression_info_field = compression_info_field.ok_or_else(|| { + syn::Error::new_spanned( + &struct_name, + "HasCompressionInfo requires a field named 'compression_info' of type Option" + ) + })?; + + // Validate that the field is Option. For now, we'll assume + // it's correct and let the compiler catch type errors + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::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; + } + } + }; + + Ok(has_compression_info_impl) +} diff --git a/sdk-libs/macros/src/compressible_derive.rs b/sdk-libs/macros/src/compressible_derive.rs new file mode 100644 index 0000000000..47def38403 --- /dev/null +++ b/sdk-libs/macros/src/compressible_derive.rs @@ -0,0 +1,354 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Data, DeriveInput, Expr, Fields, Ident, Result, Token, +}; + +/// Parse the compress_as attribute content +struct CompressAsFields { + fields: Punctuated, +} + +struct CompressAsField { + name: Ident, + value: Expr, +} + +impl Parse for CompressAsField { + fn parse(input: ParseStream) -> Result { + let name: Ident = input.parse()?; + input.parse::()?; + let value: Expr = input.parse()?; + Ok(CompressAsField { name, value }) + } +} + +impl Parse for CompressAsFields { + fn parse(input: ParseStream) -> Result { + Ok(CompressAsFields { + fields: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Generates HasCompressionInfo, Size, and CompressAs trait implementations for compressible account types +/// +/// Supports optional compress_as attribute for custom compression behavior: +/// #[derive(Compressible)] +/// #[compress_as(start_time = 0, end_time = None)] +/// pub struct GameSession { ... } +/// +/// Usage: #[derive(Compressible)] +pub fn derive_compressible(input: DeriveInput) -> Result { + let struct_name = &input.ident; + + // Validate struct has compression_info field + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + &input, + "Compressible only supports structs with named fields", + )); + } + }, + _ => { + return Err(syn::Error::new_spanned( + &input, + "Compressible only supports structs", + )); + } + }; + + // Find the compression_info field + let compression_info_field = fields.iter().find(|field| { + field + .ident + .as_ref() + .map(|ident| ident == "compression_info") + .unwrap_or(false) + }); + + if compression_info_field.is_none() { + return Err(syn::Error::new_spanned( + struct_name, + "Compressible requires a field named 'compression_info' of type Option" + )); + } + + // Parse the compress_as attribute (optional) + let compress_as_attr = input + .attrs + .iter() + .find(|attr| attr.path().is_ident("compress_as")); + + let compress_as_fields = if let Some(attr) = compress_as_attr { + Some(attr.parse_args::()?) + } else { + None + }; + + // Generate HasCompressionInfo implementation + let has_compression_info_impl = quote! { + impl light_sdk::compressible::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::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; + } + } + }; + + // Generate Size implementation + let size_impl = quote! { + impl light_sdk::account::Size for #struct_name { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } + } + }; + + // Generate CompressAs implementation + let field_assignments = fields.iter().map(|field| { + let field_name = field.ident.as_ref().unwrap(); + + // ALWAYS set compression_info to None - this is required for compressed storage + if field_name == "compression_info" { + return quote! { #field_name: None }; + } + + // Check if this field is overridden in the compress_as attribute + let override_field = compress_as_fields + .as_ref() + .and_then(|fields| fields.fields.iter().find(|f| f.name == *field_name)); + + if let Some(override_field) = override_field { + let override_value = &override_field.value; + quote! { #field_name: #override_value } + } else { + // Keep the original value - determine how to clone/copy based on field type + let field_type = &field.ty; + if is_copy_type(field_type) { + quote! { #field_name: self.#field_name } + } else { + quote! { #field_name: self.#field_name.clone() } + } + } + }); + + let compress_as_impl = quote! { + impl light_sdk::compressible::CompressAs for #struct_name { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + #(#field_assignments,)* + }) + } + } + }; + + // Compute a conservative compile-time compressed INIT_SPACE that accounts for fields overridden to None + // Specifically, for fields of type Option that are set to None via #[compress_as(field = None)] + // (and for compression_info which is always set to None), we subtract the inner T's INIT_SPACE. + // For inner types, we try to use known primitive sizes, arrays, or ::INIT_SPACE when available. + fn inner_type_size_tokens(ty: &syn::Type) -> proc_macro2::TokenStream { + use quote::quote; + match ty { + syn::Type::Path(type_path) => { + if let Some(seg) = type_path.path.segments.last() { + let ident_str = seg.ident.to_string(); + // Known primitives and common types + let primitive = match ident_str.as_str() { + "u8" => Some(quote! { 1 }), + "i8" => Some(quote! { 1 }), + "bool" => Some(quote! { 1 }), + "u16" => Some(quote! { 2 }), + "i16" => Some(quote! { 2 }), + "u32" => Some(quote! { 4 }), + "i32" => Some(quote! { 4 }), + "u64" => Some(quote! { 8 }), + "i64" => Some(quote! { 8 }), + "u128" => Some(quote! { 16 }), + "i128" => Some(quote! { 16 }), + "Pubkey" => Some(quote! { 32 }), + _ => None, + }; + if let Some(sz) = primitive { + return sz; + } + // Fall back to type-level INIT_SPACE if present + let ty_ts = quote! { #type_path }; + return quote! { <#ty_ts>::INIT_SPACE }; + } + quote! { 0 } + } + syn::Type::Array(arr) => { + let elem = &arr.elem; + let len = &arr.len; + let elem_sz = inner_type_size_tokens(elem); + quote! { (#len as usize) * (#elem_sz) } + } + _ => { + // Unknown/unsupported types: assume 0 saving to avoid compile errors + quote! { 0 } + } + } + } + + // Build tokens for total savings from fields explicitly set to None + let mut savings_tokens: Vec = Vec::new(); + for field in fields.iter() { + let field_name = field.ident.as_ref().unwrap(); + + // Determine whether this field is overridden to None via #[compress_as] or is compression_info + let mut overridden_to_none = field_name == "compression_info"; + if !overridden_to_none { + if let Some(attrs) = &compress_as_fields { + if let Some(over_attr) = attrs.fields.iter().find(|f| f.name == *field_name) { + if let syn::Expr::Path(ref p) = over_attr.value { + if let Some(last) = p.path.segments.last() { + if last.ident == "None" { + overridden_to_none = true; + } + } + } + } + } + } + + if overridden_to_none { + // Check that the field type is Option and subtract Inner's INIT_SPACE + if let syn::Type::Path(type_path) = &field.ty { + if let Some(seg) = type_path.path.segments.last() { + if seg.ident == "Option" { + if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + let inner_sz = inner_type_size_tokens(inner_ty); + savings_tokens.push(quote! { #inner_sz }); + } + } + } + } + } + } + } + + let compressed_init_space_impl = { + if savings_tokens.is_empty() { + quote! { + impl light_sdk::compressible::compression_info::CompressedInitSpace for #struct_name { const COMPRESSED_INIT_SPACE: usize = Self::INIT_SPACE; } + } + } else { + quote! { + impl light_sdk::compressible::compression_info::CompressedInitSpace for #struct_name { const COMPRESSED_INIT_SPACE: usize = Self::INIT_SPACE - (0 #( + #savings_tokens )*); } + } + } + }; + + let expanded = quote! { + #has_compression_info_impl + #size_impl + #compress_as_impl + #compressed_init_space_impl + }; + + Ok(expanded) +} + +/// Determines if a type is likely to be Copy (simple heuristic) +fn is_copy_type(ty: &syn::Type) -> bool { + match ty { + syn::Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + let type_name = segment.ident.to_string(); + matches!( + type_name.as_str(), + "u8" | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "bool" + | "char" + | "Pubkey" + ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) + } else { + false + } + } + _ => false, + } +} + +/// Check if Option where T is Copy +fn has_copy_inner_type(args: &syn::PathArguments) -> bool { + match args { + syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(ty) = arg { + is_copy_type(ty) + } else { + false + } + }), + _ => false, + } +} + +#[allow(dead_code)] +fn generate_identity_pack_unpack(struct_name: &syn::Ident) -> Result { + let pack_impl = quote! { + impl light_sdk::compressible::Pack for #struct_name { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } + } + }; + + let unpack_impl = quote! { + impl light_sdk::compressible::Unpack for #struct_name { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[solana_account_info::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + }; + + let expanded = quote! { + #pack_impl + #unpack_impl + }; + + Ok(expanded) +} diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 45bdf382d1..6c04b1c985 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -7,6 +7,9 @@ use traits::process_light_traits; mod account; mod accounts; +mod compress_as; +mod compressible; +mod compressible_derive; mod discriminator; mod hasher; mod program; @@ -280,3 +283,116 @@ pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { .unwrap_or_else(|err| err.to_compile_error()) .into() } + +/// Automatically implements the HasCompressionInfo trait for structs that have a +/// `compression_info: Option` field. +/// +/// This derive macro generates the required trait methods for managing compression +/// information in compressible account structs. +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk::compressible::{CompressionInfo, HasCompressionInfo}; +/// +/// #[derive(HasCompressionInfo)] +/// pub struct UserRecord { +/// pub compression_info: Option, +/// pub owner: Pubkey, +/// pub name: String, +/// pub score: u64, +/// } +/// ``` +/// +/// ## Requirements +/// +/// The struct must have exactly one field named `compression_info` of type +/// `Option`. +#[proc_macro_derive(HasCompressionInfo)] +pub fn has_compression_info(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + compressible::derive_has_compression_info(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Implements CompressAs trait for custom compression behavior. +/// +/// This derive macro allows you to specify which fields should be reset/overridden +/// during compression while keeping other fields as-is. Only the specified fields +/// are modified; all others retain their current values. +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk::compressible::{CompressAs, CompressionInfo}; +/// +/// #[derive(CompressAs)] +/// #[compress_as( +/// start_time = 0, +/// end_time = None, +/// score = 0 +/// )] +/// pub struct GameSession { +/// pub compression_info: Option, +/// pub session_id: u64, +/// pub player: Pubkey, +/// pub game_type: String, +/// pub start_time: u64, +/// pub end_time: Option, +/// pub score: u64, +/// } +/// ``` +/// +/// ## Note +/// +/// Use the `Compressible` derive for complete functionality - it includes this plus more. +#[proc_macro_derive(CompressAs, attributes(compress_as))] +pub fn compress_as_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + compress_as::derive_compress_as(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Automatically implements all required traits for compressible accounts. +/// +/// This derive macro generates HasCompressionInfo, Size, and CompressAs trait implementations. +/// It supports optional compress_as attribute for custom compression behavior. +/// +/// ## Example - Basic Usage +/// +/// ```ignore +/// use light_sdk::compressible::CompressionInfo; +/// +/// #[derive(Compressible)] +/// pub struct UserRecord { +/// pub compression_info: Option, +/// pub owner: Pubkey, +/// pub name: String, +/// pub score: u64, +/// } +/// ``` +/// +/// ## Example - Custom Compression +/// +/// ```ignore +/// #[derive(Compressible)] +/// #[compress_as(start_time = 0, end_time = None, score = 0)] +/// pub struct GameSession { +/// pub compression_info: Option, +/// pub session_id: u64, // KEPT +/// pub player: Pubkey, // KEPT +/// pub game_type: String, // KEPT +/// pub start_time: u64, // RESET to 0 +/// pub end_time: Option, // RESET to None +/// pub score: u64, // RESET to 0 +/// } +/// ``` +#[proc_macro_derive(Compressible, attributes(compress_as))] +pub fn compressible_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + compressible_derive::derive_compressible(input) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 52ff204f02..2c4c68dcbb 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -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..53a6ccfc2b 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -166,3 +166,147 @@ 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(); + + // Load program's compressible config to get rent_recipient + 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_recipient = cfg.rent_sponsor; + + // Discover program PDAs (simple heuristic: all non-empty, funded program-owned accounts) + let program_accounts = rpc.context.get_program_accounts(&program_id); + if program_accounts.is_empty() { + return Ok(()); + } + + // Address tree info and queue used to derive existing compressed addresses + let address_tree_info = rpc.get_address_tree_v2(); + let output_state_tree_info = rpc + .get_random_state_tree_info() + .map_err(|e| RpcError::CustomError(format!("no state tree: {e:?}")))?; + + // Prepare metas that are standard across programs + let program_metas = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(rent_recipient, false), + ]; + + // Batch to keep instructions reasonably small + const BATCH_SIZE: usize = 10; + 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_info.queue, + output_state_tree_info, + ) + .await; + chunk.clear(); + } + } + + if !chunk.is_empty() { + try_compress_chunk( + rpc, + &program_id, + &chunk, + &program_metas, + &address_tree_info.queue, + 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_queue: &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; + + // Build account pubkeys and fetch compressed inputs for proof + 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() { + // Derive existing compressed address and read hash; skip if not present + let addr = derive_address( + &pda.to_bytes(), + &address_tree_queue.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; + } + + // Get a single proof for all inputs in this batch + let proof_with_context = match rpc.get_validity_proof(hashes, vec![], None).await { + Ok(r) => r.value, + Err(_) => return, + }; + + // Signer seeds: PDAs require no seeds for compression (program owns them). Provide empty groups. + let signer_seeds: Vec>> = (0..pdas.len()).map(|_| Vec::new()).collect(); + + // Build instruction; let program enforce slot-gating. Ignore failures. + 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 { + // Avoid double-borrow of rpc by cloning payer first + 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/program_test/compressible_setup.rs b/sdk-libs/program-test/src/program_test/compressible_setup.rs new file mode 100644 index 0000000000..37b1493dab --- /dev/null +++ b/sdk-libs/program-test/src/program_test/compressible_setup.rs @@ -0,0 +1,159 @@ +//! 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 +/// +/// This is a high-level helper that handles the complete flow of initializing +/// a compression configuration for a program, including proper signer management. +/// +/// # 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_recipient` - Where to send rent from compressed accounts +/// * `address_space` - List of address trees for this program +/// +/// # Returns +/// Transaction signature on success +#[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_recipient: 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(), + )); + } + + // Use the mid-level instruction builder + let instruction = CompressibleInstruction::initialize_compression_config( + program_id, + discriminator, + &payer.pubkey(), + &authority.pubkey(), + compression_delay, + rent_recipient, + 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 +/// +/// This is a high-level helper for updating an existing compression configuration. +/// All parameters except the required ones are optional - pass None to keep existing values. +/// +/// # 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_recipient` - New rent recipient (optional) +/// * `new_address_space` - New address space list (optional) +/// * `new_update_authority` - New authority (optional) +/// +/// # Returns +/// Transaction signature on success +#[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_recipient: Option, + new_address_space: Option>, + new_update_authority: Option, + discriminator: &[u8], +) -> Result { + // Use the mid-level instruction builder + let instruction = CompressibleInstruction::update_compression_config( + program_id, + discriminator, + &authority.pubkey(), + new_compression_delay, + new_rent_recipient, + 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..6125dfd591 100644 --- a/sdk-libs/program-test/src/program_test/config.rs +++ b/sdk-libs/program-test/src/program_test/config.rs @@ -20,6 +20,9 @@ pub struct ProgramTestConfig { #[cfg(feature = "devenv")] pub protocol_config: ProtocolConfig, pub with_prover: bool, + /// Automatically register additional programs for PDA auto-compression in tests + #[cfg(feature = "devenv")] + pub auto_register_custom_programs_for_pda_compression: bool, #[cfg(feature = "devenv")] pub skip_register_programs: bool, #[cfg(feature = "devenv")] @@ -132,6 +135,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/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 86fe5e824d..d2f395aac8 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,21 @@ impl LightProgramTest { })?; } } + // Auto-register additional programs for PDA auto-compression unless opted out + 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 +423,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..73a700ccd1 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,10 @@ 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?; + // Auto-compress registered custom program PDAs (mirror c-token auto flow) + for program_id in self.auto_compress_programs.clone() { + let _ = 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..58ef6401ad 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-program = { workspace = true } 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"] } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 8c49c28982..7ce211c93e 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -302,6 +302,19 @@ 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() + } + + /// Remove the data from this account by setting it to default. + /// This is used when decompressing to ensure the compressed account is properly zeroed. + pub fn remove_data(&mut self) { + self.should_remove_data = true; + } pub fn in_account_info(&self) -> &Option { &self.account_info.input @@ -771,7 +784,7 @@ pub mod __internal { prove_by_index: tree_info.prove_by_index, }, root_index: input_account_meta.get_root_index().unwrap_or_default(), - discriminator: [0u8; 8], + discriminator: [0u8; 8], // TODO: consider 0 (need adapt client etc) } }; let output_account_info = { 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..e0dcf650c9 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -0,0 +1,29 @@ +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(); + + **info + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)? = 0; + + let dest_lamports = sol_destination.lamports(); + **sol_destination + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)? = + dest_lamports.checked_add(lamports_to_transfer).unwrap(); + + 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..9b3c472d46 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -0,0 +1,125 @@ +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; +} + +/// Convert indices back to Pubkeys using remaining_accounts. +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; + + /// Return data to store. compression_info must be None. + fn compress_as(&self) -> Cow<'_, Self::Output>; +} + +/// Last write slot and compression state. +#[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 + } +} + +/// Space calculation without anchor (like anchor_lang::Space but standalone). +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..0e09b9180f --- /dev/null +++ b/sdk-libs/sdk/src/compressible/config.rs @@ -0,0 +1,482 @@ +use std::collections::HashSet; + +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_msg::msg; +use solana_program::bpf_loader_upgradeable::UpgradeableLoaderState; +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_recipient: Pubkey, + /// Config bump seed (currently always 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_recipient` - 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_recipient: &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_recipient: *rent_recipient, + 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_recipient` - 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_recipient: 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_recipient { + config.rent_recipient = *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(()) +} + +/// Verifies 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 with program upgrade authority +/// validation +/// +/// # Security +/// This function verifies that the signer is the program's upgrade authority +/// before creating the config. This ensures only the program deployer can +/// initialize the configuration. +/// +/// # 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_recipient` - 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_recipient: &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_recipient, + 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/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 91b0bf479d..0ebe09dc08 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -1,5 +1,6 @@ pub use light_compressed_account::LightInstructionData; use light_sdk_types::constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}; +use solana_msg::msg; #[cfg(feature = "cpi-context")] use crate::AccountMeta; @@ -74,6 +75,7 @@ where let account_infos = accounts.to_account_infos(); let account_metas = accounts.to_account_metas()?; + msg!("invoking LightSystemProgramCpi, {:?}", account_metas); let instruction = Instruction { program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), accounts: account_metas, diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index a8586aba90..a2d4642b3d 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -152,6 +152,7 @@ pub mod token; pub mod transfer; pub mod utils; +pub mod compressible; #[cfg(feature = "merkle-tree")] pub mod merkle_tree; @@ -159,6 +160,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/token.rs b/sdk-libs/sdk/src/token.rs index 14c0cafb7b..22db434f66 100644 --- a/sdk-libs/sdk/src/token.rs +++ b/sdk-libs/sdk/src/token.rs @@ -1,7 +1,12 @@ use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; use light_hasher::{sha256::Sha256BE, HasherError}; +use solana_account_info::AccountInfo; -use crate::{AnchorDeserialize, AnchorSerialize, Pubkey}; +use crate::{ + compressible::compression_info::{Pack, Unpack}, + instruction::PackedAccounts, + AnchorDeserialize, AnchorSerialize, Pubkey, +}; #[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] #[repr(u8)] @@ -57,3 +62,176 @@ impl TokenDataWithMerkleContext { } } } + +/// Implementation for TokenData - packs into InputTokenDataCompressible +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), + amount: self.amount, + has_delegate: self.delegate.is_some(), + delegate: if let Some(delegate) = self.delegate { + remaining_accounts.insert_or_get(delegate) + } else { + 0 // Unused when has_delegate is false + }, + mint: remaining_accounts.insert_or_get_read_only(self.mint), + version: 3, // TokenDataVersion::ShaFlat. Default version for compressed token accounts + } + } +} + +impl Unpack for TokenData { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +/// Unpack implementation for InputTokenDataCompressible +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(solana_program_error::ProgramError::InvalidAccountData)? + .key, + amount: self.amount, + delegate: if self.has_delegate { + Some( + *remaining_accounts + .get(self.delegate as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key, + ) + } else { + None + }, + mint: *remaining_accounts + .get(self.mint as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key, + state: AccountState::Initialized, // Default state for unpacked + tlv: None, // No TLV data in packed version + }) + } +} + +/// Wrapper for token data with variant information +/// The variant is user-defined and doesn't get altered during packing +#[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, +} + +/// Pack implementation for CTokenDataWithVariant +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), + } + } +} + +/// Unpack implementation for CTokenDataWithVariant +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)?, + }) + } +} + +/// Pack implementation for TokenDataWithVariant +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), + } + } +} + +/// Unpack implementation for PackedCTokenDataWithVariant +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)?, + }) + } +} + +// custom replacement for MultiInputTokenDataWithContext +// without root_index and without merkle_context +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize, Default)] +pub struct InputTokenDataCompressible { + pub owner: u8, + pub amount: u64, + pub has_delegate: bool, // Optional delegate is set + pub delegate: u8, + pub mint: u8, + pub version: u8, +} + +// TODO: remove these and fix renaming after we're done with ci. +#[deprecated(since = "0.2.0", note = "Use `CTokenDataWithVariant` instead")] +pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; + +#[deprecated(since = "0.2.0", note = "Use `PackedCTokenDataWithVariant` instead")] +pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; + +// Shorter aliases for convenience +pub type CTokenData = CTokenDataWithVariant; +pub type PackedCTokenData = PackedCTokenDataWithVariant; 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..f1d8d6b0fa 100644 --- a/sdk-libs/token-client/src/actions/ctoken_transfer.rs +++ b/sdk-libs/token-client/src/actions/ctoken_transfer.rs @@ -1,3 +1,4 @@ +// TODO: move transfer_ctoken to compressed-token-sdk use light_client::rpc::{Rpc, RpcError}; use solana_instruction::{AccountMeta, Instruction}; use solana_keypair::Keypair; @@ -18,7 +19,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 +28,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 +39,9 @@ pub async fn ctoken_transfer( .await } -/// Create a decompressed token transfer instruction. +/// Create a ctoken transfer instruction. /// This creates an instruction that uses discriminator 3 (CTokenTransfer) to perform -/// SPL token transfers on decompressed compressed token accounts. +/// SPL token transfers on ctoken accounts. /// /// # Arguments /// * `source` - Source token account @@ -51,7 +52,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, @@ -63,6 +64,7 @@ pub fn create_ctoken_transfer_instruction( 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) + // TODO: try to remove this AccountMeta::new_readonly(Pubkey::default(), false), // System program for CPI transfers ], 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..00e89e2a26 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; @@ -14,7 +15,7 @@ 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 +/// This function wraps `create_transfer_spl_to_ctoken_instruction` to provide /// a convenient action for transferring from SPL token accounts to compressed tokens. /// /// # Arguments @@ -50,7 +51,7 @@ pub async fn spl_to_ctoken_transfer( 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,6 +60,7 @@ 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()))?; diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index 8f5d67d6dc..531b8bd342 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -1,2 +1,170 @@ pub mod actions; pub mod instructions; + +// Re-export the main utility functions for easy access +use solana_pubkey::{pubkey, Pubkey}; + +pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); + +pub mod ctoken { + use light_compressed_account::address::derive_address; + use light_compressed_token_sdk::POOL_SEED; + use solana_pubkey::Pubkey; + + use super::{CTOKEN_CPI_AUTHORITY, CTOKEN_PROGRAM_ID}; + + pub const ID: Pubkey = CTOKEN_PROGRAM_ID; + + /// Returns the program ID for the Compressed Token Program + pub fn id() -> Pubkey { + 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 { + Pubkey::find_program_address( + &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], + &id(), + ) + .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 const CTOKEN_MINT_SEED: &[u8] = &[ + // b"compressed_mint" + 99, 111, 109, 112, 114, 101, 115, 115, 101, 100, 95, 109, 105, 110, 116, + ]; + + /// Derives the cToken program mint PDA from the provided signer pubkey (keypair or PDA). + /// The signer must sign when creating the SPL mint PDA on-chain. + pub fn find_mint_address(signer: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[CTOKEN_MINT_SEED, signer.to_bytes().as_ref()], &ID) + } + + pub fn derive_compressed_address(mint: Pubkey, address_tree: &Pubkey) -> [u8; 32] { + derive_address(&mint.to_bytes(), &address_tree.to_bytes(), &ID.to_bytes()) + } + + /// Comprehensive helper that derives all addresses from a signer in one call + /// Returns: (mint_address, mint_bump, compressed_address) + pub fn derive_compressed_address_from_mint_signer( + signer: Pubkey, + address_tree: &Pubkey, + ) -> (Pubkey, u8, [u8; 32]) { + let (mint_address, mint_bump) = find_mint_address(signer); + let compressed_address = derive_compressed_address(mint_address, address_tree); + (mint_address, mint_bump, compressed_address) + } + + pub fn derive_ctoken_program_config(_version: Option) -> (Pubkey, u8) { + let version = 1u16; + let registry_program_id = + solana_pubkey::pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"); + let (compressible_config_pda, config_bump) = Pubkey::find_program_address( + &[b"compressible_config", &version.to_le_bytes()], + ®istry_program_id, + ); + println!("compressible_config: {:?}", compressible_config_pda); + (compressible_config_pda, config_bump) + } + + pub fn derive_ctoken_rent_sponsor(_version: Option) -> (Pubkey, u8) { + // Derive the rent_recipient PDA + // let version = version.unwrap_or(1); + let version = 1u16; + Pubkey::find_program_address( + &[b"rent_sponsor".as_slice(), version.to_le_bytes().as_slice()], + &solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"), + ) + } + + pub fn derive_ctoken_compression_authority(version: Option) -> (Pubkey, u8) { + let registry_program_id = + solana_pubkey::pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"); + let (compression_authority, compression_authority_bump) = Pubkey::find_program_address( + &[ + b"compression_authority".as_slice(), + version.unwrap_or(1).to_le_bytes().as_slice(), + ], + ®istry_program_id, + ); + (compression_authority, compression_authority_bump) + } + + // /// Derives the SPL mint PDA from a signer keypair + // /// + // /// # Arguments + // /// * `signer` - The signer pubkey used as seed + // /// + // /// # Returns + // /// Tuple of (mint_pda, bump_seed) + // /// Derives the Compressed Token Program mint PDA from a signer pubkey. + // /// + // /// This derives the cToken program mint PDA for a given keypair or PDA; that signer must sign. + // pub fn find_mint_address(signer: &Pubkey) -> (Pubkey, u8) { + // sdk_find_mint_address(signer) + // } + + // /// Derives the compressed address from a mint PDA and address tree + // /// + // /// # Arguments + // /// * `mint` - The mint PDA + // /// * `address_tree` - The address tree pubkey + // /// + // /// # Returns + // /// The compressed address as [u8; 32] + // pub fn derive_compressed_address(mint: &Pubkey, address_tree: &Pubkey) -> [u8; 32] { + // sdk_derive_address( + // &mint.to_bytes(), + // &address_tree.to_bytes(), + // &super::ctoken::ID.to_bytes(), + // ) + // } + + // /// Comprehensive helper that derives all addresses from a signer in one call + // /// + // /// This is the main function you should use for mint derivation. + // /// + // /// # Arguments + // /// * `signer` - The signer keypair pubkey + // /// * `address_tree` - The address tree pubkey + // /// + // /// # Returns + // /// Tuple of (mint_address, mint_bump, compressed_address) + // /// + // /// # Example + // /// ```rust + // /// use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; + // /// use light_token_client::utils::derive_compressed_address_from_signer; + // /// + // /// let signer = Keypair::new(); + // /// let address_tree = Pubkey::new_unique(); + // /// let (mint_pda, mint_bump, compressed_address) = + // /// derive_compressed_address_from_signer(&signer.pubkey(), &address_tree); + // /// + // /// println!("Mint PDA: {}", mint_pda); + // /// println!("Mint Bump: {}", mint_bump); + // /// println!("Compressed Address: {:?}", compressed_address); + // /// ``` + // pub fn derive_compressed_address_from_mint_signer( + // signer: &Pubkey, + // address_tree: &Pubkey, + // ) -> (Pubkey, u8, [u8; 32]) { + // sdk_derive_compressed_address_from_mint_signer(signer, address_tree) + // } +} diff --git a/sdk-tests/csdk-anchor-test/Cargo.toml b/sdk-tests/csdk-anchor-test/Cargo.toml new file mode 100644 index 0000000000..60f2c7b3e2 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "csdk-anchor-test" +version = "0.1.0" +description = "Simple Anchor program template with user records" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "csdk_anchor_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = ["idl-build"] +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/csdk-anchor-test/Xargo.toml b/sdk-tests/csdk-anchor-test/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/lib.rs b/sdk-tests/csdk-anchor-test/src/lib.rs new file mode 100644 index 0000000000..2bce3df686 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/src/lib.rs @@ -0,0 +1,2225 @@ +#![allow(deprecated)] + +use anchor_lang::{ + prelude::*, + solana_program::{instruction::AccountMeta, program::invoke, pubkey::Pubkey}, +}; +use anchor_spl::token_interface::TokenAccount; +use light_compressed_token_sdk::instructions::{create_mint_action_cpi, MintActionInputs}; +use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; +use light_sdk::{ + account::Size, + compressible::{ + compress_account::prepare_account_for_compression, + compress_account_on_init::prepare_compressed_account_on_init, + decompress_idempotent::{ + into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, + }, + process_initialize_compression_config_checked, process_update_compression_config, + CompressAs, CompressedInitSpace, CompressibleConfig, CompressionInfo, HasCompressionInfo, + Pack, Unpack, + }, + derive_light_cpi_signer, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + PackedAddressTreeInfo, ValidityProof, + }, + LightDiscriminator, LightHasher, +}; +use light_sdk_types::{cpi_accounts::CpiAccountsConfig, CpiSigner}; + +pub const POOL_VAULT_SEED: &str = "pool_vault"; +pub const USER_RECORD_SEED: &str = "user_record"; +pub const CTOKEN_SIGNER_SEED: &str = "ctoken_signer"; +#[repr(u32)] +pub enum ErrorCode { + InvalidAccountCount, + InvalidRentRecipient, + MintCreationFailed, + MissingCompressedTokenProgram, + MissingCompressedTokenProgramAuthorityPDA, +} +#[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" + } + }, + ) + } +} + +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", + )), + } + } +} +// extern crate alloc; +#[repr(u32)] +/// Auto-generated error codes for compressible instructions +/// These are separate from the user's ErrorCode enum to avoid conflicts +pub enum CompressibleInstructionError { + InvalidRentRecipient, + CTokenDecompressionNotImplemented, + PdaDecompressionNotImplemented, + TokenCompressionNotImplemented, + PdaCompressionNotImplemented, +} +// Auto-generated client-side seed function +pub fn get_userrecord_seeds(owner: &Pubkey) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} +/// Auto-generated client-side seed function +pub fn get_gamesession_seeds(session_id: u64) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} +/// Auto-generated client-side seed function +pub fn get_placeholderrecord_seeds( + placeholder_id: u64, +) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} +/// Auto-generated client-side CToken seed function +pub fn get_ctokensigner_seeds( + fee_payer: &anchor_lang::prelude::Pubkey, + some_mint: &anchor_lang::prelude::Pubkey, +) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} +/// Trait-based system for generic CToken variant seed handling +/// Users implement this trait for their CTokenAccountVariant enum +pub mod ctoken_seed_system { + use super::*; + /// Context struct providing access to ALL instruction accounts + /// This gives users access to any account in the instruction context + pub struct CTokenSeedContext<'a, 'info> { + pub accounts: &'a DecompressAccountsIdempotent<'info>, + pub remaining_accounts: &'a [anchor_lang::prelude::AccountInfo<'info>], + } + /// Trait that CToken variants implement to provide seed derivation + /// Completely extensible - users can implement ANY seed logic with access to ALL accounts + pub trait CTokenSeedProvider { + fn get_seeds<'a, 'info>( + &self, + ctx: &CTokenSeedContext<'a, 'info>, + ) -> (Vec>, Pubkey); + } +} +/// Auto-generated CTokenSeedProvider implementation +impl ctoken_seed_system::CTokenSeedProvider for CTokenAccountVariant { + fn get_seeds<'a, 'info>( + &self, + ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, + ) -> (Vec>, anchor_lang::prelude::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) = + anchor_lang::prelude::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) = + anchor_lang::prelude::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) = + anchor_lang::prelude::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(); // Use fee_payer as second account + let program_id_bytes = crate::ID.to_bytes(); + let seeds: &[&[u8]] = &[b"multi_account", &seed_1, &seed_2, &program_id_bytes]; + let (pda, bump) = + anchor_lang::prelude::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(); // Fixed index for this variant + let seeds: &[&[u8]] = &[b"indexed_vault", &seed_1, &seed_2, &index_bytes, b"final"]; + let (pda, bump) = + anchor_lang::prelude::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) + } + } + } +} + +declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); + +// CToken signer 1: Classic pattern with user + mint +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) +} + +// CToken signer 2: Simple user vault pattern +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) +} + +// CToken signer 3: Pool vault pattern with constant seed +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) +} + +// Authority seeds for ctoken operations: Light CPI signer PDA derived from ("cpi_authority", program_id) +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) { + // Same authority PDA as above; separate helper keeps parity with variant naming + get_ctokensigner_authority_seeds() +} + +pub fn get_ctokensigner3_authority_seeds() -> (Vec>, Pubkey) { + // Same authority PDA as above; separate helper keeps parity with variant naming + get_ctokensigner_authority_seeds() +} + +// CToken signer 4: Multi-account pattern with user + fee_payer + program_id +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) +} + +// CToken signer 5: Complex pattern with user + mint + numeric index + extra seed +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) { + // Same authority PDA as above; separate helper keeps parity with variant naming + get_ctokensigner_authority_seeds() +} + +pub fn get_ctokensigner5_authority_seeds() -> (Vec>, Pubkey) { + // Same authority PDA as above; separate helper keeps parity with variant naming + get_ctokensigner_authority_seeds() +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] +#[repr(u8)] +pub enum CTokenAccountVariant { + CTokenSigner = 0, + CTokenSigner2 = 1, + CTokenSigner3 = 2, + CTokenSigner4 = 3, + CTokenSigner5 = 4, +} + +#[program] +pub mod csdk_anchor_test { + + use light_compressed_token_sdk::instructions::{ + create_token_account::create_ctoken_account_signed, find_mint_address, + }; + use light_sdk::cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }; + use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; + + use super::*; + + // auto-derived via macro. + pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_recipient: 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_recipient, + 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(()) + } + + // auto-derived via macro. + pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_recipient: 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_recipient.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + )?; + + Ok(()) + } + + /// Compress multiple accounts (PDAs and token accounts) in a single instruction. + 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_recipient.key() != compression_config.rent_recipient { + msg!( + "rent recipient passed: {:?}", + ctx.accounts.rent_recipient.key() + ); + msg!( + "rent recipient config: {:?}", + compression_config.rent_recipient + ); + panic!("Rent recipient does not match config"); + } + + 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::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::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::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); + } + _ => { + panic!("Trying to compress with invalid account discriminator"); + } + } + } + } + let has_pdas = !compressed_pda_infos.is_empty(); + if has_pdas { + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_account_infos(&compressed_pda_infos) + // .write_to_cpi_context_first() + // .invoke_write_to_cpi_context_first( + // light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + // fee_payer: cpi_accounts.fee_payer(), + // authority: cpi_accounts.authority().unwrap(), + // cpi_context: cpi_accounts.cpi_context().unwrap(), + // cpi_signer: LIGHT_CPI_SIGNER, + // }, + // )?; + .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_recipient.clone(), + ) + .map_err(anchor_lang::prelude::ProgramError::from)?; + } + } + Ok(()) + } + + // auto-derived via macro. takes the tagged account structs via + // add_compressible_accounts macro and derives the relevant variant type and + // dispatcher. The instruction can be used with any number of any of the + // tagged account structs. It's idempotent; it will not fail if the accounts + // are already decompressed. + #[inline(never)] + 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, + ) -> 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| 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, + ) -> 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| 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, + ) -> 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| 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_sdk::token::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::ctoken_seed_system::{CTokenSeedContext, CTokenSeedProvider}; + let seed_context = CTokenSeedContext { + accounts, + remaining_accounts, + }; + let authority = cpi_accounts.authority().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + + 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); + panic!("Derived token account address must match owner_info.key"); + } + + // 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 + )?; + } + // let decompress_index = + // light_compressed_token_sdk::instructions::DecompressFullIndices::from(( + // token_data.token_data, + // meta, + // owner_index, + // )); + // 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); + } + + // log each token account to decompress + // for token_account in token_decompress_indices.clone().into_iter() { + // msg!( + // "token_account: {:?}", + // packed_accounts[token_account.destination_index as usize].key() + // ); + // } + 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_sdk::token::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 && has_pdas { + 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(); + let authority = cpi_accounts.authority().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + + // init PDAs. + if has_pdas && has_tokens { + 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(()) + } + + 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_recipient.key() != config.rent_recipient { + panic!("Rent recipient does not match config"); + // return err!(ErrorCode::InvalidRentRecipient); + } + + // 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_recipient.to_account_info())?; + + Ok(()) + } + + // Must be manually implemented. + 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_recipient.key() != config.rent_recipient { + panic!("Rent recipient does not match config"); + } + + // 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_recipient.to_account_info())?; + + Ok(()) + } + + // Must be manually implemented. + 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_recipient.key() != config.rent_recipient { + panic!("Rent recipient does not match config"); + } + + // 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_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 { + 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_recipient.to_account_info())?; + game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + + Ok(()) + } + + /// Creates an empty compressed account while keeping the PDA intact. + /// This demonstrates the compress_empty_account_on_init functionality. + 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; + + // Initialize compression_info for the PDA + *placeholder_record.compression_info_mut_opt() = + Some(super::CompressionInfo::new_decompressed()?); + placeholder_record + .compression_info_mut() + .bump_last_written_slot()?; + + // Verify rent recipient matches config + if ctx.accounts.rent_recipient.key() != config.rent_recipient { + panic!("Rent recipient does not match config"); + } + + // 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), + ); + + msg!("compressing account on init (keeps PDA open for demo)"); + + 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)?; + + msg!("...compressed account on init"); + + // Note: PDA is NOT closed in this example (compression_info is set, account remains) + Ok(()) + } + + 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(()) + } + + 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(()) + } +} + +#[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_recipient: 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_recipient: 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_recipient: 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_recipient: 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 CompressRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user.key().as_ref()], + bump, + constraint = pda_to_compress.owner == user.key() + )] + pub pda_to_compress: Account<'info, UserRecord>, + // 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_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct CompressGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + mut, + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + constraint = pda_to_compress.player == player.key() + )] + pub pda_to_compress: Account<'info, GameSession>, + /// 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_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressPlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + constraint = pda_to_compress.owner == user.key() + )] + pub pda_to_compress: Account<'info, PlaceholderRecord>, + /// 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_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressTokenAccountCtokenSigner<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + pub rent_authority: Signer<'info>, + /// CHECK: todo + pub user: UncheckedAccount<'info>, + /// CHECK: todo + ctoken_cpi_authority: UncheckedAccount<'info>, + /// CHECK: todo + ctoken_program: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [b"ctoken_signer", user.key().as_ref(), token_account_to_compress.mint.as_ref()], + bump, + )] + pub token_account_to_compress: InterfaceAccount<'info, TokenAccount>, + /// 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_recipient: AccountInfo<'info>, +} + +#[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_recipient: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct CompressMultipleTokenAccounts<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The authority that owns all token accounts being compressed + /// CHECK: Validated by the SDK + pub authority: AccountInfo<'info>, + /// CHECK: CPI authority of the compressed token program + pub ctoken_cpi_authority: UncheckedAccount<'info>, + /// CHECK: Compressed token program + pub ctoken_program: UncheckedAccount<'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_recipient: 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>, +} + +#[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, +} + +/// Auto-derived via macro. Unified enum that can hold any account type. Crucial +/// for dispatching multiple compressed accounts of different types in +/// decompress_accounts_idempotent. + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedAccountVariant { + UserRecord(UserRecord), + PackedUserRecord(PackedUserRecord), + GameSession(GameSession), + PackedGameSession(PackedGameSession), + PlaceholderRecord(PlaceholderRecord), + PackedPlaceholderRecord(PackedPlaceholderRecord), + PackedCTokenData(light_sdk::token::PackedCTokenData), + CTokenData(light_sdk::token::CTokenData), +} + +impl Default for CompressedAccountVariant { + fn default() -> Self { + Self::UserRecord(UserRecord::default()) + } +} + +// impl DataHasher for CompressedAccountVariant { +// fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { +// match self { +// Self::UserRecord(data) => data.hash::(), +// Self::PackedUserRecord(_) => unreachable!(), +// Self::GameSession(data) => data.hash::(), +// Self::PlaceholderRecord(data) => data.hash::(), +// Self::PackedCTokenData(_) => unreachable!(), +// Self::CTokenData(_) => unreachable!(), +// Self::PackedGameSession(_) => unreachable!(), +// Self::PackedPlaceholderRecord(_) => unreachable!(), +// } +// } +// } + +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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack for PlaceholderRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// #[error_code] +// pub enum CompressibleInstructionError { +// #[msg("Invalid account count: PDAs and compressed accounts must match")] +// InvalidAccountCount, +// #[msg("Rent recipient does not match config")] +// InvalidRentRecipient, +// #[msg("Failed to create compressed mint")] +// MintCreationFailed, +// #[msg("Compressed token program account not found in remaining accounts")] +// MissingCompressedTokenProgram, +// #[msg("Compressed token program authority PDA account not found in remaining accounts")] +// MissingCompressedTokenProgramAuthorityPDA, + +// #[msg("CToken decompression not yet implemented")] +// CTokenDecompressionNotImplemented, +// } + +// 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, + // TODO: Add mint compression parameters when implementing mint functionality + // pub mint_compressed_address: [u8; 32], + // pub mint_address_tree_info: PackedAddressTreeInfo, + // pub mint_output_state_tree_index: u8, + pub mint_bump: u8, + pub mint_with_context: CompressedMintWithContext, +} + +#[inline] +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, + } +} diff --git a/sdk-tests/csdk-anchor-test/tests/test.rs b/sdk-tests/csdk-anchor-test/tests/test.rs new file mode 100644 index 0000000000..a0f918ffa3 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/tests/test.rs @@ -0,0 +1,2784 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use csdk_anchor_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 light_client::indexer::CompressedAccount; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::instructions::create_compressed_mint::{ + derive_ctoken_mint_address, find_spl_mint_address, +}; +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_macros::pubkey; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + utils::simulation::simulate_cu, + AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::{CompressAs, CompressibleConfig}, + instruction::{PackedAccounts, SystemAccountMetaConfig}, + token::CTokenDataWithVariant, +}; +use light_sdk_types::C_TOKEN_PROGRAM_ID; +use light_token_client::ctoken; +use solana_account::Account; +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_RECIPIENT: 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"); +#[tokio::test] +async fn test_create_and_decompress_two_accounts() { + let program_id = csdk_anchor_test::ID; + let mut config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + vec![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) = csdk_anchor_test::get_ctoken_signer_seeds( + &combined_user.pubkey(), + &ctoken_account.token.mint, + ); + + let (_, ctoken_account_address_2) = + csdk_anchor_test::get_ctoken_signer2_seeds(&combined_user.pubkey()); + + let (_, ctoken_account_address_3) = + csdk_anchor_test::get_ctoken_signer3_seeds(&combined_user.pubkey()); + + let (_, ctoken_account_address_4) = csdk_anchor_test::get_ctoken_signer4_seeds( + &combined_user.pubkey(), + &combined_user.pubkey(), + ); + + let (_, ctoken_account_address_5) = csdk_anchor_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; +} + +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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; +} + +#[tokio::test] +async fn test_double_decompression_attack() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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().queue; + 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), + )], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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()); +} + +#[tokio::test] +async fn test_create_and_decompress_accounts_with_different_state_trees() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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; +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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 = csdk_anchor_test::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = csdk_anchor_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()); +} + +#[tokio::test] +async fn test_custom_compression_game_session() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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; +} + +#[tokio::test] +async fn test_create_empty_compressed_account() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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 = + csdk_anchor_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().queue; + 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; +} + +async fn create_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + state_tree_queue: Option, +) { + let config_pda = CompressibleConfig::derive_pda(program_id, 0).0; + + 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().queue; + + let accounts = csdk_anchor_test::accounts::CreateRecord { + user: payer.pubkey(), + user_record: *user_record_pda, + system_program: solana_sdk::system_program::ID, + config: config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + 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 = csdk_anchor_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 _cu = simulate_cu(rpc, payer, &instruction).await; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_none(), + "Account should not exist after compression" + ); +} + +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().queue; + + let accounts = csdk_anchor_test::accounts::CreateGameSession { + player: payer.pubkey(), + game_session: *game_session_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + 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 = csdk_anchor_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()); +} + +#[allow(clippy::too_many_arguments)] +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().queue; + + 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::derive_ctoken_program_config(None).0; + 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, + }), + ), + ], + &csdk_anchor_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, + 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], + csdk_anchor_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + csdk_anchor_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)] +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().queue; + + 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), + ), + ], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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 _cu = simulate_cu(rpc, payer, &instruction).await; + + 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], + csdk_anchor_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + csdk_anchor_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); +} + +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().queue; + + 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_ctoken_mint_address(&mint_signer.pubkey(), &address_tree_pubkey); + + let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); + let accounts = csdk_anchor_test::accounts::CreateUserRecordAndGameSession { + user: user.pubkey(), + user_record: *user_record_pda, + game_session: *game_session_pda, + mint_signer: mint_signer.pubkey(), + ctoken_program: light_sdk_types::constants::C_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + 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 = csdk_anchor_test::instruction::CreateUserRecordAndGameSession { + account_data: csdk_anchor_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: csdk_anchor_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, + ) +} + +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().queue; + + 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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*user_record_pda], + &[account], + &csdk_anchor_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_recipient: RENT_RECIPIENT, + } + .to_account_metas(None), + vec![csdk_anchor_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()) +} + +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().queue; + + 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), + )], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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 + ); +} + +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().queue; + + let accounts = csdk_anchor_test::accounts::CreatePlaceholderRecord { + user: payer.pubkey(), + placeholder_record: *placeholder_record_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_recipient: RENT_RECIPIENT, + }; + + 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![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 = csdk_anchor_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" + ); +} + +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().queue; + + 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 = csdk_anchor_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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &[account], + &csdk_anchor_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_recipient: RENT_RECIPIENT, + } + .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" + ); +} + +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().queue; + + 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 = csdk_anchor_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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &accounts_to_compress, + &csdk_anchor_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_recipient: RENT_RECIPIENT, + } + .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 +} + +#[allow(clippy::too_many_arguments)] +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().queue; + + 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 = + csdk_anchor_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, + csdk_anchor_test::CompressedAccountVariant::GameSession(c_game_session), + )], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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], + csdk_anchor_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + csdk_anchor_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 + ); +} + +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 = + csdk_anchor_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" + ); +} + +#[tokio::test] +async fn test_double_compression_attack() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, + 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().queue; + 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" + ); +} + +#[allow(clippy::too_many_arguments)] +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) = + csdk_anchor_test::get_userrecord_seeds(&user.pubkey()); + let (game_session_seeds, game_session_pubkey) = + csdk_anchor_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) = + csdk_anchor_test::get_ctokensigner_authority_seeds(); + + let (_token_signer_seeds_2, _ctoken_2_authority_pda) = + csdk_anchor_test::get_ctokensigner2_authority_seeds(); + + let (_token_signer_seeds_3, _ctoken_3_authority_pda) = + csdk_anchor_test::get_ctokensigner3_authority_seeds(); + + let (_token_signer_seeds_4, _ctoken_4_authority_pda) = + csdk_anchor_test::get_ctokensigner4_authority_seeds(); + + let (_token_signer_seeds_5, _ctoken_5_authority_pda) = + csdk_anchor_test::get_ctokensigner5_authority_seeds(); + + let _cpisigner = Pubkey::new_from_array(csdk_anchor_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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[user_record_pubkey, game_session_pubkey], + &[user_record_account, game_session_account], + &csdk_anchor_test::accounts::CompressAccountsIdempotent { + fee_payer: user.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_recipient: RENT_RECIPIENT, + } + .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" + ); +} 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]]; From 0b0c563621b01df38668077d091c48a3a0ee67ac Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 9 Nov 2025 20:06:03 -0500 Subject: [PATCH 02/28] clean --- sdk-tests/csdk-anchor-test/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk-tests/csdk-anchor-test/src/lib.rs b/sdk-tests/csdk-anchor-test/src/lib.rs index 2bce3df686..5ed3b9f121 100644 --- a/sdk-tests/csdk-anchor-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-test/src/lib.rs @@ -1224,6 +1224,7 @@ pub mod csdk_anchor_test { 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 From 3231085286ea3f0c8666342ac41102c4f4601d98 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 9 Nov 2025 20:10:24 -0500 Subject: [PATCH 03/28] clean --- .../src/instruction_data/compressed_proof.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index e6310fb3cd..80d0c31cd3 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -40,7 +40,6 @@ impl Default for CompressedProof { } impl CompressedProof { - /// Convert the proof to a fixed-size byte array [u8; 128] pub fn to_array(&self) -> [u8; 128] { let mut result = [0u8; 128]; result[0..32].copy_from_slice(&self.a); @@ -123,8 +122,6 @@ impl From<&Option> for ValidityProof { impl TryFrom<&[u8]> for ValidityProof { type Error = crate::CompressedAccountError; - /// Convert bytes to ValidityProof. - /// Empty slice returns None, otherwise attempts to parse as CompressedProof and returns Some. fn try_from(bytes: &[u8]) -> Result { if bytes.is_empty() { Ok(Self(None)) @@ -142,11 +139,6 @@ impl Into> for ValidityProof { } } -// Borsh compatible validity proof. Use this in your anchor program unless you -// have zero-copy instruction data. Convert to zero-copy by calling `let proof = -// compression_params.proof.into();`. -// - pub mod borsh_compat { #[cfg_attr( all(feature = "std", feature = "anchor"), @@ -182,6 +174,13 @@ pub mod borsh_compat { derive(borsh::BorshDeserialize, borsh::BorshSerialize) )] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] + /// Borsh-compatible ValidityProof. Use this in your anchor program unless + /// you have zero-copy instruction data. + /// + /// Example: + /// ```rust + /// let proof = ValidityProof::from(compression_params.proof); + /// ``` pub struct ValidityProof(pub Option); impl ValidityProof { From d4f2680c28831c8dd38887853909bbc3effa18f0 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 9 Nov 2025 20:12:21 -0500 Subject: [PATCH 04/28] rm macro --- sdk-libs/macros/src/compress_as.rs | 206 ------------ sdk-libs/macros/src/compressible.rs | 85 ----- sdk-libs/macros/src/compressible_derive.rs | 354 --------------------- sdk-libs/macros/src/lib.rs | 116 ------- 4 files changed, 761 deletions(-) delete mode 100644 sdk-libs/macros/src/compress_as.rs delete mode 100644 sdk-libs/macros/src/compressible.rs delete mode 100644 sdk-libs/macros/src/compressible_derive.rs diff --git a/sdk-libs/macros/src/compress_as.rs b/sdk-libs/macros/src/compress_as.rs deleted file mode 100644 index 2e0e82e5f9..0000000000 --- a/sdk-libs/macros/src/compress_as.rs +++ /dev/null @@ -1,206 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Expr, Ident, ItemStruct, Result, Token, -}; - -/// Parse the compress_as attribute content -struct CompressAsFields { - fields: Punctuated, -} - -struct CompressAsField { - name: Ident, - value: Expr, -} - -impl Parse for CompressAsField { - fn parse(input: ParseStream) -> Result { - let name: Ident = input.parse()?; - input.parse::()?; - let value: Expr = input.parse()?; - Ok(CompressAsField { name, value }) - } -} - -impl Parse for CompressAsFields { - fn parse(input: ParseStream) -> Result { - Ok(CompressAsFields { - fields: Punctuated::parse_terminated(input)?, - }) - } -} - -/// Generates CompressAs trait implementation for a struct with optional compress_as attribute -pub fn derive_compress_as(input: ItemStruct) -> Result { - let struct_name = &input.ident; - - // Find the compress_as attribute (optional) - let compress_as_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("compress_as")); - - // Parse the attribute content if it exists - let compress_as_fields = if let Some(attr) = compress_as_attr { - Some(attr.parse_args::()?) - } else { - None - }; - - // Get all struct fields - let struct_fields = match &input.fields { - syn::Fields::Named(fields) => &fields.named, - _ => { - return Err(syn::Error::new_spanned( - &input, - "CompressAs derive only supports structs with named fields", - )); - } - }; - - // Create field assignments for the compress_as method - let field_assignments = struct_fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); - - // ALWAYS set compression_info to None - this is required for compressed storage - if field_name == "compression_info" { - return quote! { #field_name: None }; - } - - // Check if this field is overridden in the compress_as attribute - let override_field = compress_as_fields - .as_ref() - .and_then(|fields| fields.fields.iter().find(|f| f.name == *field_name)); - - if let Some(override_field) = override_field { - let override_value = &override_field.value; - quote! { #field_name: #override_value } - } else { - // Keep the original value - determine how to clone/copy based on field type - let field_type = &field.ty; - if is_copy_type(field_type) { - quote! { #field_name: self.#field_name } - } else { - quote! { #field_name: self.#field_name.clone() } - } - } - }); - - // Determine if we need custom compression (any fields specified in compress_as attribute) - let has_custom_fields = compress_as_fields.is_some(); - - let compress_as_impl = if has_custom_fields { - // Custom compression - return Cow::Owned with modified fields - quote! { - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - std::borrow::Cow::Owned(Self { - #(#field_assignments,)* - }) - } - } - } else { - // Simple case - return Cow::Owned with compression_info = None - // We can't return Cow::Borrowed because compression_info must be None - quote! { - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - std::borrow::Cow::Owned(Self { - #(#field_assignments,)* - }) - } - } - }; - - // Generate HasCompressionInfo implementation (automatically included with Compressible) - let has_compression_info_impl = quote! { - impl light_sdk::compressible::HasCompressionInfo for #struct_name { - fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::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; - } - } - }; - - let expanded = quote! { - impl light_sdk::compressible::CompressAs for #struct_name { - type Output = Self; - - #compress_as_impl - } - - impl light_sdk::Size for #struct_name { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } - } - - // Automatically derive HasCompressionInfo when using Compressible - #has_compression_info_impl - }; - - Ok(expanded) -} - -/// Determines if a type is likely to be Copy (simple heuristic) -fn is_copy_type(ty: &syn::Type) -> bool { - match ty { - syn::Type::Path(type_path) => { - if let Some(segment) = type_path.path.segments.last() { - let type_name = segment.ident.to_string(); - matches!( - type_name.as_str(), - "u8" | "u16" - | "u32" - | "u64" - | "u128" - | "usize" - | "i8" - | "i16" - | "i32" - | "i64" - | "i128" - | "isize" - | "f32" - | "f64" - | "bool" - | "char" - | "Pubkey" - ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) - } else { - false - } - } - _ => false, - } -} - -/// Check if Option where T is Copy -fn has_copy_inner_type(args: &syn::PathArguments) -> bool { - match args { - syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { - if let syn::GenericArgument::Type(ty) = arg { - is_copy_type(ty) - } else { - false - } - }), - _ => false, - } -} diff --git a/sdk-libs/macros/src/compressible.rs b/sdk-libs/macros/src/compressible.rs deleted file mode 100644 index 37cf02cbb0..0000000000 --- a/sdk-libs/macros/src/compressible.rs +++ /dev/null @@ -1,85 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::Result; - -// TODO: remove or add. -// /// Parse a comma-separated list of identifiers -// #[derive(Clone)] -// enum CompressibleType { -// Regular(Ident), -// } - -// struct CompressibleTypeList { -// types: Punctuated, -// } - -// impl Parse for CompressibleType { -// fn parse(input: ParseStream) -> Result { -// let ident: Ident = input.parse()?; -// Ok(CompressibleType::Regular(ident)) -// } -// } - -// impl Parse for CompressibleTypeList { -// fn parse(input: ParseStream) -> Result { -// Ok(CompressibleTypeList { -// types: Punctuated::parse_terminated(input)?, -// }) -// } -// } -/// Generates HasCompressionInfo trait implementation for a struct with compression_info field -pub fn derive_has_compression_info(input: syn::ItemStruct) -> Result { - let struct_name = input.ident.clone(); - - // Find the compression_info field - let compression_info_field = match &input.fields { - syn::Fields::Named(fields) => fields.named.iter().find(|field| { - field - .ident - .as_ref() - .map(|ident| ident == "compression_info") - .unwrap_or(false) - }), - _ => { - return Err(syn::Error::new_spanned( - &struct_name, - "HasCompressionInfo can only be derived for structs with named fields", - )) - } - }; - - let _compression_info_field = compression_info_field.ok_or_else(|| { - syn::Error::new_spanned( - &struct_name, - "HasCompressionInfo requires a field named 'compression_info' of type Option" - ) - })?; - - // Validate that the field is Option. For now, we'll assume - // it's correct and let the compiler catch type errors - let has_compression_info_impl = quote! { - impl light_sdk::compressible::HasCompressionInfo for #struct_name { - fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::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; - } - } - }; - - Ok(has_compression_info_impl) -} diff --git a/sdk-libs/macros/src/compressible_derive.rs b/sdk-libs/macros/src/compressible_derive.rs deleted file mode 100644 index 47def38403..0000000000 --- a/sdk-libs/macros/src/compressible_derive.rs +++ /dev/null @@ -1,354 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Data, DeriveInput, Expr, Fields, Ident, Result, Token, -}; - -/// Parse the compress_as attribute content -struct CompressAsFields { - fields: Punctuated, -} - -struct CompressAsField { - name: Ident, - value: Expr, -} - -impl Parse for CompressAsField { - fn parse(input: ParseStream) -> Result { - let name: Ident = input.parse()?; - input.parse::()?; - let value: Expr = input.parse()?; - Ok(CompressAsField { name, value }) - } -} - -impl Parse for CompressAsFields { - fn parse(input: ParseStream) -> Result { - Ok(CompressAsFields { - fields: Punctuated::parse_terminated(input)?, - }) - } -} - -/// Generates HasCompressionInfo, Size, and CompressAs trait implementations for compressible account types -/// -/// Supports optional compress_as attribute for custom compression behavior: -/// #[derive(Compressible)] -/// #[compress_as(start_time = 0, end_time = None)] -/// pub struct GameSession { ... } -/// -/// Usage: #[derive(Compressible)] -pub fn derive_compressible(input: DeriveInput) -> Result { - let struct_name = &input.ident; - - // Validate struct has compression_info field - let fields = match &input.data { - Data::Struct(data) => match &data.fields { - Fields::Named(fields) => &fields.named, - _ => { - return Err(syn::Error::new_spanned( - &input, - "Compressible only supports structs with named fields", - )); - } - }, - _ => { - return Err(syn::Error::new_spanned( - &input, - "Compressible only supports structs", - )); - } - }; - - // Find the compression_info field - let compression_info_field = fields.iter().find(|field| { - field - .ident - .as_ref() - .map(|ident| ident == "compression_info") - .unwrap_or(false) - }); - - if compression_info_field.is_none() { - return Err(syn::Error::new_spanned( - struct_name, - "Compressible requires a field named 'compression_info' of type Option" - )); - } - - // Parse the compress_as attribute (optional) - let compress_as_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("compress_as")); - - let compress_as_fields = if let Some(attr) = compress_as_attr { - Some(attr.parse_args::()?) - } else { - None - }; - - // Generate HasCompressionInfo implementation - let has_compression_info_impl = quote! { - impl light_sdk::compressible::HasCompressionInfo for #struct_name { - fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::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; - } - } - }; - - // Generate Size implementation - let size_impl = quote! { - impl light_sdk::account::Size for #struct_name { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } - } - }; - - // Generate CompressAs implementation - let field_assignments = fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); - - // ALWAYS set compression_info to None - this is required for compressed storage - if field_name == "compression_info" { - return quote! { #field_name: None }; - } - - // Check if this field is overridden in the compress_as attribute - let override_field = compress_as_fields - .as_ref() - .and_then(|fields| fields.fields.iter().find(|f| f.name == *field_name)); - - if let Some(override_field) = override_field { - let override_value = &override_field.value; - quote! { #field_name: #override_value } - } else { - // Keep the original value - determine how to clone/copy based on field type - let field_type = &field.ty; - if is_copy_type(field_type) { - quote! { #field_name: self.#field_name } - } else { - quote! { #field_name: self.#field_name.clone() } - } - } - }); - - let compress_as_impl = quote! { - impl light_sdk::compressible::CompressAs for #struct_name { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - std::borrow::Cow::Owned(Self { - #(#field_assignments,)* - }) - } - } - }; - - // Compute a conservative compile-time compressed INIT_SPACE that accounts for fields overridden to None - // Specifically, for fields of type Option that are set to None via #[compress_as(field = None)] - // (and for compression_info which is always set to None), we subtract the inner T's INIT_SPACE. - // For inner types, we try to use known primitive sizes, arrays, or ::INIT_SPACE when available. - fn inner_type_size_tokens(ty: &syn::Type) -> proc_macro2::TokenStream { - use quote::quote; - match ty { - syn::Type::Path(type_path) => { - if let Some(seg) = type_path.path.segments.last() { - let ident_str = seg.ident.to_string(); - // Known primitives and common types - let primitive = match ident_str.as_str() { - "u8" => Some(quote! { 1 }), - "i8" => Some(quote! { 1 }), - "bool" => Some(quote! { 1 }), - "u16" => Some(quote! { 2 }), - "i16" => Some(quote! { 2 }), - "u32" => Some(quote! { 4 }), - "i32" => Some(quote! { 4 }), - "u64" => Some(quote! { 8 }), - "i64" => Some(quote! { 8 }), - "u128" => Some(quote! { 16 }), - "i128" => Some(quote! { 16 }), - "Pubkey" => Some(quote! { 32 }), - _ => None, - }; - if let Some(sz) = primitive { - return sz; - } - // Fall back to type-level INIT_SPACE if present - let ty_ts = quote! { #type_path }; - return quote! { <#ty_ts>::INIT_SPACE }; - } - quote! { 0 } - } - syn::Type::Array(arr) => { - let elem = &arr.elem; - let len = &arr.len; - let elem_sz = inner_type_size_tokens(elem); - quote! { (#len as usize) * (#elem_sz) } - } - _ => { - // Unknown/unsupported types: assume 0 saving to avoid compile errors - quote! { 0 } - } - } - } - - // Build tokens for total savings from fields explicitly set to None - let mut savings_tokens: Vec = Vec::new(); - for field in fields.iter() { - let field_name = field.ident.as_ref().unwrap(); - - // Determine whether this field is overridden to None via #[compress_as] or is compression_info - let mut overridden_to_none = field_name == "compression_info"; - if !overridden_to_none { - if let Some(attrs) = &compress_as_fields { - if let Some(over_attr) = attrs.fields.iter().find(|f| f.name == *field_name) { - if let syn::Expr::Path(ref p) = over_attr.value { - if let Some(last) = p.path.segments.last() { - if last.ident == "None" { - overridden_to_none = true; - } - } - } - } - } - } - - if overridden_to_none { - // Check that the field type is Option and subtract Inner's INIT_SPACE - if let syn::Type::Path(type_path) = &field.ty { - if let Some(seg) = type_path.path.segments.last() { - if seg.ident == "Option" { - if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { - if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { - let inner_sz = inner_type_size_tokens(inner_ty); - savings_tokens.push(quote! { #inner_sz }); - } - } - } - } - } - } - } - - let compressed_init_space_impl = { - if savings_tokens.is_empty() { - quote! { - impl light_sdk::compressible::compression_info::CompressedInitSpace for #struct_name { const COMPRESSED_INIT_SPACE: usize = Self::INIT_SPACE; } - } - } else { - quote! { - impl light_sdk::compressible::compression_info::CompressedInitSpace for #struct_name { const COMPRESSED_INIT_SPACE: usize = Self::INIT_SPACE - (0 #( + #savings_tokens )*); } - } - } - }; - - let expanded = quote! { - #has_compression_info_impl - #size_impl - #compress_as_impl - #compressed_init_space_impl - }; - - Ok(expanded) -} - -/// Determines if a type is likely to be Copy (simple heuristic) -fn is_copy_type(ty: &syn::Type) -> bool { - match ty { - syn::Type::Path(type_path) => { - if let Some(segment) = type_path.path.segments.last() { - let type_name = segment.ident.to_string(); - matches!( - type_name.as_str(), - "u8" | "u16" - | "u32" - | "u64" - | "u128" - | "usize" - | "i8" - | "i16" - | "i32" - | "i64" - | "i128" - | "isize" - | "f32" - | "f64" - | "bool" - | "char" - | "Pubkey" - ) || (type_name == "Option" && has_copy_inner_type(&segment.arguments)) - } else { - false - } - } - _ => false, - } -} - -/// Check if Option where T is Copy -fn has_copy_inner_type(args: &syn::PathArguments) -> bool { - match args { - syn::PathArguments::AngleBracketed(args) => args.args.iter().any(|arg| { - if let syn::GenericArgument::Type(ty) = arg { - is_copy_type(ty) - } else { - false - } - }), - _ => false, - } -} - -#[allow(dead_code)] -fn generate_identity_pack_unpack(struct_name: &syn::Ident) -> Result { - let pack_impl = quote! { - impl light_sdk::compressible::Pack for #struct_name { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { - self.clone() - } - } - }; - - let unpack_impl = quote! { - impl light_sdk::compressible::Unpack for #struct_name { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[solana_account_info::AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } - } - }; - - let expanded = quote! { - #pack_impl - #unpack_impl - }; - - Ok(expanded) -} diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 6c04b1c985..45bdf382d1 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -7,9 +7,6 @@ use traits::process_light_traits; mod account; mod accounts; -mod compress_as; -mod compressible; -mod compressible_derive; mod discriminator; mod hasher; mod program; @@ -283,116 +280,3 @@ pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { .unwrap_or_else(|err| err.to_compile_error()) .into() } - -/// Automatically implements the HasCompressionInfo trait for structs that have a -/// `compression_info: Option` field. -/// -/// This derive macro generates the required trait methods for managing compression -/// information in compressible account structs. -/// -/// ## Example -/// -/// ```ignore -/// use light_sdk::compressible::{CompressionInfo, HasCompressionInfo}; -/// -/// #[derive(HasCompressionInfo)] -/// pub struct UserRecord { -/// pub compression_info: Option, -/// pub owner: Pubkey, -/// pub name: String, -/// pub score: u64, -/// } -/// ``` -/// -/// ## Requirements -/// -/// The struct must have exactly one field named `compression_info` of type -/// `Option`. -#[proc_macro_derive(HasCompressionInfo)] -pub fn has_compression_info(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - compressible::derive_has_compression_info(input) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - -/// Implements CompressAs trait for custom compression behavior. -/// -/// This derive macro allows you to specify which fields should be reset/overridden -/// during compression while keeping other fields as-is. Only the specified fields -/// are modified; all others retain their current values. -/// -/// ## Example -/// -/// ```ignore -/// use light_sdk::compressible::{CompressAs, CompressionInfo}; -/// -/// #[derive(CompressAs)] -/// #[compress_as( -/// start_time = 0, -/// end_time = None, -/// score = 0 -/// )] -/// pub struct GameSession { -/// pub compression_info: Option, -/// pub session_id: u64, -/// pub player: Pubkey, -/// pub game_type: String, -/// pub start_time: u64, -/// pub end_time: Option, -/// pub score: u64, -/// } -/// ``` -/// -/// ## Note -/// -/// Use the `Compressible` derive for complete functionality - it includes this plus more. -#[proc_macro_derive(CompressAs, attributes(compress_as))] -pub fn compress_as_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - compress_as::derive_compress_as(input) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - -/// Automatically implements all required traits for compressible accounts. -/// -/// This derive macro generates HasCompressionInfo, Size, and CompressAs trait implementations. -/// It supports optional compress_as attribute for custom compression behavior. -/// -/// ## Example - Basic Usage -/// -/// ```ignore -/// use light_sdk::compressible::CompressionInfo; -/// -/// #[derive(Compressible)] -/// pub struct UserRecord { -/// pub compression_info: Option, -/// pub owner: Pubkey, -/// pub name: String, -/// pub score: u64, -/// } -/// ``` -/// -/// ## Example - Custom Compression -/// -/// ```ignore -/// #[derive(Compressible)] -/// #[compress_as(start_time = 0, end_time = None, score = 0)] -/// pub struct GameSession { -/// pub compression_info: Option, -/// pub session_id: u64, // KEPT -/// pub player: Pubkey, // KEPT -/// pub game_type: String, // KEPT -/// pub start_time: u64, // RESET to 0 -/// pub end_time: Option, // RESET to None -/// pub score: u64, // RESET to 0 -/// } -/// ``` -#[proc_macro_derive(Compressible, attributes(compress_as))] -pub fn compressible_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - compressible_derive::derive_compressible(input) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} From f3325f3ad1c162e04db7c026c19965845063409b Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 9 Nov 2025 20:18:38 -0500 Subject: [PATCH 05/28] clean --- .github/workflows/sdk-tests.yml | 2 +- .../src/get_compressible_account.rs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 09fd05e183..7b976105c1 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -50,7 +50,7 @@ jobs: - 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"]' - 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 csdk-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"]' - program: token test sub-tests: '["cargo-test-sbf -p sdk-token-test"]' - program: sdk-libs diff --git a/sdk-libs/compressible-client/src/get_compressible_account.rs b/sdk-libs/compressible-client/src/get_compressible_account.rs index de30448235..3f59594520 100644 --- a/sdk-libs/compressible-client/src/get_compressible_account.rs +++ b/sdk-libs/compressible-client/src/get_compressible_account.rs @@ -44,11 +44,10 @@ pub struct AccountInfoInterface { pub merkle_context: Option, } -/// Get account info from either compressed or onchain storage. -/// Matches TypeScript getAccountInfoInterface behavior. +/// Get account info with unified interface. /// -/// Returns account info with compression state and merkle context. -/// Fetches both onchain and compressed in parallel for optimal performance. +/// 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, @@ -118,7 +117,6 @@ where #[cfg(feature = "anchor")] #[allow(clippy::result_large_err)] -/// Deserialize account from AccountInfoInterface using Anchor (includes discriminator). pub fn deserialize_account(account: &AccountInfoInterface) -> Result where T: anchor_lang::AccountDeserialize, @@ -129,7 +127,6 @@ where #[cfg(not(feature = "anchor"))] #[allow(clippy::result_large_err)] -/// Deserialize account from AccountInfoInterface using borsh (skips 8-byte discriminator). pub fn deserialize_account(account: &AccountInfoInterface) -> Result where T: AnchorDeserialize, @@ -144,7 +141,7 @@ where } #[cfg(feature = "anchor")] -/// Fetch and deserialize a compressible account using Anchor. +/// Get and parse account with anchor discriminator. #[allow(clippy::result_large_err)] pub async fn get_anchor_account( address: &Pubkey, From 76a37ae776acde0b961739419446628a9dcb5975 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 9 Nov 2025 20:36:48 -0500 Subject: [PATCH 06/28] 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 --- .github/actions/setup-and-build/action.yml | 3 +- .github/workflows/cli-v1.yml | 2 +- .github/workflows/cli-v2.yml | 12 +- .github/workflows/sdk-tests.yml | 2 +- cli/package.json | 17 +- cli/scripts/copyLocalProgramBinaries.sh | 16 +- cli/src/commands/token-balance/index.ts | 4 +- js/compressed-token/package.json | 10 + js/stateless.js/package.json | 12 +- pnpm-lock.yaml | 2 + program-libs/batched-merkle-tree/Cargo.toml | 9 +- program-libs/compressed-account/Cargo.toml | 6 +- .../src/instruction_data/compressed_proof.rs | 5 - .../src/instruction_data/traits.rs | 5 +- .../src/instruction_data/zero_copy_set.rs | 8 +- program-libs/ctoken-types/Cargo.toml | 6 +- .../src/state/compressed_token/hash.rs | 3 +- .../src/state/compressed_token/token_data.rs | 3 +- .../src/state/ctoken/zero_copy.rs | 11 +- .../src/state/mint/compressed_mint.rs | 3 +- .../account-compression-test/Cargo.toml | 2 +- .../utils/src/assert_mint_to_compressed.rs | 5 +- .../src/instructions/migrate_state.rs | 5 +- .../program/src/ctoken_transfer.rs | 7 - .../program/src/shared/token_input.rs | 19 +- .../compression/ctoken/compress_and_close.rs | 16 - .../program/src/transfer2/compression/mod.rs | 9 - .../program/src/transfer2/processor.rs | 3 - programs/package.json | 2 +- prover/client/src/prover.rs | 100 +- sdk-libs/client/src/indexer/types.rs | 56 +- .../src/instructions/compress_and_close.rs | 23 +- .../create_associated_token_account.rs | 7 +- .../create_compressed_mint/instruction.rs | 16 +- .../create_compressed_mint/mod.rs | 42 +- .../create_token_account/instruction.rs | 1 + .../src/instructions/decompress_full.rs | 5 +- .../src/instructions/mod.rs | 1 - .../src/instructions/transfer_ctoken.rs | 14 +- .../src/instructions/transfer_interface.rs | 43 +- .../update_compressed_mint/account_metas.rs | 1 - .../src/instruction/update_compressed_mint.rs | 2 +- sdk-libs/compressible-client/Cargo.toml | 4 - .../src/get_compressible_account.rs | 12 +- sdk-libs/compressible-client/src/lib.rs | 111 +- sdk-libs/program-test/src/compressible.rs | 27 +- .../src/program_test/compressible_setup.rs | 24 +- .../program-test/src/program_test/config.rs | 1 - .../src/program_test/light_program_test.rs | 1 - .../program-test/src/program_test/test_rpc.rs | 1 - sdk-libs/sdk/src/account.rs | 2 - sdk-libs/sdk/src/compressible/close.rs | 7 +- .../sdk/src/compressible/compression_info.rs | 4 - sdk-libs/sdk/src/compressible/config.rs | 34 +- sdk-libs/sdk/src/cpi/invoke.rs | 2 - sdk-libs/sdk/src/lib.rs | 87 +- sdk-libs/sdk/src/token.rs | 21 +- .../src/actions/ctoken_transfer.rs | 7 +- .../src/actions/transfer2/spl_to_ctoken.rs | 21 +- .../src/instructions/mint_to_compressed.rs | 7 +- sdk-libs/token-client/src/lib.rs | 95 +- sdk-tests/csdk-anchor-test/Anchor.toml | 19 + sdk-tests/csdk-anchor-test/package.json | 11 + sdk-tests/csdk-anchor-test/src/constants.rs | 3 + sdk-tests/csdk-anchor-test/src/errors.rs | 81 + .../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 | 418 +++ .../initialize_compression_config.rs | 28 + .../csdk-anchor-test/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/csdk-anchor-test/src/lib.rs | 2272 +-------------- sdk-tests/csdk-anchor-test/src/seeds.rs | 219 ++ sdk-tests/csdk-anchor-test/src/state.rs | 520 ++++ .../tests/game_session_tests.rs | 232 ++ sdk-tests/csdk-anchor-test/tests/helpers.rs | 336 +++ .../tests/idempotency_tests.rs | 143 + .../tests/{test.rs => multi_account_tests.rs} | 2456 ++++------------- .../tests/placeholder_tests.rs | 535 ++++ .../tests/user_record_tests.rs | 279 ++ .../src/process_four_transfer2.rs | 2 - 87 files changed, 4647 insertions(+), 4741 deletions(-) create mode 100644 sdk-tests/csdk-anchor-test/Anchor.toml create mode 100644 sdk-tests/csdk-anchor-test/package.json create mode 100644 sdk-tests/csdk-anchor-test/src/constants.rs create mode 100644 sdk-tests/csdk-anchor-test/src/errors.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instruction_accounts.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/create_game_session.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/create_placeholder_record.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/create_record.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/initialize_compression_config.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/mod.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/update_compression_config.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/update_game_session.rs create mode 100644 sdk-tests/csdk-anchor-test/src/instructions/update_record.rs create mode 100644 sdk-tests/csdk-anchor-test/src/seeds.rs create mode 100644 sdk-tests/csdk-anchor-test/src/state.rs create mode 100644 sdk-tests/csdk-anchor-test/tests/game_session_tests.rs create mode 100644 sdk-tests/csdk-anchor-test/tests/helpers.rs create mode 100644 sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs rename sdk-tests/csdk-anchor-test/tests/{test.rs => multi_account_tests.rs} (50%) create mode 100644 sdk-tests/csdk-anchor-test/tests/placeholder_tests.rs create mode 100644 sdk-tests/csdk-anchor-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..cfa5c5c4cb 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/csdk_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', '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..54e355a6bf 100644 --- a/.github/workflows/cli-v1.yml +++ b/.github/workflows/cli-v1.yml @@ -51,7 +51,7 @@ jobs: - name: Build CLI run: | - npx nx build @lightprotocol/zk-compression-cli + npx nx run @lightprotocol/zk-compression-cli:build-ci - name: Run CLI tests with V1 run: | diff --git a/.github/workflows/cli-v2.yml b/.github/workflows/cli-v2.yml index 81dd22b13f..ec134fc526 100644 --- a/.github/workflows/cli-v2.yml +++ b/.github/workflows/cli-v2.yml @@ -49,19 +49,9 @@ jobs: skip-components: "redis,disk-cleanup,go" cache-key: "js" - - name: Build stateless.js with V2 - run: | - cd js/stateless.js - pnpm build:v2 - - - name: Build compressed-token with V2 - run: | - cd js/compressed-token - pnpm build:v2 - - name: Build CLI with V2 run: | - npx nx build @lightprotocol/zk-compression-cli + npx nx run @lightprotocol/zk-compression-cli:build-ci - name: Run CLI tests with V2 run: | diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 7b976105c1..d1cc39bfd5 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -50,7 +50,7 @@ jobs: - 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"]' - program: anchor & pinocchio - sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p csdk-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: '["cd sdk-tests/csdk-anchor-test && cargo build-sbf && cargo test-sbf", "cd ../../sdk-tests/sdk-anchor-test && cargo build-sbf && cargo test-sbf", "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/cli/package.json b/cli/package.json index 57a1e2518f..2668f622f1 100644 --- a/cli/package.json +++ b/cli/package.json @@ -132,19 +132,28 @@ "nx": { "targets": { "build": { + "dependsOn": [ + "@lightprotocol/programs:build", + "@lightprotocol/stateless.js:build", + "@lightprotocol/compressed-token:build" + ], "inputs": [ "{workspaceRoot}/js", "{workspaceRoot}/programs", "{workspaceRoot}/gnark-prover" ], "outputs": [ - "{workspaceRoot}/bin", - "{workspaceRoot}/dist", - "{workspaceRoot}/lib", - "{workspaceRoot}/test_bin" + "{projectRoot}/bin", + "{projectRoot}/dist", + "{projectRoot}/lib", + "{projectRoot}/test_bin" ] }, "build-ci": { + "executor": "nx:run-script", + "options": { + "script": "build" + }, "dependsOn": [ "@lightprotocol/stateless.js:build-ci", "@lightprotocol/compressed-token:build-ci" diff --git a/cli/scripts/copyLocalProgramBinaries.sh b/cli/scripts/copyLocalProgramBinaries.sh index afddb155e9..c67fe2ef85 100755 --- a/cli/scripts/copyLocalProgramBinaries.sh +++ b/cli/scripts/copyLocalProgramBinaries.sh @@ -11,18 +11,6 @@ fi keys="account_compression light_system_program_pinocchio light_compressed_token light_registry" for key in $keys do - # cli build process deletes target/deploy contents, so fall back to - # sbf-solana-solana - src_deploy="$root_dir/target/deploy/$key.so" - src_sbf_release="$root_dir/target/sbf-solana-solana/release/$key.so" - - if [ -f "$src_deploy" ]; then - cp "$src_deploy" "$out_dir/$key.so" - elif [ -f "$src_sbf_release" ]; then - cp "$src_sbf_release" "$out_dir/$key.so" - else - echo "Error: $key.so not found in $src_deploy or $src_sbf_release" >&2 - exit 1 - fi + cp "$root_dir/target/deploy/$key.so" "$out_dir"/"$key".so done -cp "$root_dir"/third-party/solana-program-library/spl_noop.so "$out_dir"/spl_noop.so +cp "$root_dir"/third-party/solana-program-library/spl_noop.so "$out_dir"/spl_noop.so \ No newline at end of file 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/js/compressed-token/package.json b/js/compressed-token/package.json index fdf9075985..9c6ab3df1f 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -131,11 +131,21 @@ "{workspaceRoot}/cli", "{workspaceRoot}/target/idl", "{workspaceRoot}/target/types" + ], + "outputs": [ + "{projectRoot}/dist" ] }, "build-ci": { + "executor": "nx:run-script", + "options": { + "script": "build-ci" + }, "dependsOn": [ "@lightprotocol/stateless.js:build-ci" + ], + "outputs": [ + "{projectRoot}/dist" ] }, "test-ci": { diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 798a2cc7fd..6c65f3f1a1 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -125,10 +125,20 @@ "{workspaceRoot}/cli", "{workspaceRoot}/target/idl", "{workspaceRoot}/target/types" + ], + "outputs": [ + "{projectRoot}/dist" ] }, "build-ci": { - "dependsOn": [] + "executor": "nx:run-script", + "options": { + "script": "build-ci" + }, + "dependsOn": [], + "outputs": [ + "{projectRoot}/dist" + ] }, "test-ci": { "dependsOn": [] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e3203bf38..c1860a887e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -442,6 +442,8 @@ importers: programs: {} + sdk-tests/csdk-anchor-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/compressed-account/Cargo.toml b/program-libs/compressed-account/Cargo.toml index a5f93db3f6..4d787d3bdc 100644 --- a/program-libs/compressed-account/Cargo.toml +++ b/program-libs/compressed-account/Cargo.toml @@ -15,8 +15,8 @@ anchor = ["anchor-lang", "std"] pinocchio = ["dep:pinocchio"] bytemuck-des = ["bytemuck"] new-unique = ["dep:solana-pubkey"] -profile-program = ["dep:light-program-profiler"] -profile-heap = ["dep:light-heap", "dep:light-program-profiler"] +profile-program = ["light-program-profiler/profile-program"] +profile-heap = ["dep:light-heap", "light-program-profiler/profile-heap"] poseidon = ["dep:light-poseidon", "light-hasher/poseidon"] keccak = ["light-hasher/keccak"] sha256 = ["light-hasher/sha256"] @@ -36,7 +36,7 @@ anchor-lang = { workspace = true, optional = true } bytemuck = { workspace = true, optional = true, features = ["derive"] } borsh = { workspace = true, default-features = false } solana-pubkey = { workspace = true, optional = true } -light-program-profiler = { workspace = true, optional = true } +light-program-profiler = { workspace = true } light-heap = { workspace = true, optional = true } tinyvec = { workspace = true } diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index 80d0c31cd3..275ffe970a 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -176,11 +176,6 @@ pub mod borsh_compat { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] /// Borsh-compatible ValidityProof. Use this in your anchor program unless /// you have zero-copy instruction data. - /// - /// Example: - /// ```rust - /// let proof = ValidityProof::from(compression_params.proof); - /// ``` pub struct ValidityProof(pub Option); impl ValidityProof { diff --git a/program-libs/compressed-account/src/instruction_data/traits.rs b/program-libs/compressed-account/src/instruction_data/traits.rs index 8da9e48248..f5e8a95733 100644 --- a/program-libs/compressed-account/src/instruction_data/traits.rs +++ b/program-libs/compressed-account/src/instruction_data/traits.rs @@ -5,7 +5,6 @@ use anchor_lang::AnchorSerialize; #[allow(unused_imports)] #[cfg(not(all(feature = "std", feature = "anchor")))] use borsh::BorshSerialize as AnchorSerialize; -#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use tinyvec::ArrayVec; use zerocopy::Ref; @@ -23,7 +22,7 @@ pub trait InstructionDiscriminator { pub trait LightInstructionData: InstructionDiscriminator + AnchorSerialize { #[cfg(feature = "alloc")] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] fn data(&self) -> Result, CompressedAccountError> { let inputs = AnchorSerialize::try_to_vec(self) .map_err(|_| CompressedAccountError::InvalidArgument)?; @@ -33,7 +32,7 @@ pub trait LightInstructionData: InstructionDiscriminator + AnchorSerialize { Ok(data) } - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] fn data_array(&self) -> Result, CompressedAccountError> { let mut data = ArrayVec::new(); // Add discriminator diff --git a/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs b/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs index 60facf1f67..15cb09b60c 100644 --- a/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs +++ b/program-libs/compressed-account/src/instruction_data/zero_copy_set.rs @@ -1,4 +1,3 @@ -#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::traits::ZeroCopyAt; use zerocopy::little_endian::U16; @@ -14,7 +13,6 @@ use crate::{ }; impl ZOutputCompressedAccountWithPackedContextMut<'_> { - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] #[inline] pub fn set( &mut self, @@ -50,7 +48,7 @@ impl ZOutputCompressedAccountWithPackedContextMut<'_> { impl ZInAccountMut<'_> { #[inline] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn set_z( &mut self, discriminator: [u8; 8], @@ -84,7 +82,7 @@ impl ZInAccountMut<'_> { } #[inline] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn set( &mut self, discriminator: [u8; 8], @@ -160,7 +158,7 @@ impl ZInstructionDataInvokeCpiWithReadOnlyMut<'_> { impl ZNewAddressParamsAssignedPackedMut<'_> { #[inline] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn set( &mut self, seed: [u8; 32], diff --git a/program-libs/ctoken-types/Cargo.toml b/program-libs/ctoken-types/Cargo.toml index 597a2a9c29..520116edcd 100644 --- a/program-libs/ctoken-types/Cargo.toml +++ b/program-libs/ctoken-types/Cargo.toml @@ -7,8 +7,8 @@ edition = { workspace = true } anchor = ["light-compressed-account/anchor", "dep:anchor-lang", "light-compressible/anchor"] solana = ["dep:solana-program-error", "dep:solana-sysvar", "solana-msg"] default = [] -profile-program = ["dep:light-program-profiler"] -profile-heap = ["dep:light-heap", "dep:light-program-profiler"] +profile-program = ["light-program-profiler/profile-program"] +profile-heap = ["dep:light-heap", "light-program-profiler/profile-heap"] poseidon = ["light-hasher/poseidon"] [dependencies] @@ -31,7 +31,7 @@ solana-sysvar = { workspace = true, optional = true } spl-pod = { workspace = true } spl-token-2022 = { workspace = true } solana-msg = { workspace = true, optional = true } -light-program-profiler = { workspace = true, optional = true } +light-program-profiler = { workspace = true } light-heap = { workspace = true, optional = true } light-compressible = {workspace = true } pinocchio-pubkey = {workspace = true} diff --git a/program-libs/ctoken-types/src/state/compressed_token/hash.rs b/program-libs/ctoken-types/src/state/compressed_token/hash.rs index fe045ce626..13d8979873 100644 --- a/program-libs/ctoken-types/src/state/compressed_token/hash.rs +++ b/program-libs/ctoken-types/src/state/compressed_token/hash.rs @@ -1,7 +1,6 @@ use borsh::BorshSerialize; use light_compressed_account::hash_to_bn254_field_size_be; use light_hasher::{errors::HasherError, sha256::Sha256BE, Hasher, Poseidon}; -#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use super::TokenData; @@ -82,7 +81,7 @@ impl TokenData { impl TokenData { /// TokenDataVersion 3 /// CompressedAccount Discriminator [0,0,0,0,0,0,0,4] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] #[inline(always)] pub fn hash_sha_flat(&self) -> Result<[u8; 32], HasherError> { let bytes = self.try_to_vec().map_err(|_| HasherError::BorshError)?; diff --git a/program-libs/ctoken-types/src/state/compressed_token/token_data.rs b/program-libs/ctoken-types/src/state/compressed_token/token_data.rs index 03957ab3e9..05d365b35f 100644 --- a/program-libs/ctoken-types/src/state/compressed_token/token_data.rs +++ b/program-libs/ctoken-types/src/state/compressed_token/token_data.rs @@ -1,5 +1,4 @@ use light_compressed_account::Pubkey; -#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::{num_trait::ZeroCopyNumTrait, ZeroCopy, ZeroCopyMut}; @@ -56,7 +55,7 @@ impl TokenData { impl ZTokenDataMut<'_> { /// Set all fields of the TokenData struct at once #[inline] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn set( &mut self, mint: Pubkey, diff --git a/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs b/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs index ee4c0b84b5..a708c83844 100644 --- a/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs +++ b/program-libs/ctoken-types/src/state/ctoken/zero_copy.rs @@ -1,7 +1,6 @@ use std::ops::{Deref, DerefMut}; use light_compressed_account::Pubkey; -#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::{ errors::ZeroCopyError, @@ -145,7 +144,7 @@ impl<'a> ZeroCopyAt<'a> for CTokenMeta { impl<'a> ZeroCopyAtMut<'a> for CTokenMeta { type ZeroCopyAtMut = ZCompressedTokenMetaMut<'a>; - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] #[inline(always)] fn zero_copy_at_mut( bytes: &'a mut [u8], @@ -412,7 +411,7 @@ impl DerefMut for ZCompressedTokenMut<'_> { impl<'a> ZeroCopyAt<'a> for CToken { type ZeroCopyAt = ZCToken<'a>; - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] fn zero_copy_at(bytes: &'a [u8]) -> Result<(Self::ZeroCopyAt, &'a [u8]), ZeroCopyError> { let (__meta, bytes) = >::zero_copy_at(bytes)?; let (extensions, bytes) = if !bytes.is_empty() { @@ -437,7 +436,7 @@ impl<'a> ZeroCopyAt<'a> for CToken { impl CToken { /// Zero-copy deserialization with initialization check. /// Returns an error if the account is not initialized (byte 108 must be 1). - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn zero_copy_at_checked( bytes: &[u8], ) -> Result<(ZCToken<'_>, &[u8]), crate::error::CTokenError> { @@ -457,7 +456,7 @@ impl CToken { /// Mutable zero-copy deserialization with initialization check. /// Returns an error if the account is not initialized (byte 108 must be 1). - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn zero_copy_at_mut_checked( bytes: &mut [u8], ) -> Result<(ZCompressedTokenMut<'_>, &mut [u8]), crate::error::CTokenError> { @@ -478,7 +477,7 @@ impl CToken { impl<'a> ZeroCopyAtMut<'a> for CToken { type ZeroCopyAtMut = ZCompressedTokenMut<'a>; - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] #[inline(always)] fn zero_copy_at_mut( bytes: &'a mut [u8], diff --git a/program-libs/ctoken-types/src/state/mint/compressed_mint.rs b/program-libs/ctoken-types/src/state/mint/compressed_mint.rs index 0c38b07937..bc3e5e7ae5 100644 --- a/program-libs/ctoken-types/src/state/mint/compressed_mint.rs +++ b/program-libs/ctoken-types/src/state/mint/compressed_mint.rs @@ -1,7 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_compressed_account::Pubkey; use light_hasher::{sha256::Sha256BE, Hasher}; -#[cfg(any(feature = "profile-program", feature = "profile-heap"))] use light_program_profiler::profile; use light_zero_copy::{ZeroCopy, ZeroCopyMut}; #[cfg(feature = "solana")] @@ -71,7 +70,7 @@ impl CompressedMint { impl ZCompressedMintMut<'_> { /// Set all fields of the CompressedMint struct at once #[inline] - #[cfg_attr(any(feature = "profile-program", feature = "profile-heap"), profile)] + #[profile] pub fn set( &mut self, ix_data: &>::ZeroCopyAt, 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/utils/src/assert_mint_to_compressed.rs b/program-tests/utils/src/assert_mint_to_compressed.rs index d777d6088f..9703f15b9e 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; diff --git a/programs/account-compression/src/instructions/migrate_state.rs b/programs/account-compression/src/instructions/migrate_state.rs index 3f62280f25..535d76f01e 100644 --- a/programs/account-compression/src/instructions/migrate_state.rs +++ b/programs/account-compression/src/instructions/migrate_state.rs @@ -128,8 +128,11 @@ fn migrate_state( nullified_leaves_indices: vec![migrate_leaf_params.leaf_index], seq: merkle_tree.sequence_number() as u64, }; + #[cfg(target_os = "solana")] let slot = Clock::get()?.slot; - // 3. Inserts the leaf in the output queue. + #[cfg(not(target_os = "solana"))] + let slot = 0u64; // Mock slot for unit tests + // 3. Inserts the leaf in the output queue. output_queue .insert_into_current_batch(&migrate_leaf_params.leaf, &slot) .map_err(ProgramError::from)?; diff --git a/programs/compressed-token/program/src/ctoken_transfer.rs b/programs/compressed-token/program/src/ctoken_transfer.rs index 0d93a9d4c5..58e1fabcf7 100644 --- a/programs/compressed-token/program/src/ctoken_transfer.rs +++ b/programs/compressed-token/program/src/ctoken_transfer.rs @@ -27,12 +27,8 @@ pub fn process_ctoken_transfer<'a>( return Err(ProgramError::NotEnoughAccountKeys); } - msg!("processing transfer"); - msg!("accounts: {:?}", accounts); - process_transfer(accounts, instruction_data) .map_err(|e| ProgramError::Custom(u64::from(e) as u32))?; - msg!("transfer processed"); calculate_and_execute_top_up_transfers(accounts) } @@ -45,9 +41,6 @@ fn calculate_and_execute_top_up_transfers( // Initialize transfers array with account references, amounts will be updated let account0 = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; let account1 = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; - msg!("accounts: {:?}", accounts); - msg!("account0: {:?}", account0.key()); - msg!("account1: {:?}", account1.key()); let mut transfers = [ Transfer { account: account0, diff --git a/programs/compressed-token/program/src/shared/token_input.rs b/programs/compressed-token/program/src/shared/token_input.rs index 0b1af32f16..f8e4306f32 100644 --- a/programs/compressed-token/program/src/shared/token_input.rs +++ b/programs/compressed-token/program/src/shared/token_input.rs @@ -93,21 +93,6 @@ fn set_input_compressed_account_inner( ProgramError::Custom(AccountError::NotEnoughAccountKeys.into()) })?; - anchor_lang::solana_program::msg!("DEBUG token_input - mint_account: {:?}", mint_account.key()); - anchor_lang::solana_program::msg!( - "DEBUG token_input - owner_account: {:?}", - owner_account.key() - ); - anchor_lang::solana_program::msg!("DEBUG token_input - amount: {:?}", input_token_data.amount); - anchor_lang::solana_program::msg!( - "DEBUG token_input - has_delegate: {:?}", - input_token_data.has_delegate - ); - anchor_lang::solana_program::msg!( - "DEBUG token_input - leaf_index: {:?}", - input_token_data.merkle_context.leaf_index - ); - let data_hash = { match token_version { TokenDataVersion::ShaFlat => { @@ -155,15 +140,13 @@ fn set_input_compressed_account_inner( } }; - anchor_lang::solana_program::msg!("DEBUG token_input - computed data_hash: {:?}", data_hash); - input_compressed_account.set_z( token_version.discriminator(), data_hash, &input_token_data.merkle_context, *input_token_data.root_index, lamports, - None, // Token accounts don't have addresses + None, )?; Ok(()) } diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs index 3e4fc68e06..f12b767fb7 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs @@ -195,22 +195,6 @@ pub fn close_for_compress_and_close( let authority = validated_accounts .packed_accounts .get_u8(compression.authority, "CompressAndClose: authority")?; - msg!( - "Closing ctoken account: {}", - solana_pubkey::Pubkey::new_from_array(*token_account_info.key()) - ); - msg!( - " destination: {}", - solana_pubkey::Pubkey::new_from_array(*destination.key()) - ); - msg!( - " authority: {}", - solana_pubkey::Pubkey::new_from_array(*authority.key()) - ); - msg!( - " rent_sponsor: {}", - solana_pubkey::Pubkey::new_from_array(*rent_sponsor.key()) - ); close_token_account(&CloseTokenAccountAccounts { token_account: token_account_info, destination, diff --git a/programs/compressed-token/program/src/transfer2/compression/mod.rs b/programs/compressed-token/program/src/transfer2/compression/mod.rs index 2869b8734f..31430354d7 100644 --- a/programs/compressed-token/program/src/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/transfer2/compression/mod.rs @@ -37,7 +37,6 @@ pub fn process_token_compression( packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, cpi_authority: &AccountInfo, ) -> Result<(), ProgramError> { - msg!("processing token compression"); if let Some(compressions) = inputs.compressions.as_ref() { // Array to accumulate transfer amounts by account index (max 40 packed accounts) let mut transfer_map = [0u64; 40]; @@ -125,14 +124,6 @@ pub fn process_token_compression( .collect::, ProgramError>>()?; if !transfers.is_empty() { - msg!("Top-up payer: {:?}", fee_payer.key()); - for transfer in &transfers { - msg!( - "Top-up recipient: {} ({} lamports)", - solana_pubkey::Pubkey::new_from_array(*transfer.account.key()), - transfer.amount - ); - } multi_transfer_lamports(fee_payer, &transfers).map_err(convert_program_error)? } } diff --git a/programs/compressed-token/program/src/transfer2/processor.rs b/programs/compressed-token/program/src/transfer2/processor.rs index 99520f5ab8..d64e70f798 100644 --- a/programs/compressed-token/program/src/transfer2/processor.rs +++ b/programs/compressed-token/program/src/transfer2/processor.rs @@ -202,7 +202,6 @@ fn process_with_system_program_cpi( .map_err(|e| ProgramError::Custom(e as u32 + 6000))?; if let Some(system_accounts) = validated_accounts.system.as_ref() { - msg!("processing token compression with system program"); // Process token compressions/decompressions/close_and_compress process_token_compression( system_accounts.fee_payer, @@ -226,13 +225,11 @@ fn process_with_system_program_cpi( false, )?; - msg!("closing ctoken accounts"); // Close ctoken accounts at the end of the instruction. if let Some(compressions) = inputs.compressions.as_ref() { close_for_compress_and_close(compressions.as_slice(), validated_accounts)?; } } else if let Some(system_accounts) = validated_accounts.write_to_cpi_context_system.as_ref() { - msg!("processing token compression with system program and write to cpi context"); // CPI context write mode expects exactly 4 accounts: // 0 - light-system-program - skip // 1 - fee_payer diff --git a/programs/package.json b/programs/package.json index 244d22318d..893c592431 100644 --- a/programs/package.json +++ b/programs/package.json @@ -3,7 +3,7 @@ "version": "0.3.0", "license": "Apache-2.0", "scripts": { - "build": "cd system/ && cargo build-sbf && cd .. && cd account-compression/ && cargo build-sbf --features 'test, migrate-state' && cd .. && cd registry/ && cargo build-sbf && cd .. && cd compressed-token/program && cargo build-sbf && cd ../../..", + "build": "ROOT_DIR=$(git rev-parse --show-toplevel) && export SBF_OUT_DIR=\"$ROOT_DIR/target/deploy\" && mkdir -p \"$SBF_OUT_DIR\" && cd system/ && cargo build-sbf && cd .. && cd account-compression/ && cargo build-sbf --features 'test, migrate-state' && cd .. && cd registry/ && cargo build-sbf && cd .. && cd compressed-token/program && cargo build-sbf && cd ../../..", "build-compressed-token-small": "cd compressed-token && cargo build-sbf --features cpi-without-program-ids && cd ..", "build-system": "anchor build --program-name light_system_program -- --features idl-build custom-heap", "build-compressed-token": "anchor build --program-name light_compressed_token -- --features idl-build custom-heap", diff --git a/prover/client/src/prover.rs b/prover/client/src/prover.rs index 3bf1bab785..dc2005be18 100644 --- a/prover/client/src/prover.rs +++ b/prover/client/src/prover.rs @@ -1,6 +1,11 @@ use std::{ - process::Command, - sync::atomic::{AtomicBool, Ordering}, + fs::{File, OpenOptions}, + path::PathBuf, + process::{Command, Stdio}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, thread::sleep, time::Duration, }; @@ -31,17 +36,98 @@ pub async fn spawn_prover() { if !health_check(10, 1).await && !IS_LOADING.load(Ordering::Relaxed) { IS_LOADING.store(true, Ordering::Relaxed); - let command = Command::new(prover_path) - .arg("start-prover") - .spawn() - .expect("Failed to start prover process"); + let is_ci = std::env::var("CI").is_ok(); + let output_buffer: Arc>> = Arc::new(Mutex::new(Vec::new())); - let _ = command.wait_with_output(); + if is_ci { + use tokio::{io::AsyncReadExt, process::Command as TokioCommand}; + + let mut command = TokioCommand::new(prover_path); + command.arg("start-prover").stdin(Stdio::null()); + command.stdout(Stdio::piped()); + command.stderr(Stdio::piped()); + + let mut child = command.spawn().expect("Failed to start prover process"); + + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + let output_buffer_stdout = Arc::clone(&output_buffer); + let output_buffer_stderr = Arc::clone(&output_buffer); + + if let Some(mut stdout) = stdout { + tokio::spawn(async move { + let mut buffer = vec![0u8; 1024]; + loop { + match stdout.read(&mut buffer).await { + Ok(0) => break, + Ok(n) => { + let mut buf = output_buffer_stdout.lock().unwrap(); + buf.extend_from_slice(&buffer[..n]); + } + Err(_) => break, + } + } + }); + } + + if let Some(mut stderr) = stderr { + tokio::spawn(async move { + let mut buffer = vec![0u8; 1024]; + loop { + match stderr.read(&mut buffer).await { + Ok(0) => break, + Ok(n) => { + let mut buf = output_buffer_stderr.lock().unwrap(); + buf.extend_from_slice(&buffer[..n]); + } + Err(_) => break, + } + } + }); + } + + std::mem::drop(child); + } else { + let log_dir = PathBuf::from("test-ledger"); + std::fs::create_dir_all(&log_dir).ok(); + let log_path = log_dir.join("prover.log"); + let log_file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .unwrap_or_else(|_| { + File::create(&log_path).expect("Failed to create prover log file") + }); + let log_file_stderr = log_file.try_clone().ok(); + + let child = Command::new(prover_path) + .arg("start-prover") + .stdin(Stdio::null()) + .stdout(Stdio::from(log_file)) + .stderr(log_file_stderr.map(Stdio::from).unwrap_or(Stdio::null())) + .spawn() + .expect("Failed to start prover process"); + + std::mem::drop(child); + } + + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; let health_result = health_check(120, 1).await; if health_result { info!("Prover started successfully"); } else { + if is_ci { + let output = output_buffer.lock().unwrap(); + let output_str = String::from_utf8_lossy(&output); + eprintln!("=== Prover output (stdout/stderr) ==="); + eprintln!("{}", output_str); + eprintln!("=== End of prover output ==="); + } + println!( + "Failed to start prover, health check failed. {:?}", + health_result + ); panic!("Failed to start prover, health check failed."); } } diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index 43fa71fae8..c49b449ff5 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -527,30 +527,13 @@ impl TryFrom for CompressedAccount { let hash = account .hash() .map_err(|_| IndexerError::InvalidResponseData)?; - // Breaks light-program-test - // let tree_info = QUEUE_TREE_MAPPING.get( - // &Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()) - // .to_string(), - // ); let tree_pubkey = Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()); - let tree_info = QUEUE_TREE_MAPPING + + let cpi_context = QUEUE_TREE_MAPPING .get(&tree_pubkey.to_string()) - .ok_or_else(|| { - println!( - "ERROR: No tree_info found for tree pubkey: {:?}", - tree_pubkey.to_string() - ); - IndexerError::InvalidResponseData - })?; - - if tree_info.cpi_context.is_none() { - panic!( - "Cpi context not found in queue tree mapping for tree pubkey: {:?}", - tree_pubkey.to_string() - ); - } + .and_then(|tree_info| tree_info.cpi_context); Ok(CompressedAccount { address: account.compressed_account.address, @@ -562,13 +545,13 @@ impl TryFrom for CompressedAccount { tree: tree_pubkey, queue: Pubkey::new_from_array(account.merkle_context.queue_pubkey.to_bytes()), tree_type: account.merkle_context.tree_type, - cpi_context: tree_info.cpi_context, + cpi_context, next_tree_info: None, }, owner: Pubkey::new_from_array(account.compressed_account.owner.to_bytes()), prove_by_index: account.merkle_context.prove_by_index, seq: None, - slot_created: u64::MAX, + slot_created: u64::MAX, // TODO: use actual slot }) } } @@ -687,16 +670,27 @@ impl TryFrom<&photon_api::models::Account> for CompressedAccount { let lamports = account.lamports; let leaf_index = account.leaf_index; - let tree_info = QUEUE_TREE_MAPPING - .get(&account.tree) - .ok_or(IndexerError::InvalidResponseData)?; + let tree_pubkey = Pubkey::new_from_array(decode_base58_to_fixed_array(&account.tree)?); - let tree_info = TreeInfo { - cpi_context: tree_info.cpi_context, - queue: tree_info.queue, - tree_type: tree_info.tree_type, - next_tree_info: None, - tree: tree_info.tree, + let tree_info = if let Some(static_tree_info) = QUEUE_TREE_MAPPING.get(&account.tree) { + TreeInfo { + cpi_context: static_tree_info.cpi_context, + queue: static_tree_info.queue, + tree_type: static_tree_info.tree_type, + next_tree_info: None, + tree: static_tree_info.tree, + } + } else { + // Unknown tree + // TODO: users should provide their own custom QUEUE_TREE_MAPPING if they run their own indexer + TreeInfo { + cpi_context: None, + queue: tree_pubkey, + // FIXME: this should be dynamic + tree_type: light_compressed_account::TreeType::StateV2, + next_tree_info: None, + tree: tree_pubkey, + } }; Ok(CompressedAccount { 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 41ce6cf049..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 @@ -95,7 +95,6 @@ pub fn pack_for_compress_and_close( false, ), owner_index, // User funds go to owner - // recipient_index, // User funds go to rent sponsor (destination) ) }; Ok(CompressAndCloseIndices { @@ -219,9 +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 )?; - // Ensure destination (rent sponsor) is writable to receive lamports - // TODO: Checked remove - packed_account_metas[idx.destination_index as usize].is_writable = true; + if rent_sponsor_is_signer { packed_account_metas[idx.authority_index as usize].is_signer = true; } else { @@ -371,10 +368,12 @@ pub fn compress_and_close_ctoken_accounts<'info>( rent_sponsor_pubkey.unwrap() }; - // Destination for lamports on close is ALWAYS the rent sponsor - let destination_pubkey = actual_rent_sponsor; + let destination_pubkey = if with_compression_authority { + actual_rent_sponsor + } else { + 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, @@ -415,7 +412,7 @@ 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_recipient: AccountInfo<'info>, + compressed_token_rent_sponsor: AccountInfo<'info>, compressed_token_cpi_authority: AccountInfo<'info>, cpi_authority: AccountInfo<'info>, post_system: &[AccountInfo<'info>], @@ -425,7 +422,7 @@ pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>( 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_recipient.clone()); + packed_accounts.push(compressed_token_rent_sponsor.clone()); let ctoken_infos: Vec<&AccountInfo<'info>> = token_accounts_to_compress .iter() @@ -438,9 +435,7 @@ pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>( output_queue, &ctoken_infos, &packed_accounts, - ) - .map_err(|_| TokenSdkError::InvalidAccountData)?; - + )?; // infos let total_capacity = packed_accounts.len() + remaining_accounts.len() + 1; let mut account_infos: Vec> = Vec::with_capacity(total_capacity); 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 120dacb009..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 @@ -410,7 +410,7 @@ pub fn create_associated_ctoken_account<'info>( associated_token_account: AccountInfo<'info>, system_program: AccountInfo<'info>, compressible_config: AccountInfo<'info>, - rent_recipient: AccountInfo<'info>, + rent_sponsor: AccountInfo<'info>, authority: AccountInfo<'info>, mint: Pubkey, bump: u8, @@ -422,12 +422,13 @@ pub fn create_associated_ctoken_account<'info>( owner: *authority.key, mint, compressible_config: *compressible_config.key, - rent_sponsor: *rent_recipient.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, @@ -441,7 +442,7 @@ pub fn create_associated_ctoken_account<'info>( associated_token_account, system_program, compressible_config, - rent_recipient, + rent_sponsor, authority, ], ) 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 65aff76ec4..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(), @@ -218,14 +215,3 @@ pub fn find_spl_mint_address(mint_seed: &Pubkey) -> (Pubkey, u8) { &Pubkey::new_from_array(light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID), ) } - -/// DEPRECATED: Use derive_compressed_mint_address instead -/// Derives the compressed mint address from the mint seed and address tree -pub fn derive_ctoken_mint_address(mint_seed: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] { - derive_compressed_mint_address(mint_seed, address_tree_pubkey) -} - -/// Alias for find_spl_mint_address -pub fn find_mint_address(signer: &Pubkey) -> (Pubkey, u8) { - find_spl_mint_address(signer) -} 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 6d8fb47836..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,43 +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, derive_ctoken_mint_address, find_mint_address, - 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, }; - -// #[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 a41a68deeb..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 @@ -156,6 +156,7 @@ pub fn create_ctoken_account_signed<'info>( 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, &[ 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 91d5f068b1..5a5f29ff14 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs @@ -75,9 +75,10 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( token_accounts.push(token_account); let owner_idx = idx.source.owner as usize; - if owner_idx < signer_flags.len() { - signer_flags[owner_idx] = true; + if owner_idx >= signer_flags.len() { + return Err(TokenSdkError::InvalidAccountData); } + signer_flags[owner_idx] = true; } let mut packed_account_metas = Vec::with_capacity(packed_accounts.len()); diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index a10fa08c11..918abded89 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -53,7 +53,6 @@ pub use mint_to_compressed::{ DecompressedMintConfig, MintToCompressedInputs, MintToCompressedMetaConfig, }; pub use transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed}; -// TODO: export the others too. pub use transfer_interface::{ create_transfer_ctoken_to_spl_instruction, create_transfer_spl_to_ctoken_instruction, transfer_interface, transfer_interface_signed, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs index 35d62108b0..c5f1b97dac 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs @@ -5,9 +5,7 @@ use solana_instruction::{AccountMeta, Instruction}; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; -/// Create a decompressed token transfer instruction. This creates an -/// instruction that uses discriminator 3 (DecompressedTransfer) to perform SPL -/// token transfers on decompressed compressed token accounts. +/// Create a c-token transfer instruction. /// /// # Arguments /// * `source` - Source token account @@ -31,15 +29,16 @@ fn create_transfer_ctoken_instruction( AccountMeta::new_readonly(authority, true), ], data: { - let mut data = vec![3u8]; // DecompressedTransfer discriminator - data.push(3u8); // SPL Transfer discriminator + // 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 decompressed ctokens +/// Transfer c-tokens pub fn transfer_ctoken<'info>( from: &AccountInfo<'info>, to: &AccountInfo<'info>, @@ -48,11 +47,10 @@ pub fn transfer_ctoken<'info>( ) -> Result<(), ProgramError> { let ix = create_transfer_ctoken_instruction(*from.key, *to.key, amount, *authority.key); - // Return Result directly, as is best practice for CPI helpers in native Solana programs. invoke(&ix, &[from.clone(), to.clone(), authority.clone()]) } -/// Transfer decompressed ctokens with signer seeds +/// Transfer c-tokens CPI pub fn transfer_ctoken_signed<'info>( from: &AccountInfo<'info>, to: &AccountInfo<'info>, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs index 159670e8a9..206720498e 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -161,9 +161,6 @@ pub fn create_transfer_ctoken_to_spl_instruction( } /// Transfer SPL tokens to compressed tokens -/// -/// This function creates the instruction and immediately invokes it. -/// Similar to SPL Token's transfer wrapper functions. #[allow(clippy::too_many_arguments)] pub fn transfer_spl_to_ctoken<'info>( payer: AccountInfo<'info>, @@ -192,7 +189,7 @@ pub fn transfer_spl_to_ctoken<'info>( // let mut account_infos = remaining_accounts.to_vec(); let account_infos = vec![ - authority.clone(), + payer, compressed_token_program_authority, mint, // Index 0: Mint destination_ctoken_account, // Index 1: Destination owner @@ -207,10 +204,7 @@ pub fn transfer_spl_to_ctoken<'info>( } // TODO: must test this. -/// Transfer SPL tokens to compressed tokens via CPI signer. -/// -/// This function creates the instruction and invokes it with the provided -/// signer seeds. +/// 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>, @@ -236,10 +230,10 @@ pub fn transfer_spl_to_ctoken_signed<'info>( compressed_token_pool_pda_bump, *spl_token_program.key, ) - .map_err(|_| TokenSdkError::MethodUsed)?; + .map_err(|_| ProgramError::InvalidInstructionData)?; let account_infos = vec![ - payer.clone(), + payer, compressed_token_program_authority, mint, // Index 0: Mint destination_ctoken_account, // Index 1: Destination owner @@ -249,15 +243,12 @@ pub fn transfer_spl_to_ctoken_signed<'info>( spl_token_program, // Index 5: SPL Token program ]; - invoke_signed(&instruction, &account_infos, signer_seeds) - .map_err(|_| TokenSdkError::MethodUsed)?; + invoke_signed(&instruction, &account_infos, signer_seeds)?; Ok(()) } // TODO: TEST. /// Transfer compressed tokens to SPL tokens -/// -/// This function creates the instruction and invokes it. #[allow(clippy::too_many_arguments)] pub fn transfer_ctoken_to_spl<'info>( payer: AccountInfo<'info>, @@ -285,7 +276,7 @@ pub fn transfer_ctoken_to_spl<'info>( .map_err(|_| ProgramError::InvalidInstructionData)?; let account_infos = vec![ - authority.clone(), + payer, compressed_token_program_authority, mint, // Index 0: Mint destination_spl_token_account, // Index 1: Destination owner @@ -299,10 +290,7 @@ pub fn transfer_ctoken_to_spl<'info>( Ok(()) } -/// Transfer compressed tokens to SPL tokens via CPI signer. -/// -/// This function creates the instruction and invokes it with the provided -/// signer seeds. +/// 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>, @@ -331,7 +319,7 @@ pub fn transfer_ctoken_to_spl_signed<'info>( .map_err(|_| ProgramError::InvalidInstructionData)?; let account_infos = vec![ - payer.clone(), + payer, compressed_token_program_authority, mint, // Index 0: Mint destination_spl_token_account, // Index 1: Destination owner @@ -345,10 +333,7 @@ pub fn transfer_ctoken_to_spl_signed<'info>( Ok(()) } -/// Unified transfer interface that automatically handles both ctoken<->ctoken and ctoken<->spl transfers -/// -/// This function inspects the source and destination accounts to determine the transfer type -/// and validates that the correct optional parameters are provided. +/// Unified transfer interface for ctoken<->ctoken and ctoken<->spl transfers /// /// # Arguments /// * `source_account` - Source token account (can be ctoken or SPL) @@ -379,19 +364,15 @@ pub fn transfer_interface<'info>( compressed_token_pool_pda: Option<&AccountInfo<'info>>, compressed_token_pool_pda_bump: Option, ) -> 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(source_account, destination_account, authority, amount), - // 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, @@ -420,9 +401,7 @@ pub fn transfer_interface<'info>( ) } - // 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, @@ -458,9 +437,7 @@ pub fn transfer_interface<'info>( } } -/// Unified transfer interface with signer seeds for CPI -/// -/// Same as `transfer_interface` but uses invoke_signed for CPI calls +/// Unified transfer interface with CPI #[allow(clippy::too_many_arguments)] pub fn transfer_interface_signed<'info>( source_account: &AccountInfo<'info>, 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-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/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml index 7132e73142..c368d3db35 100644 --- a/sdk-libs/compressible-client/Cargo.toml +++ b/sdk-libs/compressible-client/Cargo.toml @@ -10,18 +10,14 @@ description = "Client instruction builders for Light Protocol compressible accou anchor = ["anchor-lang", "light-sdk/anchor"] [dependencies] -# Solana dependencies solana-instruction = { workspace = true } solana-pubkey = { workspace = true } solana-account = { workspace = true } -# Light Protocol dependencies light-client = { workspace = true, features = ["v2"] } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } -# Conditional dependencies anchor-lang = { workspace = true, features = ["idl-build"], optional = true } borsh = { workspace = true } -# External dependencies 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 index 3f59594520..70f063478d 100644 --- a/sdk-libs/compressible-client/src/get_compressible_account.rs +++ b/sdk-libs/compressible-client/src/get_compressible_account.rs @@ -60,11 +60,12 @@ where let (compressed_address, _) = derive_address(&[&address.to_bytes()], &address_tree_info.tree, program_id); - let onchain_result = rpc.get_account(*address).await; - let compressed_result = rpc.get_compressed_account(compressed_address, None).await; - - let onchain_account = onchain_result.ok().flatten(); - let compressed_account = compressed_result.ok().and_then(|r| r.value); + // 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 { @@ -141,7 +142,6 @@ where } #[cfg(feature = "anchor")] -/// Get and parse account with anchor discriminator. #[allow(clippy::result_large_err)] pub async fn get_anchor_account( address: &Pubkey, diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index 6cd0b0e70a..3ae906800d 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -18,29 +18,23 @@ use solana_account::Account; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -/// Generic instruction data for initialize config -/// Note: Real programs should use their specific instruction format #[derive(AnchorSerialize, AnchorDeserialize)] pub struct InitializeCompressionConfigData { pub compression_delay: u32, - pub rent_recipient: Pubkey, + pub rent_sponsor: Pubkey, pub address_space: Vec, pub config_bump: u8, } -/// Generic instruction data for update config -/// Note: Real programs should use their specific instruction format #[derive(AnchorSerialize, AnchorDeserialize)] pub struct UpdateCompressionConfigData { pub new_compression_delay: Option, - pub new_rent_recipient: Option, + pub new_rent_sponsor: Option, pub new_address_space: Option>, pub new_update_authority: Option, } -/// Instruction data structure for decompress_accounts_idempotent -/// This matches the exact format expected by Anchor programs -/// T is the packed type (result of calling .pack() on the original type) +/// T is the packed type from calling .pack() on the original type #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct DecompressMultipleAccountsIdempotentData { pub proof: ValidityProof, @@ -48,8 +42,6 @@ pub struct DecompressMultipleAccountsIdempotentData { pub system_accounts_offset: u8, } -/// Instruction data structure for compress_accounts_idempotent -/// This matches the exact format expected by Anchor programs #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct CompressAccountsIdempotentData { pub proof: ValidityProof, @@ -58,42 +50,27 @@ pub struct CompressAccountsIdempotentData { pub system_accounts_offset: u8, } -/// Instruction builders for compressible accounts, following Solana SDK patterns -/// These are generic builders that work with any program implementing the compressible pattern +/// Instruction builders for compressible accounts pub struct CompressibleInstruction; impl CompressibleInstruction { + /// 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]; - /// Hardcoded discriminator for the standardized decompress_accounts_idempotent instruction - /// This is calculated as SHA256("global:decompress_accounts_idempotent")[..8] (Anchor format) + /// SHA256("global:decompress_accounts_idempotent")[..8] pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = [114, 67, 61, 123, 234, 31, 1, 112]; - /// Hardcoded discriminator for compress_token_account_ctoken_signer instruction - /// This is calculated as SHA256("global:compress_token_account_ctoken_signer")[..8] (Anchor format) + /// 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]; - /// Hardcoded discriminator for the standardized compress_accounts_idempotent instruction - /// This is calculated as SHA256("global:compress_accounts_idempotent")[..8] (Anchor format) + /// 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 - /// - /// Following Solana SDK patterns like system_instruction::transfer() - /// Returns Instruction directly - errors surface at execution time - /// - /// # Arguments - /// * `program_id` - The program ID - /// * `discriminator` - The instruction discriminator bytes (flexible length) - /// * `payer` - The payer account - /// * `authority` - The authority account - /// * `compression_delay` - The compression delay - /// * `rent_recipient` - The rent recipient - /// * `address_space` - The address space - /// * `config_bump` - The config bump #[allow(clippy::too_many_arguments)] pub fn initialize_compression_config( program_id: &Pubkey, @@ -101,7 +78,7 @@ impl CompressibleInstruction { payer: &Pubkey, authority: &Pubkey, compression_delay: u32, - rent_recipient: Pubkey, + rent_sponsor: Pubkey, address_space: Vec, config_bump: Option, ) -> Instruction { @@ -125,7 +102,7 @@ impl CompressibleInstruction { let instruction_data = InitializeCompressionConfigData { compression_delay, - rent_recipient, + rent_sponsor, address_space, config_bump, }; @@ -146,24 +123,13 @@ impl CompressibleInstruction { } } - /// Creates an update config instruction - /// - /// Following Solana SDK patterns - returns Instruction directly - /// - /// # Arguments - /// * `program_id` - The program ID - /// * `discriminator` - The instruction discriminator bytes (flexible length) - /// * `authority` - The authority account - /// * `new_compression_delay` - Optional new compression delay - /// * `new_rent_recipient` - Optional new rent recipient - /// * `new_address_space` - Optional new address space - /// * `new_update_authority` - Optional new update authority + /// Updates compression config pub fn update_compression_config( program_id: &Pubkey, discriminator: &[u8], authority: &Pubkey, new_compression_delay: Option, - new_rent_recipient: Option, + new_rent_sponsor: Option, new_address_space: Option>, new_update_authority: Option, ) -> Instruction { @@ -176,7 +142,7 @@ impl CompressibleInstruction { let instruction_data = UpdateCompressionConfigData { new_compression_delay, - new_rent_recipient, + new_rent_sponsor, new_address_space, new_update_authority, }; @@ -196,18 +162,7 @@ impl CompressibleInstruction { } } - /// Build a `decompress_accounts_idempotent` instruction for any program's compressed account variant. - /// - /// # Arguments - /// * `program_id` - Target program - /// * `discriminator` - The instruction discriminator bytes (flexible length) - /// * `decompressed_account_addresses` - addresses of the accounts to decompress into - /// * `compressed_accounts` - Compressed accounts with their data (which implements Pack trait) - /// * `program_account_metas` - Additional accounts required for seed derivation (e.g., amm_config, token_mints) - /// * `validity_proof_with_context` - Validity proof with context - /// * `output_state_tree_info` - Output state tree info - /// - /// Returns `Ok(Instruction)` or error. + /// Builds decompress_accounts_idempotent instruction #[allow(clippy::too_many_arguments)] pub fn decompress_accounts_idempotent( program_id: &Pubkey, @@ -223,7 +178,6 @@ impl CompressibleInstruction { { let mut remaining_accounts = PackedAccounts::default(); - // check if pdas/tokens let mut has_tokens = false; let mut has_pdas = false; for (compressed_account, _) in compressed_accounts.iter() { @@ -261,23 +215,19 @@ impl CompressibleInstruction { let output_state_tree_index = remaining_accounts.insert_or_get(output_state_tree_info.queue); - // pack all tree infos + // pack tree infos let packed_tree_infos = validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); - // Add remaining program accounts - // accounts.extend(remaining_program_accounts); let mut accounts = program_account_metas.to_vec(); - // Pack all account data using the Pack trait. This converts types with - // Pubkeys to their packed versions with u8 indices. PDAs must implement - // pack trait. Tokens have a standard implementation. + // 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 + // create compressed_account_meta let compressed_meta = CompressedAccountMetaNoLamportsNoAddress { tree_info: packed_tree_infos .state_trees @@ -295,7 +245,7 @@ impl CompressibleInstruction { )?, output_state_tree_index, }; - // Pack data. Is standardized for TokenData and user-implemented for other types. + let packed_data = data.pack(&mut remaining_accounts); Ok(CompressedAccountData { meta: compressed_meta, @@ -304,11 +254,9 @@ impl CompressibleInstruction { }) .collect::, Box>>()?; - // add all packed systemaccounts to anchor metas. let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); accounts.extend(system_accounts); - // decompressed account addresses must be the last metas. for account in decompressed_account_addresses { accounts.push(AccountMeta::new(*account, false)); } @@ -319,7 +267,6 @@ impl CompressibleInstruction { system_accounts_offset: system_accounts_offset as u8, }; - // Serialize instruction data with discriminator let serialized_data = instruction_data.try_to_vec()?; let mut data = Vec::new(); data.extend_from_slice(discriminator); @@ -332,19 +279,7 @@ impl CompressibleInstruction { }) } - /// Build a `compress_accounts_idempotent` instruction for compressing multiple accounts (PDAs and token accounts). - /// - /// # Arguments - /// * `program_id` - Target program - /// * `discriminator` - The instruction discriminator bytes (flexible length) - /// * `account_pubkeys` - Accounts to compress (PDAs and token accounts) - /// * `accounts_to_compress` - Account data to compress - /// * `program_account_metas` - Program-specific accounts (assembled from Anchor accounts struct) - /// * `signer_seeds` - Signer seeds for each account (empty vec if no seeds needed) - /// * `validity_proof_with_context` - Validity proof with context - /// * `output_state_tree_info` - Output state tree info - /// - /// Returns `Ok(Instruction)` or error. + /// Builds compress_accounts_idempotent instruction for PDAs and token accounts #[allow(clippy::too_many_arguments)] pub fn compress_accounts_idempotent( program_id: &Pubkey, @@ -367,8 +302,6 @@ impl CompressibleInstruction { 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()); } - - // Sanity check for better error messages. for (i, account) in account_pubkeys.iter().enumerate() { if !signer_seeds.is_empty() { let seeds = &signer_seeds[i]; @@ -428,13 +361,11 @@ impl CompressibleInstruction { ); } - // Use program-provided account metas (from Anchor accounts struct) let mut accounts = program_account_metas.to_vec(); let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); accounts.extend(system_accounts); - // Accounts to compress must be at the end. for account in account_pubkeys { accounts.push(AccountMeta::new(*account, false)); } @@ -459,6 +390,4 @@ impl CompressibleInstruction { } } -/// Generic instruction data for decompress multiple PDAs -// Re-export for easy access following Solana SDK patterns pub use CompressibleInstruction as compressible_instruction; diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 53a6ccfc2b..64d4e2e1f4 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -177,7 +177,6 @@ pub async fn auto_compress_program_pdas( let payer = rpc.get_payer().insecure_clone(); - // Load program's compressible config to get rent_recipient let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; let cfg_acc = rpc .get_account(config_pda) @@ -185,29 +184,25 @@ pub async fn auto_compress_program_pdas( .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_recipient = cfg.rent_sponsor; + let rent_sponsor = cfg.rent_sponsor; + let address_tree = cfg.address_space[0]; - // Discover program PDAs (simple heuristic: all non-empty, funded program-owned accounts) let program_accounts = rpc.context.get_program_accounts(&program_id); if program_accounts.is_empty() { return Ok(()); } - // Address tree info and queue used to derive existing compressed addresses - let address_tree_info = rpc.get_address_tree_v2(); let output_state_tree_info = rpc .get_random_state_tree_info() .map_err(|e| RpcError::CustomError(format!("no state tree: {e:?}")))?; - // Prepare metas that are standard across programs let program_metas = vec![ AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(config_pda, false), - AccountMeta::new(rent_recipient, false), + AccountMeta::new(rent_sponsor, false), ]; - // Batch to keep instructions reasonably small - const BATCH_SIZE: usize = 10; + const BATCH_SIZE: usize = 5; let mut chunk = Vec::with_capacity(BATCH_SIZE); for (pubkey, account) in program_accounts .into_iter() @@ -220,7 +215,7 @@ pub async fn auto_compress_program_pdas( &program_id, &chunk, &program_metas, - &address_tree_info.queue, + &address_tree, output_state_tree_info, ) .await; @@ -234,7 +229,7 @@ pub async fn auto_compress_program_pdas( &program_id, &chunk, &program_metas, - &address_tree_info.queue, + &address_tree, output_state_tree_info, ) .await; @@ -249,7 +244,7 @@ async fn try_compress_chunk( program_id: &Pubkey, chunk: &[(Pubkey, solana_sdk::account::Account)], program_metas: &[solana_instruction::AccountMeta], - address_tree_queue: &Pubkey, + address_tree: &Pubkey, output_state_tree_info: light_client::indexer::TreeInfo, ) { use light_client::indexer::Indexer; @@ -257,15 +252,13 @@ async fn try_compress_chunk( use light_compressible_client::CompressibleInstruction; use solana_sdk::signature::Signer; - // Build account pubkeys and fetch compressed inputs for proof 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() { - // Derive existing compressed address and read hash; skip if not present let addr = derive_address( &pda.to_bytes(), - &address_tree_queue.to_bytes(), + &address_tree.to_bytes(), &program_id.to_bytes(), ); if let Ok(resp) = rpc.get_compressed_account(addr, None).await { @@ -280,16 +273,13 @@ async fn try_compress_chunk( return; } - // Get a single proof for all inputs in this batch let proof_with_context = match rpc.get_validity_proof(hashes, vec![], None).await { Ok(r) => r.value, Err(_) => return, }; - // Signer seeds: PDAs require no seeds for compression (program owns them). Provide empty groups. let signer_seeds: Vec>> = (0..pdas.len()).map(|_| Vec::new()).collect(); - // Build instruction; let program enforce slot-gating. Ignore failures. let ix_res = CompressibleInstruction::compress_accounts_idempotent( program_id, &CompressibleInstruction::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, @@ -302,7 +292,6 @@ async fn try_compress_chunk( ) .map_err(|e| e.to_string()); if let Ok(ix) = ix_res { - // Avoid double-borrow of rpc by cloning payer first let payer = rpc.get_payer().insecure_clone(); let payer_pubkey = payer.pubkey(); let _ = rpc diff --git a/sdk-libs/program-test/src/program_test/compressible_setup.rs b/sdk-libs/program-test/src/program_test/compressible_setup.rs index 37b1493dab..ed187d34a5 100644 --- a/sdk-libs/program-test/src/program_test/compressible_setup.rs +++ b/sdk-libs/program-test/src/program_test/compressible_setup.rs @@ -60,20 +60,17 @@ pub fn setup_mock_program_data( /// Initialize compression config for a program /// -/// This is a high-level helper that handles the complete flow of initializing -/// a compression configuration for a program, including proper signer management. -/// /// # 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_recipient` - Where to send rent from compressed accounts +/// * `rent_sponsor` - Where to send rent from compressed accounts /// * `address_space` - List of address trees for this program /// /// # Returns -/// Transaction signature on success +/// `Result` - The transaction signature #[allow(clippy::too_many_arguments)] pub async fn initialize_compression_config( rpc: &mut T, @@ -81,7 +78,7 @@ pub async fn initialize_compression_config( program_id: &Pubkey, authority: &Keypair, compression_delay: u32, - rent_recipient: Pubkey, + rent_sponsor: Pubkey, address_space: Vec, discriminator: &[u8], config_bump: Option, @@ -92,14 +89,13 @@ pub async fn initialize_compression_config( )); } - // Use the mid-level instruction builder let instruction = CompressibleInstruction::initialize_compression_config( program_id, discriminator, &payer.pubkey(), &authority.pubkey(), compression_delay, - rent_recipient, + rent_sponsor, address_space, config_bump, ); @@ -116,21 +112,18 @@ pub async fn initialize_compression_config( /// Update compression config for a program /// -/// This is a high-level helper for updating an existing compression configuration. -/// All parameters except the required ones are optional - pass None to keep existing values. -/// /// # 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_recipient` - New rent recipient (optional) +/// * `new_rent_sponsor` - New rent recipient (optional) /// * `new_address_space` - New address space list (optional) /// * `new_update_authority` - New authority (optional) /// /// # Returns -/// Transaction signature on success +/// `Result` - The transaction signature #[allow(clippy::too_many_arguments)] pub async fn update_compression_config( rpc: &mut T, @@ -138,18 +131,17 @@ pub async fn update_compression_config( program_id: &Pubkey, authority: &Keypair, new_compression_delay: Option, - new_rent_recipient: Option, + new_rent_sponsor: Option, new_address_space: Option>, new_update_authority: Option, discriminator: &[u8], ) -> Result { - // Use the mid-level instruction builder let instruction = CompressibleInstruction::update_compression_config( program_id, discriminator, &authority.pubkey(), new_compression_delay, - new_rent_recipient, + new_rent_sponsor, new_address_space, new_update_authority, ); diff --git a/sdk-libs/program-test/src/program_test/config.rs b/sdk-libs/program-test/src/program_test/config.rs index 6125dfd591..662431a453 100644 --- a/sdk-libs/program-test/src/program_test/config.rs +++ b/sdk-libs/program-test/src/program_test/config.rs @@ -20,7 +20,6 @@ pub struct ProgramTestConfig { #[cfg(feature = "devenv")] pub protocol_config: ProtocolConfig, pub with_prover: bool, - /// Automatically register additional programs for PDA auto-compression in tests #[cfg(feature = "devenv")] pub auto_register_custom_programs_for_pda_compression: bool, #[cfg(feature = "devenv")] 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 d2f395aac8..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 @@ -154,7 +154,6 @@ impl LightProgramTest { })?; } } - // Auto-register additional programs for PDA auto-compression unless opted out let (auto_register, additional_programs) = { let auto = context .config 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 73a700ccd1..09076e4d3c 100644 --- a/sdk-libs/program-test/src/program_test/test_rpc.rs +++ b/sdk-libs/program-test/src/program_test/test_rpc.rs @@ -157,7 +157,6 @@ 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?; - // Auto-compress registered custom program PDAs (mirror c-token auto flow) for program_id in self.auto_compress_programs.clone() { let _ = crate::compressible::auto_compress_program_pdas(self, program_id).await; } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 7ce211c93e..fbb0cea1aa 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -310,8 +310,6 @@ pub mod __internal { self.account.size() } - /// Remove the data from this account by setting it to default. - /// This is used when decompressing to ensure the compressed account is properly zeroed. pub fn remove_data(&mut self) { self.should_remove_data = true; } diff --git a/sdk-libs/sdk/src/compressible/close.rs b/sdk-libs/sdk/src/compressible/close.rs index e0dcf650c9..22edc1ffba 100644 --- a/sdk-libs/sdk/src/compressible/close.rs +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -13,11 +13,12 @@ pub fn close<'info>( .try_borrow_mut_lamports() .map_err(|_| LightSdkError::ConstraintViolation)? = 0; - let dest_lamports = sol_destination.lamports(); **sol_destination .try_borrow_mut_lamports() - .map_err(|_| LightSdkError::ConstraintViolation)? = - dest_lamports.checked_add(lamports_to_transfer).unwrap(); + .map_err(|_| LightSdkError::ConstraintViolation)? = sol_destination + .lamports() + .checked_add(lamports_to_transfer) + .ok_or(LightSdkError::ConstraintViolation)?; let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); diff --git a/sdk-libs/sdk/src/compressible/compression_info.rs b/sdk-libs/sdk/src/compressible/compression_info.rs index 9b3c472d46..3027385bd7 100644 --- a/sdk-libs/sdk/src/compressible/compression_info.rs +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -15,7 +15,6 @@ pub trait Pack { fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; } -/// Convert indices back to Pubkeys using remaining_accounts. pub trait Unpack { type Unpacked; @@ -54,11 +53,9 @@ pub trait CompressAs { + Default + Clone; - /// Return data to store. compression_info must be None. fn compress_as(&self) -> Cow<'_, Self::Output>; } -/// Last write slot and compression state. #[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)] pub struct CompressionInfo { pub last_written_slot: u64, @@ -103,7 +100,6 @@ impl CompressionInfo { } } -/// Space calculation without anchor (like anchor_lang::Space but standalone). pub trait Space { const INIT_SPACE: usize; } diff --git a/sdk-libs/sdk/src/compressible/config.rs b/sdk-libs/sdk/src/compressible/config.rs index 0e09b9180f..f246eb1cf3 100644 --- a/sdk-libs/sdk/src/compressible/config.rs +++ b/sdk-libs/sdk/src/compressible/config.rs @@ -26,8 +26,8 @@ pub struct CompressibleConfig { /// Authority that can update the config pub update_authority: Pubkey, /// Account that receives rent from compressed PDAs - pub rent_recipient: Pubkey, - /// Config bump seed (currently always 0)å + pub rent_sponsor: Pubkey, + /// Config bump seed (0) pub config_bump: u8, /// PDA bump seed pub bump: u8, @@ -130,7 +130,7 @@ impl CompressibleConfig { /// # Arguments /// * `config_account` - The config PDA account to initialize /// * `update_authority` - Authority that can update the config after creation -/// * `rent_recipient` - Account that receives rent from compressed PDAs +/// * `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) @@ -150,7 +150,7 @@ impl CompressibleConfig { pub fn process_initialize_compression_config_account_info<'info>( config_account: &AccountInfo<'info>, update_authority: &AccountInfo<'info>, - rent_recipient: &Pubkey, + rent_sponsor: &Pubkey, address_space: Vec, compression_delay: u32, config_bump: u8, @@ -223,7 +223,7 @@ pub fn process_initialize_compression_config_account_info<'info>( version: 1, compression_delay, update_authority: *update_authority.key, - rent_recipient: *rent_recipient, + rent_sponsor: *rent_sponsor, config_bump, address_space, bump, @@ -245,7 +245,7 @@ pub fn process_initialize_compression_config_account_info<'info>( /// * `config_account` - The config PDA account to update /// * `authority` - Current update authority (must match config) /// * `new_update_authority` - Optional new update authority -/// * `new_rent_recipient` - Optional new rent recipient +/// * `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 @@ -257,7 +257,7 @@ pub fn process_update_compression_config<'info>( config_account: &AccountInfo<'info>, authority: &AccountInfo<'info>, new_update_authority: Option<&Pubkey>, - new_rent_recipient: Option<&Pubkey>, + new_rent_sponsor: Option<&Pubkey>, new_address_space: Option>, new_compression_delay: Option, owner_program_id: &Pubkey, @@ -279,8 +279,8 @@ pub fn process_update_compression_config<'info>( if let Some(new_authority) = new_update_authority { config.update_authority = *new_authority; } - if let Some(new_recipient) = new_rent_recipient { - config.rent_recipient = *new_recipient; + 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 @@ -314,7 +314,7 @@ pub fn process_update_compression_config<'info>( Ok(()) } -/// Verifies that the signer is the program's upgrade authority +/// Checks that the signer is the program's upgrade authority /// /// # Arguments /// * `program_id` - The program to check @@ -389,19 +389,13 @@ pub fn check_program_upgrade_authority( Ok(()) } -/// Creates a new compressible config PDA with program upgrade authority -/// validation -/// -/// # Security -/// This function verifies that the signer is the program's upgrade authority -/// before creating the config. This ensures only the program deployer can -/// initialize the configuration. +/// 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_recipient` - Account that receives rent from compressed PDAs +/// * `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 @@ -418,7 +412,7 @@ pub fn process_initialize_compression_config_checked<'info>( config_account: &AccountInfo<'info>, update_authority: &AccountInfo<'info>, program_data_account: &AccountInfo<'info>, - rent_recipient: &Pubkey, + rent_sponsor: &Pubkey, address_space: Vec, compression_delay: u32, config_bump: u8, @@ -441,7 +435,7 @@ pub fn process_initialize_compression_config_checked<'info>( process_initialize_compression_config_account_info( config_account, update_authority, - rent_recipient, + rent_sponsor, address_space, compression_delay, config_bump, diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 0ebe09dc08..91b0bf479d 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -1,6 +1,5 @@ pub use light_compressed_account::LightInstructionData; use light_sdk_types::constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}; -use solana_msg::msg; #[cfg(feature = "cpi-context")] use crate::AccountMeta; @@ -75,7 +74,6 @@ where let account_infos = accounts.to_account_infos(); let account_metas = accounts.to_account_metas()?; - msg!("invoking LightSystemProgramCpi, {:?}", account_metas); let instruction = Instruction { program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), accounts: account_metas, diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index a2d4642b3d..47f10abd7a 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -1,69 +1,25 @@ -//! The base library to use Compressed Accounts in Solana on-chain Rust and Anchor programs. -//! -//! Compressed Accounts stores state as account hashes in State Merkle trees. -//! and unique addresses in Address Merkle trees. -//! Validity proofs (zero-knowledge proofs) verify that compressed account -//! state exists and new addresses do not exist yet. -//! -//! - No rent exemption payment required. -//! - Constant 128-byte validity proof per transaction for one or multiple compressed accounts and addresses. -//! - Compressed account data is sent as instruction data when accessed. -//! - State and address trees are managed by the protocol. -//! -//! For full program examples, see the [Program Examples](https://github.com/Lightprotocol/program-examples). -//! For detailed documentation, visit [zkcompression.com](https://www.zkcompression.com/). -//! For pinocchio solana program development see [`light-sdk-pinocchio`](https://docs.rs/light-sdk-pinocchio). -//! For rust client developement see [`light-client`](https://docs.rs/light-client). -//! For rust program testing see [`light-program-test`](https://docs.rs/light-program-test). -//! For local test validator with light system programs see [Light CLI](https://www.npmjs.com/package/@lightprotocol/zk-compression-cli). -//! -//! # Using Compressed Accounts in Solana Programs -//! 1. [`Instruction`](crate::instruction) -//! - [`CompressedAccountMeta`](crate::instruction::account_meta::CompressedAccountMeta) - Compressed account metadata structs for instruction data. -//! - [`PackedAccounts`](crate::instruction::PackedAccounts) - Abstraction to prepare accounts offchain for instructions with compressed accounts. -//! - [`ValidityProof`](crate::instruction::ValidityProof) - Proves that new addresses don't exist yet, and compressed account state exists. -//! 2. Compressed Account in Program -//! - [`LightAccount`](crate::account) - Compressed account abstraction similar to anchor Account. -//! - [`derive_address`](crate::address) - Create a compressed account address. -//! - [`LightDiscriminator`] - DeriveMacro to derive a compressed account discriminator. -//! 3. [`Cpi`](crate::cpi) -//! - [`CpiAccounts`](crate::cpi::v1::CpiAccounts) - Prepare accounts to cpi the light system program. -//! - [`LightSystemProgramCpi`](crate::cpi::v1::LightSystemProgramCpi) - Prepare instruction data to cpi the light system program. -//! - [`InvokeLightSystemProgram::invoke`](crate::cpi) - Invoke the light system program via cpi. -//! -//! ```text -//! ├─ 𝐂𝐥𝐢𝐞𝐧𝐭 -//! │ ├─ Get ValidityProof from RPC. -//! │ ├─ pack accounts with PackedAccounts into PackedAddressTreeInfo and PackedStateTreeInfo. -//! │ ├─ pack CompressedAccountMeta. -//! │ ├─ Build Instruction from PackedAccounts and CompressedAccountMetas. -//! │ └─ Send transaction. -//! │ -//! └─ 𝐂𝐮𝐬𝐭𝐨𝐦 𝐏𝐫𝐨𝐠𝐫𝐚𝐦 -//! ├─ CpiAccounts parse accounts consistent with PackedAccounts. -//! ├─ LightAccount instantiates from CompressedAccountMeta. -//! │ -//! └─ 𝐋𝐢𝐠𝐡𝐭 𝐒𝐲𝐬𝐭𝐞𝐦 𝐏𝐫𝐨𝐠𝐫𝐚𝐦 𝐂𝐏𝐈 -//! ├─ Verify ValidityProof. -//! ├─ Update State Merkle tree. -//! ├─ Update Address Merkle tree. -//! └─ Complete atomic state transition. -//! ``` +//! SDK for building programs with compressed accounts on Solana. //! -//! # Features -//! 1. `anchor` - Derives AnchorSerialize, AnchorDeserialize instead of BorshSerialize, BorshDeserialize. +//! State is stored as hashes in Merkle trees. Validity proofs verify state exists. +//! No rent required. Constant 128-byte proof per transaction. +//! +//! See [zkcompression.com](https://www.zkcompression.com/) for docs and [Program Examples](https://github.com/Lightprotocol/program-examples). //! -//! 2. `v2` -//! - available on devnet, localnet, and light-program-test. -//! - Support for optimized v2 light system program instructions. +//! Related crates: +//! - [`light-sdk-pinocchio`](https://docs.rs/light-sdk-pinocchio) - Pinocchio programs +//! - [`light-client`](https://docs.rs/light-client) - Client development +//! - [`light-program-test`](https://docs.rs/light-program-test) - Testing //! -//! 3. `cpi-context` - Enables CPI context operations for batched compressed account operations. -//! - available on devnet, localnet, and light-program-test. -//! - Enables the use of one validity proof across multiple cpis from different programs in one instruction. -//! - For example spending compressed tokens (owned by the ctoken program) and updating a compressed pda (owned by a custom program) -//! with one validity proof. -//! - An instruction should not use more than one validity proof. -//! - Requires the v2 feature. +//! # Main modules +//! - [`instruction`] - Build instructions with compressed accounts +//! - [`account`] - LightAccount abstraction +//! - [`address`] - Derive compressed addresses +//! - [`cpi`] - CPI to light system program +//! +//! # Features +//! - `anchor` - Use AnchorSerialize/AnchorDeserialize +//! - `v2` - Optimized v2 instructions (devnet, localnet) +//! - `cpi-context` - Share one validity proof across multiple CPIs (requires v2) //! //! ### Example Solana program code to create a compressed account //! ```rust, compile_fail @@ -135,20 +91,15 @@ //!} //! ``` -/// Compressed account abstraction similar to anchor Account. pub mod account; pub use account::sha::LightAccount; -/// Functions to derive compressed account addresses. pub mod address; -/// Utilities to invoke the light-system-program via cpi. pub mod cpi; pub mod error; -/// Utilities to build instructions for programs with compressed accounts. pub mod instruction; pub mod legacy; pub mod token; -/// Transfer compressed sol between compressed accounts. pub mod transfer; pub mod utils; diff --git a/sdk-libs/sdk/src/token.rs b/sdk-libs/sdk/src/token.rs index 22db434f66..c02808893f 100644 --- a/sdk-libs/sdk/src/token.rs +++ b/sdk-libs/sdk/src/token.rs @@ -63,7 +63,7 @@ impl TokenDataWithMerkleContext { } } -/// Implementation for TokenData - packs into InputTokenDataCompressible +/// Standard Pack for c-token accounts. impl Pack for TokenData { type Packed = InputTokenDataCompressible; @@ -94,7 +94,7 @@ impl Unpack for TokenData { } } -/// Unpack implementation for InputTokenDataCompressible +/// Standard Unpack for c-token accounts. impl Unpack for InputTokenDataCompressible { type Unpacked = TokenData; @@ -128,8 +128,7 @@ impl Unpack for InputTokenDataCompressible { } } -/// Wrapper for token data with variant information -/// The variant is user-defined and doesn't get altered during packing +/// Wrapper for token data with variant information. #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] pub struct TokenDataWithVariant { pub variant: V, @@ -147,7 +146,7 @@ pub struct CTokenDataWithVariant { pub token_data: TokenData, } -/// Pack implementation for CTokenDataWithVariant +/// Standard Pack for c-token accounts with variant. impl Pack for CTokenDataWithVariant where V: AnchorSerialize + Clone + std::fmt::Debug, @@ -162,7 +161,7 @@ where } } -/// Unpack implementation for CTokenDataWithVariant +/// Standard Unpack for c-token accounts with variant. impl Unpack for CTokenDataWithVariant where V: Clone, @@ -180,7 +179,7 @@ where } } -/// Pack implementation for TokenDataWithVariant +/// Standard Pack for token data with variant. impl Pack for TokenDataWithVariant where V: AnchorSerialize + Clone + std::fmt::Debug, @@ -195,7 +194,7 @@ where } } -/// Unpack implementation for PackedCTokenDataWithVariant +/// Standard Unpack for c-token accounts with variant. impl Unpack for PackedCTokenDataWithVariant where V: Clone, @@ -213,8 +212,7 @@ where } } -// custom replacement for MultiInputTokenDataWithContext -// without root_index and without merkle_context +/// Standard Pack for c-token accounts with variant. #[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize, Default)] pub struct InputTokenDataCompressible { pub owner: u8, @@ -228,10 +226,7 @@ pub struct InputTokenDataCompressible { // TODO: remove these and fix renaming after we're done with ci. #[deprecated(since = "0.2.0", note = "Use `CTokenDataWithVariant` instead")] pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; - #[deprecated(since = "0.2.0", note = "Use `PackedCTokenDataWithVariant` instead")] pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; - -// Shorter aliases for convenience pub type CTokenData = CTokenDataWithVariant; pub type PackedCTokenData = PackedCTokenDataWithVariant; diff --git a/sdk-libs/token-client/src/actions/ctoken_transfer.rs b/sdk-libs/token-client/src/actions/ctoken_transfer.rs index f1d8d6b0fa..24e813b914 100644 --- a/sdk-libs/token-client/src/actions/ctoken_transfer.rs +++ b/sdk-libs/token-client/src/actions/ctoken_transfer.rs @@ -6,8 +6,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 @@ -40,8 +39,6 @@ pub async fn transfer_ctoken( } /// Create a ctoken transfer instruction. -/// This creates an instruction that uses discriminator 3 (CTokenTransfer) to perform -/// SPL token transfers on ctoken accounts. /// /// # Arguments /// * `source` - Source token account @@ -64,7 +61,7 @@ pub fn create_transfer_ctoken_instruction( 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) - // TODO: try to remove this + // TODO: try to remove this so we can reuse this from the compressed-token-sdk AccountMeta::new_readonly(Pubkey::default(), false), // System program for CPI transfers ], data: { 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 00e89e2a26..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 @@ -13,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_transfer_spl_to_ctoken_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, @@ -36,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? @@ -47,10 +32,8 @@ 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_transfer_spl_to_ctoken_instruction( source_spl_token_account, to, @@ -64,13 +47,11 @@ pub async fn spl_to_ctoken_transfer( ) .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/mint_to_compressed.rs b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs index 11fa87ab2d..b49a1e49ec 100644 --- a/sdk-libs/token-client/src/instructions/mint_to_compressed.rs +++ b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs @@ -5,8 +5,8 @@ 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, derive_token_pool, + DecompressedMintConfig, MintToCompressedInputs, }, token_pool::find_token_pool_pda_with_index, }; @@ -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 531b8bd342..b39e53f709 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -9,7 +9,6 @@ pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); pub mod ctoken { - use light_compressed_account::address::derive_address; use light_compressed_token_sdk::POOL_SEED; use solana_pubkey::Pubkey; @@ -45,31 +44,9 @@ pub mod ctoken { ) } - pub const CTOKEN_MINT_SEED: &[u8] = &[ - // b"compressed_mint" - 99, 111, 109, 112, 114, 101, 115, 115, 101, 100, 95, 109, 105, 110, 116, - ]; - - /// Derives the cToken program mint PDA from the provided signer pubkey (keypair or PDA). - /// The signer must sign when creating the SPL mint PDA on-chain. - pub fn find_mint_address(signer: Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[CTOKEN_MINT_SEED, signer.to_bytes().as_ref()], &ID) - } - - pub fn derive_compressed_address(mint: Pubkey, address_tree: &Pubkey) -> [u8; 32] { - derive_address(&mint.to_bytes(), &address_tree.to_bytes(), &ID.to_bytes()) - } - - /// Comprehensive helper that derives all addresses from a signer in one call - /// Returns: (mint_address, mint_bump, compressed_address) - pub fn derive_compressed_address_from_mint_signer( - signer: Pubkey, - address_tree: &Pubkey, - ) -> (Pubkey, u8, [u8; 32]) { - let (mint_address, mint_bump) = find_mint_address(signer); - let compressed_address = derive_compressed_address(mint_address, address_tree); - (mint_address, mint_bump, compressed_address) - } + pub use light_compressed_token_sdk::instructions::{ + create_compressed_mint::find_spl_mint_address, derive_cmint_from_spl_mint, + }; pub fn derive_ctoken_program_config(_version: Option) -> (Pubkey, u8) { let version = 1u16; @@ -79,13 +56,11 @@ pub mod ctoken { &[b"compressible_config", &version.to_le_bytes()], ®istry_program_id, ); - println!("compressible_config: {:?}", compressible_config_pda); (compressible_config_pda, config_bump) } + // TODO: add version. pub fn derive_ctoken_rent_sponsor(_version: Option) -> (Pubkey, u8) { - // Derive the rent_recipient PDA - // let version = version.unwrap_or(1); let version = 1u16; Pubkey::find_program_address( &[b"rent_sponsor".as_slice(), version.to_le_bytes().as_slice()], @@ -105,66 +80,4 @@ pub mod ctoken { ); (compression_authority, compression_authority_bump) } - - // /// Derives the SPL mint PDA from a signer keypair - // /// - // /// # Arguments - // /// * `signer` - The signer pubkey used as seed - // /// - // /// # Returns - // /// Tuple of (mint_pda, bump_seed) - // /// Derives the Compressed Token Program mint PDA from a signer pubkey. - // /// - // /// This derives the cToken program mint PDA for a given keypair or PDA; that signer must sign. - // pub fn find_mint_address(signer: &Pubkey) -> (Pubkey, u8) { - // sdk_find_mint_address(signer) - // } - - // /// Derives the compressed address from a mint PDA and address tree - // /// - // /// # Arguments - // /// * `mint` - The mint PDA - // /// * `address_tree` - The address tree pubkey - // /// - // /// # Returns - // /// The compressed address as [u8; 32] - // pub fn derive_compressed_address(mint: &Pubkey, address_tree: &Pubkey) -> [u8; 32] { - // sdk_derive_address( - // &mint.to_bytes(), - // &address_tree.to_bytes(), - // &super::ctoken::ID.to_bytes(), - // ) - // } - - // /// Comprehensive helper that derives all addresses from a signer in one call - // /// - // /// This is the main function you should use for mint derivation. - // /// - // /// # Arguments - // /// * `signer` - The signer keypair pubkey - // /// * `address_tree` - The address tree pubkey - // /// - // /// # Returns - // /// Tuple of (mint_address, mint_bump, compressed_address) - // /// - // /// # Example - // /// ```rust - // /// use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; - // /// use light_token_client::utils::derive_compressed_address_from_signer; - // /// - // /// let signer = Keypair::new(); - // /// let address_tree = Pubkey::new_unique(); - // /// let (mint_pda, mint_bump, compressed_address) = - // /// derive_compressed_address_from_signer(&signer.pubkey(), &address_tree); - // /// - // /// println!("Mint PDA: {}", mint_pda); - // /// println!("Mint Bump: {}", mint_bump); - // /// println!("Compressed Address: {:?}", compressed_address); - // /// ``` - // pub fn derive_compressed_address_from_mint_signer( - // signer: &Pubkey, - // address_tree: &Pubkey, - // ) -> (Pubkey, u8, [u8; 32]) { - // sdk_derive_compressed_address_from_mint_signer(signer, address_tree) - // } } diff --git a/sdk-tests/csdk-anchor-test/Anchor.toml b/sdk-tests/csdk-anchor-test/Anchor.toml new file mode 100644 index 0000000000..5f10ee82ca --- /dev/null +++ b/sdk-tests/csdk-anchor-test/Anchor.toml @@ -0,0 +1,19 @@ +[toolchain] + +[features] +resolution = true +skip-lint = false + +[programs.localnet] +csdk_anchor_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/csdk-anchor-test/package.json b/sdk-tests/csdk-anchor-test/package.json new file mode 100644 index 0000000000..74a7ddd3e0 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@lightprotocol/csdk-anchor-test", + "version": "0.1.0", + "license": "Apache-2.0", + "scripts": { + "build": "cargo build-sbf", + "test": "cargo test-sbf -- --nocapture" + }, + "nx": {} +} + diff --git a/sdk-tests/csdk-anchor-test/src/constants.rs b/sdk-tests/csdk-anchor-test/src/constants.rs new file mode 100644 index 0000000000..2fcae0aa3a --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/errors.rs b/sdk-tests/csdk-anchor-test/src/errors.rs new file mode 100644 index 0000000000..c9b2f73fc5 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/src/errors.rs @@ -0,0 +1,81 @@ +use anchor_lang::prelude::*; + +#[repr(u32)] +pub enum ErrorCode { + InvalidAccountCount, + InvalidRentRecipient, + MintCreationFailed, + MissingCompressedTokenProgram, + MissingCompressedTokenProgramAuthorityPDA, + RentRecipientMismatch, + InvalidAccountDiscriminator, + DerivedTokenAccountMismatch, +} + +#[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", + }, + ) + } +} + +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" + )), + } + } +} + +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/csdk-anchor-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-test/src/instruction_accounts.rs new file mode 100644 index 0000000000..560219358d --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rs b/sdk-tests/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rs new file mode 100644 index 0000000000..1b2a77f7a0 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/create_game_session.rs b/sdk-tests/csdk-anchor-test/src/instructions/create_game_session.rs new file mode 100644 index 0000000000..e6fcd68e13 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/create_placeholder_record.rs b/sdk-tests/csdk-anchor-test/src/instructions/create_placeholder_record.rs new file mode 100644 index 0000000000..b8ee6fbe06 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/create_record.rs b/sdk-tests/csdk-anchor-test/src/instructions/create_record.rs new file mode 100644 index 0000000000..eb50dfc649 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rs b/sdk-tests/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rs new file mode 100644 index 0000000000..2d82dc0884 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs new file mode 100644 index 0000000000..9e397aa6a9 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs @@ -0,0 +1,418 @@ +// 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_sdk::token::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().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + + 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_sdk::token::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 && has_pdas { + 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(); + let authority = cpi_accounts.authority().unwrap(); + let cpi_context = cpi_accounts.cpi_context().unwrap(); + + // init PDAs. + if has_pdas && has_tokens { + 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/csdk-anchor-test/src/instructions/initialize_compression_config.rs b/sdk-tests/csdk-anchor-test/src/instructions/initialize_compression_config.rs new file mode 100644 index 0000000000..ec1f8f046e --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/mod.rs b/sdk-tests/csdk-anchor-test/src/instructions/mod.rs new file mode 100644 index 0000000000..5c7bd74407 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/update_compression_config.rs b/sdk-tests/csdk-anchor-test/src/instructions/update_compression_config.rs new file mode 100644 index 0000000000..c0e96f83f2 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/update_game_session.rs b/sdk-tests/csdk-anchor-test/src/instructions/update_game_session.rs new file mode 100644 index 0000000000..25b45b4528 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/instructions/update_record.rs b/sdk-tests/csdk-anchor-test/src/instructions/update_record.rs new file mode 100644 index 0000000000..6b8c7cb618 --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/lib.rs b/sdk-tests/csdk-anchor-test/src/lib.rs index 5ed3b9f121..152048862c 100644 --- a/sdk-tests/csdk-anchor-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-test/src/lib.rs @@ -1,960 +1,39 @@ #![allow(deprecated)] -use anchor_lang::{ - prelude::*, - solana_program::{instruction::AccountMeta, program::invoke, pubkey::Pubkey}, +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, }; -use anchor_spl::token_interface::TokenAccount; -use light_compressed_token_sdk::instructions::{create_mint_action_cpi, MintActionInputs}; -use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; -use light_sdk::{ - account::Size, - compressible::{ - compress_account::prepare_account_for_compression, - compress_account_on_init::prepare_compressed_account_on_init, - decompress_idempotent::{ - into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, - }, - process_initialize_compression_config_checked, process_update_compression_config, - CompressAs, CompressedInitSpace, CompressibleConfig, CompressionInfo, HasCompressionInfo, - Pack, Unpack, - }, - derive_light_cpi_signer, - instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, - PackedAddressTreeInfo, ValidityProof, - }, - LightDiscriminator, LightHasher, -}; -use light_sdk_types::{cpi_accounts::CpiAccountsConfig, CpiSigner}; - -pub const POOL_VAULT_SEED: &str = "pool_vault"; -pub const USER_RECORD_SEED: &str = "user_record"; -pub const CTOKEN_SIGNER_SEED: &str = "ctoken_signer"; -#[repr(u32)] -pub enum ErrorCode { - InvalidAccountCount, - InvalidRentRecipient, - MintCreationFailed, - MissingCompressedTokenProgram, - MissingCompressedTokenProgramAuthorityPDA, -} -#[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" - } - }, - ) - } -} - -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", - )), - } - } -} -// extern crate alloc; -#[repr(u32)] -/// Auto-generated error codes for compressible instructions -/// These are separate from the user's ErrorCode enum to avoid conflicts -pub enum CompressibleInstructionError { - InvalidRentRecipient, - CTokenDecompressionNotImplemented, - PdaDecompressionNotImplemented, - TokenCompressionNotImplemented, - PdaCompressionNotImplemented, -} -// Auto-generated client-side seed function -pub fn get_userrecord_seeds(owner: &Pubkey) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} -/// Auto-generated client-side seed function -pub fn get_gamesession_seeds(session_id: u64) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} -/// Auto-generated client-side seed function -pub fn get_placeholderrecord_seeds( - placeholder_id: u64, -) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} -/// Auto-generated client-side CToken seed function -pub fn get_ctokensigner_seeds( - fee_payer: &anchor_lang::prelude::Pubkey, - some_mint: &anchor_lang::prelude::Pubkey, -) -> (Vec>, anchor_lang::prelude::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) = anchor_lang::prelude::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} -/// Trait-based system for generic CToken variant seed handling -/// Users implement this trait for their CTokenAccountVariant enum -pub mod ctoken_seed_system { - use super::*; - /// Context struct providing access to ALL instruction accounts - /// This gives users access to any account in the instruction context - pub struct CTokenSeedContext<'a, 'info> { - pub accounts: &'a DecompressAccountsIdempotent<'info>, - pub remaining_accounts: &'a [anchor_lang::prelude::AccountInfo<'info>], - } - /// Trait that CToken variants implement to provide seed derivation - /// Completely extensible - users can implement ANY seed logic with access to ALL accounts - pub trait CTokenSeedProvider { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> (Vec>, Pubkey); - } -} -/// Auto-generated CTokenSeedProvider implementation -impl ctoken_seed_system::CTokenSeedProvider for CTokenAccountVariant { - fn get_seeds<'a, 'info>( - &self, - ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, - ) -> (Vec>, anchor_lang::prelude::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) = - anchor_lang::prelude::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) = - anchor_lang::prelude::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) = - anchor_lang::prelude::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(); // Use fee_payer as second account - let program_id_bytes = crate::ID.to_bytes(); - let seeds: &[&[u8]] = &[b"multi_account", &seed_1, &seed_2, &program_id_bytes]; - let (pda, bump) = - anchor_lang::prelude::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(); // Fixed index for this variant - let seeds: &[&[u8]] = &[b"indexed_vault", &seed_1, &seed_2, &index_bytes, b"final"]; - let (pda, bump) = - anchor_lang::prelude::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) - } - } - } -} +pub use seeds::*; +pub use state::*; declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -// CToken signer 1: Classic pattern with user + mint -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) -} - -// CToken signer 2: Simple user vault pattern -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) -} - -// CToken signer 3: Pool vault pattern with constant seed -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) -} - -// Authority seeds for ctoken operations: Light CPI signer PDA derived from ("cpi_authority", program_id) -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) { - // Same authority PDA as above; separate helper keeps parity with variant naming - get_ctokensigner_authority_seeds() -} - -pub fn get_ctokensigner3_authority_seeds() -> (Vec>, Pubkey) { - // Same authority PDA as above; separate helper keeps parity with variant naming - get_ctokensigner_authority_seeds() -} - -// CToken signer 4: Multi-account pattern with user + fee_payer + program_id -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) -} - -// CToken signer 5: Complex pattern with user + mint + numeric index + extra seed -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) { - // Same authority PDA as above; separate helper keeps parity with variant naming - get_ctokensigner_authority_seeds() -} - -pub fn get_ctokensigner5_authority_seeds() -> (Vec>, Pubkey) { - // Same authority PDA as above; separate helper keeps parity with variant naming - get_ctokensigner_authority_seeds() -} - -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -#[repr(u8)] -pub enum CTokenAccountVariant { - CTokenSigner = 0, - CTokenSigner2 = 1, - CTokenSigner3 = 2, - CTokenSigner4 = 3, - CTokenSigner5 = 4, -} - #[program] pub mod csdk_anchor_test { - - use light_compressed_token_sdk::instructions::{ - create_token_account::create_ctoken_account_signed, find_mint_address, + use light_sdk::instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, + ValidityProof, }; - use light_sdk::cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }; - use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; use super::*; - // auto-derived via macro. - pub fn initialize_compression_config( - ctx: Context, - compression_delay: u32, - rent_recipient: 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_recipient, - 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(()) - } - - // auto-derived via macro. - pub fn update_compression_config( - ctx: Context, - new_compression_delay: Option, - new_rent_recipient: 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_recipient.as_ref(), - new_address_space, - new_compression_delay, - &crate::ID, - )?; - - Ok(()) - } - - /// Compress multiple accounts (PDAs and token accounts) in a single instruction. - 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_recipient.key() != compression_config.rent_recipient { - msg!( - "rent recipient passed: {:?}", - ctx.accounts.rent_recipient.key() - ); - msg!( - "rent recipient config: {:?}", - compression_config.rent_recipient - ); - panic!("Rent recipient does not match config"); - } - - 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::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::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::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); - } - _ => { - panic!("Trying to compress with invalid account discriminator"); - } - } - } - } - let has_pdas = !compressed_pda_infos.is_empty(); - if has_pdas { - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_account_infos(&compressed_pda_infos) - // .write_to_cpi_context_first() - // .invoke_write_to_cpi_context_first( - // light_sdk_types::cpi_context_write::CpiContextWriteAccounts { - // fee_payer: cpi_accounts.fee_payer(), - // authority: cpi_accounts.authority().unwrap(), - // cpi_context: cpi_accounts.cpi_context().unwrap(), - // cpi_signer: LIGHT_CPI_SIGNER, - // }, - // )?; - .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_recipient.clone(), - ) - .map_err(anchor_lang::prelude::ProgramError::from)?; - } - } - Ok(()) - } - - // auto-derived via macro. takes the tagged account structs via - // add_compressible_accounts macro and derives the relevant variant type and - // dispatcher. The instruction can be used with any number of any of the - // tagged account structs. It's idempotent; it will not fail if the accounts - // are already decompressed. - #[inline(never)] - 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, - ) -> 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| 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, - ) -> 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| 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, - ) -> 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| 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_sdk::token::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::ctoken_seed_system::{CTokenSeedContext, CTokenSeedProvider}; - let seed_context = CTokenSeedContext { - accounts, - remaining_accounts, - }; - let authority = cpi_accounts.authority().unwrap(); - let cpi_context = cpi_accounts.cpi_context().unwrap(); - - 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); - panic!("Derived token account address must match owner_info.key"); - } - - // 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 - )?; - } - // let decompress_index = - // light_compressed_token_sdk::instructions::DecompressFullIndices::from(( - // token_data.token_data, - // meta, - // owner_index, - // )); - // 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); - } - - // log each token account to decompress - // for token_account in token_decompress_indices.clone().into_iter() { - // msg!( - // "token_account: {:?}", - // packed_accounts[token_account.destination_index as usize].key() - // ); - // } - 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_sdk::token::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 && has_pdas { - 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(); - let authority = cpi_accounts.authority().unwrap(); - let cpi_context = cpi_accounts.cpi_context().unwrap(); - - // init PDAs. - if has_pdas && has_tokens { - 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(()) - } - pub fn create_record<'info>( ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, name: String, @@ -963,54 +42,36 @@ pub mod csdk_anchor_test { 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_recipient.key() != config.rent_recipient { - panic!("Rent recipient does not match config"); - // return err!(ErrorCode::InvalidRentRecipient); - } - - // 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, + instructions::create_record::create_record( + ctx, + name, + proof, compressed_address, - new_address_params, + address_tree_info, 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_recipient.to_account_info())?; + 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, + ) + } - Ok(()) + 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) } - // Must be manually implemented. pub fn create_game_session<'info>( ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, session_id: u64, @@ -1020,248 +81,35 @@ pub mod csdk_anchor_test { 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_recipient.key() != config.rent_recipient { - panic!("Rent recipient does not match config"); - } - - // 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, + instructions::create_game_session::create_game_session( + ctx, + session_id, + game_type, + proof, compressed_address, - new_address_params, + address_tree_info, 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_recipient.to_account_info())?; - - Ok(()) + ) } - // Must be manually implemented. - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, + pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, ) -> 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_recipient.key() != config.rent_recipient { - panic!("Rent recipient does not match config"); - } - - // 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_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), + instructions::initialize_compression_config::initialize_compression_config( + ctx, + compression_delay, + rent_sponsor, + address_space, ) - .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_recipient.to_account_info())?; - game_session.close(ctx.accounts.rent_recipient.to_account_info())?; + } - Ok(()) + pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + instructions::update_record::update_record(ctx, name, score) } - /// Creates an empty compressed account while keeping the PDA intact. - /// This demonstrates the compress_empty_account_on_init functionality. pub fn create_placeholder_record<'info>( ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, placeholder_id: u64, @@ -1271,956 +119,60 @@ pub mod csdk_anchor_test { 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; - - // Initialize compression_info for the PDA - *placeholder_record.compression_info_mut_opt() = - Some(super::CompressionInfo::new_decompressed()?); - placeholder_record - .compression_info_mut() - .bump_last_written_slot()?; - - // Verify rent recipient matches config - if ctx.accounts.rent_recipient.key() != config.rent_recipient { - panic!("Rent recipient does not match config"); - } - - // 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), - ); - - msg!("compressing account on init (keeps PDA open for demo)"); - - 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, + instructions::create_placeholder_record::create_placeholder_record( + ctx, + placeholder_id, + name, + proof, compressed_address, - new_address_params, + address_tree_info, 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)?; - - msg!("...compressed account on init"); - - // Note: PDA is NOT closed in this example (compression_info is set, account remains) - Ok(()) - } - - 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(()) + ) } - pub fn update_game_session( - ctx: Context, - _session_id: u64, - new_score: u64, + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, ) -> 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(()) - } -} - -#[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_recipient: 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_recipient: 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_recipient: 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_recipient: 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 CompressRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - mut, - seeds = [b"user_record", user.key().as_ref()], - bump, - constraint = pda_to_compress.owner == user.key() - )] - pub pda_to_compress: Account<'info, UserRecord>, - // 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_recipient: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(session_id: u64)] -pub struct CompressGameSession<'info> { - #[account(mut)] - pub player: Signer<'info>, - #[account( - mut, - seeds = [b"game_session", session_id.to_le_bytes().as_ref()], - bump, - constraint = pda_to_compress.player == player.key() - )] - pub pda_to_compress: Account<'info, GameSession>, - /// 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_recipient: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct CompressPlaceholderRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - mut, - constraint = pda_to_compress.owner == user.key() - )] - pub pda_to_compress: Account<'info, PlaceholderRecord>, - /// 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_recipient: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct CompressTokenAccountCtokenSigner<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - pub rent_authority: Signer<'info>, - /// CHECK: todo - pub user: UncheckedAccount<'info>, - /// CHECK: todo - ctoken_cpi_authority: UncheckedAccount<'info>, - /// CHECK: todo - ctoken_program: UncheckedAccount<'info>, - - #[account( - mut, - seeds = [b"ctoken_signer", user.key().as_ref(), token_account_to_compress.mint.as_ref()], - bump, - )] - pub token_account_to_compress: InterfaceAccount<'info, TokenAccount>, - /// 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_recipient: AccountInfo<'info>, -} - -#[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_recipient: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct CompressMultipleTokenAccounts<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// The authority that owns all token accounts being compressed - /// CHECK: Validated by the SDK - pub authority: AccountInfo<'info>, - /// CHECK: CPI authority of the compressed token program - pub ctoken_cpi_authority: UncheckedAccount<'info>, - /// CHECK: Compressed token program - pub ctoken_program: UncheckedAccount<'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_recipient: 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>, -} - -#[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, -} - -/// Auto-derived via macro. Unified enum that can hold any account type. Crucial -/// for dispatching multiple compressed accounts of different types in -/// decompress_accounts_idempotent. - -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum CompressedAccountVariant { - UserRecord(UserRecord), - PackedUserRecord(PackedUserRecord), - GameSession(GameSession), - PackedGameSession(PackedGameSession), - PlaceholderRecord(PlaceholderRecord), - PackedPlaceholderRecord(PackedPlaceholderRecord), - PackedCTokenData(light_sdk::token::PackedCTokenData), - CTokenData(light_sdk::token::CTokenData), -} - -impl Default for CompressedAccountVariant { - fn default() -> Self { - Self::UserRecord(UserRecord::default()) - } -} - -// impl DataHasher for CompressedAccountVariant { -// fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { -// match self { -// Self::UserRecord(data) => data.hash::(), -// Self::PackedUserRecord(_) => unreachable!(), -// Self::GameSession(data) => data.hash::(), -// Self::PlaceholderRecord(data) => data.hash::(), -// Self::PackedCTokenData(_) => unreachable!(), -// Self::CTokenData(_) => unreachable!(), -// Self::PackedGameSession(_) => unreachable!(), -// Self::PackedPlaceholderRecord(_) => unreachable!(), -// } -// } -// } - -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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack for PlaceholderRecord { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() + instructions::decompress_accounts_idempotent::decompress_accounts_idempotent( + ctx, + proof, + compressed_accounts, + system_accounts_offset, + ) } -} -// Identity Unpack implementation - PDA types are sent unpacked -impl Unpack for PlaceholderRecord { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) + 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, + ) } -} - -// #[error_code] -// pub enum CompressibleInstructionError { -// #[msg("Invalid account count: PDAs and compressed accounts must match")] -// InvalidAccountCount, -// #[msg("Rent recipient does not match config")] -// InvalidRentRecipient, -// #[msg("Failed to create compressed mint")] -// MintCreationFailed, -// #[msg("Compressed token program account not found in remaining accounts")] -// MissingCompressedTokenProgram, -// #[msg("Compressed token program authority PDA account not found in remaining accounts")] -// MissingCompressedTokenProgramAuthorityPDA, - -// #[msg("CToken decompression not yet implemented")] -// CTokenDecompressionNotImplemented, -// } - -// 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, - // TODO: Add mint compression parameters when implementing mint functionality - // pub mint_compressed_address: [u8; 32], - // pub mint_address_tree_info: PackedAddressTreeInfo, - // pub mint_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} -#[inline] -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, + 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/csdk-anchor-test/src/seeds.rs b/sdk-tests/csdk-anchor-test/src/seeds.rs new file mode 100644 index 0000000000..414bbd6d5b --- /dev/null +++ b/sdk-tests/csdk-anchor-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/csdk-anchor-test/src/state.rs b/sdk-tests/csdk-anchor-test/src/state.rs new file mode 100644 index 0000000000..e0ceb446a4 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/src/state.rs @@ -0,0 +1,520 @@ +// 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_ctoken_types::instructions::mint_action::CompressedMintWithContext; +use light_sdk::{ + account::Size, + compressible::{ + CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Unpack, + }, + 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_sdk::token::PackedCTokenData), + CTokenData(light_sdk::token::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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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 Pack 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 Unpack 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/csdk-anchor-test/tests/game_session_tests.rs b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs new file mode 100644 index 0000000000..a8b0d78ddc --- /dev/null +++ b/sdk-tests/csdk-anchor-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_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 light_token_client::ctoken; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_game_session, ADDRESS_SPACE, CTOKEN_RENT_SPONSOR, 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 = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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 = + csdk_anchor_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, + csdk_anchor_test::CompressedAccountVariant::GameSession(c_game_session), + )], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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], + csdk_anchor_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + csdk_anchor_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 = + csdk_anchor_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/csdk-anchor-test/tests/helpers.rs b/sdk-tests/csdk-anchor-test/tests/helpers.rs new file mode 100644 index 0000000000..eaa529d76f --- /dev/null +++ b/sdk-tests/csdk-anchor-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 csdk_anchor_test::{CompressedAccountVariant, GameSession, UserRecord}; +use light_compressed_account::address::derive_address; +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 light_token_client::ctoken; +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 = csdk_anchor_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 = csdk_anchor_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), + )], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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 = csdk_anchor_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 = csdk_anchor_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/csdk-anchor-test/tests/idempotency_tests.rs b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs new file mode 100644 index 0000000000..4cd6598346 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs @@ -0,0 +1,143 @@ +use anchor_lang::{AccountDeserialize, AnchorDeserialize, ToAccountMetas}; +use csdk_anchor_test::{CompressedAccountVariant, 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, +}; +use light_sdk::compressible::CompressibleConfig; +use light_token_client::ctoken; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{ + create_record, decompress_single_user_record, ADDRESS_SPACE, CTOKEN_RENT_SPONSOR, RENT_SPONSOR, +}; + +#[tokio::test] +async fn test_double_decompression_attack() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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), + )], + &csdk_anchor_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, + ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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/csdk-anchor-test/tests/test.rs b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs similarity index 50% rename from sdk-tests/csdk-anchor-test/tests/test.rs rename to sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs index a0f918ffa3..7d44ce304e 100644 --- a/sdk-tests/csdk-anchor-test/tests/test.rs +++ b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs @@ -8,8 +8,8 @@ use csdk_anchor_test::{ }; use light_client::indexer::CompressedAccount; use light_compressed_account::address::derive_address; -use light_compressed_token_sdk::instructions::create_compressed_mint::{ - derive_ctoken_mint_address, find_spl_mint_address, +use light_compressed_token_sdk::instructions::{ + create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address, }; use light_compressed_token_types::CPI_AUTHORITY_PDA; use light_compressible_client::CompressibleInstruction; @@ -17,33 +17,33 @@ use light_ctoken_types::{ instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, state::CompressedMintMetadata, }; -use light_macros::pubkey; use light_program_test::{ program_test::{ initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, }, - utils::simulation::simulate_cu, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, + AddressWithTree, Indexer, ProgramTestConfig, Rpc, }; use light_sdk::{ - compressible::{CompressAs, CompressibleConfig}, + compressible::CompressibleConfig, instruction::{PackedAccounts, SystemAccountMetaConfig}, token::CTokenDataWithVariant, }; use light_sdk_types::C_TOKEN_PROGRAM_ID; use light_token_client::ctoken; -use solana_account::Account; 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_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); -pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +mod helpers; +use helpers::{ + create_game_session, create_record, ADDRESS_SPACE, CTOKEN_RENT_SPONSOR, RENT_SPONSOR, +}; -pub const CTOKEN_RENT_SPONSOR: Pubkey = pubkey!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); -pub const CTOKEN_RENT_AUTHORITY: Pubkey = pubkey!("8r3QmazwoLHYppYWysXPgUxYJ3Khn7vh3e313jYDcCKy"); +// 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 = csdk_anchor_test::ID; @@ -63,8 +63,8 @@ async fn test_create_and_decompress_two_accounts() { &program_id, &payer, 100, - RENT_RECIPIENT, - vec![ADDRESS_SPACE[0]], + RENT_SPONSOR, + vec![crate::helpers::ADDRESS_SPACE[0]], &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, None, ) @@ -204,1395 +204,216 @@ async fn test_create_and_decompress_two_accounts() { .await; } -#[tokio::test] -async fn test_create_decompress_compress_single_account() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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); +#[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 result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - 100, - RENT_RECIPIENT, - vec![ADDRESS_SPACE[0]], - &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); + 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 (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + let address_tree_pubkey = rpc.get_address_tree_v2().tree; - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + 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); - rpc.warp_to_slot(100).unwrap(); + let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); + let accounts = csdk_anchor_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(), + ); - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; + 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; - rpc.warp_to_slot(101).unwrap(); + 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 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; -} + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); -#[tokio::test] -async fn test_double_decompression_attack() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); + 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 _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - 100, - RENT_RECIPIENT, - vec![ADDRESS_SPACE[0]], - &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); + let instruction_data = csdk_anchor_test::instruction::CreateUserRecordAndGameSession { + account_data: csdk_anchor_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: csdk_anchor_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 (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - let address_tree_pubkey = rpc.get_address_tree_v2().queue; - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), + 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(); - let c_user_record = - UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); - rpc.warp_to_slot(100).unwrap(); + assert_eq!( + compressed_user_record.address, + Some(user_compressed_address) + ); + assert!(compressed_user_record.data.is_some()); - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; + let user_buf = compressed_user_record.data.unwrap().data; - 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 user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) + 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(); - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash], vec![], None) - .await - .unwrap() - .value; + assert_eq!( + compressed_game_session.address, + Some(game_compressed_address) + ); + assert!(compressed_game_session.data.is_some()); - 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), - )], - &csdk_anchor_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, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, - 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()); -} - -#[tokio::test] -async fn test_create_and_decompress_accounts_with_different_state_trees() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, - 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; -} - -#[tokio::test] -async fn test_update_record_compression_info() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, - 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 = csdk_anchor_test::accounts::UpdateRecord { - user: payer.pubkey(), - user_record: user_record_pda, - }; - - let instruction_data = csdk_anchor_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()); -} - -#[tokio::test] -async fn test_custom_compression_game_session() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, - 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; -} - -#[tokio::test] -async fn test_create_empty_compressed_account() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, - 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 = - csdk_anchor_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().queue; - 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; -} - -async fn create_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - state_tree_queue: Option, -) { - let config_pda = CompressibleConfig::derive_pda(program_id, 0).0; - - 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().queue; - - let accounts = csdk_anchor_test::accounts::CreateRecord { - user: payer.pubkey(), - user_record: *user_record_pda, - system_program: solana_sdk::system_program::ID, - config: config_pda, - rent_recipient: RENT_RECIPIENT, - }; - - 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 = csdk_anchor_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 _cu = simulate_cu(rpc, payer, &instruction).await; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Transaction should succeed"); - - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_record_account.is_none(), - "Account should not exist after compression" - ); -} - -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().queue; - - let accounts = csdk_anchor_test::accounts::CreateGameSession { - player: payer.pubkey(), - game_session: *game_session_pda, - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_recipient: RENT_RECIPIENT, - }; - - 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 = csdk_anchor_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()); -} - -#[allow(clippy::too_many_arguments)] -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().queue; - - 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::derive_ctoken_program_config(None).0; - 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, - }), - ), - ], - &csdk_anchor_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, - 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], - csdk_anchor_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - csdk_anchor_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)] -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().queue; - - 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), - ), - ], - &csdk_anchor_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, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, - 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 _cu = simulate_cu(rpc, payer, &instruction).await; - - 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], - csdk_anchor_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - csdk_anchor_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); -} - -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().queue; - - 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_ctoken_mint_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); - let accounts = csdk_anchor_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - mint_signer: mint_signer.pubkey(), - ctoken_program: light_sdk_types::constants::C_TOKEN_PROGRAM_ID.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_recipient: RENT_RECIPIENT, - 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 = csdk_anchor_test::instruction::CreateUserRecordAndGameSession { - account_data: csdk_anchor_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: csdk_anchor_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 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(), @@ -1669,120 +490,29 @@ async fn create_user_record_and_game_session( ) } -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().queue; - - 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, - csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*user_record_pda], - &[account], - &csdk_anchor_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_recipient: RENT_RECIPIENT, - } - .to_account_metas(None), - vec![csdk_anchor_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()) -} - -async fn decompress_single_user_record( +#[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, - _user_record_bump: &u8, + 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().queue; + let address_tree_pubkey = rpc.get_address_tree_v2().tree; let user_compressed_address = derive_address( &user_record_pda.to_bytes(), @@ -1799,31 +529,122 @@ async fn decompress_single_user_record( 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], vec![], None) + .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::derive_ctoken_program_config(None).0; 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), - )], + &[ + *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, + }), + ), + ], &csdk_anchor_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, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, + ctoken_config, ctoken_program: ctoken::id(), ctoken_cpi_authority: ctoken::cpi_authority(), - some_mint: payer.pubkey(), + some_mint: ctoken_account.token.mint, } .to_account_metas(None), rpc_result, @@ -1838,21 +659,19 @@ async fn decompress_single_user_record( "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(); - - 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" @@ -1882,246 +701,119 @@ async fn decompress_single_user_record( .last_written_slot(), expected_slot ); -} - -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().queue; - - let accounts = csdk_anchor_test::accounts::CreatePlaceholderRecord { - user: payer.pubkey(), - placeholder_record: *placeholder_record_pda, - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_recipient: RENT_RECIPIENT, - }; - - 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![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 = csdk_anchor_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; + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); assert!( - result.is_ok(), - "CreatePlaceholderRecord transaction should succeed" + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" ); -} - -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().queue; - let placeholder_compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + csdk_anchor_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" ); - 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 + let decompressed_game_session = + csdk_anchor_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() - .value; - - let placeholder_seeds = csdk_anchor_test::get_placeholderrecord_seeds(placeholder_id); + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); - let account = rpc - .get_account(*placeholder_record_pda) + let token_account_data = rpc + .get_account(native_token_account) .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, - csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*placeholder_record_pda], - &[account], - &csdk_anchor_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_recipient: RENT_RECIPIENT, - } - .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 + !token_account_data.data.is_empty(), + "Token account should have data" ); + assert_eq!(token_account_data.owner, C_TOKEN_PROGRAM_ID.into()); - let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); - - let compressed_placeholder_after = rpc - .get_compressed_account(placeholder_compressed_address, None) + 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_placeholder_after.data.is_some(), - "Compressed account should have data after compression" + compressed_user_record_data.data.unwrap().data.is_empty(), + "Compressed user record should be closed/empty after decompression" ); - - let compressed_data_after = compressed_placeholder_after.data.unwrap(); - assert!( - !compressed_data_after.data.is_empty(), - "Compressed account should contain the PDA data" + compressed_game_session_data.data.unwrap().data.is_empty(), + "Compressed game session should be closed/empty after decompression" ); } -async fn compress_placeholder_record_for_double_test( +#[allow(clippy::too_many_arguments)] +pub async fn decompress_multiple_pdas( 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().queue; - - let placeholder_compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), + 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 compressed_placeholder = rpc - .get_compressed_account(placeholder_compressed_address, None) + let c_user_pda = rpc + .get_compressed_account(user_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 = csdk_anchor_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, - csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*placeholder_record_pda], - &accounts_to_compress, - &csdk_anchor_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_recipient: RENT_RECIPIENT, - } - .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 -} + let user_account_data = c_user_pda.data.as_ref().unwrap(); -#[allow(clippy::too_many_arguments)] -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().queue; + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); let game_compressed_address = derive_address( &game_session_pda.to_bytes(), @@ -2134,13 +826,12 @@ async fn decompress_single_game_session( .unwrap() .value .unwrap(); - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = - csdk_anchor_test::GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); let rpc_result = rpc - .get_validity_proof(vec![c_game_pda.hash], vec![], None) + .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) .await .unwrap() .value; @@ -2151,11 +842,17 @@ async fn decompress_single_game_session( light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( program_id, &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*game_session_pda], - &[( - c_game_pda, - csdk_anchor_test::CompressedAccountVariant::GameSession(c_game_session), - )], + &[*user_record_pda, *game_session_pda], + &[ + ( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda, + CompressedAccountVariant::GameSession(c_game_session), + ), + ], &csdk_anchor_test::accounts::DecompressAccountsIdempotent { fee_payer: payer.pubkey(), config: CompressibleConfig::derive_pda(program_id, 0).0, @@ -2172,11 +869,56 @@ async fn decompress_single_game_session( ) .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, @@ -2195,7 +937,7 @@ async fn decompress_single_game_session( 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_eq!(decompressed_game_session.score, 0); assert!(!decompressed_game_session .compression_info .as_ref() @@ -2209,228 +951,20 @@ async fn decompress_single_game_session( .last_written_slot(), expected_slot ); -} - -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 = - csdk_anchor_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" - ); -} - -#[tokio::test] -async fn test_double_compression_attack() { - let program_id = csdk_anchor_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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_RECIPIENT, - 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().queue; - 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) + let c_game_pda = rpc + .get_compressed_account(game_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" - ); + assert!(c_game_pda.data.is_some()); + assert_eq!(c_game_pda.data.unwrap().data.len(), 0); } #[allow(clippy::too_many_arguments)] -async fn compress_token_account_after_decompress( +pub async fn compress_token_account_after_decompress( rpc: &mut LightProgramTest, user: &Keypair, program_id: &Pubkey, @@ -2576,7 +1110,7 @@ async fn compress_token_account_after_decompress( &csdk_anchor_test::accounts::CompressAccountsIdempotent { fee_payer: user.pubkey(), config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_recipient: RENT_RECIPIENT, + rent_sponsor: RENT_SPONSOR, } .to_account_metas(None), vec![user_record_seeds, game_session_seeds], @@ -2782,3 +1316,75 @@ async fn compress_token_account_after_decompress( "Token account 5 should be empty" ); } + +#[tokio::test] +async fn test_create_and_decompress_accounts_with_different_state_trees() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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/csdk-anchor-test/tests/placeholder_tests.rs b/sdk-tests/csdk-anchor-test/tests/placeholder_tests.rs new file mode 100644 index 0000000000..76fce8d16a --- /dev/null +++ b/sdk-tests/csdk-anchor-test/tests/placeholder_tests.rs @@ -0,0 +1,535 @@ +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 = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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 = + csdk_anchor_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 = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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 = csdk_anchor_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 = csdk_anchor_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 = csdk_anchor_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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &[account], + &csdk_anchor_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 = csdk_anchor_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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &accounts_to_compress, + &csdk_anchor_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/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..d4a23821f9 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs @@ -0,0 +1,279 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use csdk_anchor_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 = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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; +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = csdk_anchor_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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 = csdk_anchor_test::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = csdk_anchor_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, + csdk_anchor_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*user_record_pda], + &[account], + &csdk_anchor_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![csdk_anchor_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_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 b514962de78e52de7377e9d2171f98781bd8e949 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 16:39:48 -0500 Subject: [PATCH 07/28] reset close_for_compress_and_close to main --- .../compression/ctoken/compress_and_close.rs | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs index f12b767fb7..cde3cf0dcd 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs @@ -169,7 +169,7 @@ fn validate_compressed_token_account( /// Close ctoken accounts after compress and close operations pub fn close_for_compress_and_close( compressions: &[ZCompression<'_>], - validated_accounts: &Transfer2Accounts, + _validated_accounts: &Transfer2Accounts, ) -> Result<(), ProgramError> { // Track used compressed account indices for CompressAndClose to prevent duplicate outputs let mut used_compressed_account_indices = [0u8; 32]; // 256 bits @@ -179,28 +179,48 @@ pub fn close_for_compress_and_close( .iter() .filter(|c| c.mode == ZCompressionMode::CompressAndClose) { - let token_account_info = validated_accounts.packed_accounts.get_u8( - compression.source_or_recipient, - "CompressAndClose: source_or_recipient", - )?; - let destination = validated_accounts.packed_accounts.get_u8( - compression.get_destination_index()?, - "CompressAndClose: destination", - )?; - let rent_sponsor = validated_accounts.packed_accounts.get_u8( - compression.get_rent_sponsor_index()?, - "CompressAndClose: rent_sponsor", - )?; + // Check for duplicate compressed account indices in CompressAndClose operations + let compressed_idx = compression.get_compressed_token_account_index()?; + if let Some(mut bit) = used_bits.get_mut(compressed_idx as usize) { + if *bit { + msg!( + "Duplicate compressed account index {} in CompressAndClose operations", + compressed_idx + ); + return Err(ErrorCode::CompressAndCloseDuplicateOutput.into()); + } + *bit = true; + } else { + msg!("Compressed account index {} out of bounds", compressed_idx); + return Err(ProgramError::InvalidInstructionData); + } - let authority = validated_accounts - .packed_accounts - .get_u8(compression.authority, "CompressAndClose: authority")?; - close_token_account(&CloseTokenAccountAccounts { - token_account: token_account_info, - destination, - authority, - rent_sponsor: Some(rent_sponsor), - })?; + #[cfg(target_os = "solana")] + { + let validated_accounts = _validated_accounts; + let token_account_info = validated_accounts.packed_accounts.get_u8( + compression.source_or_recipient, + "CompressAndClose: source_or_recipient", + )?; + let destination = validated_accounts.packed_accounts.get_u8( + compression.get_destination_index()?, + "CompressAndClose: destination", + )?; + let rent_sponsor = validated_accounts.packed_accounts.get_u8( + compression.get_rent_sponsor_index()?, + "CompressAndClose: rent_sponsor", + )?; + let authority = validated_accounts + .packed_accounts + .get_u8(compression.authority, "CompressAndClose: authority")?; + use crate::close_token_account::processor::close_token_account; + close_token_account(&CloseTokenAccountAccounts { + token_account: token_account_info, + destination, + authority, + rent_sponsor: Some(rent_sponsor), + })?; + } } Ok(()) } From 13ba98c316ed6d9f09cdbf929843bd3736fcb400 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 16:42:49 -0500 Subject: [PATCH 08/28] fmt --- .../src/transfer2/compression/ctoken/compress_and_close.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs index cde3cf0dcd..78abf4ff8b 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs @@ -13,8 +13,7 @@ use spl_pod::solana_msg::msg; use super::inputs::CompressAndCloseInputs; use crate::{ close_token_account::{ - accounts::CloseTokenAccountAccounts, - processor::{close_token_account, validate_token_account_for_close_transfer2}, + accounts::CloseTokenAccountAccounts, processor::validate_token_account_for_close_transfer2, }, transfer2::accounts::Transfer2Accounts, }; From 92c51cc3d84fe601f9b5a36b086985771d9e644f Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 17:41:25 -0500 Subject: [PATCH 09/28] try revert --- program-libs/compressible/Cargo.toml | 6 +- .../compressible/src/compression_info.rs | 4 +- .../src/instructions/migrate_state.rs | 5 +- prover/client/src/prover.rs | 100 ++---------------- sdk-libs/client/src/indexer/types.rs | 65 ++++-------- sdk-tests/csdk-anchor-test/src/errors.rs | 10 ++ .../decompress_accounts_idempotent.rs | 18 +++- 7 files changed, 62 insertions(+), 146 deletions(-) diff --git a/program-libs/compressible/Cargo.toml b/program-libs/compressible/Cargo.toml index 5c8d69822f..c644202214 100644 --- a/program-libs/compressible/Cargo.toml +++ b/program-libs/compressible/Cargo.toml @@ -8,8 +8,8 @@ default = ["pinocchio", "solana"] solana = ["dep:solana-program-error", "light-compressed-account/solana", "solana-sysvar", "solana-msg"] anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressed-account/std"] pinocchio = ["light-compressed-account/pinocchio"] -profile-program = ["dep:light-program-profiler"] -profile-heap = ["dep:light-heap", "dep:light-program-profiler"] +profile-program = [] +profile-heap = ["dep:light-heap"] [dependencies] thiserror = { workspace = true } @@ -26,7 +26,7 @@ bytemuck = { workspace = true, features = ["derive"] } borsh = { workspace = true } solana-pubkey = { workspace = true, features = ["std", "sha2", "curve25519", "borsh", "bytemuck"] } pinocchio-pubkey = { workspace = true } -light-program-profiler = { workspace = true, optional = true } +light-program-profiler = { workspace = true } light-heap = { workspace = true, optional = true } light-account-checks = { workspace= true } light-compressed-account = { workspace= true } diff --git a/program-libs/compressible/src/compression_info.rs b/program-libs/compressible/src/compression_info.rs index 1007be970e..5371065817 100644 --- a/program-libs/compressible/src/compression_info.rs +++ b/program-libs/compressible/src/compression_info.rs @@ -1,5 +1,6 @@ use aligned_sized::aligned_sized; use bytemuck::{Pod, Zeroable}; +use light_program_profiler::profile; use light_zero_copy::{ZeroCopy, ZeroCopyMut}; use pinocchio::pubkey::Pubkey; use zerocopy::U64; @@ -61,7 +62,7 @@ macro_rules! impl_is_compressible { /// available_balance = current_lamports - last_lamports /// (we can never claim more lamports than rent is due) /// remaining_balance = available_balance - rent_due - + #[profile] pub fn is_compressible( &self, bytes: u64, @@ -87,6 +88,7 @@ macro_rules! impl_is_compressible { /// Returns 0 if no top-up is needed (account is well-funded). /// Returns write_top_up + rent_deficit if account is compressible. /// Returns write_top_up if account needs more funding but isn't compressible yet. + #[profile] pub fn calculate_top_up_lamports( &self, num_bytes: u64, diff --git a/programs/account-compression/src/instructions/migrate_state.rs b/programs/account-compression/src/instructions/migrate_state.rs index 535d76f01e..d48c027e08 100644 --- a/programs/account-compression/src/instructions/migrate_state.rs +++ b/programs/account-compression/src/instructions/migrate_state.rs @@ -130,9 +130,10 @@ fn migrate_state( }; #[cfg(target_os = "solana")] let slot = Clock::get()?.slot; + // Mock slot for unit tests #[cfg(not(target_os = "solana"))] - let slot = 0u64; // Mock slot for unit tests - // 3. Inserts the leaf in the output queue. + let slot = 0u64; + // 3. Inserts the leaf in the output queue. output_queue .insert_into_current_batch(&migrate_leaf_params.leaf, &slot) .map_err(ProgramError::from)?; diff --git a/prover/client/src/prover.rs b/prover/client/src/prover.rs index dc2005be18..3bf1bab785 100644 --- a/prover/client/src/prover.rs +++ b/prover/client/src/prover.rs @@ -1,11 +1,6 @@ use std::{ - fs::{File, OpenOptions}, - path::PathBuf, - process::{Command, Stdio}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex, - }, + process::Command, + sync::atomic::{AtomicBool, Ordering}, thread::sleep, time::Duration, }; @@ -36,98 +31,17 @@ pub async fn spawn_prover() { if !health_check(10, 1).await && !IS_LOADING.load(Ordering::Relaxed) { IS_LOADING.store(true, Ordering::Relaxed); - let is_ci = std::env::var("CI").is_ok(); - let output_buffer: Arc>> = Arc::new(Mutex::new(Vec::new())); + let command = Command::new(prover_path) + .arg("start-prover") + .spawn() + .expect("Failed to start prover process"); - if is_ci { - use tokio::{io::AsyncReadExt, process::Command as TokioCommand}; - - let mut command = TokioCommand::new(prover_path); - command.arg("start-prover").stdin(Stdio::null()); - command.stdout(Stdio::piped()); - command.stderr(Stdio::piped()); - - let mut child = command.spawn().expect("Failed to start prover process"); - - let stdout = child.stdout.take(); - let stderr = child.stderr.take(); - let output_buffer_stdout = Arc::clone(&output_buffer); - let output_buffer_stderr = Arc::clone(&output_buffer); - - if let Some(mut stdout) = stdout { - tokio::spawn(async move { - let mut buffer = vec![0u8; 1024]; - loop { - match stdout.read(&mut buffer).await { - Ok(0) => break, - Ok(n) => { - let mut buf = output_buffer_stdout.lock().unwrap(); - buf.extend_from_slice(&buffer[..n]); - } - Err(_) => break, - } - } - }); - } - - if let Some(mut stderr) = stderr { - tokio::spawn(async move { - let mut buffer = vec![0u8; 1024]; - loop { - match stderr.read(&mut buffer).await { - Ok(0) => break, - Ok(n) => { - let mut buf = output_buffer_stderr.lock().unwrap(); - buf.extend_from_slice(&buffer[..n]); - } - Err(_) => break, - } - } - }); - } - - std::mem::drop(child); - } else { - let log_dir = PathBuf::from("test-ledger"); - std::fs::create_dir_all(&log_dir).ok(); - let log_path = log_dir.join("prover.log"); - let log_file = OpenOptions::new() - .create(true) - .append(true) - .open(&log_path) - .unwrap_or_else(|_| { - File::create(&log_path).expect("Failed to create prover log file") - }); - let log_file_stderr = log_file.try_clone().ok(); - - let child = Command::new(prover_path) - .arg("start-prover") - .stdin(Stdio::null()) - .stdout(Stdio::from(log_file)) - .stderr(log_file_stderr.map(Stdio::from).unwrap_or(Stdio::null())) - .spawn() - .expect("Failed to start prover process"); - - std::mem::drop(child); - } - - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + let _ = command.wait_with_output(); let health_result = health_check(120, 1).await; if health_result { info!("Prover started successfully"); } else { - if is_ci { - let output = output_buffer.lock().unwrap(); - let output_str = String::from_utf8_lossy(&output); - eprintln!("=== Prover output (stdout/stderr) ==="); - eprintln!("{}", output_str); - eprintln!("=== End of prover output ==="); - } - println!( - "Failed to start prover, health check failed. {:?}", - health_result - ); panic!("Failed to start prover, health check failed."); } } diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index c49b449ff5..d88d51d1cf 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -13,6 +13,7 @@ use light_sdk::{ }; use num_bigint::BigUint; use solana_pubkey::Pubkey; +use tracing::warn; use super::{ base58::{decode_base58_option_to_pubkey, decode_base58_to_fixed_array}, @@ -527,14 +528,17 @@ impl TryFrom for CompressedAccount { let hash = account .hash() .map_err(|_| IndexerError::InvalidResponseData)?; - - let tree_pubkey = - Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()); - - let cpi_context = QUEUE_TREE_MAPPING - .get(&tree_pubkey.to_string()) - .and_then(|tree_info| tree_info.cpi_context); - + // Breaks light-program-test + let tree_info = QUEUE_TREE_MAPPING.get( + &Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()) + .to_string(), + ); + let cpi_context = if let Some(tree_info) = tree_info { + tree_info.cpi_context + } else { + warn!("Cpi context not found in queue tree mapping"); + None + }; Ok(CompressedAccount { address: account.compressed_account.address, data: account.compressed_account.data, @@ -542,7 +546,7 @@ impl TryFrom for CompressedAccount { lamports: account.compressed_account.lamports, leaf_index: account.merkle_context.leaf_index, tree_info: TreeInfo { - tree: tree_pubkey, + tree: Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()), queue: Pubkey::new_from_array(account.merkle_context.queue_pubkey.to_bytes()), tree_type: account.merkle_context.tree_type, cpi_context, @@ -551,7 +555,7 @@ impl TryFrom for CompressedAccount { owner: Pubkey::new_from_array(account.compressed_account.owner.to_bytes()), prove_by_index: account.merkle_context.prove_by_index, seq: None, - slot_created: u64::MAX, // TODO: use actual slot + slot_created: u64::MAX, }) } } @@ -616,18 +620,6 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { .map(|ctx| NextTreeInfo::try_from(ctx.as_ref())) .transpose()?, }; - // TODO: check if the above handles it fine. - // let tree_pubkey = - // Pubkey::new_from_array(decode_base58_to_fixed_array(&account.merkle_context.tree)?); - // let tree_info = QUEUE_TREE_MAPPING - // .get(&tree_pubkey.to_string()) - // .ok_or_else(|| { - // println!( - // "ERROR: No tree_info found for tree pubkey: {}", - // account.merkle_context.tree - // ); - // IndexerError::InvalidResponseData - // })?; Ok(CompressedAccount { owner, @@ -670,27 +662,16 @@ impl TryFrom<&photon_api::models::Account> for CompressedAccount { let lamports = account.lamports; let leaf_index = account.leaf_index; - let tree_pubkey = Pubkey::new_from_array(decode_base58_to_fixed_array(&account.tree)?); + let tree_info = QUEUE_TREE_MAPPING + .get(&account.tree) + .ok_or(IndexerError::InvalidResponseData)?; - let tree_info = if let Some(static_tree_info) = QUEUE_TREE_MAPPING.get(&account.tree) { - TreeInfo { - cpi_context: static_tree_info.cpi_context, - queue: static_tree_info.queue, - tree_type: static_tree_info.tree_type, - next_tree_info: None, - tree: static_tree_info.tree, - } - } else { - // Unknown tree - // TODO: users should provide their own custom QUEUE_TREE_MAPPING if they run their own indexer - TreeInfo { - cpi_context: None, - queue: tree_pubkey, - // FIXME: this should be dynamic - tree_type: light_compressed_account::TreeType::StateV2, - next_tree_info: None, - tree: tree_pubkey, - } + let tree_info = TreeInfo { + cpi_context: tree_info.cpi_context, + queue: tree_info.queue, + tree_type: tree_info.tree_type, + next_tree_info: None, + tree: tree_info.tree, }; Ok(CompressedAccount { diff --git a/sdk-tests/csdk-anchor-test/src/errors.rs b/sdk-tests/csdk-anchor-test/src/errors.rs index c9b2f73fc5..78da53fab4 100644 --- a/sdk-tests/csdk-anchor-test/src/errors.rs +++ b/sdk-tests/csdk-anchor-test/src/errors.rs @@ -10,6 +10,8 @@ pub enum ErrorCode { RentRecipientMismatch, InvalidAccountDiscriminator, DerivedTokenAccountMismatch, + MissingAuthority, + MissingCpiContext, } #[automatically_derived] @@ -29,6 +31,8 @@ impl ::core::fmt::Debug for ErrorCode { ErrorCode::RentRecipientMismatch => "RentRecipientMismatch", ErrorCode::InvalidAccountDiscriminator => "InvalidAccountDiscriminator", ErrorCode::DerivedTokenAccountMismatch => "DerivedTokenAccountMismatch", + ErrorCode::MissingAuthority => "MissingAuthority", + ErrorCode::MissingCpiContext => "MissingCpiContext", }, ) } @@ -61,6 +65,12 @@ impl std::fmt::Display for ErrorCode { 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" + )), } } } diff --git a/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs index 9e397aa6a9..f67baabb55 100644 --- a/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs +++ b/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs @@ -176,8 +176,12 @@ pub fn decompress_accounts_idempotent<'info>( accounts, remaining_accounts, }; - let authority = cpi_accounts.authority().unwrap(); - let cpi_context = cpi_accounts.cpi_context().unwrap(); + 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; @@ -299,7 +303,7 @@ pub fn decompress_accounts_idempotent<'info>( )> = Vec::with_capacity(token_count); let mut compressed_pda_infos = Vec::with_capacity(pda_count); - let cpi_accounts = if has_tokens && has_pdas { + let cpi_accounts = if has_tokens { CpiAccounts::new_with_config( ctx.accounts.fee_payer.as_ref(), &ctx.remaining_accounts[system_accounts_offset as usize..], @@ -375,11 +379,15 @@ pub fn decompress_accounts_idempotent<'info>( return Ok(()); } let fee_payer = ctx.accounts.fee_payer.as_ref(); - let authority = cpi_accounts.authority().unwrap(); - let cpi_context = cpi_accounts.cpi_context().unwrap(); // 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, From c8f1338f09fda2dcce64e70dffa351c73da41bcb Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 17:45:40 -0500 Subject: [PATCH 10/28] build account-compression with test flag --- .github/workflows/programs.yml | 4 ++++ programs/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/programs.yml b/.github/workflows/programs.yml index a6b5239c09..a2053fdf9d 100644 --- a/.github/workflows/programs.yml +++ b/.github/workflows/programs.yml @@ -111,6 +111,10 @@ jobs: done echo "Test passed on attempt $attempt" else + # Build account-compression with test feature before running its tests + if [ "$subtest" == "cargo-test-sbf -p account-compression-test" ]; then + cd programs/account-compression && cargo build-sbf --features 'test, migrate-state' && cd ../.. + fi RUSTFLAGS="-D warnings" eval "$subtest" if [ "$subtest" == "cargo-test-sbf -p e2e-test" ]; then pnpm --filter @lightprotocol/programs run build-compressed-token-small diff --git a/programs/package.json b/programs/package.json index 893c592431..37aa1e4bf5 100644 --- a/programs/package.json +++ b/programs/package.json @@ -8,7 +8,7 @@ "build-system": "anchor build --program-name light_system_program -- --features idl-build custom-heap", "build-compressed-token": "anchor build --program-name light_compressed_token -- --features idl-build custom-heap", "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-account-compression && pnpm test-compressed-token && pnpm e2e-test && pnpm test-registry && pnpm sdk-test-program && pnpm test-system && pnpm test-system-cpi", - "test-account-compression": "cargo-test-sbf -p account-compression-test", + "test-account-compression": "cd account-compression && cargo build-sbf --features 'test, migrate-state' && cd .. && cargo-test-sbf -p account-compression-test", "test-compressed-token": "cargo test-sbf -p compressed-token-test", "e2e-test": "cargo-test-sbf -p e2e-test", "test-registry": "cargo-test-sbf -p registry-test", From 8a89f293baf26057c0326469d1f0adb98c7dfa4d Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 21:30:30 -0500 Subject: [PATCH 11/28] fmt --- .github/workflows/cli-v1.yml | 12 ++++++++- .github/workflows/cli-v2.yml | 12 ++++++++- Cargo.lock | 2 +- Cargo.toml | 1 + cli/package.json | 19 ++++---------- js/compressed-token/package.json | 10 -------- js/stateless.js/package.json | 12 +-------- sdk-libs/compressed-token-types/src/lib.rs | 2 -- .../compressed-token-types/src/token_data.rs | 25 ------------------- sdk-libs/sdk/Cargo.toml | 2 +- sdk-libs/sdk/src/compressible/config.rs | 2 +- .../src/actions/ctoken_transfer.rs | 12 ++++----- sdk-tests/csdk-anchor-test/package.json | 4 +-- .../tests/user_record_tests.rs | 3 ++- 14 files changed, 42 insertions(+), 76 deletions(-) delete mode 100644 sdk-libs/compressed-token-types/src/token_data.rs diff --git a/.github/workflows/cli-v1.yml b/.github/workflows/cli-v1.yml index 54e355a6bf..f10695b31a 100644 --- a/.github/workflows/cli-v1.yml +++ b/.github/workflows/cli-v1.yml @@ -49,9 +49,19 @@ 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 run @lightprotocol/zk-compression-cli:build-ci + npx nx build @lightprotocol/zk-compression-cli - name: Run CLI tests with V1 run: | diff --git a/.github/workflows/cli-v2.yml b/.github/workflows/cli-v2.yml index ec134fc526..81dd22b13f 100644 --- a/.github/workflows/cli-v2.yml +++ b/.github/workflows/cli-v2.yml @@ -49,9 +49,19 @@ jobs: skip-components: "redis,disk-cleanup,go" cache-key: "js" + - name: Build stateless.js with V2 + run: | + cd js/stateless.js + pnpm build:v2 + + - name: Build compressed-token with V2 + run: | + cd js/compressed-token + pnpm build:v2 + - name: Build CLI with V2 run: | - npx nx run @lightprotocol/zk-compression-cli:build-ci + npx nx build @lightprotocol/zk-compression-cli - name: Run CLI tests with V2 run: | diff --git a/Cargo.lock b/Cargo.lock index 6c34f8b5a9..4bfdf202c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4105,8 +4105,8 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-instruction", + "solana-loader-v3-interface", "solana-msg 2.2.1", - "solana-program", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", "solana-system-interface 1.0.0", diff --git a/Cargo.toml b/Cargo.toml index d0224bdb97..6c5781a195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,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" } diff --git a/cli/package.json b/cli/package.json index 2668f622f1..b6a8f7052b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -132,28 +132,19 @@ "nx": { "targets": { "build": { - "dependsOn": [ - "@lightprotocol/programs:build", - "@lightprotocol/stateless.js:build", - "@lightprotocol/compressed-token:build" - ], "inputs": [ "{workspaceRoot}/js", "{workspaceRoot}/programs", "{workspaceRoot}/gnark-prover" ], "outputs": [ - "{projectRoot}/bin", - "{projectRoot}/dist", - "{projectRoot}/lib", - "{projectRoot}/test_bin" + "{workspaceRoot}/bin", + "{workspaceRoot}/dist", + "{workspaceRoot}/lib", + "{workspaceRoot}/test_bin" ] }, "build-ci": { - "executor": "nx:run-script", - "options": { - "script": "build" - }, "dependsOn": [ "@lightprotocol/stateless.js:build-ci", "@lightprotocol/compressed-token:build-ci" @@ -175,4 +166,4 @@ } } } -} +} \ No newline at end of file diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 9c6ab3df1f..fdf9075985 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -131,21 +131,11 @@ "{workspaceRoot}/cli", "{workspaceRoot}/target/idl", "{workspaceRoot}/target/types" - ], - "outputs": [ - "{projectRoot}/dist" ] }, "build-ci": { - "executor": "nx:run-script", - "options": { - "script": "build-ci" - }, "dependsOn": [ "@lightprotocol/stateless.js:build-ci" - ], - "outputs": [ - "{projectRoot}/dist" ] }, "test-ci": { diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 6c65f3f1a1..798a2cc7fd 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -125,20 +125,10 @@ "{workspaceRoot}/cli", "{workspaceRoot}/target/idl", "{workspaceRoot}/target/types" - ], - "outputs": [ - "{projectRoot}/dist" ] }, "build-ci": { - "executor": "nx:run-script", - "options": { - "script": "build-ci" - }, - "dependsOn": [], - "outputs": [ - "{projectRoot}/dist" - ] + "dependsOn": [] }, "test-ci": { "dependsOn": [] 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/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index 58ef6401ad..978f308940 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -37,7 +37,7 @@ solana-instruction = { workspace = true } solana-clock = { workspace = true } solana-sysvar = { workspace = true } solana-system-interface = { workspace = true } -solana-program = { workspace = true } +solana-loader-v3-interface = { workspace = true, features = ["serde"] } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } diff --git a/sdk-libs/sdk/src/compressible/config.rs b/sdk-libs/sdk/src/compressible/config.rs index f246eb1cf3..c5836a8dff 100644 --- a/sdk-libs/sdk/src/compressible/config.rs +++ b/sdk-libs/sdk/src/compressible/config.rs @@ -2,8 +2,8 @@ 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_program::bpf_loader_upgradeable::UpgradeableLoaderState; use solana_pubkey::Pubkey; use solana_system_interface::instruction as system_instruction; use solana_sysvar::{rent::Rent, Sysvar}; diff --git a/sdk-libs/token-client/src/actions/ctoken_transfer.rs b/sdk-libs/token-client/src/actions/ctoken_transfer.rs index 24e813b914..38c9b9be54 100644 --- a/sdk-libs/token-client/src/actions/ctoken_transfer.rs +++ b/sdk-libs/token-client/src/actions/ctoken_transfer.rs @@ -1,4 +1,3 @@ -// TODO: move transfer_ctoken to compressed-token-sdk use light_client::rpc::{Rpc, RpcError}; use solana_instruction::{AccountMeta, Instruction}; use solana_keypair::Keypair; @@ -38,6 +37,7 @@ pub async fn transfer_ctoken( .await } +// TODO: consume the variant from compressed-token-sdk instead /// Create a ctoken transfer instruction. /// /// # Arguments @@ -60,13 +60,13 @@ pub fn create_transfer_ctoken_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) - // TODO: try to remove this so we can reuse this from the compressed-token-sdk - 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-tests/csdk-anchor-test/package.json b/sdk-tests/csdk-anchor-test/package.json index 74a7ddd3e0..8be19c2e7b 100644 --- a/sdk-tests/csdk-anchor-test/package.json +++ b/sdk-tests/csdk-anchor-test/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "license": "Apache-2.0", "scripts": { - "build": "cargo build-sbf", - "test": "cargo test-sbf -- --nocapture" + "build": "cd sdk-tests/csdk-anchor-test && cargo build-sbf", + "test": "cargo test-sbf -p csdk-anchor-test -- --nocapture" }, "nx": {} } diff --git a/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs index d4a23821f9..ca141da70b 100644 --- a/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs @@ -78,7 +78,8 @@ async fn test_create_decompress_compress_single_account() { ); } rpc.warp_to_slot(200).unwrap(); - let _result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; + assert!(result.is_ok(), "Compression should succeed"); } #[tokio::test] From d98604241c87215850f705dceaeaf6b75514bfdf Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 23:26:51 -0500 Subject: [PATCH 12/28] fix workflow to ensure we build account-compression with test feature --- .github/workflows/programs.yml | 5 ++++- programs/package.json | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/programs.yml b/.github/workflows/programs.yml index a2053fdf9d..52d4a52c43 100644 --- a/.github/workflows/programs.yml +++ b/.github/workflows/programs.yml @@ -114,8 +114,11 @@ jobs: # Build account-compression with test feature before running its tests if [ "$subtest" == "cargo-test-sbf -p account-compression-test" ]; then cd programs/account-compression && cargo build-sbf --features 'test, migrate-state' && cd ../.. + # Ensure the program executes with the 'test' cfg active during this subtest + RUSTFLAGS="-D warnings --cfg feature=\"test\"" eval "$subtest" + else + RUSTFLAGS="-D warnings" eval "$subtest" fi - RUSTFLAGS="-D warnings" eval "$subtest" if [ "$subtest" == "cargo-test-sbf -p e2e-test" ]; then pnpm --filter @lightprotocol/programs run build-compressed-token-small RUSTFLAGS="-D warnings" eval "$subtest -- --test test_10_all" diff --git a/programs/package.json b/programs/package.json index 37aa1e4bf5..7cd2a92d54 100644 --- a/programs/package.json +++ b/programs/package.json @@ -7,8 +7,9 @@ "build-compressed-token-small": "cd compressed-token && cargo build-sbf --features cpi-without-program-ids && cd ..", "build-system": "anchor build --program-name light_system_program -- --features idl-build custom-heap", "build-compressed-token": "anchor build --program-name light_compressed_token -- --features idl-build custom-heap", - "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-account-compression && pnpm test-compressed-token && pnpm e2e-test && pnpm test-registry && pnpm sdk-test-program && pnpm test-system && pnpm test-system-cpi", + "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-account-compression && pnpm test-compressed-token && pnpm e2e-test && pnpm test-registry && pnpm sdk-test-program && pnpm test-system && pnpm test-system-cpi && pnpm test-batched-merkle-tree", "test-account-compression": "cd account-compression && cargo build-sbf --features 'test, migrate-state' && cd .. && cargo-test-sbf -p account-compression-test", + "test-batched-merkle-tree": "cd account-compression && cargo build-sbf --features 'test, migrate-state' && cd .. && cargo test-sbf -p batched-merkle-tree-test -- --skip test_simulate_transactions --skip test_e2e", "test-compressed-token": "cargo test-sbf -p compressed-token-test", "e2e-test": "cargo-test-sbf -p e2e-test", "test-registry": "cargo-test-sbf -p registry-test", From 45d0f1e49aa87d506a015f2a4334b9e5b4fa1f6c Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 11 Nov 2025 23:33:32 -0500 Subject: [PATCH 13/28] fix sdk test nav --- .github/workflows/sdk-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index d1cc39bfd5..8373f4367e 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -50,7 +50,7 @@ jobs: - 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"]' - program: anchor & pinocchio - sub-tests: '["cd sdk-tests/csdk-anchor-test && cargo build-sbf && cargo test-sbf", "cd ../../sdk-tests/sdk-anchor-test && cargo build-sbf && cargo test-sbf", "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: '["cd sdk-tests/csdk-anchor-test && cargo build-sbf && cargo test-sbf", "cd sdk-tests/sdk-anchor-test && cargo build-sbf && cargo test-sbf", "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 From 1180a64f2741b86181573d668291ec7e3dfc8b74 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 00:14:05 -0500 Subject: [PATCH 14/28] try sdk-tests.yml with hyphen --- .github/workflows/sdk-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 8373f4367e..1522fe8465 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: '["cd sdk-tests/csdk-anchor-test && cargo build-sbf && cargo test-sbf", "cd sdk-tests/sdk-anchor-test && cargo build-sbf && cargo test-sbf", "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 csdk-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"]' - program: token test sub-tests: '["cargo-test-sbf -p sdk-token-test"]' - program: sdk-libs From a21dbcf2463b076b0fc84cc8f1abf43612ccd093 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 00:50:48 -0500 Subject: [PATCH 15/28] rm idl build --- sdk-tests/csdk-anchor-test/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-tests/csdk-anchor-test/Cargo.toml b/sdk-tests/csdk-anchor-test/Cargo.toml index 60f2c7b3e2..98aeafdc53 100644 --- a/sdk-tests/csdk-anchor-test/Cargo.toml +++ b/sdk-tests/csdk-anchor-test/Cargo.toml @@ -13,7 +13,7 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -default = ["idl-build"] +default = [] idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] test-sbf = [] From b22289f055b475010f9db30486e3303aca87d401 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 01:01:38 -0500 Subject: [PATCH 16/28] csdk anchor test artifact --- .github/workflows/sdk-tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 1522fe8465..528fbdec84 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -79,6 +79,14 @@ jobs: run: | npx nx build @lightprotocol/zk-compression-cli + - name: Prepare csdk-anchor-test artifact + if: matrix.program == 'anchor & pinocchio' + run: | + + cargo build-sbf -p csdk-anchor-test + mkdir -p target/sbpf-solana-solana/release + cp -f target/deploy/csdk_anchor_test.so target/sbpf-solana-solana/release/csdk_anchor_test.so + - name: Run sub-tests for ${{ matrix.program }} if: matrix.sub-tests != null run: | From 13cbd5e66932d262d6b5a48dbd5bc03ddd6e0d45 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 01:12:55 -0500 Subject: [PATCH 17/28] wip --- .github/workflows/sdk-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 528fbdec84..15562bc0a2 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -83,7 +83,7 @@ jobs: if: matrix.program == 'anchor & pinocchio' run: | - cargo build-sbf -p csdk-anchor-test + cd sdk-tests/csdk-anchor-test && cargo build-sbf mkdir -p target/sbpf-solana-solana/release cp -f target/deploy/csdk_anchor_test.so target/sbpf-solana-solana/release/csdk_anchor_test.so From 3bf791ccb9ac292aa3b0ff4689a520e7e9623284 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 10:49:55 -0500 Subject: [PATCH 18/28] reuse ctoken_types --- Cargo.lock | 2 ++ program-libs/compressible/src/config.rs | 25 +++++++++++++ sdk-libs/sdk/Cargo.toml | 1 + sdk-libs/sdk/src/token.rs | 20 +++-------- sdk-libs/token-client/Cargo.toml | 1 + sdk-libs/token-client/src/lib.rs | 35 ++++--------------- .../tests/game_session_tests.rs | 4 +-- sdk-tests/csdk-anchor-test/tests/helpers.rs | 4 +-- .../tests/idempotency_tests.rs | 4 +-- .../tests/multi_account_tests.rs | 8 ++--- 10 files changed, 51 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bfdf202c2..742a5a5541 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4095,6 +4095,7 @@ dependencies = [ "light-account-checks", "light-compressed-account", "light-concurrent-merkle-tree", + "light-ctoken-types", "light-hasher", "light-macros", "light-sdk-macros", @@ -4272,6 +4273,7 @@ dependencies = [ "light-compressed-account", "light-compressed-token-sdk", "light-compressed-token-types", + "light-compressible", "light-ctoken-types", "light-sdk", "light-zero-copy", 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/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index 978f308940..024adcd982 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -54,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/token.rs b/sdk-libs/sdk/src/token.rs index c02808893f..8e539d55f2 100644 --- a/sdk-libs/sdk/src/token.rs +++ b/sdk-libs/sdk/src/token.rs @@ -15,7 +15,8 @@ pub enum AccountState { Initialized, Frozen, } -// TODO: extract token data from program into into a separate crate, import it and remove this file. +// TODO: extract token data from program into into a separate crate, import it +// and remove this file. Do this in a separate PR. #[derive(Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Clone, Default)] pub struct TokenData { /// The mint associated with this account @@ -212,21 +213,10 @@ where } } -/// Standard Pack for c-token accounts with variant. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize, Default)] -pub struct InputTokenDataCompressible { - pub owner: u8, - pub amount: u64, - pub has_delegate: bool, // Optional delegate is set - pub delegate: u8, - pub mint: u8, - pub version: u8, -} - -// TODO: remove these and fix renaming after we're done with ci. -#[deprecated(since = "0.2.0", note = "Use `CTokenDataWithVariant` instead")] +// TODO: remove these aliases and fix renaming in a separate PR. +pub type InputTokenDataCompressible = + light_ctoken_types::instructions::transfer2::MultiTokenTransferOutputData; pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; -#[deprecated(since = "0.2.0", note = "Use `PackedCTokenDataWithVariant` instead")] pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; pub type CTokenData = CTokenDataWithVariant; pub type PackedCTokenData = PackedCTokenDataWithVariant; 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/lib.rs b/sdk-libs/token-client/src/lib.rs index b39e53f709..8f4a2353f0 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -10,6 +10,7 @@ pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4 pub mod ctoken { use light_compressed_token_sdk::POOL_SEED; + use light_compressible::config::CompressibleConfig; use solana_pubkey::Pubkey; use super::{CTOKEN_CPI_AUTHORITY, CTOKEN_PROGRAM_ID}; @@ -48,36 +49,14 @@ pub mod ctoken { create_compressed_mint::find_spl_mint_address, derive_cmint_from_spl_mint, }; - pub fn derive_ctoken_program_config(_version: Option) -> (Pubkey, u8) { - let version = 1u16; - let registry_program_id = - solana_pubkey::pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"); - let (compressible_config_pda, config_bump) = Pubkey::find_program_address( - &[b"compressible_config", &version.to_le_bytes()], - ®istry_program_id, - ); - (compressible_config_pda, config_bump) + pub fn config_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_config_pda() } - // TODO: add version. - pub fn derive_ctoken_rent_sponsor(_version: Option) -> (Pubkey, u8) { - let version = 1u16; - Pubkey::find_program_address( - &[b"rent_sponsor".as_slice(), version.to_le_bytes().as_slice()], - &solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"), - ) + pub fn rent_sponsor_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_rent_sponsor_pda() } - - pub fn derive_ctoken_compression_authority(version: Option) -> (Pubkey, u8) { - let registry_program_id = - solana_pubkey::pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"); - let (compression_authority, compression_authority_bump) = Pubkey::find_program_address( - &[ - b"compression_authority".as_slice(), - version.unwrap_or(1).to_le_bytes().as_slice(), - ], - ®istry_program_id, - ); - (compression_authority, compression_authority_bump) + pub fn compression_authority_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_compression_authority_pda() } } diff --git a/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs index a8b0d78ddc..f7c7de4e0a 100644 --- a/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs @@ -137,8 +137,8 @@ pub async fn decompress_single_game_session( fee_payer: payer.pubkey(), config: CompressibleConfig::derive_pda(program_id, 0).0, rent_payer: payer.pubkey(), - ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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(), diff --git a/sdk-tests/csdk-anchor-test/tests/helpers.rs b/sdk-tests/csdk-anchor-test/tests/helpers.rs index eaa529d76f..c4d987407d 100644 --- a/sdk-tests/csdk-anchor-test/tests/helpers.rs +++ b/sdk-tests/csdk-anchor-test/tests/helpers.rs @@ -170,8 +170,8 @@ pub async fn decompress_single_user_record( fee_payer: payer.pubkey(), config: CompressibleConfig::derive_pda(program_id, 0).0, rent_payer: payer.pubkey(), - ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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(), diff --git a/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs index 4cd6598346..661c111e9c 100644 --- a/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs @@ -107,8 +107,8 @@ async fn test_double_decompression_attack() { fee_payer: payer.pubkey(), config: CompressibleConfig::derive_pda(&program_id, 0).0, rent_payer: payer.pubkey(), - ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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(), diff --git a/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs index 7d44ce304e..d4b226a9f7 100644 --- a/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs @@ -563,7 +563,7 @@ pub async fn decompress_multiple_pdas_with_ctoken( let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); - let ctoken_config = ctoken::derive_ctoken_program_config(None).0; + let ctoken_config = ctoken::config_pda(); let instruction = light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( program_id, @@ -640,7 +640,7 @@ pub async fn decompress_multiple_pdas_with_ctoken( fee_payer: payer.pubkey(), config: CompressibleConfig::derive_pda(program_id, 0).0, rent_payer: payer.pubkey(), - ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), ctoken_config, ctoken_program: ctoken::id(), ctoken_cpi_authority: ctoken::cpi_authority(), @@ -857,8 +857,8 @@ pub async fn decompress_multiple_pdas( fee_payer: payer.pubkey(), config: CompressibleConfig::derive_pda(program_id, 0).0, rent_payer: payer.pubkey(), - ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, - ctoken_config: ctoken::derive_ctoken_program_config(None).0, + 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(), From 2034b35ecb6355b13d8ca437b38bd9d2f818aea2 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 20:14:16 -0500 Subject: [PATCH 19/28] move ctoken to light-compressed-token-sdk --- Cargo.lock | 1 + sdk-libs/compressed-token-sdk/Cargo.toml | 1 + sdk-libs/compressed-token-sdk/src/ctoken.rs | 66 +++++++++++++++++++ .../src/instructions/transfer_interface.rs | 1 - sdk-libs/compressed-token-sdk/src/lib.rs | 1 + sdk-libs/token-client/src/lib.rs | 2 +- 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 sdk-libs/compressed-token-sdk/src/ctoken.rs diff --git a/Cargo.lock b/Cargo.lock index 742a5a5541..75c33e5587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3689,6 +3689,7 @@ dependencies = [ "light-compressed-account", "light-compressed-token", "light-compressed-token-types", + "light-compressible", "light-ctoken-types", "light-macros", "light-program-profiler", 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/ctoken.rs b/sdk-libs/compressed-token-sdk/src/ctoken.rs new file mode 100644 index 0000000000..f22e5be531 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/ctoken.rs @@ -0,0 +1,66 @@ +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"); + +pub const ID: Pubkey = CTOKEN_PROGRAM_ID; + +/// Returns the program ID for the Compressed Token Program +pub fn id() -> Pubkey { + 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 { + Pubkey::find_program_address( + &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], + &id(), + ) + .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() +} + +/// Alias for `rent_sponsor_pda()` for convenience +pub fn rent_sponsor() -> Pubkey { + rent_sponsor_pda() +} + +/// Alias for `compression_authority_pda()` for convenience +pub fn compression_authority() -> Pubkey { + compression_authority_pda() +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs index 206720498e..0dbca6282a 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -70,7 +70,6 @@ pub fn create_transfer_spl_to_ctoken_instruction( method_used: true, }; - // Create Transfer2Inputs following the test let inputs = Transfer2Inputs { validity_proof: ValidityProof::new(None).into(), transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 535a493d89..c9309ce47a 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -1,5 +1,6 @@ pub mod account; pub mod account2; +pub mod ctoken; pub mod error; pub mod instructions; pub mod token_metadata_ui; diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index 8f4a2353f0..f175d75451 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -9,7 +9,7 @@ pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); pub mod ctoken { - use light_compressed_token_sdk::POOL_SEED; + use light_compressed_token_types::POOL_SEED; use light_compressible::config::CompressibleConfig; use solana_pubkey::Pubkey; From 5df92f9feba502efa1ce3a4cc7daa1d38ba5b6a9 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 12 Nov 2025 20:25:17 -0500 Subject: [PATCH 20/28] clean --- sdk-libs/token-client/src/lib.rs | 61 +------------------ .../tests/game_session_tests.rs | 2 +- sdk-tests/csdk-anchor-test/tests/helpers.rs | 2 +- .../tests/idempotency_tests.rs | 2 +- .../tests/multi_account_tests.rs | 10 ++- 5 files changed, 9 insertions(+), 68 deletions(-) diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index f175d75451..b9be729e99 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -1,62 +1,5 @@ pub mod actions; pub mod instructions; -// Re-export the main utility functions for easy access -use solana_pubkey::{pubkey, Pubkey}; - -pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); - -pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); - -pub mod ctoken { - use light_compressed_token_types::POOL_SEED; - use light_compressible::config::CompressibleConfig; - use solana_pubkey::Pubkey; - - use super::{CTOKEN_CPI_AUTHORITY, CTOKEN_PROGRAM_ID}; - - pub const ID: Pubkey = CTOKEN_PROGRAM_ID; - - /// Returns the program ID for the Compressed Token Program - pub fn id() -> Pubkey { - 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 { - Pubkey::find_program_address( - &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], - &id(), - ) - .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 light_compressed_token_sdk::instructions::{ - create_compressed_mint::find_spl_mint_address, derive_cmint_from_spl_mint, - }; - - 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() - } -} +// Re-export ctoken from compressed-token-sdk for convenience +pub use light_compressed_token_sdk::ctoken; diff --git a/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs index f7c7de4e0a..76624099d8 100644 --- a/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs @@ -1,5 +1,6 @@ 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::{ @@ -8,7 +9,6 @@ use light_program_test::{ Indexer, ProgramTestConfig, Rpc, }; use light_sdk::compressible::{CompressAs, CompressibleConfig}; -use light_token_client::ctoken; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; diff --git a/sdk-tests/csdk-anchor-test/tests/helpers.rs b/sdk-tests/csdk-anchor-test/tests/helpers.rs index c4d987407d..9bde8541ef 100644 --- a/sdk-tests/csdk-anchor-test/tests/helpers.rs +++ b/sdk-tests/csdk-anchor-test/tests/helpers.rs @@ -6,6 +6,7 @@ use anchor_lang::{ }; use csdk_anchor_test::{CompressedAccountVariant, GameSession, UserRecord}; 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}; @@ -13,7 +14,6 @@ use light_sdk::{ compressible::CompressibleConfig, instruction::{PackedAccounts, SystemAccountMetaConfig}, }; -use light_token_client::ctoken; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; diff --git a/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs index 661c111e9c..2fade97dc9 100644 --- a/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs @@ -1,6 +1,7 @@ use anchor_lang::{AccountDeserialize, AnchorDeserialize, ToAccountMetas}; use csdk_anchor_test::{CompressedAccountVariant, UserRecord}; use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; use light_compressible_client::CompressibleInstruction; use light_program_test::{ program_test::{ @@ -9,7 +10,6 @@ use light_program_test::{ Indexer, ProgramTestConfig, Rpc, }; use light_sdk::compressible::CompressibleConfig; -use light_token_client::ctoken; use solana_pubkey::Pubkey; use solana_signer::Signer; diff --git a/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs index d4b226a9f7..3a04e74503 100644 --- a/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs @@ -8,8 +8,9 @@ use csdk_anchor_test::{ }; use light_client::indexer::CompressedAccount; use light_compressed_account::address::derive_address; -use light_compressed_token_sdk::instructions::{ - create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address, +use light_compressed_token_sdk::{ + ctoken, + instructions::{create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address}, }; use light_compressed_token_types::CPI_AUTHORITY_PDA; use light_compressible_client::CompressibleInstruction; @@ -29,16 +30,13 @@ use light_sdk::{ token::CTokenDataWithVariant, }; use light_sdk_types::C_TOKEN_PROGRAM_ID; -use light_token_client::ctoken; 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, CTOKEN_RENT_SPONSOR, RENT_SPONSOR, -}; +use helpers::{create_game_session, create_record, ADDRESS_SPACE, RENT_SPONSOR}; // Tests // 1. create and decompress two accounts and compress token accounts after From 58921e67048918a04d148548699c2e861207f244 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 13:16:28 -0500 Subject: [PATCH 21/28] move pack to compressed-token-sdk --- Cargo.lock | 3 + forester/Cargo.toml | 1 + forester/tests/e2e_test.rs | 2 +- .../batched_state_async_indexer_test.rs | 2 +- .../compressed-token-test/tests/v1.rs | 630 ++++++++++-------- program-tests/system-cpi-test/Cargo.toml | 1 + program-tests/system-cpi-test/tests/test.rs | 2 +- .../utils/src/assert_mint_to_compressed.rs | 4 +- program-tests/utils/src/assert_token_tx.rs | 2 +- program-tests/utils/src/assert_transfer2.rs | 22 +- program-tests/utils/src/conversions.rs | 29 +- program-tests/utils/src/e2e_test_env.rs | 2 +- program-tests/utils/src/spl.rs | 2 +- sdk-libs/client/Cargo.toml | 1 + sdk-libs/client/src/indexer/types.rs | 28 +- .../src/instructions/decompress_full.rs | 2 +- .../src/instructions/mod.rs | 10 - sdk-libs/compressed-token-sdk/src/lib.rs | 6 +- sdk-libs/compressed-token-sdk/src/pack.rs | 330 +++++++++ .../compressed-token-sdk/src/token_pool.rs | 15 + .../tests/pack_test.rs | 11 +- sdk-libs/program-test/Cargo.toml | 4 +- .../program-test/src/indexer/extensions.rs | 2 +- .../program-test/src/indexer/test_indexer.rs | 6 +- .../src/program_test/extensions.rs | 2 +- sdk-libs/sdk/src/lib.rs | 1 - sdk-libs/sdk/src/token.rs | 222 ------ .../src/instructions/create_spl_mint.rs | 9 +- .../src/instructions/mint_action.rs | 9 +- .../src/instructions/mint_to_compressed.rs | 6 +- sdk-tests/client-test/Cargo.toml | 1 + sdk-tests/client-test/tests/light_client.rs | 6 +- .../client-test/tests/light_program_test.rs | 6 +- .../decompress_accounts_idempotent.rs | 4 +- sdk-tests/csdk-anchor-test/src/state.rs | 28 +- .../tests/game_session_tests.rs | 2 +- .../tests/idempotency_tests.rs | 4 +- .../tests/multi_account_tests.rs | 2 +- 38 files changed, 803 insertions(+), 616 deletions(-) create mode 100644 sdk-libs/compressed-token-sdk/src/pack.rs rename sdk-libs/{compressible-client => compressed-token-sdk}/tests/pack_test.rs (93%) delete mode 100644 sdk-libs/sdk/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index 75c33e5587..8b1d3af669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1379,6 +1379,7 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressed-token", + "light-compressed-token-sdk", "light-hasher", "light-indexed-array", "light-macros", @@ -3574,6 +3575,7 @@ dependencies = [ "bytemuck", "lazy_static", "light-compressed-account", + "light-compressed-token-sdk", "light-concurrent-merkle-tree", "light-event", "light-hasher", @@ -10279,6 +10281,7 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressed-token", + "light-compressed-token-sdk", "light-hasher", "light-merkle-tree-metadata", "light-program-test", 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..9b32f17130 100644 --- a/forester/tests/e2e_test.rs +++ b/forester/tests/e2e_test.rs @@ -36,10 +36,10 @@ use light_compressed_token::process_transfer::{ TokenTransferOutputData, }; use light_ctoken_types::state::TokenDataVersion; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; 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/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index 287aa2089d..f99ad824b7 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,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); decompress_test( &payer, &mut rpc, @@ -1379,7 +1380,9 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1595,11 +1598,12 @@ async fn test_mint_to_and_burn_from_all_token_pools() { iterator }; for i in iterator { - let accounts: Vec = test_indexer - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .into(); + let accounts: Vec = + test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_account = accounts[0].clone(); let change_account_merkle_tree = input_compressed_account .compressed_account @@ -1690,11 +1694,12 @@ 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 - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let accounts: Vec = + test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_account = accounts .iter() .filter(|x| x.token_data.amount != 0) @@ -1742,11 +1747,12 @@ async fn test_multiple_decompression() { // Decompress from all token pools { - let all_accounts: Vec = test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let all_accounts: Vec = + test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = all_accounts[0..4].to_vec(); let amount = input_compressed_accounts .iter() @@ -1770,11 +1776,12 @@ async fn test_multiple_decompression() { Some(add_token_pool_accounts.clone()), ) .await; - let all_accounts: Vec = test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let all_accounts: Vec = + test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = all_accounts .iter() .filter(|x| x.token_data.amount != 0) @@ -1839,12 +1846,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +1876,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +1907,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +1990,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2020,25 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2069,25 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2120,25 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2235,9 @@ async fn test_approve_failing() { ) .await; - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2506,12 +2525,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2556,18 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2653,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2681,9 @@ async fn test_revoke_failing() { .await; } - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2855,12 +2879,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2908,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2937,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +2971,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3022,19 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3075,18 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3159,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3188,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3223,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3261,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3300,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3338,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3377,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3416,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3450,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3531,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3556,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3585,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3613,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3638,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3725,16 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +3992,16 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +4019,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +4167,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +4301,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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 +5554,13 @@ 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< + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, + > = 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]; @@ -5647,7 +5697,7 @@ async fn batch_compress_with_batched_tree() { for i in 0..num_recipients { let recipient_compressed_token_accounts: Vec< - light_sdk::token::TokenDataWithMerkleContext, + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, > = test_indexer .get_compressed_token_accounts_by_owner(&recipients[i as usize], None, None) .await @@ -5655,7 +5705,7 @@ async fn batch_compress_with_batched_tree() { .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), @@ -5715,7 +5765,7 @@ async fn batch_compress_with_batched_tree() { for recipient in &recipients { let recipient_compressed_token_accounts: Vec< - light_sdk::token::TokenDataWithMerkleContext, + light_compressed_token_sdk::compat::TokenDataWithMerkleContext, > = test_indexer .get_compressed_token_accounts_by_owner(recipient, None, None) .await @@ -5723,7 +5773,7 @@ async fn batch_compress_with_batched_tree() { .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/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 9703f15b9e..52c3bf5fa4 100644 --- a/program-tests/utils/src/assert_mint_to_compressed.rs +++ b/program-tests/utils/src/assert_mint_to_compressed.rs @@ -51,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..7f51079362 100644 --- a/program-tests/utils/src/conversions.rs +++ b/program-tests/utils/src/conversions.rs @@ -1,5 +1,4 @@ use light_ctoken_types::state::{CompressedTokenAccountState, TokenData as ProgramTokenData}; -use light_sdk::{self as sdk}; // pub fn sdk_to_program_merkle_context( // sdk_merkle_context: sdk::merkle_context::MerkleContext, @@ -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/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index 0e14530484..4578878857 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -47,6 +47,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/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/compressed-token-sdk/src/instructions/decompress_full.rs b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs index 5a5f29ff14..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::{ diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index 918abded89..ade4b9ee1c 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -62,13 +62,3 @@ pub use update_compressed_mint::{ 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/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index c9309ce47a..3eb913ffdc 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -3,6 +3,7 @@ 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; @@ -12,5 +13,8 @@ pub mod utils; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -pub use light_compressed_token_types::*; +// Re-export all types and utilities +pub use pack::*; +// Re-export Pack/Unpack traits at crate root for convenience +pub use pack::{Pack, Unpack}; pub use utils::*; 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..ce5cf41f97 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/pack.rs @@ -0,0 +1,330 @@ +//! Pack implementation for TokenData types for c-tokens. +use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +pub use light_compressed_token_types::*; +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/compressible-client/tests/pack_test.rs b/sdk-libs/compressed-token-sdk/tests/pack_test.rs similarity index 93% rename from sdk-libs/compressible-client/tests/pack_test.rs rename to sdk-libs/compressed-token-sdk/tests/pack_test.rs index 88d9c330b6..67f7502bc4 100644 --- a/sdk-libs/compressible-client/tests/pack_test.rs +++ b/sdk-libs/compressed-token-sdk/tests/pack_test.rs @@ -1,10 +1,11 @@ #[cfg(test)] mod tests { - use light_sdk::{ - compressible::Pack, - instruction::PackedAccounts, - token::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + + use light_compressed_token_sdk::{ + compat::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + Pack, }; + use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; #[test] @@ -33,7 +34,7 @@ mod tests { assert_eq!(packed.delegate, 2); // Third pubkey gets index 2 assert_eq!(packed.amount, 1000); assert!(packed.has_delegate); - assert_eq!(packed.version, 2); + assert_eq!(packed.version, 3); // TokenDataVersion::ShaFlat // Verify remaining_accounts contains the pubkeys let pubkeys = remaining_accounts.packed_pubkeys(); diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 2c4c68dcbb..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 } 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/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/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index 47f10abd7a..17f8edbf52 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -99,7 +99,6 @@ pub mod cpi; pub mod error; pub mod instruction; pub mod legacy; -pub mod token; pub mod transfer; pub mod utils; diff --git a/sdk-libs/sdk/src/token.rs b/sdk-libs/sdk/src/token.rs deleted file mode 100644 index 8e539d55f2..0000000000 --- a/sdk-libs/sdk/src/token.rs +++ /dev/null @@ -1,222 +0,0 @@ -use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; -use light_hasher::{sha256::Sha256BE, HasherError}; -use solana_account_info::AccountInfo; - -use crate::{ - compressible::compression_info::{Pack, Unpack}, - instruction::PackedAccounts, - 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. Do this in a separate PR. -#[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) - } - } -} - -/// Standard Pack for c-token accounts. -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), - amount: self.amount, - has_delegate: self.delegate.is_some(), - delegate: if let Some(delegate) = self.delegate { - remaining_accounts.insert_or_get(delegate) - } else { - 0 // Unused when has_delegate is false - }, - mint: remaining_accounts.insert_or_get_read_only(self.mint), - version: 3, // TokenDataVersion::ShaFlat. Default version for compressed token accounts - } - } -} - -impl Unpack for TokenData { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -/// Standard Unpack for c-token accounts. -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(solana_program_error::ProgramError::InvalidAccountData)? - .key, - amount: self.amount, - delegate: if self.has_delegate { - Some( - *remaining_accounts - .get(self.delegate as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key, - ) - } else { - None - }, - mint: *remaining_accounts - .get(self.mint as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key, - state: AccountState::Initialized, // Default state for unpacked - tlv: None, // No TLV data in packed version - }) - } -} - -/// 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, -} - -/// Standard Pack for c-token accounts with variant. -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), - } - } -} - -/// Standard Unpack for c-token accounts with variant. -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)?, - }) - } -} - -/// Standard Pack for token data with variant. -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), - } - } -} - -/// Standard Unpack for c-token accounts with variant. -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 these aliases and fix renaming in a 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/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 b49a1e49ec..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_cmint_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}, 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/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs index f67baabb55..de7bb34f5f 100644 --- a/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs +++ b/sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs @@ -156,7 +156,7 @@ pub fn decompress_accounts_idempotent<'info>( ctoken_config: &anchor_lang::prelude::AccountInfo<'info>, config: &anchor_lang::prelude::AccountInfo<'info>, ctoken_accounts: Vec<( - light_sdk::token::PackedCTokenData, + light_compressed_token_sdk::compat::PackedCTokenData, light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, )>, proof: light_sdk::instruction::ValidityProof, @@ -298,7 +298,7 @@ pub fn decompress_accounts_idempotent<'info>( } let mut ctoken_accounts: Vec<( - light_sdk::token::PackedCTokenData, + 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); diff --git a/sdk-tests/csdk-anchor-test/src/state.rs b/sdk-tests/csdk-anchor-test/src/state.rs index e0ceb446a4..dd3446c331 100644 --- a/sdk-tests/csdk-anchor-test/src/state.rs +++ b/sdk-tests/csdk-anchor-test/src/state.rs @@ -3,11 +3,13 @@ // 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, Unpack, + CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack as SdkPack, + Unpack as SdkUnpack, }, instruction::{ account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, @@ -34,8 +36,8 @@ pub enum CompressedAccountVariant { PackedGameSession(PackedGameSession), PlaceholderRecord(PlaceholderRecord), PackedPlaceholderRecord(PackedPlaceholderRecord), - PackedCTokenData(light_sdk::token::PackedCTokenData), - CTokenData(light_sdk::token::CTokenData), + PackedCTokenData(light_compressed_token_sdk::compat::PackedCTokenData), + CTokenData(light_compressed_token_sdk::compat::CTokenData), } impl Default for CompressedAccountVariant { @@ -120,7 +122,7 @@ impl Size for CompressedAccountVariant { // Pack implementation for CompressedAccountVariant // This delegates to the underlying type's Pack implementation -impl Pack for CompressedAccountVariant { +impl SdkPack for CompressedAccountVariant { type Packed = Self; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { @@ -141,7 +143,7 @@ impl Pack for CompressedAccountVariant { // Unpack implementation for CompressedAccountVariant // This delegates to the underlying type's Unpack implementation -impl Unpack for CompressedAccountVariant { +impl SdkUnpack for CompressedAccountVariant { type Unpacked = Self; fn unpack( @@ -247,7 +249,7 @@ pub struct PackedUserRecord { } // Identity Pack implementation - no custom packing needed for PDA types -impl Pack for UserRecord { +impl SdkPack for UserRecord { type Packed = PackedUserRecord; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { @@ -261,7 +263,7 @@ impl Pack for UserRecord { } // Identity Unpack implementation - PDA types are sent unpacked -impl Unpack for UserRecord { +impl SdkUnpack for UserRecord { type Unpacked = Self; fn unpack( @@ -273,7 +275,7 @@ impl Unpack for UserRecord { } // Identity Pack implementation - no custom packing needed for PDA types -impl Pack for PackedUserRecord { +impl SdkPack for PackedUserRecord { type Packed = Self; fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { @@ -282,7 +284,7 @@ impl Pack for PackedUserRecord { } // Identity Unpack implementation - PDA types are sent unpacked -impl Unpack for PackedUserRecord { +impl SdkUnpack for PackedUserRecord { type Unpacked = UserRecord; fn unpack( @@ -367,7 +369,7 @@ impl CompressAs for GameSession { } // Identity Pack implementation - no custom packing needed for PDA types -impl Pack for GameSession { +impl SdkPack for GameSession { type Packed = Self; fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { @@ -376,7 +378,7 @@ impl Pack for GameSession { } // Identity Unpack implementation - PDA types are sent unpacked -impl Unpack for GameSession { +impl SdkUnpack for GameSession { type Unpacked = Self; fn unpack( @@ -443,7 +445,7 @@ impl CompressAs for PlaceholderRecord { } // Identity Pack implementation - no custom packing needed for PDA types -impl Pack for PlaceholderRecord { +impl SdkPack for PlaceholderRecord { type Packed = Self; fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { @@ -452,7 +454,7 @@ impl Pack for PlaceholderRecord { } // Identity Unpack implementation - PDA types are sent unpacked -impl Unpack for PlaceholderRecord { +impl SdkUnpack for PlaceholderRecord { type Unpacked = Self; fn unpack( diff --git a/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs index 76624099d8..c60f939638 100644 --- a/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/game_session_tests.rs @@ -14,7 +14,7 @@ use solana_pubkey::Pubkey; use solana_signer::Signer; mod helpers; -use helpers::{create_game_session, ADDRESS_SPACE, CTOKEN_RENT_SPONSOR, RENT_SPONSOR}; +use helpers::{create_game_session, ADDRESS_SPACE, RENT_SPONSOR}; // Test: create, decompress game session, compress with custom data at // compression diff --git a/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs index 2fade97dc9..e22029a95c 100644 --- a/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs @@ -14,9 +14,7 @@ use solana_pubkey::Pubkey; use solana_signer::Signer; mod helpers; -use helpers::{ - create_record, decompress_single_user_record, ADDRESS_SPACE, CTOKEN_RENT_SPONSOR, RENT_SPONSOR, -}; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; #[tokio::test] async fn test_double_decompression_attack() { diff --git a/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs index 3a04e74503..dda1451401 100644 --- a/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs +++ b/sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs @@ -11,6 +11,7 @@ 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; @@ -27,7 +28,6 @@ use light_program_test::{ use light_sdk::{ compressible::CompressibleConfig, instruction::{PackedAccounts, SystemAccountMetaConfig}, - token::CTokenDataWithVariant, }; use light_sdk_types::C_TOKEN_PROGRAM_ID; use solana_instruction::Instruction; From ad61398b798d2d5cd9958c556c43fa0ebe0c634e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 13:46:26 -0500 Subject: [PATCH 22/28] clean --- .../src/instruction_data/compressed_proof.rs | 90 ------------------- .../src/instructions/transfer_interface.rs | 4 +- sdk-libs/sdk/src/lib.rs | 2 + sdk-libs/sdk/src/proof.rs | 88 ++++++++++++++++++ sdk-tests/csdk-anchor-test/package.json | 2 +- 5 files changed, 93 insertions(+), 93 deletions(-) create mode 100644 sdk-libs/sdk/src/proof.rs diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index 275ffe970a..90cc618388 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -138,93 +138,3 @@ impl Into> for ValidityProof { self.0 } } - -pub mod borsh_compat { - #[cfg_attr( - all(feature = "std", feature = "anchor"), - derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize) - )] - #[cfg_attr( - not(feature = "anchor"), - derive(borsh::BorshDeserialize, borsh::BorshSerialize) - )] - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - 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], - } - } - } - - #[cfg_attr( - all(feature = "std", feature = "anchor"), - derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize) - )] - #[cfg_attr( - not(feature = "anchor"), - derive(borsh::BorshDeserialize, borsh::BorshSerialize) - )] - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] - /// 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: super::CompressedProof) -> Self { - Self { - a: proof.a, - b: proof.b, - c: proof.c, - } - } - } - - impl From for super::CompressedProof { - fn from(proof: CompressedProof) -> Self { - Self { - a: proof.a, - b: proof.b, - c: proof.c, - } - } - } - - impl From for ValidityProof { - fn from(proof: super::ValidityProof) -> Self { - Self(proof.0.map(|p| p.into())) - } - } - - impl From for super::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/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs index 0dbca6282a..d82207f294 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -1,4 +1,4 @@ -use light_compressed_account::instruction_data::compressed_proof::borsh_compat::ValidityProof; +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; @@ -144,7 +144,7 @@ pub fn create_transfer_ctoken_to_spl_instruction( }; let inputs = Transfer2Inputs { - validity_proof: ValidityProof::new(None).into(), + validity_proof: ValidityProof::new(None), transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( payer, diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index 17f8edbf52..79934ccd9a 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -99,9 +99,11 @@ pub mod cpi; pub mod error; pub mod instruction; pub mod legacy; +pub mod proof; pub mod transfer; pub mod utils; +pub use proof::borsh_compat; pub mod compressible; #[cfg(feature = "merkle-tree")] pub mod merkle_tree; 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-tests/csdk-anchor-test/package.json b/sdk-tests/csdk-anchor-test/package.json index 8be19c2e7b..8f410b2082 100644 --- a/sdk-tests/csdk-anchor-test/package.json +++ b/sdk-tests/csdk-anchor-test/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "license": "Apache-2.0", "scripts": { - "build": "cd sdk-tests/csdk-anchor-test && cargo build-sbf", + "build": "cargo build-sbf", "test": "cargo test-sbf -p csdk-anchor-test -- --nocapture" }, "nx": {} From 881b743c0e41b90d38948afcd894714a4d86c7a5 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 14:13:49 -0500 Subject: [PATCH 23/28] clean --- .github/workflows/programs.yml | 9 +- .github/workflows/sdk-tests.yml | 8 - cli/package.json | 2 +- cli/scripts/copyLocalProgramBinaries.sh | 2 +- .../src/instruction_data/compressed_proof.rs | 3 + .../compressed-token-test/tests/v1.rs | 228 +++++------------- 6 files changed, 73 insertions(+), 179 deletions(-) diff --git a/.github/workflows/programs.yml b/.github/workflows/programs.yml index 52d4a52c43..a6b5239c09 100644 --- a/.github/workflows/programs.yml +++ b/.github/workflows/programs.yml @@ -111,14 +111,7 @@ jobs: done echo "Test passed on attempt $attempt" else - # Build account-compression with test feature before running its tests - if [ "$subtest" == "cargo-test-sbf -p account-compression-test" ]; then - cd programs/account-compression && cargo build-sbf --features 'test, migrate-state' && cd ../.. - # Ensure the program executes with the 'test' cfg active during this subtest - RUSTFLAGS="-D warnings --cfg feature=\"test\"" eval "$subtest" - else - RUSTFLAGS="-D warnings" eval "$subtest" - fi + RUSTFLAGS="-D warnings" eval "$subtest" if [ "$subtest" == "cargo-test-sbf -p e2e-test" ]; then pnpm --filter @lightprotocol/programs run build-compressed-token-small RUSTFLAGS="-D warnings" eval "$subtest -- --test test_10_all" diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 15562bc0a2..1522fe8465 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -79,14 +79,6 @@ jobs: run: | npx nx build @lightprotocol/zk-compression-cli - - name: Prepare csdk-anchor-test artifact - if: matrix.program == 'anchor & pinocchio' - run: | - - cd sdk-tests/csdk-anchor-test && cargo build-sbf - mkdir -p target/sbpf-solana-solana/release - cp -f target/deploy/csdk_anchor_test.so target/sbpf-solana-solana/release/csdk_anchor_test.so - - name: Run sub-tests for ${{ matrix.program }} if: matrix.sub-tests != null run: | diff --git a/cli/package.json b/cli/package.json index b6a8f7052b..57a1e2518f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -166,4 +166,4 @@ } } } -} \ No newline at end of file +} diff --git a/cli/scripts/copyLocalProgramBinaries.sh b/cli/scripts/copyLocalProgramBinaries.sh index c67fe2ef85..deddd04e8a 100755 --- a/cli/scripts/copyLocalProgramBinaries.sh +++ b/cli/scripts/copyLocalProgramBinaries.sh @@ -13,4 +13,4 @@ for key in $keys do cp "$root_dir/target/deploy/$key.so" "$out_dir"/"$key".so done -cp "$root_dir"/third-party/solana-program-library/spl_noop.so "$out_dir"/spl_noop.so \ No newline at end of file +cp "$root_dir"/third-party/solana-program-library/spl_noop.so "$out_dir"/spl_noop.so diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index 90cc618388..de6c7c6a15 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -40,6 +40,7 @@ impl Default for CompressedProof { } impl CompressedProof { + /// Convert the proof to a fixed-size byte array [u8; 128] pub fn to_array(&self) -> [u8; 128] { let mut result = [0u8; 128]; result[0..32].copy_from_slice(&self.a); @@ -122,6 +123,8 @@ impl From<&Option> for ValidityProof { impl TryFrom<&[u8]> for ValidityProof { type Error = crate::CompressedAccountError; + /// Convert bytes to ValidityProof. + /// Empty slice returns None, otherwise attempts to parse as CompressedProof and returns Some. fn try_from(bytes: &[u8]) -> Result { if bytes.is_empty() { Ok(Self(None)) diff --git a/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index f99ad824b7..ffceb76d85 100644 --- a/program-tests/compressed-token-test/tests/v1.rs +++ b/program-tests/compressed-token-test/tests/v1.rs @@ -667,9 +667,7 @@ async fn test_wrapped_sol() { None, ) .await; - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) .await .unwrap() @@ -1380,9 +1378,7 @@ async fn perform_transfer_22_test( for _ in 0..outputs { recipients.push(Pubkey::new_unique()); } - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1598,12 +1594,11 @@ async fn test_mint_to_and_burn_from_all_token_pools() { iterator }; for i in iterator { - let accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .into(); + let accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_account = accounts[0].clone(); let change_account_merkle_tree = input_compressed_account .compressed_account @@ -1694,12 +1689,11 @@ 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 - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_account = accounts .iter() .filter(|x| x.token_data.amount != 0) @@ -1747,12 +1741,11 @@ async fn test_multiple_decompression() { // Decompress from all token pools { - let all_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let all_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = all_accounts[0..4].to_vec(); let amount = input_compressed_accounts .iter() @@ -1776,12 +1769,11 @@ async fn test_multiple_decompression() { Some(add_token_pool_accounts.clone()), ) .await; - let all_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let all_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = all_accounts .iter() .filter(|x| x.token_data.amount != 0) @@ -1846,9 +1838,7 @@ async fn test_delegation( .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1876,9 +1866,7 @@ async fn test_delegation( let recipient = Pubkey::new_unique(); // 2. Transfer partial delegated amount { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1907,9 +1895,7 @@ async fn test_delegation( } // 3. Transfer full delegated amount { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1990,9 +1976,7 @@ async fn test_delegation_mixed() { .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2020,9 +2004,7 @@ async fn test_delegation_mixed() { let recipient = Pubkey::new_unique(); // 2. Transfer partial delegated amount with delegate change account { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2032,9 +2014,7 @@ async fn test_delegation_mixed() { .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let delegate_input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) .await .unwrap() @@ -2069,9 +2049,7 @@ async fn test_delegation_mixed() { let recipient = Pubkey::new_unique(); // 3. Transfer partial delegated amount without delegate change account { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2081,9 +2059,7 @@ async fn test_delegation_mixed() { .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let delegate_input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) .await .unwrap() @@ -2120,9 +2096,7 @@ async fn test_delegation_mixed() { } // 3. Transfer full delegated amount { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2132,9 +2106,7 @@ async fn test_delegation_mixed() { .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let delegate_input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) .await .unwrap() @@ -2235,9 +2207,7 @@ async fn test_approve_failing() { ) .await; - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2525,9 +2495,7 @@ async fn test_revoke(num_inputs: usize, mint_amount: u64, delegated_amount: u64) .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2556,9 +2524,7 @@ async fn test_revoke(num_inputs: usize, mint_amount: u64, delegated_amount: u64) } // 2. Revoke { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2653,9 +2619,7 @@ async fn test_revoke_failing() { .await; // Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2681,9 +2645,7 @@ async fn test_revoke_failing() { .await; } - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2879,9 +2841,7 @@ async fn test_burn() { .await; // 1. Burn tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2908,9 +2868,7 @@ async fn test_burn() { } // 2. Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2937,9 +2895,7 @@ async fn test_burn() { } // 3. Burn delegated tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2971,9 +2927,7 @@ async fn test_burn() { } // 3. Burn all delegated tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3022,9 +2976,7 @@ async fn test_burn() { ) .await .unwrap(); - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3075,9 +3027,7 @@ 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< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3159,9 +3109,7 @@ async fn failing_tests_burn() { .await; // Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3188,9 +3136,7 @@ async fn failing_tests_burn() { } // 1. invalid proof { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3223,9 +3169,7 @@ async fn failing_tests_burn() { } // 2. Signer is delegate but token data has no delegate. { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3261,9 +3205,7 @@ async fn failing_tests_burn() { } // 3. Signer is delegate but token data has no delegate. { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3300,9 +3242,7 @@ async fn failing_tests_burn() { } // 4. invalid authority (use delegate as authority) { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3338,9 +3278,7 @@ async fn failing_tests_burn() { } // 5. invalid mint { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3377,9 +3315,7 @@ async fn failing_tests_burn() { } // 6. invalid change merkle tree { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3416,9 +3352,7 @@ async fn failing_tests_burn() { } // 6. invalid token pool (not initialized) { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3450,9 +3384,7 @@ async fn failing_tests_burn() { } // 7. invalid token pool (invalid mint) { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3531,9 +3463,7 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { .await; // 1. Freeze tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3556,9 +3486,7 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 2. Thaw tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3585,9 +3513,7 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 3. Delegate tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3613,9 +3539,7 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 4. Freeze delegated tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3638,9 +3562,7 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 5. Thaw delegated tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3725,9 +3647,7 @@ async fn test_failing_freeze() { ) .await; - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = vec![test_indexer + let input_compressed_accounts: Vec = vec![test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -3992,9 +3912,7 @@ async fn test_failing_thaw() { // Freeze tokens { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = vec![test_indexer + let input_compressed_accounts: Vec = vec![test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -4019,9 +3937,7 @@ async fn test_failing_thaw() { .await; } - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -4167,9 +4083,7 @@ async fn test_failing_thaw() { } // 4. thaw compressed account which is not frozen { - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -4301,9 +4215,7 @@ async fn test_failing_decompression() { ) .await .unwrap(); - let input_compressed_account: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_account: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -5554,9 +5466,7 @@ async fn test_transfer_with_photon_and_batched_tree() { Pubkey::from_str("24fLJv6tHmsxQg5vDD7XWy85TMhFzJdkqZ9Ta3LtVReU").unwrap(), ]; println!("recipients {:?}", recipients); - let input_compressed_accounts: Vec< - light_compressed_token_sdk::compat::TokenDataWithMerkleContext, - > = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) .await .unwrap() @@ -5696,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_compressed_token_sdk::compat::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_compressed_token_sdk::compat::TokenData { + let expected_token_data = tokenData { mint, owner: recipients[i as usize], amount: (i + 1), @@ -5764,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_compressed_token_sdk::compat::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_compressed_token_sdk::compat::TokenData { + let expected_token_data = tokenData { mint, owner: *recipient, amount, From 35d68b1dba400d9686fb40e5a41c0eda8662aa58 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 14:31:30 -0500 Subject: [PATCH 24/28] clean --- program-tests/compressed-token-test/tests/v1.rs | 4 ++-- program-tests/utils/src/conversions.rs | 2 +- .../account-compression/src/instructions/migrate_state.rs | 4 ---- programs/package.json | 7 +++---- .../src/instructions/transfer_interface.rs | 2 +- sdk-libs/program-test/src/program_test/test_rpc.rs | 2 +- 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index ffceb76d85..c037652362 100644 --- a/program-tests/compressed-token-test/tests/v1.rs +++ b/program-tests/compressed-token-test/tests/v1.rs @@ -5613,7 +5613,7 @@ async fn batch_compress_with_batched_tree() { .into(); assert_eq!(recipient_compressed_token_accounts.len(), 1); let recipient_compressed_token_account = &recipient_compressed_token_accounts[0]; - let expected_token_data = tokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint, owner: recipients[i as usize], amount: (i + 1), @@ -5679,7 +5679,7 @@ async fn batch_compress_with_batched_tree() { .into(); assert_eq!(recipient_compressed_token_accounts.len(), 1); let recipient_compressed_token_account = &recipient_compressed_token_accounts[0]; - let expected_token_data = tokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint, owner: *recipient, amount, diff --git a/program-tests/utils/src/conversions.rs b/program-tests/utils/src/conversions.rs index 7f51079362..b12d6ed4a7 100644 --- a/program-tests/utils/src/conversions.rs +++ b/program-tests/utils/src/conversions.rs @@ -1,5 +1,5 @@ use light_ctoken_types::state::{CompressedTokenAccountState, TokenData as ProgramTokenData}; - +// 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 { diff --git a/programs/account-compression/src/instructions/migrate_state.rs b/programs/account-compression/src/instructions/migrate_state.rs index d48c027e08..3f62280f25 100644 --- a/programs/account-compression/src/instructions/migrate_state.rs +++ b/programs/account-compression/src/instructions/migrate_state.rs @@ -128,11 +128,7 @@ fn migrate_state( nullified_leaves_indices: vec![migrate_leaf_params.leaf_index], seq: merkle_tree.sequence_number() as u64, }; - #[cfg(target_os = "solana")] let slot = Clock::get()?.slot; - // Mock slot for unit tests - #[cfg(not(target_os = "solana"))] - let slot = 0u64; // 3. Inserts the leaf in the output queue. output_queue .insert_into_current_batch(&migrate_leaf_params.leaf, &slot) diff --git a/programs/package.json b/programs/package.json index 7cd2a92d54..244d22318d 100644 --- a/programs/package.json +++ b/programs/package.json @@ -3,13 +3,12 @@ "version": "0.3.0", "license": "Apache-2.0", "scripts": { - "build": "ROOT_DIR=$(git rev-parse --show-toplevel) && export SBF_OUT_DIR=\"$ROOT_DIR/target/deploy\" && mkdir -p \"$SBF_OUT_DIR\" && cd system/ && cargo build-sbf && cd .. && cd account-compression/ && cargo build-sbf --features 'test, migrate-state' && cd .. && cd registry/ && cargo build-sbf && cd .. && cd compressed-token/program && cargo build-sbf && cd ../../..", + "build": "cd system/ && cargo build-sbf && cd .. && cd account-compression/ && cargo build-sbf --features 'test, migrate-state' && cd .. && cd registry/ && cargo build-sbf && cd .. && cd compressed-token/program && cargo build-sbf && cd ../../..", "build-compressed-token-small": "cd compressed-token && cargo build-sbf --features cpi-without-program-ids && cd ..", "build-system": "anchor build --program-name light_system_program -- --features idl-build custom-heap", "build-compressed-token": "anchor build --program-name light_compressed_token -- --features idl-build custom-heap", - "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-account-compression && pnpm test-compressed-token && pnpm e2e-test && pnpm test-registry && pnpm sdk-test-program && pnpm test-system && pnpm test-system-cpi && pnpm test-batched-merkle-tree", - "test-account-compression": "cd account-compression && cargo build-sbf --features 'test, migrate-state' && cd .. && cargo-test-sbf -p account-compression-test", - "test-batched-merkle-tree": "cd account-compression && cargo build-sbf --features 'test, migrate-state' && cd .. && cargo test-sbf -p batched-merkle-tree-test -- --skip test_simulate_transactions --skip test_e2e", + "test": "RUSTFLAGS=\"-D warnings\" && pnpm test-account-compression && pnpm test-compressed-token && pnpm e2e-test && pnpm test-registry && pnpm sdk-test-program && pnpm test-system && pnpm test-system-cpi", + "test-account-compression": "cargo-test-sbf -p account-compression-test", "test-compressed-token": "cargo test-sbf -p compressed-token-test", "e2e-test": "cargo-test-sbf -p e2e-test", "test-registry": "cargo-test-sbf -p registry-test", diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs index d82207f294..414d5012ff 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -71,7 +71,7 @@ pub fn create_transfer_spl_to_ctoken_instruction( }; let inputs = Transfer2Inputs { - validity_proof: ValidityProof::new(None).into(), + validity_proof: ValidityProof::new(None), transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( payer, 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 09076e4d3c..8660095ab3 100644 --- a/sdk-libs/program-test/src/program_test/test_rpc.rs +++ b/sdk-libs/program-test/src/program_test/test_rpc.rs @@ -158,7 +158,7 @@ impl TestRpc for LightProgramTest { let mut store = CompressibleAccountStore::new(); crate::compressible::claim_and_compress(self, &mut store).await?; for program_id in self.auto_compress_programs.clone() { - let _ = crate::compressible::auto_compress_program_pdas(self, program_id).await; + crate::compressible::auto_compress_program_pdas(self, program_id).await?; } Ok(()) } From 1c6e634160be3b9da714adaf3469e5848cc04cbf Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 14:52:07 -0500 Subject: [PATCH 25/28] clean --- sdk-libs/compressed-token-sdk/src/ctoken.rs | 20 ++----------------- sdk-libs/compressed-token-sdk/src/lib.rs | 12 ++++++----- sdk-libs/compressed-token-sdk/src/pack.rs | 1 - .../src/get_compressible_account.rs | 1 + 4 files changed, 10 insertions(+), 24 deletions(-) diff --git a/sdk-libs/compressed-token-sdk/src/ctoken.rs b/sdk-libs/compressed-token-sdk/src/ctoken.rs index f22e5be531..5e02ced318 100644 --- a/sdk-libs/compressed-token-sdk/src/ctoken.rs +++ b/sdk-libs/compressed-token-sdk/src/ctoken.rs @@ -6,11 +6,9 @@ pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); -pub const ID: Pubkey = CTOKEN_PROGRAM_ID; - /// Returns the program ID for the Compressed Token Program pub fn id() -> Pubkey { - ID + CTOKEN_PROGRAM_ID } /// Return the cpi authority pda of the Compressed Token Program. @@ -24,11 +22,7 @@ pub fn get_token_pool_address_and_bump(mint: &Pubkey) -> (Pubkey, u8) { /// Returns the associated ctoken address for a given owner and mint. pub fn get_associated_ctoken_address(owner: &Pubkey, mint: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], - &id(), - ) - .0 + get_associated_ctoken_address_and_bump(owner, mint).0 } /// Returns the associated ctoken address and bump for a given owner and mint. @@ -54,13 +48,3 @@ pub fn rent_sponsor_pda() -> Pubkey { pub fn compression_authority_pda() -> Pubkey { CompressibleConfig::ctoken_v1_compression_authority_pda() } - -/// Alias for `rent_sponsor_pda()` for convenience -pub fn rent_sponsor() -> Pubkey { - rent_sponsor_pda() -} - -/// Alias for `compression_authority_pda()` for convenience -pub fn compression_authority() -> Pubkey { - compression_authority_pda() -} diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 3eb913ffdc..c38cdd1255 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -13,8 +13,10 @@ pub mod utils; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -// Re-export all types and utilities -pub use pack::*; -// Re-export Pack/Unpack traits at crate root for convenience -pub use pack::{Pack, Unpack}; -pub use utils::*; +// 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 index ce5cf41f97..60eeab2330 100644 --- a/sdk-libs/compressed-token-sdk/src/pack.rs +++ b/sdk-libs/compressed-token-sdk/src/pack.rs @@ -1,6 +1,5 @@ //! Pack implementation for TokenData types for c-tokens. use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; -pub use light_compressed_token_types::*; pub use light_ctoken_types::state::TokenData; use light_ctoken_types::state::TokenDataVersion; use light_sdk::{ diff --git a/sdk-libs/compressible-client/src/get_compressible_account.rs b/sdk-libs/compressible-client/src/get_compressible_account.rs index 70f063478d..cd056e5bbd 100644 --- a/sdk-libs/compressible-client/src/get_compressible_account.rs +++ b/sdk-libs/compressible-client/src/get_compressible_account.rs @@ -126,6 +126,7 @@ where 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 From 80bfac3cbfee1cd200108f7b1381ee9e62314eed Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 14:59:32 -0500 Subject: [PATCH 26/28] clean --- Cargo.lock | 1 - Cargo.toml | 1 + sdk-libs/client/Cargo.toml | 4 +--- sdk-libs/client/src/rpc/mod.rs | 2 +- sdk-libs/sdk/src/account.rs | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b1d3af669..8ef338804c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3567,7 +3567,6 @@ dependencies = [ name = "light-client" version = "0.16.0" dependencies = [ - "anchor-lang", "async-trait", "base64 0.13.1", "borsh 0.10.4", diff --git a/Cargo.toml b/Cargo.toml index 6c5781a195..3f679a02fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,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" diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index 4578878857..3f33c3935d 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -36,9 +36,7 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [ "bytemuck", "bincode", ] } -solana-message = "2.2" -# TODO: check if we can move. -anchor-lang = { workspace = true, features = ["idl-build"], optional = true } +solana-message = { workspace = true } # Light Protocol dependencies light-merkle-tree-metadata = { workspace = true, features = ["solana"] } diff --git a/sdk-libs/client/src/rpc/mod.rs b/sdk-libs/client/src/rpc/mod.rs index bfb9210295..724a71c106 100644 --- a/sdk-libs/client/src/rpc/mod.rs +++ b/sdk-libs/client/src/rpc/mod.rs @@ -12,5 +12,5 @@ pub use errors::RpcError; pub use rpc_trait::{LightClientConfig, Rpc}; pub mod get_light_state_tree_infos; -pub use lut::load_lookup_table; pub mod lut; +pub use lut::load_lookup_table; diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index fbb0cea1aa..fc865d689d 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -782,7 +782,7 @@ pub mod __internal { prove_by_index: tree_info.prove_by_index, }, root_index: input_account_meta.get_root_index().unwrap_or_default(), - discriminator: [0u8; 8], // TODO: consider 0 (need adapt client etc) + discriminator: [0u8; 8], } }; let output_account_info = { From 170285fd1c6f5c8181fb8b5cbae5c7cc89adda10 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Thu, 13 Nov 2025 15:11:02 -0500 Subject: [PATCH 27/28] clean --- sdk-libs/sdk/src/compressible/close.rs | 28 ++++++--- sdk-libs/sdk/src/lib.rs | 87 ++++++++++++++++++++------ sdk-libs/token-client/src/lib.rs | 3 +- 3 files changed, 88 insertions(+), 30 deletions(-) diff --git a/sdk-libs/sdk/src/compressible/close.rs b/sdk-libs/sdk/src/compressible/close.rs index 22edc1ffba..95b7e85074 100644 --- a/sdk-libs/sdk/src/compressible/close.rs +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -9,21 +9,31 @@ pub fn close<'info>( ) -> Result<()> { let lamports_to_transfer = info.lamports(); - **info - .try_borrow_mut_lamports() - .map_err(|_| LightSdkError::ConstraintViolation)? = 0; - - **sol_destination - .try_borrow_mut_lamports() - .map_err(|_| LightSdkError::ConstraintViolation)? = sol_destination + let new_destination_lamports = sol_destination .lamports() .checked_add(lamports_to_transfer) .ok_or(LightSdkError::ConstraintViolation)?; - let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + if info.lamports() != lamports_to_transfer { + return Err(LightSdkError::ConstraintViolation); + } - info.assign(&system_program_id); + { + 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/lib.rs b/sdk-libs/sdk/src/lib.rs index 79934ccd9a..87606a1dda 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -1,25 +1,69 @@ -//! SDK for building programs with compressed accounts on Solana. -//! -//! State is stored as hashes in Merkle trees. Validity proofs verify state exists. -//! No rent required. Constant 128-byte proof per transaction. -//! -//! See [zkcompression.com](https://www.zkcompression.com/) for docs and [Program Examples](https://github.com/Lightprotocol/program-examples). +//! The base library to use Compressed Accounts in Solana on-chain Rust and Anchor programs. +//! +//! Compressed Accounts stores state as account hashes in State Merkle trees. +//! and unique addresses in Address Merkle trees. +//! Validity proofs (zero-knowledge proofs) verify that compressed account +//! state exists and new addresses do not exist yet. +//! +//! - No rent exemption payment required. +//! - Constant 128-byte validity proof per transaction for one or multiple compressed accounts and addresses. +//! - Compressed account data is sent as instruction data when accessed. +//! - State and address trees are managed by the protocol. +//! +//! For full program examples, see the [Program Examples](https://github.com/Lightprotocol/program-examples). +//! For detailed documentation, visit [zkcompression.com](https://www.zkcompression.com/). +//! For pinocchio solana program development see [`light-sdk-pinocchio`](https://docs.rs/light-sdk-pinocchio). +//! For rust client developement see [`light-client`](https://docs.rs/light-client). +//! For rust program testing see [`light-program-test`](https://docs.rs/light-program-test). +//! For local test validator with light system programs see [Light CLI](https://www.npmjs.com/package/@lightprotocol/zk-compression-cli). +//! +//! # Using Compressed Accounts in Solana Programs +//! 1. [`Instruction`](crate::instruction) +//! - [`CompressedAccountMeta`](crate::instruction::account_meta::CompressedAccountMeta) - Compressed account metadata structs for instruction data. +//! - [`PackedAccounts`](crate::instruction::PackedAccounts) - Abstraction to prepare accounts offchain for instructions with compressed accounts. +//! - [`ValidityProof`](crate::instruction::ValidityProof) - Proves that new addresses don't exist yet, and compressed account state exists. +//! 2. Compressed Account in Program +//! - [`LightAccount`](crate::account) - Compressed account abstraction similar to anchor Account. +//! - [`derive_address`](crate::address) - Create a compressed account address. +//! - [`LightDiscriminator`] - DeriveMacro to derive a compressed account discriminator. +//! 3. [`Cpi`](crate::cpi) +//! - [`CpiAccounts`](crate::cpi::v1::CpiAccounts) - Prepare accounts to cpi the light system program. +//! - [`LightSystemProgramCpi`](crate::cpi::v1::LightSystemProgramCpi) - Prepare instruction data to cpi the light system program. +//! - [`InvokeLightSystemProgram::invoke`](crate::cpi) - Invoke the light system program via cpi. +//! +//! ```text +//! ├─ 𝐂𝐥𝐢𝐞𝐧𝐭 +//! │ ├─ Get ValidityProof from RPC. +//! │ ├─ pack accounts with PackedAccounts into PackedAddressTreeInfo and PackedStateTreeInfo. +//! │ ├─ pack CompressedAccountMeta. +//! │ ├─ Build Instruction from PackedAccounts and CompressedAccountMetas. +//! │ └─ Send transaction. +//! │ +//! └─ 𝐂𝐮𝐬𝐭𝐨𝐦 𝐏𝐫𝐨𝐠𝐫𝐚𝐦 +//! ├─ CpiAccounts parse accounts consistent with PackedAccounts. +//! ├─ LightAccount instantiates from CompressedAccountMeta. +//! │ +//! └─ 𝐋𝐢𝐠𝐡𝐭 𝐒𝐲𝐬𝐭𝐞𝐦 𝐏𝐫𝐨𝐠𝐫𝐚𝐦 𝐂𝐏𝐈 +//! ├─ Verify ValidityProof. +//! ├─ Update State Merkle tree. +//! ├─ Update Address Merkle tree. +//! └─ Complete atomic state transition. +//! ``` //! -//! Related crates: -//! - [`light-sdk-pinocchio`](https://docs.rs/light-sdk-pinocchio) - Pinocchio programs -//! - [`light-client`](https://docs.rs/light-client) - Client development -//! - [`light-program-test`](https://docs.rs/light-program-test) - Testing +//! # Features +//! 1. `anchor` - Derives AnchorSerialize, AnchorDeserialize instead of BorshSerialize, BorshDeserialize. //! -//! # Main modules -//! - [`instruction`] - Build instructions with compressed accounts -//! - [`account`] - LightAccount abstraction -//! - [`address`] - Derive compressed addresses -//! - [`cpi`] - CPI to light system program +//! 2. `v2` +//! - available on devnet, localnet, and light-program-test. +//! - Support for optimized v2 light system program instructions. //! -//! # Features -//! - `anchor` - Use AnchorSerialize/AnchorDeserialize -//! - `v2` - Optimized v2 instructions (devnet, localnet) -//! - `cpi-context` - Share one validity proof across multiple CPIs (requires v2) +//! 3. `cpi-context` - Enables CPI context operations for batched compressed account operations. +//! - available on devnet, localnet, and light-program-test. +//! - Enables the use of one validity proof across multiple cpis from different programs in one instruction. +//! - For example spending compressed tokens (owned by the ctoken program) and updating a compressed pda (owned by a custom program) +//! with one validity proof. +//! - An instruction should not use more than one validity proof. +//! - Requires the v2 feature. //! //! ### Example Solana program code to create a compressed account //! ```rust, compile_fail @@ -91,15 +135,20 @@ //!} //! ``` +/// Compressed account abstraction similar to anchor Account. pub mod account; pub use account::sha::LightAccount; +/// Functions to derive compressed account addresses. pub mod address; +/// Utilities to invoke the light-system-program via cpi. pub mod cpi; pub mod error; +/// Utilities to build instructions for programs with compressed accounts. pub mod instruction; pub mod legacy; pub mod proof; +/// Transfer compressed sol between compressed accounts. pub mod transfer; pub mod utils; diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index b9be729e99..3e6a942795 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -1,5 +1,4 @@ pub mod actions; pub mod instructions; - -// Re-export ctoken from compressed-token-sdk for convenience +// re-export pub use light_compressed_token_sdk::ctoken; From b0c20f0929528041e284c7a0c584ba9d5ae1e853 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 14 Nov 2025 18:24:31 +0000 Subject: [PATCH 28/28] fix idl build --- sdk-tests/csdk-anchor-test/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk-tests/csdk-anchor-test/Cargo.toml b/sdk-tests/csdk-anchor-test/Cargo.toml index 98aeafdc53..d1763346e2 100644 --- a/sdk-tests/csdk-anchor-test/Cargo.toml +++ b/sdk-tests/csdk-anchor-test/Cargo.toml @@ -14,19 +14,19 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build", "anchor-spl/idl-build"] test-sbf = [] [dependencies] -light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk = { workspace = true, features = ["anchor", "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"] } +anchor-lang = { workspace = true } +anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata"] } light-ctoken-types = { workspace = true, features = ["anchor"] } light-compressed-token-sdk = { workspace = true, features = ["anchor"] } light-compressed-token-types = { workspace = true, features = ["anchor"] }