diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index 38477abab2..1d0113cc02 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -196,7 +196,8 @@ runs: path: | target/deploy/create_address_test_program.so target/deploy/sdk_anchor_test.so - key: ${{ runner.os }}-program-tests-${{ hashFiles('program-tests/**/Cargo.toml', 'program-tests/**/Cargo.lock', 'program-tests/**/*.rs', 'test-programs/**/Cargo.toml', 'test-programs/**/*.rs') }} + target/deploy/sdk-compressible-test.so + key: ${{ runner.os }}-program-tests-${{ hashFiles('program-tests/**/Cargo.toml', 'program-tests/**/Cargo.lock', 'program-tests/**/*.rs', 'test-programs/**/Cargo.toml', 'test-programs/**/*.rs', 'sdk-tests/**/Cargo.toml', 'sdk-tests/**/*.rs') }} restore-keys: | ${{ runner.os }}-program-tests- diff --git a/.github/workflows/cli-v1.yml b/.github/workflows/cli-v1.yml index 8290ee90f2..f10695b31a 100644 --- a/.github/workflows/cli-v1.yml +++ b/.github/workflows/cli-v1.yml @@ -49,6 +49,16 @@ jobs: skip-components: "redis,disk-cleanup,go" cache-key: "js" + - name: Build stateless.js with V1 + run: | + cd js/stateless.js + pnpm build:v1 + + - name: Build compressed-token with V1 + run: | + cd js/compressed-token + pnpm build:v1 + - name: Build CLI run: | npx nx build @lightprotocol/zk-compression-cli diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 09fd05e183..8f19fe0e6c 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -48,9 +48,9 @@ jobs: matrix: include: - program: native - sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo test-sbf -p client-test"]' + sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo-test-sbf -p client-test"]' - program: anchor & pinocchio - sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]' + sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]' - program: token test sub-tests: '["cargo-test-sbf -p sdk-token-test"]' - program: sdk-libs diff --git a/Cargo.lock b/Cargo.lock index bdae838091..96e69b955e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "Inflector" @@ -44,7 +44,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "ark-bn254 0.5.0", "ark-ff 0.5.0", "light-account-checks", @@ -294,7 +294,7 @@ version = "2.0.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "light-compressed-account", "light-ctoken-types", "light-hasher", @@ -410,6 +410,22 @@ dependencies = [ "spl-token-metadata-interface 0.6.0", ] +[[package]] +name = "anchor-spl" +version = "0.31.1" +source = "git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9#d8a2b3d99d61ef900d1f6cdaabcef14eb9af6279" +dependencies = [ + "anchor-lang", + "mpl-token-metadata", + "spl-associated-token-account 6.0.0", + "spl-memo", + "spl-pod", + "spl-token 7.0.0", + "spl-token-2022 6.0.0", + "spl-token-group-interface 0.5.0", + "spl-token-metadata-interface 0.6.0", +] + [[package]] name = "anchor-syn" version = "0.31.1" @@ -1363,6 +1379,7 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressed-token", + "light-compressed-token-sdk", "light-hasher", "light-indexed-array", "light-macros", @@ -1436,7 +1453,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "forester-utils", "light-batched-merkle-tree", "light-client", @@ -3523,6 +3540,7 @@ dependencies = [ "bytemuck", "lazy_static", "light-compressed-account", + "light-compressed-token-sdk", "light-concurrent-merkle-tree", "light-event", "light-hasher", @@ -3546,11 +3564,13 @@ dependencies = [ "solana-hash 2.3.0", "solana-instruction", "solana-keypair", + "solana-message", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", + "solana-signer", "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", @@ -3636,6 +3656,7 @@ dependencies = [ "light-compressed-account", "light-compressed-token", "light-compressed-token-types", + "light-compressible", "light-ctoken-types", "light-macros", "light-program-profiler", @@ -3693,6 +3714,20 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "light-compressible-client" +version = "0.13.1" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-client", + "light-sdk", + "solana-account", + "solana-instruction", + "solana-pubkey 2.4.0", + "thiserror 2.0.17", +] + [[package]] name = "light-concurrent-merkle-tree" version = "4.0.1" @@ -3931,6 +3966,7 @@ dependencies = [ "light-compressed-token", "light-compressed-token-sdk", "light-compressible", + "light-compressible-client", "light-concurrent-merkle-tree", "light-ctoken-types", "light-event", @@ -4022,10 +4058,12 @@ name = "light-sdk" version = "0.16.0" dependencies = [ "anchor-lang", + "bincode", "borsh 0.10.4", "light-account-checks", "light-compressed-account", "light-concurrent-merkle-tree", + "light-ctoken-types", "light-hasher", "light-macros", "light-sdk-macros", @@ -4033,11 +4071,15 @@ dependencies = [ "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", + "solana-clock", "solana-cpi", "solana-instruction", + "solana-loader-v3-interface", "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", + "solana-system-interface 1.0.0", + "solana-sysvar", "thiserror 2.0.17", ] @@ -4151,7 +4193,7 @@ version = "1.2.1" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.13.1", "create-address-test-program", "forester-utils", @@ -4199,6 +4241,7 @@ dependencies = [ "light-compressed-account", "light-compressed-token-sdk", "light-compressed-token-types", + "light-compressible", "light-ctoken-types", "light-sdk", "light-zero-copy", @@ -4448,6 +4491,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "mpl-token-metadata" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046f0779684ec348e2759661361c8798d79021707b1392cb49f3b5eb911340ff" +dependencies = [ + "borsh 0.10.4", + "num-derive 0.3.3", + "num-traits", + "solana-program", + "thiserror 1.0.69", +] + [[package]] name = "multer" version = "2.1.0" @@ -4586,6 +4642,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -6013,6 +6080,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "sdk-compressible-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", + "bincode", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressed-token-sdk", + "light-compressed-token-types", + "light-compressible", + "light-compressible-client", + "light-ctoken-types", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-types", + "light-test-utils", + "light-token-client", + "solana-account", + "solana-instruction", + "solana-keypair", + "solana-logger", + "solana-program", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signature", + "solana-signer", + "tokio", +] + [[package]] name = "sdk-native-test" version = "1.0.0" @@ -6069,7 +6170,7 @@ name = "sdk-token-test" version = "1.0.0" dependencies = [ "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "arrayvec", "light-batched-merkle-tree", "light-client", @@ -7805,7 +7906,7 @@ dependencies = [ "log", "memoffset", "num-bigint 0.4.6", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -8089,7 +8190,7 @@ dependencies = [ "dialoguer", "hidapi", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot", "qstring", @@ -9125,7 +9226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" dependencies = [ "bincode", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -9151,7 +9252,7 @@ dependencies = [ "agave-feature-set", "bincode", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -9184,7 +9285,7 @@ checksum = "70cea14481d8efede6b115a2581f27bc7c6fdfba0752c20398456c3ac1245fc4" dependencies = [ "agave-feature-set", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -9208,7 +9309,7 @@ dependencies = [ "itertools 0.12.1", "js-sys", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -9237,7 +9338,7 @@ checksum = "579752ad6ea2a671995f13c763bf28288c3c895cb857a518cc4ebab93c9a8dde" dependencies = [ "agave-feature-set", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-instruction", "solana-log-collector", @@ -9260,7 +9361,7 @@ dependencies = [ "curve25519-dalek 4.1.3", "itertools 0.12.1", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -9303,7 +9404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-associated-token-account-client", @@ -9319,7 +9420,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-associated-token-account-client", @@ -9455,7 +9556,7 @@ dependencies = [ "borsh 1.5.7", "bytemuck", "bytemuck_derive", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-msg 2.2.1", @@ -9472,7 +9573,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" dependencies = [ - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-program-error-derive 0.4.1", @@ -9485,7 +9586,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdebc8b42553070b75aa5106f071fef2eb798c64a7ec63375da4b1f058688c6" dependencies = [ - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-msg 2.2.1", @@ -9525,7 +9626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -9547,7 +9648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1408e961215688715d5a1063cbdcf982de225c45f99c82b4f7d7e1dd22b998d7" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -9570,7 +9671,7 @@ checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9585,7 +9686,7 @@ checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-account-info", @@ -9613,7 +9714,7 @@ checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9641,7 +9742,7 @@ checksum = "9048b26b0df0290f929ff91317c83db28b3ef99af2b3493dd35baa146774924c" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9668,7 +9769,7 @@ source = "git+https://github.com/Lightprotocol/token-2022?rev=06d12f50a06db25d73 dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -9696,7 +9797,7 @@ checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-account-info", @@ -9864,7 +9965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-instruction", @@ -9883,7 +9984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5597b4cd76f85ce7cd206045b7dc22da8c25516573d42d267c8d1fd128db5129" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-decode-error", "solana-instruction", @@ -9902,7 +10003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-borsh", "solana-decode-error", @@ -9923,7 +10024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "304d6e06f0de0c13a621464b1fd5d4b1bebf60d15ca71a44d3839958e0da16ee" dependencies = [ "borsh 1.5.7", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-borsh", "solana-decode-error", @@ -9945,7 +10046,7 @@ checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9970,7 +10071,7 @@ checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-cpi", @@ -9994,7 +10095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -10012,7 +10113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d417eb548214fa822d93f84444024b4e57c13ed6719d4dcc68eec24fb481e9f5" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-account-info", "solana-decode-error", @@ -10172,13 +10273,14 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "create-address-test-program", "light-account-checks", "light-batched-merkle-tree", "light-client", "light-compressed-account", "light-compressed-token", + "light-compressed-token-sdk", "light-hasher", "light-merkle-tree-metadata", "light-program-test", @@ -10201,7 +10303,7 @@ version = "0.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "create-address-test-program", "light-account-checks", "light-batched-merkle-tree", diff --git a/Cargo.toml b/Cargo.toml index 8dd688f5a4..8d2ed244ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "sdk-libs/sdk-types", "sdk-libs/photon-api", "sdk-libs/program-test", + "sdk-libs/compressible-client", "xtask", "program-tests/account-compression-test", "program-tests/batched-merkle-tree-test", @@ -53,6 +54,7 @@ members = [ "sdk-tests/sdk-native-test", "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", + "sdk-tests/sdk-compressible-test", "forester-utils", "forester", "sparse-merkle-tree", @@ -91,6 +93,7 @@ solana-transaction-context = "2.3" solana-frozen-abi = "2.3" solana-frozen-abi-macro = "2.3" solana-msg = { version = "2.2" } +solana-message = "2.2" solana-zk-token-sdk = "2.3" solana-logger = "2.3" solana-bn254 = "2.2" @@ -102,6 +105,7 @@ solana-transaction-error = { version = "2.2" } solana-hash = { version = "2.3" } solana-clock = { version = "2.2" } solana-signature = { version = "2.3" } +solana-loader-v3-interface = { version = "5.0" } solana-commitment-config = { version = "2.2" } solana-account = { version = "2.2" } solana-epoch-info = { version = "2.2" } @@ -180,6 +184,7 @@ light-sdk-macros = { path = "sdk-libs/macros", version = "0.16.0" } light-sdk-types = { path = "sdk-libs/sdk-types", version = "0.16.0", default-features = false } light-compressed-account = { path = "program-libs/compressed-account", version = "0.6.2", default-features = false } light-compressible = { path = "program-libs/compressible", version = "0.1.0" } +light-compressible-client = { path = "sdk-libs/compressible-client", version = "0.13.1" } light-ctoken-types = { path = "program-libs/ctoken-types", version = "0.1.0" } light-account-checks = { path = "program-libs/account-checks", version = "0.5.1", default-features = false } light-verifier = { path = "program-libs/verifier", version = "5.0.0" } diff --git a/cli/src/commands/token-balance/index.ts b/cli/src/commands/token-balance/index.ts index f1f4b8859c..19471f1c61 100644 --- a/cli/src/commands/token-balance/index.ts +++ b/cli/src/commands/token-balance/index.ts @@ -44,7 +44,7 @@ class TokenBalanceCommand extends Command { return; } - const compressedTokenAccounts = tokenAccounts.items.filter((acc) => + const compressedTokenAccounts = tokenAccounts.items.filter((acc: any) => acc.parsed.mint.equals(refMint), ); @@ -56,7 +56,7 @@ class TokenBalanceCommand extends Command { let totalBalance = BigInt(0); - compressedTokenAccounts.forEach((account) => { + compressedTokenAccounts.forEach((account: any) => { const amount = account.parsed.amount; totalBalance += BigInt(amount.toString()); }); diff --git a/forester/Cargo.toml b/forester/Cargo.toml index fa8a495042..714d2f2e70 100644 --- a/forester/Cargo.toml +++ b/forester/Cargo.toml @@ -79,5 +79,6 @@ light-batched-merkle-tree = { workspace = true, features = ["test-only"] } light-token-client = { workspace = true } dotenvy = "0.15" light-compressed-token = { workspace = true } +light-compressed-token-sdk = { workspace = true } rand = { workspace = true } create-address-test-program = { workspace = true } diff --git a/forester/tests/e2e_test.rs b/forester/tests/e2e_test.rs index d15fd0cedb..dd51f28efb 100644 --- a/forester/tests/e2e_test.rs +++ b/forester/tests/e2e_test.rs @@ -35,11 +35,11 @@ use light_compressed_token::process_transfer::{ transfer_sdk::{create_transfer_instruction, to_account_metas}, TokenTransferOutputData, }; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_ctoken_types::state::TokenDataVersion; use light_hasher::Poseidon; use light_program_test::accounts::test_accounts::TestAccounts; use light_prover_client::prover::spawn_prover; -use light_sdk::token::TokenDataWithMerkleContext; use light_test_utils::{ conversions::sdk_to_program_token_data, get_concurrent_merkle_tree, get_indexed_merkle_tree, pack::pack_new_address_params_assigned, spl::create_mint_helper_with_keypair, diff --git a/forester/tests/legacy/batched_state_async_indexer_test.rs b/forester/tests/legacy/batched_state_async_indexer_test.rs index 935d7e6492..2fd28955bb 100644 --- a/forester/tests/legacy/batched_state_async_indexer_test.rs +++ b/forester/tests/legacy/batched_state_async_indexer_test.rs @@ -23,13 +23,13 @@ use light_compressed_account::{ use light_compressed_token::process_transfer::{ transfer_sdk::create_transfer_instruction, TokenTransferOutputData, }; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_program_test::accounts::test_accounts::TestAccounts; use light_prover_client::prover::spawn_prover; use light_registry::{ protocol_config::state::{ProtocolConfig, ProtocolConfigPda}, utils::get_protocol_config_pda_address, }; -use light_sdk::token::TokenDataWithMerkleContext; use light_test_utils::{ conversions::sdk_to_program_token_data, spl::create_mint_helper_with_keypair, system_program::create_invoke_instruction, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e3203bf38..3335823363 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -442,6 +442,8 @@ importers: programs: {} + sdk-tests/sdk-compressible-test: {} + sdk-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': diff --git a/program-libs/batched-merkle-tree/Cargo.toml b/program-libs/batched-merkle-tree/Cargo.toml index 5208858c75..5fdce5de90 100644 --- a/program-libs/batched-merkle-tree/Cargo.toml +++ b/program-libs/batched-merkle-tree/Cargo.toml @@ -17,10 +17,12 @@ solana = [ "solana-msg", "light-zero-copy/solana", "light-hasher/solana", + "light-hasher/keccak", "light-account-checks/solana", "light-bloom-filter/solana", "light-macros/solana", "light-compressed-account/solana", + "light-compressed-account/keccak", "light-merkle-tree-metadata/solana", ] pinocchio = [ @@ -31,6 +33,7 @@ pinocchio = [ "light-bloom-filter/pinocchio", "light-macros/pinocchio", "light-compressed-account/pinocchio", + "light-compressed-account/keccak", "light-merkle-tree-metadata/pinocchio", ] @@ -43,7 +46,7 @@ solana-sysvar = { workspace = true, optional = true } solana-msg = { workspace = true, optional = true } solana-account-info = { workspace = true, optional = true } light-zero-copy = { workspace = true, features = ["std"] } -light-hasher = { workspace = true, features = ["poseidon"] } +light-hasher = { workspace = true, features = ["poseidon", "keccak"] } light-bloom-filter = { workspace = true } light-verifier = { workspace = true } thiserror = { workspace = true } @@ -51,14 +54,14 @@ light-merkle-tree-metadata = { workspace = true } borsh = { workspace = true } zerocopy = { workspace = true } pinocchio = { workspace = true, optional = true } -light-compressed-account = { workspace = true, features = ["std"] } +light-compressed-account = { workspace = true, features = ["std", "keccak"] } light-macros = { workspace = true } [dev-dependencies] rand = { workspace = true } light-merkle-tree-reference = { workspace = true } light-account-checks = { workspace = true, features = ["test-only"] } -light-compressed-account = { workspace = true, features = ["new-unique"] } +light-compressed-account = { workspace = true, features = ["new-unique", "keccak"] } light-hasher = { workspace = true, features = ["keccak"] } [lints.rust.unexpected_cfgs] diff --git a/program-libs/compressible/src/config.rs b/program-libs/compressible/src/config.rs index 347e4dfbf8..09536e951f 100644 --- a/program-libs/compressible/src/config.rs +++ b/program-libs/compressible/src/config.rs @@ -258,4 +258,29 @@ impl CompressibleConfig { pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) { Self::derive_pda(program_id, 0) } + + pub fn derive_compression_authority_pda(program_id: &Pubkey, version: u16) -> (Pubkey, u8) { + let seeds = Self::get_compression_authority_seeds(version); + let seeds_refs: [&[u8]; 2] = [seeds[0].as_slice(), seeds[1].as_slice()]; + Pubkey::find_program_address(&seeds_refs, program_id) + } + + pub fn derive_rent_sponsor_pda(program_id: &Pubkey, version: u16) -> (Pubkey, u8) { + let seeds = Self::get_rent_sponsor_seeds(version); + let seeds_refs: [&[u8]; 2] = [seeds[0].as_slice(), seeds[1].as_slice()]; + Pubkey::find_program_address(&seeds_refs, program_id) + } + + /// Derives the default ctoken compression authority PDA (version = 1) + pub fn ctoken_v1_compression_authority_pda() -> Pubkey { + Self::derive_compression_authority_pda( + &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"), + 1, + ) + .0 + } + /// Derives the default ctoken rent sponsor PDA (version = 1) + pub fn ctoken_v1_rent_sponsor_pda() -> Pubkey { + Self::derive_rent_sponsor_pda(&pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"), 1).0 + } } diff --git a/program-tests/account-compression-test/Cargo.toml b/program-tests/account-compression-test/Cargo.toml index 5c5fb01c91..d59b6e4461 100644 --- a/program-tests/account-compression-test/Cargo.toml +++ b/program-tests/account-compression-test/Cargo.toml @@ -31,7 +31,7 @@ light-prover-client = { workspace = true, features = ["devenv"] } num-bigint = { workspace = true } anchor-spl = { workspace = true } anchor-lang = { workspace = true } -account-compression = { workspace = true } +account-compression = { workspace = true, features = ["test"] } light-hasher = { workspace = true, features = ["poseidon"] } light-hash-set = { workspace = true } light-concurrent-merkle-tree = { workspace = true } diff --git a/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs b/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs index 9c882ed62d..b41ccaa264 100644 --- a/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs +++ b/program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs @@ -440,7 +440,7 @@ async fn test_compressible_account_with_custom_rent_payer_close_with_compression // Initialize compressible token account let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/ctoken/create.rs b/program-tests/compressed-token-test/tests/ctoken/create.rs index dbdeade9b5..4ea62aeba8 100644 --- a/program-tests/compressed-token-test/tests/ctoken/create.rs +++ b/program-tests/compressed-token-test/tests/ctoken/create.rs @@ -9,7 +9,7 @@ use solana_sdk::instruction::Instruction; use super::shared::*; #[tokio::test] -async fn test_create_compressible_token_account() { +async fn test_create_compressible_token_account_instruction() { let mut context = setup_account_test().await.unwrap(); let payer_pubkey = context.payer.pubkey(); @@ -227,7 +227,7 @@ async fn test_create_compressible_token_account_failing() { let token_account_pubkey = Keypair::new(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey.pubkey(), mint_pubkey: context.mint_pubkey, @@ -364,7 +364,7 @@ async fn test_create_compressible_token_account_failing() { }; let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, @@ -412,7 +412,7 @@ async fn test_create_compressible_token_account_failing() { .unwrap(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: context.token_account_keypair.pubkey(), mint_pubkey: context.mint_pubkey, @@ -452,7 +452,7 @@ async fn test_create_compressible_token_account_failing() { let wrong_account_type = context.rpc.test_accounts.protocol.governance_authority_pda; let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: context.token_account_keypair.pubkey(), mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/ctoken/functional.rs b/program-tests/compressed-token-test/tests/ctoken/functional.rs index e1ce56d148..222ff42845 100644 --- a/program-tests/compressed-token-test/tests/ctoken/functional.rs +++ b/program-tests/compressed-token-test/tests/ctoken/functional.rs @@ -123,7 +123,7 @@ async fn test_compressible_account_with_compression_authority_lifecycle() { // Initialize compressible token account let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/ctoken/shared.rs b/program-tests/compressed-token-test/tests/ctoken/shared.rs index 0822e9bcf7..c54f568295 100644 --- a/program-tests/compressed-token-test/tests/ctoken/shared.rs +++ b/program-tests/compressed-token-test/tests/ctoken/shared.rs @@ -91,7 +91,7 @@ pub async fn create_and_assert_token_account( let token_account_pubkey = context.token_account_keypair.pubkey(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, @@ -143,7 +143,7 @@ pub async fn create_and_assert_token_account_fails( let token_account_pubkey = context.token_account_keypair.pubkey(); let create_token_account_ix = - light_compressed_token_sdk::instructions::create_compressible_token_account( + light_compressed_token_sdk::instructions::create_compressible_token_account_instruction( light_compressed_token_sdk::instructions::CreateCompressibleTokenAccount { account_pubkey: token_account_pubkey, mint_pubkey: context.mint_pubkey, diff --git a/program-tests/compressed-token-test/tests/mint/functional.rs b/program-tests/compressed-token-test/tests/mint/functional.rs index 1c0b6b6dd5..51b95818a1 100644 --- a/program-tests/compressed-token-test/tests/mint/functional.rs +++ b/program-tests/compressed-token-test/tests/mint/functional.rs @@ -29,7 +29,7 @@ use light_test_utils::{ Rpc, }; use light_token_client::{ - actions::{create_mint, ctoken_transfer, mint_to_compressed, transfer2}, + actions::{create_mint, mint_to_compressed, transfer2, transfer_ctoken}, instructions::transfer2::{ create_decompress_instruction, create_generic_transfer2_instruction, CompressInput, DecompressInput, Transfer2InstructionType, TransferInput, @@ -820,7 +820,7 @@ async fn test_ctoken_transfer() { second_recipient_ata_balance ); // Execute the decompressed transfer - let transfer_result = ctoken_transfer( + let transfer_result = transfer_ctoken( &mut rpc, recipient_ata, // Source account (has 1000 tokens) second_recipient_ata, // Destination account diff --git a/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs b/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs index 859ed58628..3df688aada 100644 --- a/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs +++ b/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs @@ -128,7 +128,7 @@ async fn test_spl_to_ctoken_transfer() { println!("Testing reverse transfer: ctoken to SPL"); // Transfer from recipient's compressed token account back to sender's SPL token account - transfer2::ctoken_to_spl_transfer( + transfer2::transfer_ctoken_to_spl( &mut rpc, associated_token_account, spl_token_account_keypair.pubkey(), diff --git a/program-tests/compressed-token-test/tests/v1.rs b/program-tests/compressed-token-test/tests/v1.rs index 287aa2089d..c037652362 100644 --- a/program-tests/compressed-token-test/tests/v1.rs +++ b/program-tests/compressed-token-test/tests/v1.rs @@ -38,6 +38,7 @@ use light_compressed_token::{ spl_compression::check_spl_token_pool_derivation_with_index, ErrorCode, TokenData, }; +use light_compressed_token_sdk::compat::{AccountState, TokenDataWithMerkleContext}; use light_program_test::{ accounts::{test_accounts::TestAccounts, test_keypairs::TestKeypairs}, indexer::{TestIndexer, TestIndexerExtensions}, @@ -45,7 +46,6 @@ use light_program_test::{ LightProgramTest, ProgramTestConfig, }; use light_prover_client::prover::spawn_prover; -use light_sdk::token::{AccountState, TokenDataWithMerkleContext}; use light_system_program::{errors::SystemProgramError, utils::get_sol_pool_pda}; use light_test_utils::{ assert_custom_error_or_program_error, @@ -667,12 +667,11 @@ async fn test_wrapped_sol() { None, ) .await; - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); decompress_test( &payer, &mut rpc, @@ -1379,7 +1378,7 @@ async fn perform_transfer_22_test( for _ in 0..outputs { recipients.push(Pubkey::new_unique()); } - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1595,7 +1594,7 @@ async fn test_mint_to_and_burn_from_all_token_pools() { iterator }; for i in iterator { - let accounts: Vec = test_indexer + let accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) .await .unwrap() @@ -1690,7 +1689,7 @@ async fn test_multiple_decompression() { let mut iterator = vec![0, 1, 2, 3, 4]; iterator.shuffle(rng); for i in iterator { - let accounts: Vec = test_indexer + let accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1742,7 +1741,7 @@ async fn test_multiple_decompression() { // Decompress from all token pools { - let all_accounts: Vec = test_indexer + let all_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1770,7 +1769,7 @@ async fn test_multiple_decompression() { Some(add_token_pool_accounts.clone()), ) .await; - let all_accounts: Vec = test_indexer + let all_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -1839,12 +1838,11 @@ async fn test_delegation( .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -1868,12 +1866,11 @@ async fn test_delegation( let recipient = Pubkey::new_unique(); // 2. Transfer partial delegated amount { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -1898,12 +1895,11 @@ async fn test_delegation( } // 3. Transfer full delegated amount { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -1980,12 +1976,11 @@ async fn test_delegation_mixed() { .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -2009,23 +2004,21 @@ async fn test_delegation_mixed() { let recipient = Pubkey::new_unique(); // 2. Transfer partial delegated amount with delegate change account { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let mut input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) - .await - .unwrap() - .into(); + let delegate_input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) + .await + .unwrap() + .into(); input_compressed_accounts .extend_from_slice(&[delegate_input_compressed_accounts[0].clone()]); let delegate_lamports = delegate_input_compressed_accounts[0] @@ -2056,23 +2049,21 @@ async fn test_delegation_mixed() { let recipient = Pubkey::new_unique(); // 3. Transfer partial delegated amount without delegate change account { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let mut input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) - .await - .unwrap() - .into(); + let delegate_input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) + .await + .unwrap() + .into(); input_compressed_accounts .extend_from_slice(&[delegate_input_compressed_accounts[0].clone()]); let delegate_input_amount = input_compressed_accounts @@ -2105,23 +2096,21 @@ async fn test_delegation_mixed() { } // 3. Transfer full delegated amount { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let mut input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) .cloned() .collect::>(); - let delegate_input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) - .await - .unwrap() - .into(); + let delegate_input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&delegate.pubkey(), None, None) + .await + .unwrap() + .into(); input_compressed_accounts.extend_from_slice(&delegate_input_compressed_accounts); let input_amount = input_compressed_accounts @@ -2218,7 +2207,7 @@ async fn test_approve_failing() { ) .await; - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2506,12 +2495,11 @@ async fn test_revoke(num_inputs: usize, mint_amount: u64, delegated_amount: u64) .await; // 1. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); for input in input_compressed_accounts.iter() { let input_compressed_accounts = vec![input.clone()]; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] @@ -2536,17 +2524,16 @@ async fn test_revoke(num_inputs: usize, mint_amount: u64, delegated_amount: u64) } // 2. Revoke { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items - .iter() - .filter(|x| x.token.delegate.is_some()) - .map(|x| x.clone().into()) - .collect::>(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items + .iter() + .filter(|x| x.token.delegate.is_some()) + .map(|x| x.clone().into()) + .collect::>(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -2632,12 +2619,11 @@ async fn test_revoke_failing() { .await; // Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_amount = 1000u64; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -2659,7 +2645,7 @@ async fn test_revoke_failing() { .await; } - let input_compressed_accounts: Vec = test_indexer + let input_compressed_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) .await .unwrap() @@ -2855,12 +2841,11 @@ async fn test_burn() { .await; // 1. Burn tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1000u64; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -2883,12 +2868,11 @@ async fn test_burn() { } // 2. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_amount = 1000u64; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -2911,12 +2895,11 @@ async fn test_burn() { } // 3. Burn delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -2944,12 +2927,11 @@ async fn test_burn() { } // 3. Burn all delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -2994,18 +2976,17 @@ async fn test_burn() { ) .await .unwrap(); - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items - .iter() - .filter(|x| x.token.amount != 0) - .map(|x| x.clone().into()) - .collect::>()[0..4] - .to_vec(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items + .iter() + .filter(|x| x.token.amount != 0) + .map(|x| x.clone().into()) + .collect::>()[0..4] + .to_vec(); let burn_amount = input_compressed_accounts .iter() .map(|x| x.token_data.amount) @@ -3046,17 +3027,16 @@ async fn test_burn() { .unwrap(); let slot = rpc.get_slot().await.unwrap(); test_indexer.add_event_and_compressed_accounts(slot, &event); - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items - .iter() - .filter(|x| x.token.amount != 0) - .map(|x| x.clone().into()) - .collect::>(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items + .iter() + .filter(|x| x.token.amount != 0) + .map(|x| x.clone().into()) + .collect::>(); let burn_amount = input_compressed_accounts .iter() .map(|x| x.token_data.amount) @@ -3129,12 +3109,11 @@ async fn failing_tests_burn() { .await; // Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_amount = 1000u64; let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3157,12 +3136,11 @@ async fn failing_tests_burn() { } // 1. invalid proof { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3191,12 +3169,11 @@ async fn failing_tests_burn() { } // 2. Signer is delegate but token data has no delegate. { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3228,12 +3205,11 @@ async fn failing_tests_burn() { } // 3. Signer is delegate but token data has no delegate. { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.delegate.is_some()) @@ -3266,12 +3242,11 @@ async fn failing_tests_burn() { } // 4. invalid authority (use delegate as authority) { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3303,12 +3278,11 @@ async fn failing_tests_burn() { } // 5. invalid mint { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3341,12 +3315,11 @@ async fn failing_tests_burn() { } // 6. invalid change merkle tree { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3379,12 +3352,11 @@ async fn failing_tests_burn() { } // 6. invalid token pool (not initialized) { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3412,12 +3384,11 @@ async fn failing_tests_burn() { } // 7. invalid token pool (invalid mint) { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let burn_amount = 1; let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account @@ -3492,12 +3463,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { .await; // 1. Freeze tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let output_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3516,12 +3486,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 2. Thaw tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Frozen) @@ -3544,12 +3513,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 3. Delegate tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let delegated_compressed_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3571,12 +3539,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 4. Freeze delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let output_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3595,12 +3562,11 @@ async fn test_freeze_and_thaw(mint_amount: u64, delegated_amount: u64) { } // 5. Thaw delegated tokens { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Frozen) @@ -3681,15 +3647,14 @@ async fn test_failing_freeze() { ) .await; - let input_compressed_accounts: Vec = - vec![test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items[0] - .clone() - .into()]; + let input_compressed_accounts: Vec = vec![test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items[0] + .clone() + .into()]; let outputs_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3947,15 +3912,14 @@ async fn test_failing_thaw() { // Freeze tokens { - let input_compressed_accounts: Vec = - vec![test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .value - .items[0] - .clone() - .into()]; + let input_compressed_accounts: Vec = vec![test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .value + .items[0] + .clone() + .into()]; let output_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context @@ -3973,12 +3937,11 @@ async fn test_failing_thaw() { .await; } - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Frozen) @@ -4120,12 +4083,11 @@ async fn test_failing_thaw() { } // 4. thaw compressed account which is not frozen { - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let input_compressed_accounts = input_compressed_accounts .iter() .filter(|x| x.token_data.state == AccountState::Initialized) @@ -4253,12 +4215,11 @@ async fn test_failing_decompression() { ) .await .unwrap(); - let input_compressed_account: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_account: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey(), None, None) + .await + .unwrap() + .into(); let decompress_amount = amount - 1000; // Test 1: invalid decompress account { @@ -5505,12 +5466,11 @@ async fn test_transfer_with_photon_and_batched_tree() { Pubkey::from_str("24fLJv6tHmsxQg5vDD7XWy85TMhFzJdkqZ9Ta3LtVReU").unwrap(), ]; println!("recipients {:?}", recipients); - let input_compressed_accounts: Vec = - test_indexer - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .into(); + let input_compressed_accounts: Vec = test_indexer + .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap() + .into(); let equal_amount = (amount * inputs as u64) / outputs as u64; let rest_amount = (amount * inputs as u64) % outputs as u64; let mut output_amounts = vec![equal_amount; outputs - 1]; @@ -5646,16 +5606,14 @@ async fn batch_compress_with_batched_tree() { test_indexer.add_compressed_accounts_with_token_data(slot, &event); for i in 0..num_recipients { - let recipient_compressed_token_accounts: Vec< - light_sdk::token::TokenDataWithMerkleContext, - > = test_indexer + let recipient_compressed_token_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(&recipients[i as usize], None, None) .await .unwrap() .into(); assert_eq!(recipient_compressed_token_accounts.len(), 1); let recipient_compressed_token_account = &recipient_compressed_token_accounts[0]; - let expected_token_data = light_sdk::token::TokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint, owner: recipients[i as usize], amount: (i + 1), @@ -5714,16 +5672,14 @@ async fn batch_compress_with_batched_tree() { test_indexer.add_compressed_accounts_with_token_data(slot, &event); for recipient in &recipients { - let recipient_compressed_token_accounts: Vec< - light_sdk::token::TokenDataWithMerkleContext, - > = test_indexer + let recipient_compressed_token_accounts: Vec = test_indexer .get_compressed_token_accounts_by_owner(recipient, None, None) .await .unwrap() .into(); assert_eq!(recipient_compressed_token_accounts.len(), 1); let recipient_compressed_token_account = &recipient_compressed_token_accounts[0]; - let expected_token_data = light_sdk::token::TokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint, owner: *recipient, amount, diff --git a/program-tests/registry-test/tests/tests.rs b/program-tests/registry-test/tests/tests.rs index 3c20c6fac4..cb2d08f2b7 100644 --- a/program-tests/registry-test/tests/tests.rs +++ b/program-tests/registry-test/tests/tests.rs @@ -191,6 +191,7 @@ async fn test_initialize_protocol_config() { config: ProgramTestConfig::default(), transaction_counter: 0, pre_context: None, + auto_compress_programs: Vec::new(), }; let payer = rpc.get_payer().insecure_clone(); diff --git a/program-tests/system-cpi-test/Cargo.toml b/program-tests/system-cpi-test/Cargo.toml index 5e7c6bf40d..2cf113e912 100644 --- a/program-tests/system-cpi-test/Cargo.toml +++ b/program-tests/system-cpi-test/Cargo.toml @@ -24,6 +24,7 @@ default = ["custom-heap"] anchor-lang = { workspace = true } anchor-spl = { workspace = true } light-compressed-token = { workspace = true, features = ["cpi"] } +light-compressed-token-sdk = { workspace = true } light-system-program-anchor = { workspace = true, features = ["cpi"] } light-registry = { workspace = true, features = ["cpi"] } account-compression = { workspace = true, features = ["cpi"] } diff --git a/program-tests/system-cpi-test/tests/test.rs b/program-tests/system-cpi-test/tests/test.rs index 9f4623ed02..48af7923d3 100644 --- a/program-tests/system-cpi-test/tests/test.rs +++ b/program-tests/system-cpi-test/tests/test.rs @@ -18,6 +18,7 @@ use light_compressed_account::{ TreeType, }; use light_compressed_token::process_transfer::InputTokenDataWithContext; +use light_compressed_token_sdk::compat::{AccountState, TokenDataWithMerkleContext}; use light_hasher::{Hasher, Poseidon}; use light_merkle_tree_metadata::errors::MerkleTreeMetadataError; use light_program_test::{ @@ -28,7 +29,6 @@ use light_program_test::{ ProgramTestConfig, }; use light_registry::account_compression_cpi::sdk::create_batch_update_address_tree_instruction; -use light_sdk::token::{AccountState, TokenDataWithMerkleContext}; use light_system_program::errors::SystemProgramError; use light_test_utils::{ e2e_test_env::init_program_test_env, diff --git a/program-tests/utils/src/assert_mint_to_compressed.rs b/program-tests/utils/src/assert_mint_to_compressed.rs index d777d6088f..52c3bf5fa4 100644 --- a/program-tests/utils/src/assert_mint_to_compressed.rs +++ b/program-tests/utils/src/assert_mint_to_compressed.rs @@ -5,7 +5,7 @@ use light_client::{ rpc::Rpc, }; use light_compressed_token::instructions::create_token_pool::find_token_pool_pda_with_index; -use light_compressed_token_sdk::instructions::derive_compressed_mint_from_spl_mint; +use light_compressed_token_sdk::instructions::derive_cmint_from_spl_mint; use light_ctoken_types::{ instructions::mint_action::Recipient, state::CompressedMint, COMPRESSED_TOKEN_PROGRAM_ID, }; @@ -21,8 +21,7 @@ pub async fn assert_mint_to_compressed( ) -> Vec { // Derive compressed mint address from SPL mint PDA (same as instruction) let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_mint_address = - derive_compressed_mint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); + let compressed_mint_address = derive_cmint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); // Verify each recipient received their tokens let mut all_token_accounts = Vec::new(); let mut total_minted = 0u64; @@ -52,12 +51,12 @@ pub async fn assert_mint_to_compressed( }); // Create expected token data - let expected_token_data = light_sdk::token::TokenData { + let expected_token_data = light_compressed_token_sdk::compat::TokenData { mint: spl_mint_pda, owner: recipient_pubkey, amount: recipient.amount, delegate: None, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; diff --git a/program-tests/utils/src/assert_token_tx.rs b/program-tests/utils/src/assert_token_tx.rs index f59326447c..d9f774f3d1 100644 --- a/program-tests/utils/src/assert_token_tx.rs +++ b/program-tests/utils/src/assert_token_tx.rs @@ -2,9 +2,9 @@ use anchor_lang::AnchorSerialize; use light_client::{indexer::Indexer, rpc::Rpc}; use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; use light_compressed_token::process_transfer::{get_cpi_authority_pda, TokenTransferOutputData}; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_event::event::PublicTransactionEvent; use light_program_test::indexer::TestIndexerExtensions; -use light_sdk::token::TokenDataWithMerkleContext; use solana_sdk::{program_pack::Pack, pubkey::Pubkey}; use crate::assert_compressed_tx::{ diff --git a/program-tests/utils/src/assert_transfer2.rs b/program-tests/utils/src/assert_transfer2.rs index ea49ec2459..2eab0525c4 100644 --- a/program-tests/utils/src/assert_transfer2.rs +++ b/program-tests/utils/src/assert_transfer2.rs @@ -88,12 +88,12 @@ pub async fn assert_transfer2_with_delegate( }; // Get mint from the source compressed token account - let expected_recipient_token_data = light_sdk::token::TokenData { + let expected_recipient_token_data = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: transfer_input.to, amount: transfer_input.amount, delegate: None, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -157,12 +157,12 @@ pub async fn assert_transfer2_with_delegate( None // No delegate to preserve }; - let expected_change_token = light_sdk::token::TokenData { + let expected_change_token = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: source_owner, amount: change_amount, delegate: expected_delegate, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -216,12 +216,12 @@ pub async fn assert_transfer2_with_delegate( None // Default to None if no authority specified }; - let expected_change_token = light_sdk::token::TokenData { + let expected_change_token = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: source_owner, amount: change_amount, delegate: expected_delegate, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -278,12 +278,12 @@ pub async fn assert_transfer2_with_delegate( .value .items; - let expected_change_token = light_sdk::token::TokenData { + let expected_change_token = light_compressed_token_sdk::compat::TokenData { mint: source_mint, owner: source_owner, amount: change_amount, delegate: Some(approve_input.delegate), - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; @@ -336,12 +336,12 @@ pub async fn assert_transfer2_with_delegate( .map(|accounts| accounts.iter().map(|a| a.token.amount).sum::()) .unwrap_or(0); - let expected_recipient_token_data = light_sdk::token::TokenData { + let expected_recipient_token_data = light_compressed_token_sdk::compat::TokenData { mint: compress_input.mint, owner: compress_input.to, amount: compress_input.amount + compressed_input_amount, delegate: None, - state: light_sdk::token::AccountState::Initialized, + state: light_compressed_token_sdk::compat::AccountState::Initialized, tlv: None, }; recipient_accounts.iter().for_each(|account| { @@ -482,7 +482,7 @@ pub async fn assert_transfer2_with_delegate( ); assert_eq!( compressed_account.token.state, - light_sdk::token::AccountState::Initialized, + light_compressed_token_sdk::compat::AccountState::Initialized, "CompressAndClose compressed account should be initialized" ); assert_eq!( diff --git a/program-tests/utils/src/conversions.rs b/program-tests/utils/src/conversions.rs index 00d4dab456..b12d6ed4a7 100644 --- a/program-tests/utils/src/conversions.rs +++ b/program-tests/utils/src/conversions.rs @@ -1,6 +1,5 @@ use light_ctoken_types::state::{CompressedTokenAccountState, TokenData as ProgramTokenData}; -use light_sdk::{self as sdk}; - +// TODO: most of this seems to be legacy and can be replaced with into(). // pub fn sdk_to_program_merkle_context( // sdk_merkle_context: sdk::merkle_context::MerkleContext, // ) -> ProgramMerkleContext { @@ -84,23 +83,31 @@ use light_sdk::{self as sdk}; // } pub fn sdk_to_program_account_state( - sdk_state: sdk::token::AccountState, + sdk_state: light_compressed_token_sdk::compat::AccountState, ) -> CompressedTokenAccountState { match sdk_state { - sdk::token::AccountState::Initialized => CompressedTokenAccountState::Initialized, - sdk::token::AccountState::Frozen => CompressedTokenAccountState::Frozen, + light_compressed_token_sdk::compat::AccountState::Initialized => { + CompressedTokenAccountState::Initialized + } + light_compressed_token_sdk::compat::AccountState::Frozen => { + CompressedTokenAccountState::Frozen + } } } -pub fn program_to_sdk_account_state(program_state: u8) -> sdk::token::AccountState { +pub fn program_to_sdk_account_state( + program_state: u8, +) -> light_compressed_token_sdk::compat::AccountState { match program_state { - 0 => sdk::token::AccountState::Initialized, - 1 => sdk::token::AccountState::Frozen, + 0 => light_compressed_token_sdk::compat::AccountState::Initialized, + 1 => light_compressed_token_sdk::compat::AccountState::Frozen, _ => panic!("program_to_sdk_account_state: invalid account state"), } } -pub fn sdk_to_program_token_data(sdk_token: sdk::token::TokenData) -> ProgramTokenData { +pub fn sdk_to_program_token_data( + sdk_token: light_compressed_token_sdk::compat::TokenData, +) -> ProgramTokenData { ProgramTokenData { mint: sdk_token.mint.into(), owner: sdk_token.owner.into(), @@ -111,8 +118,10 @@ pub fn sdk_to_program_token_data(sdk_token: sdk::token::TokenData) -> ProgramTok } } -pub fn program_to_sdk_token_data(program_token: ProgramTokenData) -> sdk::token::TokenData { - sdk::token::TokenData { +pub fn program_to_sdk_token_data( + program_token: ProgramTokenData, +) -> light_compressed_token_sdk::compat::TokenData { + light_compressed_token_sdk::compat::TokenData { mint: program_token.mint.into(), owner: program_token.owner.into(), amount: program_token.amount, diff --git a/program-tests/utils/src/e2e_test_env.rs b/program-tests/utils/src/e2e_test_env.rs index aca8fc6596..09ea74ed6a 100644 --- a/program-tests/utils/src/e2e_test_env.rs +++ b/program-tests/utils/src/e2e_test_env.rs @@ -117,6 +117,7 @@ use light_compressed_account::{ TreeType, }; use light_compressed_token::process_transfer::transfer_sdk::to_account_metas; +use light_compressed_token_sdk::compat::{AccountState, TokenDataWithMerkleContext}; use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; use light_indexed_merkle_tree::{ array::IndexedArray, reference::IndexedMerkleTree, HIGHEST_ADDRESS_PLUS_ONE, @@ -147,7 +148,6 @@ use light_registry::{ use light_sdk::{ address::NewAddressParamsAssignedPacked, constants::{ADDRESS_MERKLE_TREE_ROOTS, CPI_AUTHORITY_PDA_SEED, STATE_MERKLE_TREE_ROOTS}, - token::{AccountState, TokenDataWithMerkleContext}, }; use light_sparse_merkle_tree::{ changelog::ChangelogEntry, indexed_changelog::IndexedChangelogEntry, SparseMerkleTree, diff --git a/program-tests/utils/src/spl.rs b/program-tests/utils/src/spl.rs index 96eb90265f..6780f1e5f5 100644 --- a/program-tests/utils/src/spl.rs +++ b/program-tests/utils/src/spl.rs @@ -25,10 +25,10 @@ use light_compressed_token::{ process_compress_spl_token_account::sdk::create_compress_spl_token_account_instruction, process_transfer::{transfer_sdk::create_transfer_instruction, TokenTransferOutputData}, }; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_ctoken_types::state::{CompressedTokenAccountState, TokenData}; use light_hasher::Poseidon; use light_program_test::{indexer::TestIndexerExtensions, program_test::TestRpc}; -use light_sdk::token::TokenDataWithMerkleContext; use solana_banks_client::BanksClientError; use solana_sdk::{ instruction::Instruction, diff --git a/programs/compressed-token/program/src/shared/token_input.rs b/programs/compressed-token/program/src/shared/token_input.rs index decd9f7426..f8e4306f32 100644 --- a/programs/compressed-token/program/src/shared/token_input.rs +++ b/programs/compressed-token/program/src/shared/token_input.rs @@ -146,7 +146,7 @@ fn set_input_compressed_account_inner( &input_token_data.merkle_context, *input_token_data.root_index, lamports, - None, // Token accounts don't have addresses + None, )?; Ok(()) } diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index ea89387e43..3f33c3935d 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -27,6 +27,7 @@ solana-clock = { workspace = true } solana-signature = { workspace = true } solana-commitment-config = { workspace = true } solana-account = { workspace = true } +solana-signer = { workspace = true } solana-epoch-info = { workspace = true } solana-keypair = { workspace = true } solana-compute-budget-interface = { workspace = true } @@ -35,6 +36,7 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [ "bytemuck", "bincode", ] } +solana-message = { workspace = true } # Light Protocol dependencies light-merkle-tree-metadata = { workspace = true, features = ["solana"] } @@ -43,6 +45,7 @@ light-indexed-merkle-tree = { workspace = true } light-sdk = { workspace = true } light-hasher = { workspace = true, features = ["poseidon"] } light-compressed-account = { workspace = true, features = ["solana", "poseidon"] } +light-compressed-token-sdk = { workspace = true } light-event = { workspace = true } photon-api = { workspace = true } diff --git a/sdk-libs/client/src/constants.rs b/sdk-libs/client/src/constants.rs index ec1c0432b9..1ad9dfb6e1 100644 --- a/sdk-libs/client/src/constants.rs +++ b/sdk-libs/client/src/constants.rs @@ -19,3 +19,28 @@ pub const STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = /// Used to reduce transaction size by referencing queues via lookup table indices. pub const NULLIFIED_STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = pubkey!("5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP"); + +/// Address lookup table with zk compression related keys. Use to reduce +/// transaction size. +/// +/// Keys include: all protocol pubkeys, default state trees, address trees, and +/// more. +/// +/// Example usage: +/// ```bash +/// +/// # By cloning from mainnet +/// light test-validator --validator-args "\ +/// --clone 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ \ +/// --url https://api.mainnet-beta.solana.com \ +/// --upgradeable-program ~/.config/solana/id.json" +/// +/// # With a local LUT file +/// light test-validator --validator-args "\ +/// --account 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ ./scripts/lut.json \ +/// --url https://api.mainnet-beta.solana.com \ +/// --upgradeable-program ~/.config/solana/id.json" +/// +/// ``` +pub const LIGHT_PROTOCOL_LOOKUP_TABLE_ADDRESS: Pubkey = + pubkey!("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index d88d51d1cf..723d450cf3 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -6,10 +6,10 @@ use light_compressed_account::{ instruction_data::compressed_proof::CompressedProof, TreeType, }; +use light_compressed_token_sdk::compat::{AccountState, TokenData}; use light_indexed_merkle_tree::array::IndexedElement; -use light_sdk::{ - instruction::{PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof}, - token::{AccountState, TokenData}, +use light_sdk::instruction::{ + PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof, }; use num_bigint::BigUint; use solana_pubkey::Pubkey; @@ -813,11 +813,13 @@ impl TryFrom<&photon_api::models::TokenAccountV2> for CompressedTokenAccount { } #[allow(clippy::from_over_into)] -impl Into for CompressedTokenAccount { - fn into(self) -> light_sdk::token::TokenDataWithMerkleContext { +impl Into + for CompressedTokenAccount +{ + fn into(self) -> light_compressed_token_sdk::compat::TokenDataWithMerkleContext { let compressed_account = CompressedAccountWithMerkleContext::from(self.account); - light_sdk::token::TokenDataWithMerkleContext { + light_compressed_token_sdk::compat::TokenDataWithMerkleContext { token_data: self.token, compressed_account, } @@ -825,30 +827,32 @@ impl Into for CompressedTokenAccou } #[allow(clippy::from_over_into)] -impl Into> +impl Into> for super::response::Response> { - fn into(self) -> Vec { + fn into(self) -> Vec { self.value .items .into_iter() .map( - |token_account| light_sdk::token::TokenDataWithMerkleContext { + |token_account| light_compressed_token_sdk::compat::TokenDataWithMerkleContext { token_data: token_account.token, compressed_account: CompressedAccountWithMerkleContext::from( token_account.account.clone(), ), }, ) - .collect::>() + .collect::>() } } -impl TryFrom for CompressedTokenAccount { +impl TryFrom + for CompressedTokenAccount +{ type Error = IndexerError; fn try_from( - token_data_with_context: light_sdk::token::TokenDataWithMerkleContext, + token_data_with_context: light_compressed_token_sdk::compat::TokenDataWithMerkleContext, ) -> Result { let account = CompressedAccount::try_from(token_data_with_context.compressed_account)?; diff --git a/sdk-libs/client/src/rpc/lut.rs b/sdk-libs/client/src/rpc/lut.rs new file mode 100644 index 0000000000..e1adfe9872 --- /dev/null +++ b/sdk-libs/client/src/rpc/lut.rs @@ -0,0 +1,37 @@ +pub use solana_address_lookup_table_interface::{ + error, instruction, program, state::AddressLookupTable, +}; +use solana_message::AddressLookupTableAccount; +use solana_pubkey::Pubkey; +use solana_rpc_client::rpc_client::RpcClient; + +use crate::rpc::errors::RpcError; + +/// Gets a lookup table account state from the network. +/// +/// # Arguments +/// +/// * `client` - The RPC client to use to get the lookup table account state. +/// * `lookup_table_address` - The address of the lookup table account to get. +/// +/// # Returns +/// +/// * `AddressLookupTableAccount` - The lookup table account state. +pub fn load_lookup_table( + client: &RpcClient, + lookup_table_address: &Pubkey, +) -> Result { + let raw_account = client.get_account(lookup_table_address)?; + let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| { + RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}")) + })?; + let address_lookup_table_account = AddressLookupTableAccount { + key: lookup_table_address.to_bytes().into(), + addresses: address_lookup_table + .addresses + .iter() + .map(|p| p.to_bytes().into()) + .collect(), + }; + Ok(address_lookup_table_account) +} diff --git a/sdk-libs/client/src/rpc/mod.rs b/sdk-libs/client/src/rpc/mod.rs index 0b968c26c5..724a71c106 100644 --- a/sdk-libs/client/src/rpc/mod.rs +++ b/sdk-libs/client/src/rpc/mod.rs @@ -11,3 +11,6 @@ pub use client::{LightClient, RetryConfig}; pub use errors::RpcError; pub use rpc_trait::{LightClientConfig, Rpc}; pub mod get_light_state_tree_infos; + +pub mod lut; +pub use lut::load_lookup_table; diff --git a/sdk-libs/compressed-token-sdk/Cargo.toml b/sdk-libs/compressed-token-sdk/Cargo.toml index 1cb9a01765..794c9d2b17 100644 --- a/sdk-libs/compressed-token-sdk/Cargo.toml +++ b/sdk-libs/compressed-token-sdk/Cargo.toml @@ -22,6 +22,7 @@ profile-heap = [ # Light Protocol dependencies light-compressed-token-types = { workspace = true } light-compressed-account = { workspace = true, features = ["std"] } +light-compressible = { workspace = true } light-ctoken-types = { workspace = true } light-sdk = { workspace = true, features = ["v2"] } light-macros = { workspace = true } diff --git a/sdk-libs/compressed-token-sdk/src/account2.rs b/sdk-libs/compressed-token-sdk/src/account2.rs index bbd8b69e2c..dfeeec39cd 100644 --- a/sdk-libs/compressed-token-sdk/src/account2.rs +++ b/sdk-libs/compressed-token-sdk/src/account2.rs @@ -1,22 +1,13 @@ use std::ops::Deref; -use light_compressed_token_types::ValidityProof; use light_ctoken_types::instructions::transfer2::{ Compression, CompressionMode, MultiInputTokenDataWithContext, MultiTokenTransferOutputData, }; use light_program_profiler::profile; use solana_account_info::AccountInfo; -use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -use crate::{ - error::TokenSdkError, - instructions::transfer2::{ - account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config, - Transfer2Inputs, - }, - utils::get_token_account_balance, -}; +use crate::{error::TokenSdkError, utils::get_token_account_balance}; #[derive(Debug, PartialEq, Clone)] pub struct CTokenAccount2 { @@ -410,152 +401,3 @@ impl Deref for CTokenAccount2 { &self.output } } - -#[allow(clippy::too_many_arguments)] -#[profile] -pub fn create_spl_to_ctoken_transfer_instruction( - source_spl_token_account: Pubkey, - to: Pubkey, - amount: u64, - authority: Pubkey, - mint: Pubkey, - payer: Pubkey, - token_pool_pda: Pubkey, - token_pool_pda_bump: u8, -) -> Result { - let packed_accounts = vec![ - // Mint (index 0) - AccountMeta::new_readonly(mint, false), - // Destination token account (index 1) - AccountMeta::new(to, false), - // Authority for compression (index 2) - signer - AccountMeta::new_readonly(authority, true), - // Source SPL token account (index 3) - writable - AccountMeta::new(source_spl_token_account, false), - // Token pool PDA (index 4) - writable - AccountMeta::new(token_pool_pda, false), - // SPL Token program (index 5) - needed for CPI - AccountMeta::new_readonly( - Pubkey::from(light_compressed_token_types::constants::SPL_TOKEN_PROGRAM_ID), - false, - ), - ]; - - let wrap_spl_to_ctoken_account = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::compress_spl( - amount, - 0, // mint - 3, // source or recpient - 2, // authority - 4, // pool_account_index: - 0, // pool_index - token_pool_pda_bump, - )), - delegate_is_set: false, - method_used: true, - }; - - let ctoken_account = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::decompress_ctoken(amount, 0, 1)), - delegate_is_set: false, - method_used: true, - }; - - // Create Transfer2Inputs following the test pattern - let inputs = Transfer2Inputs { - validity_proof: ValidityProof::default(), - transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), - meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( - payer, - packed_accounts, - ), - in_lamports: None, - out_lamports: None, - token_accounts: vec![wrap_spl_to_ctoken_account, ctoken_account], - output_queue: 0, // Decompressed accounts only, no output queue needed - }; - - // Create the actual transfer2 instruction - create_transfer2_instruction(inputs) -} - -#[allow(clippy::too_many_arguments)] -#[profile] -pub fn create_ctoken_to_spl_transfer_instruction( - source_ctoken_account: Pubkey, - destination_spl_token_account: Pubkey, - amount: u64, - authority: Pubkey, - mint: Pubkey, - payer: Pubkey, - token_pool_pda: Pubkey, - token_pool_pda_bump: u8, -) -> Result { - let packed_accounts = vec![ - // Mint (index 0) - AccountMeta::new_readonly(mint, false), - // Source ctoken account (index 1) - writable - AccountMeta::new(source_ctoken_account, false), - // Destination SPL token account (index 2) - writable - AccountMeta::new(destination_spl_token_account, false), - // Authority (index 3) - signer - AccountMeta::new_readonly(authority, true), - // Token pool PDA (index 4) - writable - AccountMeta::new(token_pool_pda, false), - // SPL Token program (index 5) - needed for CPI - AccountMeta::new_readonly( - Pubkey::from(light_compressed_token_types::constants::SPL_TOKEN_PROGRAM_ID), - false, - ), - ]; - - // First operation: compress from ctoken account to pool using compress_spl - let compress_to_pool = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::compress_ctoken( - amount, 0, // mint index - 1, // source ctoken account index - 3, // authority index - )), - delegate_is_set: false, - method_used: true, - }; - - // Second operation: decompress from pool to SPL token account using decompress_spl - let decompress_to_spl = CTokenAccount2 { - inputs: vec![], - output: MultiTokenTransferOutputData::default(), - compression: Some(Compression::decompress_spl( - amount, - 0, // mint index - 2, // destination SPL token account index - 4, // pool_account_index - 0, // pool_index (TODO: make dynamic) - token_pool_pda_bump, - )), - delegate_is_set: false, - method_used: true, - }; - - // Create Transfer2Inputs - let inputs = Transfer2Inputs { - validity_proof: ValidityProof::default(), - transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), - meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( - payer, - packed_accounts, - ), - in_lamports: None, - out_lamports: None, - token_accounts: vec![compress_to_pool, decompress_to_spl], - output_queue: 0, // Decompressed accounts only, no output queue needed - }; - - // Create the actual transfer2 instruction - create_transfer2_instruction(inputs) -} diff --git a/sdk-libs/compressed-token-sdk/src/ctoken.rs b/sdk-libs/compressed-token-sdk/src/ctoken.rs new file mode 100644 index 0000000000..5e02ced318 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/ctoken.rs @@ -0,0 +1,50 @@ +use light_compressed_token_types::POOL_SEED; +use light_compressible::config::CompressibleConfig; +use solana_pubkey::{pubkey, Pubkey}; + +pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); + +/// Returns the program ID for the Compressed Token Program +pub fn id() -> Pubkey { + CTOKEN_PROGRAM_ID +} + +/// Return the cpi authority pda of the Compressed Token Program. +pub fn cpi_authority() -> Pubkey { + CTOKEN_CPI_AUTHORITY +} + +pub fn get_token_pool_address_and_bump(mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[POOL_SEED, mint.as_ref()], &CTOKEN_PROGRAM_ID) +} + +/// Returns the associated ctoken address for a given owner and mint. +pub fn get_associated_ctoken_address(owner: &Pubkey, mint: &Pubkey) -> Pubkey { + get_associated_ctoken_address_and_bump(owner, mint).0 +} + +/// Returns the associated ctoken address and bump for a given owner and mint. +pub fn get_associated_ctoken_address_and_bump(owner: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()], + &id(), + ) +} + +pub use crate::instructions::create_compressed_mint::{ + derive_cmint_from_spl_mint, find_spl_mint_address, +}; + +pub fn config_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_config_pda() +} + +pub fn rent_sponsor_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_rent_sponsor_pda() +} + +pub fn compression_authority_pda() -> Pubkey { + CompressibleConfig::ctoken_v1_compression_authority_pda() +} diff --git a/sdk-libs/compressed-token-sdk/src/error.rs b/sdk-libs/compressed-token-sdk/src/error.rs index 3f4206a02a..f067db9542 100644 --- a/sdk-libs/compressed-token-sdk/src/error.rs +++ b/sdk-libs/compressed-token-sdk/src/error.rs @@ -49,6 +49,14 @@ pub enum TokenSdkError { CannotMintWithDecompressedInCpiWrite, #[error("RentAuthorityIsNone")] RentAuthorityIsNone, + #[error("Incomplete SPL bridge config")] + IncompleteSplBridgeConfig, + #[error("SPL bridge config required")] + SplBridgeConfigRequired, + #[error("Use regular SPL transfer")] + UseRegularSplTransfer, + #[error("Cannot determine account type")] + CannotDetermineAccountType, #[error(transparent)] CompressedTokenTypes(#[from] LightTokenSdkTypeError), #[error(transparent)] @@ -97,6 +105,10 @@ impl From for u32 { TokenSdkError::PackedAccountIndexOutOfBounds => 17017, TokenSdkError::CannotMintWithDecompressedInCpiWrite => 17018, TokenSdkError::RentAuthorityIsNone => 17019, + TokenSdkError::SplBridgeConfigRequired => 17020, + TokenSdkError::IncompleteSplBridgeConfig => 17021, + TokenSdkError::UseRegularSplTransfer => 17022, + TokenSdkError::CannotDetermineAccountType => 17023, TokenSdkError::CompressedTokenTypes(e) => e.into(), TokenSdkError::CTokenError(e) => e.into(), TokenSdkError::LightSdkTypesError(e) => e.into(), diff --git a/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs b/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs index e326f2c5ab..0ddee8502a 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs @@ -9,6 +9,7 @@ use light_sdk::{ }; use light_zero_copy::traits::ZeroCopyAt; use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; use solana_instruction::{AccountMeta, Instruction}; use solana_msg::msg; use solana_pubkey::Pubkey; @@ -23,6 +24,7 @@ use crate::{ }, CTokenDefaultAccounts, }, + AccountInfoToCompress, }; /// Struct to hold all the indices needed for CompressAndClose operation @@ -216,6 +218,7 @@ pub fn compress_and_close_ctoken_accounts_with_indices<'info>( i as u8, // Pass the index in the output array idx.destination_index, // destination for user funds )?; + if rent_sponsor_is_signer { packed_account_metas[idx.authority_index as usize].is_signer = true; } else { @@ -365,16 +368,12 @@ pub fn compress_and_close_ctoken_accounts<'info>( rent_sponsor_pubkey.unwrap() }; - // Determine destination based on authority type let destination_pubkey = if with_compression_authority { - // When rent authority closes, everything goes to rent recipient actual_rent_sponsor } else { - // When owner closes, user funds go to owner owner_pubkey }; - // Find indices for all required accounts let indices = find_account_indices( find_index, ctoken_account_info.key, @@ -383,7 +382,6 @@ pub fn compress_and_close_ctoken_accounts<'info>( &authority, &actual_rent_sponsor, &destination_pubkey, - // &output_queue_pubkey, )?; indices_vec.push(indices); } @@ -391,7 +389,6 @@ pub fn compress_and_close_ctoken_accounts<'info>( packed_accounts_vec.push(output_queue); packed_accounts_vec.extend_from_slice(packed_accounts); - // Delegate to the with_indices version compress_and_close_ctoken_accounts_with_indices( fee_payer, with_compression_authority, @@ -401,6 +398,65 @@ pub fn compress_and_close_ctoken_accounts<'info>( ) } +/// Compress and close ctoken accounts, and invoke cpi. +/// +/// Wraps `compress_and_close_ctoken_accounts`, builds the instruction, and +/// calls `invoke_signed` with provided seeds. +/// +/// `remaining_accounts` must include required Light system accounts for +/// `transfer2`, followed by any additional accounts. Post_system accounts are a +/// subset of `remaining_accounts`. +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>( + token_accounts_to_compress: &[AccountInfoToCompress<'info>], + fee_payer: AccountInfo<'info>, + output_queue: AccountInfo<'info>, + compressed_token_rent_sponsor: AccountInfo<'info>, + compressed_token_cpi_authority: AccountInfo<'info>, + cpi_authority: AccountInfo<'info>, + post_system: &[AccountInfo<'info>], + remaining_accounts: &[AccountInfo<'info>], + with_compression_authority: bool, +) -> Result<(), TokenSdkError> { + let mut packed_accounts = Vec::with_capacity(post_system.len() + 4); + packed_accounts.extend_from_slice(post_system); + packed_accounts.push(cpi_authority); + packed_accounts.push(compressed_token_rent_sponsor.clone()); + + let ctoken_infos: Vec<&AccountInfo<'info>> = token_accounts_to_compress + .iter() + .map(|t| t.account_info.as_ref()) + .collect(); + + let instruction = compress_and_close_ctoken_accounts( + *fee_payer.key, + with_compression_authority, + output_queue, + &ctoken_infos, + &packed_accounts, + )?; + // infos + let total_capacity = packed_accounts.len() + remaining_accounts.len() + 1; + let mut account_infos: Vec> = Vec::with_capacity(total_capacity); + account_infos.extend_from_slice(&packed_accounts); + account_infos.push(compressed_token_cpi_authority); + account_infos.extend_from_slice(remaining_accounts); + + let token_seeds_refs: Vec> = token_accounts_to_compress + .iter() + .map(|t| t.signer_seeds.iter().map(|v| v.as_slice()).collect()) + .collect(); + let mut all_signer_seeds: Vec<&[&[u8]]> = Vec::with_capacity(token_seeds_refs.len()); + for seeds in &token_seeds_refs { + all_signer_seeds.push(seeds.as_slice()); + } + + invoke_signed(&instruction, &account_infos, &all_signer_seeds) + .map_err(|e| TokenSdkError::CpiError(e.to_string()))?; + Ok(()) +} + pub struct CompressAndCloseAccounts { pub compressed_token_program: Pubkey, pub cpi_authority_pda: Pubkey, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs index f7e7280654..9b19afe417 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs @@ -7,6 +7,7 @@ use light_ctoken_types::{ }, state::TokenDataVersion, }; +use solana_account_info::AccountInfo; use solana_instruction::Instruction; use solana_pubkey::Pubkey; @@ -401,3 +402,92 @@ fn create_ata2_instruction_unified( + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + compressible_config: AccountInfo<'info>, + rent_sponsor: AccountInfo<'info>, + authority: AccountInfo<'info>, + mint: Pubkey, + bump: u8, + pre_pay_num_epochs: Option, + lamports_per_write: Option, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let inputs = CreateCompressibleAssociatedTokenAccountInputs { + payer: *payer.key, + owner: *authority.key, + mint, + compressible_config: *compressible_config.key, + rent_sponsor: *rent_sponsor.key, + pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(1), + lamports_per_write, + token_account_version: TokenDataVersion::ShaFlat, + }; + + // TODO: switch to wrapper ixn using accounts instead of ixdata. + let ix = create_compressible_associated_token_account_with_bump( + inputs, + *associated_token_account.key, + bump, + )?; + + solana_cpi::invoke( + &ix, + &[ + payer, + associated_token_account, + system_program, + compressible_config, + rent_sponsor, + authority, + ], + ) +} + +/// CPI wrapper to create a compressible c-token associated token account +/// idempotently. +#[allow(clippy::too_many_arguments)] +pub fn create_associated_ctoken_account_idempotent<'info>( + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + compressible_config: AccountInfo<'info>, + rent_sponsor: AccountInfo<'info>, + authority: Pubkey, + mint: Pubkey, + bump: u8, + pre_pay_num_epochs: Option, + lamports_per_write: Option, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let inputs = CreateCompressibleAssociatedTokenAccountInputs { + payer: *payer.key, + owner: authority, + mint, + compressible_config: *compressible_config.key, + rent_sponsor: *rent_sponsor.key, + pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(1), + lamports_per_write, + token_account_version: TokenDataVersion::ShaFlat, + }; + + let ix = create_compressible_associated_token_account_with_bump_and_mode::( + inputs, + *associated_token_account.key, + bump, + )?; + + solana_cpi::invoke( + &ix, + &[ + payer, + associated_token_account, + system_program, + compressible_config, + rent_sponsor, + ], + ) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs index a73a23fbe3..a3706648ef 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs @@ -201,10 +201,7 @@ pub fn derive_compressed_mint_address( ) } -pub fn derive_compressed_mint_from_spl_mint( - mint: &Pubkey, - address_tree_pubkey: &Pubkey, -) -> [u8; 32] { +pub fn derive_cmint_from_spl_mint(mint: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] { light_compressed_account::address::derive_address( &mint.to_bytes(), &address_tree_pubkey.to_bytes(), diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs index 3d2f390bf6..9569f03812 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs @@ -5,45 +5,7 @@ pub use account_metas::{ get_create_compressed_mint_instruction_account_metas, CreateCompressedMintMetaConfig, }; pub use instruction::{ - create_compressed_mint, create_compressed_mint_cpi, derive_compressed_mint_address, - derive_compressed_mint_from_spl_mint, find_spl_mint_address, CreateCompressedMintInputs, - CREATE_COMPRESSED_MINT_DISCRIMINATOR, + create_compressed_mint, create_compressed_mint_cpi, create_compressed_mint_cpi_write, + derive_cmint_from_spl_mint, derive_compressed_mint_address, find_spl_mint_address, + CreateCompressedMintInputs, CREATE_COMPRESSED_MINT_DISCRIMINATOR, }; -use light_account_checks::AccountInfoTrait; -use light_sdk::cpi::CpiSigner; - -#[derive(Clone, Debug)] -pub struct CpiContextWriteAccounts<'a, T: AccountInfoTrait + Clone> { - pub mint_signer: &'a T, - pub light_system_program: &'a T, - pub fee_payer: &'a T, - pub cpi_authority_pda: &'a T, - pub cpi_context: &'a T, - pub cpi_signer: CpiSigner, -} - -impl CpiContextWriteAccounts<'_, T> { - pub fn bump(&self) -> u8 { - self.cpi_signer.bump - } - - pub fn invoking_program(&self) -> [u8; 32] { - self.cpi_signer.program_id - } - - pub fn to_account_infos(&self) -> Vec { - // The 5 accounts expected by create_compressed_mint_cpi_write: - // [mint_signer, light_system_program, fee_payer, cpi_authority_pda, cpi_context] - vec![ - self.mint_signer.clone(), - self.light_system_program.clone(), - self.fee_payer.clone(), - self.cpi_authority_pda.clone(), - self.cpi_context.clone(), - ] - } - - pub fn to_account_info_refs(&self) -> [&T; 3] { - [self.mint_signer, self.fee_payer, self.cpi_context] - } -} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs index 8a7c0c6952..d69989ef4a 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs @@ -5,7 +5,9 @@ use light_ctoken_types::{ extensions::compressible::{CompressToPubkey, CompressibleExtensionInstructionData}, }, state::TokenDataVersion, + CTokenError, }; +use solana_account_info::AccountInfo; use solana_instruction::Instruction; use solana_pubkey::Pubkey; @@ -35,7 +37,7 @@ pub struct CreateCompressibleTokenAccount { pub token_account_version: TokenDataVersion, } -pub fn create_compressible_token_account( +pub fn create_compressible_token_account_instruction( inputs: CreateCompressibleTokenAccount, ) -> Result { // Create the CompressibleExtensionInstructionData @@ -114,3 +116,56 @@ pub fn create_token_account( data, }) } + +/// Create a c-token account with signer seeds. +#[allow(clippy::too_many_arguments)] +pub fn create_ctoken_account_signed<'info>( + program_id: Pubkey, + payer: AccountInfo<'info>, + token_account: AccountInfo<'info>, + mint_account: AccountInfo<'info>, + authority: Pubkey, + signer_seeds: &[&[u8]], + ctoken_rent_sponsor: AccountInfo<'info>, + ctoken_config_account: AccountInfo<'info>, + pre_pay_num_epochs: Option, + lamports_per_write: Option, +) -> std::result::Result<(), solana_program_error::ProgramError> { + let bump = signer_seeds[signer_seeds.len() - 1][0]; + let seeds: Vec> = signer_seeds[..signer_seeds.len() - 1] + .iter() + .map(|seed| seed.to_vec()) + .collect(); + + let params = CreateCompressibleTokenAccount { + payer: *payer.key, + account_pubkey: *token_account.key, + mint_pubkey: *mint_account.key, + owner_pubkey: authority, + compressible_config: *ctoken_config_account.key, + rent_sponsor: *ctoken_rent_sponsor.key, + pre_pay_num_epochs: pre_pay_num_epochs.unwrap_or(0), + lamports_per_write, + compress_to_account_pubkey: Some(CompressToPubkey { + bump, + program_id: program_id.to_bytes(), + seeds, + }), + token_account_version: TokenDataVersion::ShaFlat, + }; + let ix = create_compressible_token_account_instruction(params) + .map_err(|_| TokenSdkError::CTokenError(CTokenError::InvalidInstructionData))?; + + // TODO: check whether we need to pass c-token program / system program + solana_cpi::invoke_signed( + &ix, + &[ + payer, + token_account, + mint_account, + ctoken_rent_sponsor, + ctoken_config_account, + ], + &[signer_seeds], + ) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs index 5297a9d6e1..9e0e827234 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs @@ -6,7 +6,6 @@ use light_program_profiler::profile; use light_sdk::{ error::LightSdkError, instruction::{AccountMetasVec, PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig}, - token::TokenData, }; use solana_account_info::AccountInfo; use solana_instruction::{AccountMeta, Instruction}; @@ -14,6 +13,7 @@ use solana_pubkey::Pubkey; use crate::{ account2::CTokenAccount2, + compat::TokenData, error::TokenSdkError, instructions::{ transfer2::{ @@ -59,6 +59,12 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( // Process each set of indices let mut token_accounts = Vec::with_capacity(indices.len()); + // Convert packed_accounts to AccountMetas + // TODO: we may have to add conditional delegate signers for delegate + // support via CPI. + // Build signer flags in O(n) instead of scanning on every meta push + let mut signer_flags = vec![false; packed_accounts.len()]; + for idx in indices.iter() { // Create CTokenAccount2 with the source data // For decompress_full, we don't have an output tree since everything goes to the destination @@ -67,18 +73,23 @@ pub fn decompress_full_ctoken_accounts_with_indices<'info>( // Set up decompress_full - decompress entire balance to destination ctoken account token_account.decompress_ctoken(idx.source.amount, idx.destination_index)?; token_accounts.push(token_account); + + let owner_idx = idx.source.owner as usize; + if owner_idx >= signer_flags.len() { + return Err(TokenSdkError::InvalidAccountData); + } + signer_flags[owner_idx] = true; } - // Convert packed_accounts to AccountMetas let mut packed_account_metas = Vec::with_capacity(packed_accounts.len()); - for info in packed_accounts.iter() { + + for (i, info) in packed_accounts.iter().enumerate() { packed_account_metas.push(AccountMeta { pubkey: *info.key, - is_signer: info.is_signer, + is_signer: info.is_signer || signer_flags[i], is_writable: info.is_writable, }); } - let (meta_config, transfer_config) = if let Some(cpi_context) = cpi_context_pubkey { let cpi_context_config = CompressedCpiContext { set_context: false, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index b5234076ee..ade4b9ee1c 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -13,6 +13,8 @@ pub mod mint_action; pub mod mint_to_compressed; pub mod transfer; pub mod transfer2; +pub mod transfer_ctoken; +pub mod transfer_interface; pub mod update_compressed_mint; pub mod withdraw_funding_pool; @@ -34,7 +36,8 @@ pub use create_associated_token_account::*; pub use create_compressed_mint::*; pub use create_spl_mint::*; pub use create_token_account::{ - create_compressible_token_account, create_token_account, CreateCompressibleTokenAccount, + create_compressible_token_account_instruction, create_ctoken_account_signed, + create_token_account, CreateCompressibleTokenAccount, }; pub use ctoken_accounts::*; pub use decompress_full::{decompress_full_ctoken_accounts_with_indices, DecompressFullIndices}; @@ -49,18 +52,13 @@ pub use mint_to_compressed::{ create_mint_to_compressed_instruction, get_mint_to_compressed_instruction_account_metas, DecompressedMintConfig, MintToCompressedInputs, MintToCompressedMetaConfig, }; +pub use transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed}; +pub use transfer_interface::{ + create_transfer_ctoken_to_spl_instruction, create_transfer_spl_to_ctoken_instruction, + transfer_interface, transfer_interface_signed, +}; pub use update_compressed_mint::{ update_compressed_mint, update_compressed_mint_cpi, UpdateCompressedMintInputs, UPDATE_COMPRESSED_MINT_DISCRIMINATOR, }; pub use withdraw_funding_pool::withdraw_funding_pool; - -/// Derive token pool information for a given mint -pub fn derive_token_pool(mint: &solana_pubkey::Pubkey, index: u8) -> mint_action::TokenPool { - let (pubkey, bump) = crate::token_pool::find_token_pool_pda_with_index(mint, index); - mint_action::TokenPool { - pubkey, - bump, - index, - } -} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs new file mode 100644 index 0000000000..c5f1b97dac --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs @@ -0,0 +1,68 @@ +use light_sdk_types::C_TOKEN_PROGRAM_ID; +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +/// Create a c-token transfer instruction. +/// +/// # Arguments +/// * `source` - Source token account +/// * `destination` - Destination token account +/// * `amount` - Amount to transfer +/// * `authority` - Authority pubkey +/// +/// # Returns +/// `Instruction` +fn create_transfer_ctoken_instruction( + source: Pubkey, + destination: Pubkey, + amount: u64, + authority: Pubkey, +) -> Instruction { + Instruction { + program_id: Pubkey::from(C_TOKEN_PROGRAM_ID), + accounts: vec![ + AccountMeta::new(source, false), + AccountMeta::new(destination, false), + AccountMeta::new_readonly(authority, true), + ], + data: { + // TODO: check why we have 2 discriminators + let mut data = vec![3u8]; + data.push(3u8); + data.extend_from_slice(&amount.to_le_bytes()); + data + }, + } +} + +/// Transfer c-tokens +pub fn transfer_ctoken<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let ix = create_transfer_ctoken_instruction(*from.key, *to.key, amount, *authority.key); + + invoke(&ix, &[from.clone(), to.clone(), authority.clone()]) +} + +/// Transfer c-tokens CPI +pub fn transfer_ctoken_signed<'info>( + from: &AccountInfo<'info>, + to: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let ix = create_transfer_ctoken_instruction(*from.key, *to.key, amount, *authority.key); + + invoke_signed( + &ix, + &[from.clone(), to.clone(), authority.clone()], + signer_seeds, + ) +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs new file mode 100644 index 0000000000..414d5012ff --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -0,0 +1,539 @@ +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_ctoken_types::instructions::transfer2::{Compression, MultiTokenTransferOutputData}; +use light_program_profiler::profile; +use solana_account_info::AccountInfo; +use solana_cpi::{invoke, invoke_signed}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +use super::transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed}; +use crate::{ + account2::CTokenAccount2, + error::TokenSdkError, + instructions::transfer2::{ + account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config, + Transfer2Inputs, + }, + utils::is_ctoken_account, +}; + +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn create_transfer_spl_to_ctoken_instruction( + source_spl_token_account: Pubkey, + to: Pubkey, + amount: u64, + authority: Pubkey, + mint: Pubkey, + payer: Pubkey, + token_pool_pda: Pubkey, + token_pool_pda_bump: u8, + spl_token_program: Pubkey, +) -> Result { + let packed_accounts = vec![ + // Mint (index 0) + AccountMeta::new_readonly(mint, false), + // Destination token account (index 1) + AccountMeta::new(to, false), + // Authority for compression (index 2) - signer + AccountMeta::new_readonly(authority, true), + // Source SPL token account (index 3) - writable + AccountMeta::new(source_spl_token_account, false), + // Token pool PDA (index 4) - writable + AccountMeta::new(token_pool_pda, false), + // SPL Token program (index 5) - needed for CPI + AccountMeta::new_readonly(spl_token_program, false), + ]; + + let wrap_spl_to_ctoken_account = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::compress_spl( + amount, + 0, // mint + 3, // source or recpient + 2, // authority + 4, // pool_account_index: + 0, // pool_index + token_pool_pda_bump, + )), + delegate_is_set: false, + method_used: true, + }; + + let ctoken_account = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::decompress_ctoken(amount, 0, 1)), + delegate_is_set: false, + method_used: true, + }; + + let inputs = Transfer2Inputs { + validity_proof: ValidityProof::new(None), + transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), + meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( + payer, + packed_accounts, + ), + in_lamports: None, + out_lamports: None, + token_accounts: vec![wrap_spl_to_ctoken_account, ctoken_account], + output_queue: 0, // Decompressed accounts only, no output queue needed + }; + + create_transfer2_instruction(inputs) +} + +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn create_transfer_ctoken_to_spl_instruction( + source_ctoken_account: Pubkey, + destination_spl_token_account: Pubkey, + amount: u64, + authority: Pubkey, + mint: Pubkey, + payer: Pubkey, + token_pool_pda: Pubkey, + token_pool_pda_bump: u8, + spl_token_program: Pubkey, +) -> Result { + let packed_accounts = vec![ + // Mint (index 0) + AccountMeta::new_readonly(mint, false), + // Source ctoken account (index 1) - writable + AccountMeta::new(source_ctoken_account, false), + // Destination SPL token account (index 2) - writable + AccountMeta::new(destination_spl_token_account, false), + // Authority (index 3) - signer + AccountMeta::new_readonly(authority, true), + // Token pool PDA (index 4) - writable + AccountMeta::new(token_pool_pda, false), + // SPL Token program (index 5) - needed for CPI + AccountMeta::new_readonly(spl_token_program, false), + ]; + + // First operation: compress from ctoken account to pool using compress_spl + let compress_to_pool = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::compress_ctoken( + amount, 0, // mint index + 1, // source ctoken account index + 3, // authority index + )), + delegate_is_set: false, + method_used: true, + }; + + // Second operation: decompress from pool to SPL token account using decompress_spl + let decompress_to_spl = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::decompress_spl( + amount, + 0, // mint index + 2, // destination SPL token account index + 4, // pool_account_index + 0, // pool_index (TODO: make dynamic) + token_pool_pda_bump, + )), + delegate_is_set: false, + method_used: true, + }; + + let inputs = Transfer2Inputs { + validity_proof: ValidityProof::new(None), + transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), + meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( + payer, + packed_accounts, + ), + in_lamports: None, + out_lamports: None, + token_accounts: vec![compress_to_pool, decompress_to_spl], + output_queue: 0, // Decompressed accounts only, no output queue needed + }; + + create_transfer2_instruction(inputs) +} + +/// Transfer SPL tokens to compressed tokens +#[allow(clippy::too_many_arguments)] +pub fn transfer_spl_to_ctoken<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_spl_token_account: AccountInfo<'info>, + destination_ctoken_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let instruction = create_transfer_spl_to_ctoken_instruction( + *source_spl_token_account.key, + *destination_ctoken_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // let mut account_infos = remaining_accounts.to_vec(); + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_ctoken_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_spl_token_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke(&instruction, &account_infos)?; + Ok(()) +} + +// TODO: must test this. +/// Transfer SPL tokens to compressed tokens via CPI signer +#[allow(clippy::too_many_arguments)] +pub fn transfer_spl_to_ctoken_signed<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_spl_token_account: AccountInfo<'info>, + destination_ctoken_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let instruction = create_transfer_spl_to_ctoken_instruction( + *source_spl_token_account.key, + *destination_ctoken_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_ctoken_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_spl_token_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke_signed(&instruction, &account_infos, signer_seeds)?; + Ok(()) +} + +// TODO: TEST. +/// Transfer compressed tokens to SPL tokens +#[allow(clippy::too_many_arguments)] +pub fn transfer_ctoken_to_spl<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_ctoken_account: AccountInfo<'info>, + destination_spl_token_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, +) -> Result<(), ProgramError> { + let instruction = create_transfer_ctoken_to_spl_instruction( + *source_ctoken_account.key, + *destination_spl_token_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_spl_token_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_ctoken_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke(&instruction, &account_infos)?; + Ok(()) +} + +/// Transfer compressed tokens to SPL tokens via CPI signer +#[allow(clippy::too_many_arguments)] +pub fn transfer_ctoken_to_spl_signed<'info>( + payer: AccountInfo<'info>, + authority: AccountInfo<'info>, + source_ctoken_account: AccountInfo<'info>, + destination_spl_token_account: AccountInfo<'info>, + mint: AccountInfo<'info>, + spl_token_program: AccountInfo<'info>, + compressed_token_pool_pda: AccountInfo<'info>, + compressed_token_pool_pda_bump: u8, + compressed_token_program_authority: AccountInfo<'info>, + amount: u64, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + let instruction = create_transfer_ctoken_to_spl_instruction( + *source_ctoken_account.key, + *destination_spl_token_account.key, + amount, + *authority.key, + *mint.key, + *payer.key, + *compressed_token_pool_pda.key, + compressed_token_pool_pda_bump, + *spl_token_program.key, + ) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + let account_infos = vec![ + payer, + compressed_token_program_authority, + mint, // Index 0: Mint + destination_spl_token_account, // Index 1: Destination owner + authority, // Index 2: Authority (signer) + source_ctoken_account, // Index 3: Source SPL token account + compressed_token_pool_pda, // Index 4: Token pool PDA + spl_token_program, // Index 5: SPL Token program + ]; + + invoke_signed(&instruction, &account_infos, signer_seeds)?; + Ok(()) +} + +/// Unified transfer interface for ctoken<->ctoken and ctoken<->spl transfers +/// +/// # Arguments +/// * `source_account` - Source token account (can be ctoken or SPL) +/// * `destination_account` - Destination token account (can be ctoken or SPL) +/// * `authority` - Authority for the transfer (must be signer) +/// * `amount` - Amount to transfer +/// * `payer` - Payer for the transaction +/// * `compressed_token_program_authority` - Compressed token program authority +/// * `mint` - Optional mint account (required for SPL<->ctoken transfers) +/// * `spl_token_program` - Optional SPL token program (required for SPL<->ctoken transfers) +/// * `compressed_token_pool_pda` - Optional token pool PDA (required for SPL<->ctoken transfers) +/// * `compressed_token_pool_pda_bump` - Optional bump seed for token pool PDA +/// +/// # Errors +/// * `SplBridgeConfigRequired` - If transferring to/from SPL without required accounts +/// * `UseRegularSplTransfer` - If both source and destination are SPL accounts +/// * `CannotDetermineAccountType` - If account type cannot be determined +#[allow(clippy::too_many_arguments)] +pub fn transfer_interface<'info>( + source_account: &AccountInfo<'info>, + destination_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + payer: &AccountInfo<'info>, + compressed_token_program_authority: &AccountInfo<'info>, + mint: Option<&AccountInfo<'info>>, + spl_token_program: Option<&AccountInfo<'info>>, + compressed_token_pool_pda: Option<&AccountInfo<'info>>, + compressed_token_pool_pda_bump: Option, +) -> Result<(), ProgramError> { + let source_is_ctoken = + is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?; + let dest_is_ctoken = + is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?; + + match (source_is_ctoken, dest_is_ctoken) { + (true, true) => transfer_ctoken(source_account, destination_account, authority, amount), + + (true, false) => { + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_ctoken_to_spl( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + ) + } + + (false, true) => { + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_spl_to_ctoken( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + ) + } + + // spl -> spl: Not supported + (false, false) => Err(ProgramError::Custom( + TokenSdkError::UseRegularSplTransfer.into(), + )), + } +} + +/// Unified transfer interface with CPI +#[allow(clippy::too_many_arguments)] +pub fn transfer_interface_signed<'info>( + source_account: &AccountInfo<'info>, + destination_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + amount: u64, + payer: &AccountInfo<'info>, + compressed_token_program_authority: &AccountInfo<'info>, + mint: Option<&AccountInfo<'info>>, + spl_token_program: Option<&AccountInfo<'info>>, + compressed_token_pool_pda: Option<&AccountInfo<'info>>, + compressed_token_pool_pda_bump: Option, + signer_seeds: &[&[&[u8]]], +) -> Result<(), ProgramError> { + // Determine account types + let source_is_ctoken = + is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?; + let dest_is_ctoken = + is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?; + + match (source_is_ctoken, dest_is_ctoken) { + // ctoken -> ctoken: Direct transfer (bridge accounts not needed) + (true, true) => transfer_ctoken_signed( + source_account, + destination_account, + authority, + amount, + signer_seeds, + ), + + // ctoken -> spl: Requires bridge accounts + (true, false) => { + // Validate all required accounts are provided + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_ctoken_to_spl_signed( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + signer_seeds, + ) + } + + // spl -> ctoken: Requires bridge accounts + (false, true) => { + // Validate all required accounts are provided + let (mint_acct, spl_program, pool_pda, bump) = match ( + mint, + spl_token_program, + compressed_token_pool_pda, + compressed_token_pool_pda_bump, + ) { + (Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b), + _ => { + return Err(ProgramError::Custom( + TokenSdkError::IncompleteSplBridgeConfig.into(), + )) + } + }; + + transfer_spl_to_ctoken_signed( + payer.clone(), + authority.clone(), + source_account.clone(), + destination_account.clone(), + mint_acct.clone(), + spl_program.clone(), + pool_pda.clone(), + bump, + compressed_token_program_authority.clone(), + amount, + signer_seeds, + ) + } + + // spl -> spl: Not supported + (false, false) => Err(ProgramError::Custom( + TokenSdkError::UseRegularSplTransfer.into(), + )), + } +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs b/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs index 8c574b1c12..480e42a421 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs @@ -15,7 +15,6 @@ pub struct UpdateCompressedMintMetaConfig { } /// Generates account metas for the update compressed mint instruction -/// Following the same pattern as other compressed token instructions pub fn get_update_compressed_mint_instruction_account_metas( config: UpdateCompressedMintMetaConfig, ) -> Vec { diff --git a/sdk-libs/compressed-token-sdk/src/lib.rs b/sdk-libs/compressed-token-sdk/src/lib.rs index 45ac38becd..c38cdd1255 100644 --- a/sdk-libs/compressed-token-sdk/src/lib.rs +++ b/sdk-libs/compressed-token-sdk/src/lib.rs @@ -1,7 +1,9 @@ pub mod account; pub mod account2; +pub mod ctoken; pub mod error; pub mod instructions; +pub mod pack; pub mod token_metadata_ui; pub mod token_pool; pub mod utils; @@ -11,4 +13,10 @@ pub mod utils; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +// Re-export pub use light_compressed_token_types::*; +pub use pack::{compat, Pack, Unpack}; +pub use utils::{ + account_meta_from_account_info, is_ctoken_account, AccountInfoToCompress, + PackedCompressedTokenDataWithContext, +}; diff --git a/sdk-libs/compressed-token-sdk/src/pack.rs b/sdk-libs/compressed-token-sdk/src/pack.rs new file mode 100644 index 0000000000..60eeab2330 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/pack.rs @@ -0,0 +1,329 @@ +//! Pack implementation for TokenData types for c-tokens. +use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +pub use light_ctoken_types::state::TokenData; +use light_ctoken_types::state::TokenDataVersion; +use light_sdk::{ + instruction::PackedAccounts, + light_hasher::{sha256::Sha256BE, HasherError}, +}; +use solana_account_info::AccountInfo; +use solana_program_error::ProgramError; + +use crate::{AnchorDeserialize, AnchorSerialize}; + +// We define the traits here to circumvent the orphan rule. +pub trait Pack { + type Packed; + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; +} +pub trait Unpack { + type Unpacked; + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result; +} + +impl Pack for TokenData { + type Packed = light_ctoken_types::instructions::transfer2::MultiTokenTransferOutputData; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + Self::Packed { + owner: remaining_accounts.insert_or_get(self.owner.to_bytes().into()), + mint: remaining_accounts.insert_or_get_read_only(self.mint.to_bytes().into()), + amount: self.amount, + has_delegate: self.delegate.is_some(), + delegate: if let Some(delegate) = self.delegate { + remaining_accounts.insert_or_get(delegate.to_bytes().into()) + } else { + 0 + }, + version: TokenDataVersion::ShaFlat as u8, + } + } +} + +impl Unpack for TokenData { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +/// Solana-compatible token types using `solana_pubkey::Pubkey` +pub mod compat { + use solana_pubkey::Pubkey; + + use super::*; + + #[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] + #[repr(u8)] + pub enum AccountState { + #[default] + Initialized = 0, + Frozen = 1, + } + + impl From for light_ctoken_types::state::CompressedTokenAccountState { + fn from(state: AccountState) -> Self { + match state { + AccountState::Initialized => { + light_ctoken_types::state::CompressedTokenAccountState::Initialized + } + AccountState::Frozen => { + light_ctoken_types::state::CompressedTokenAccountState::Frozen + } + } + } + } + + impl TryFrom for AccountState { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(AccountState::Initialized), + 1 => Ok(AccountState::Frozen), + _ => Err(ProgramError::InvalidAccountData), + } + } + } + + /// TokenData using standard Solana pubkeys. + /// + /// For zero-copy operations, use [`TokenData`](crate::types::TokenData) from the crate root. + #[derive(Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Clone, Default)] + pub struct TokenData { + /// The mint associated with this account + pub mint: Pubkey, + /// The owner of this account + pub owner: Pubkey, + /// The amount of tokens this account holds + pub amount: u64, + /// Optional delegate authorized to transfer tokens + pub delegate: Option, + /// The account's state + pub state: AccountState, + /// Placeholder for TokenExtension tlv data (unimplemented) + pub tlv: Option>, + } + + impl TokenData { + /// TokenDataVersion 3 + /// CompressedAccount Discriminator [0,0,0,0,0,0,0,4] + #[inline(always)] + pub fn hash_sha_flat(&self) -> Result<[u8; 32], HasherError> { + use light_sdk::light_hasher::Hasher; + let bytes = self.try_to_vec().map_err(|_| HasherError::BorshError)?; + Sha256BE::hash(bytes.as_slice()) + } + } + + /// TokenData with merkle context for verification + #[derive(Debug, Clone, PartialEq)] + pub struct TokenDataWithMerkleContext { + pub token_data: TokenData, + pub compressed_account: CompressedAccountWithMerkleContext, + } + + impl TokenDataWithMerkleContext { + /// Only works for sha flat hash + pub fn hash(&self) -> Result<[u8; 32], HasherError> { + if let Some(data) = self.compressed_account.compressed_account.data.as_ref() { + match data.discriminator { + [0, 0, 0, 0, 0, 0, 0, 4] => self.token_data.hash_sha_flat(), + _ => Err(HasherError::EmptyInput), + } + } else { + Err(HasherError::EmptyInput) + } + } + } + + impl From for crate::pack::TokenData { + fn from(data: TokenData) -> Self { + use light_ctoken_types::state::CompressedTokenAccountState; + + Self { + mint: data.mint.to_bytes().into(), + owner: data.owner.to_bytes().into(), + amount: data.amount, + delegate: data.delegate.map(|d| d.to_bytes().into()), + state: match data.state { + AccountState::Initialized => CompressedTokenAccountState::Initialized as u8, + AccountState::Frozen => CompressedTokenAccountState::Frozen as u8, + }, + tlv: data.tlv, + } + } + } + + impl From for TokenData { + fn from(data: crate::pack::TokenData) -> Self { + Self { + mint: Pubkey::new_from_array(data.mint.to_bytes()), + owner: Pubkey::new_from_array(data.owner.to_bytes()), + amount: data.amount, + delegate: data.delegate.map(|d| Pubkey::new_from_array(d.to_bytes())), + state: AccountState::try_from(data.state).unwrap_or(AccountState::Initialized), + tlv: data.tlv, + } + } + } + + impl Pack for TokenData { + type Packed = InputTokenDataCompressible; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + InputTokenDataCompressible { + owner: remaining_accounts.insert_or_get(self.owner), + mint: remaining_accounts.insert_or_get_read_only(self.mint), + amount: self.amount, + has_delegate: self.delegate.is_some(), + delegate: if let Some(delegate) = self.delegate { + remaining_accounts.insert_or_get(delegate) + } else { + 0 + }, + version: TokenDataVersion::ShaFlat as u8, + } + } + } + + impl Unpack for TokenData { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } + } + + impl Unpack for InputTokenDataCompressible { + type Unpacked = TokenData; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenData { + owner: *remaining_accounts + .get(self.owner as usize) + .ok_or(ProgramError::InvalidAccountData)? + .key, + amount: self.amount, + delegate: if self.has_delegate { + Some( + *remaining_accounts + .get(self.delegate as usize) + .ok_or(ProgramError::InvalidAccountData)? + .key, + ) + } else { + None + }, + mint: *remaining_accounts + .get(self.mint as usize) + .ok_or(ProgramError::InvalidAccountData)? + .key, + state: AccountState::Initialized, + tlv: None, + }) + } + } + + /// Wrapper for token data with variant information + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] + pub struct TokenDataWithVariant { + pub variant: V, + pub token_data: TokenData, + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] + pub struct PackedCTokenDataWithVariant { + pub variant: V, + pub token_data: InputTokenDataCompressible, + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] + pub struct CTokenDataWithVariant { + pub variant: V, + pub token_data: TokenData, + } + + impl Pack for CTokenDataWithVariant + where + V: AnchorSerialize + Clone + std::fmt::Debug, + { + type Packed = PackedCTokenDataWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedCTokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.pack(remaining_accounts), + } + } + } + + impl Unpack for CTokenDataWithVariant + where + V: Clone, + { + type Unpacked = TokenDataWithVariant; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.unpack(remaining_accounts)?, + }) + } + } + + impl Pack for TokenDataWithVariant + where + V: AnchorSerialize + Clone + std::fmt::Debug, + { + type Packed = PackedCTokenDataWithVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedCTokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.pack(remaining_accounts), + } + } + } + + impl Unpack for PackedCTokenDataWithVariant + where + V: Clone, + { + type Unpacked = TokenDataWithVariant; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(TokenDataWithVariant { + variant: self.variant.clone(), + token_data: self.token_data.unpack(remaining_accounts)?, + }) + } + } + + // TODO: remove aliases in separate PR + pub type InputTokenDataCompressible = + light_ctoken_types::instructions::transfer2::MultiTokenTransferOutputData; + pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; + pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; + pub type CTokenData = CTokenDataWithVariant; + pub type PackedCTokenData = PackedCTokenDataWithVariant; +} diff --git a/sdk-libs/compressed-token-sdk/src/token_pool.rs b/sdk-libs/compressed-token-sdk/src/token_pool.rs index 605706100d..e354eaf268 100644 --- a/sdk-libs/compressed-token-sdk/src/token_pool.rs +++ b/sdk-libs/compressed-token-sdk/src/token_pool.rs @@ -2,10 +2,14 @@ use light_compressed_token_types::constants::POOL_SEED; use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID; use solana_pubkey::Pubkey; +use crate::instructions::mint_action::TokenPool; + +/// Derive the token pool pda for a given mint pub fn get_token_pool_pda(mint: &Pubkey) -> Pubkey { get_token_pool_pda_with_index(mint, 0) } +/// Find the token pool pda for a given mint and index pub fn find_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> (Pubkey, u8) { let seeds = &[POOL_SEED, mint.as_ref(), &[token_pool_index]]; let seeds = if token_pool_index == 0 { @@ -16,6 +20,17 @@ pub fn find_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> (P Pubkey::find_program_address(seeds, &Pubkey::from(COMPRESSED_TOKEN_PROGRAM_ID)) } +/// Get the token pool pda for a given mint and index pub fn get_token_pool_pda_with_index(mint: &Pubkey, token_pool_index: u8) -> Pubkey { find_token_pool_pda_with_index(mint, token_pool_index).0 } + +/// Derive token pool information for a given mint +pub fn derive_token_pool(mint: &solana_pubkey::Pubkey, index: u8) -> TokenPool { + let (pubkey, bump) = find_token_pool_pda_with_index(mint, index); + TokenPool { + pubkey, + bump, + index, + } +} diff --git a/sdk-libs/compressed-token-sdk/src/utils.rs b/sdk-libs/compressed-token-sdk/src/utils.rs index b8d3050649..220be9a846 100644 --- a/sdk-libs/compressed-token-sdk/src/utils.rs +++ b/sdk-libs/compressed-token-sdk/src/utils.rs @@ -1,18 +1,60 @@ +use light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext; +use light_sdk_types::C_TOKEN_PROGRAM_ID; use solana_account_info::AccountInfo; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; use spl_pod::bytemuck::pod_from_bytes; use spl_token_2022::pod::PodAccount; -use crate::error::TokenSdkError; +use crate::{error::TokenSdkError, AnchorDeserialize, AnchorSerialize}; -/// Get token account balance from account info pub fn get_token_account_balance(token_account_info: &AccountInfo) -> Result { let token_account_data = token_account_info .try_borrow_data() .map_err(|_| TokenSdkError::AccountBorrowFailed)?; - // Use zero-copy PodAccount to access the token account let pod_account = pod_from_bytes::(&token_account_data) .map_err(|_| TokenSdkError::InvalidAccountData)?; Ok(pod_account.amount.into()) } + +pub fn is_ctoken_account(account_info: &AccountInfo) -> Result { + let ctoken_program_id = Pubkey::from(C_TOKEN_PROGRAM_ID); + + if account_info.owner == &ctoken_program_id { + return Ok(true); + } + + let token_22 = spl_token_2022::ID; + let spl_token = Pubkey::from_str_const("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + + if account_info.owner == &token_22 || account_info.owner == &spl_token { + return Ok(false); + } + + Err(TokenSdkError::CannotDetermineAccountType) +} + +pub const CLOSE_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 9; + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedCompressedTokenDataWithContext { + pub mint: u8, + pub source_or_recipient_token_account: u8, + pub multi_input_token_data_with_context: MultiInputTokenDataWithContext, +} + +pub fn account_meta_from_account_info(account_info: &AccountInfo) -> AccountMeta { + AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct AccountInfoToCompress<'info> { + pub account_info: AccountInfo<'info>, + pub signer_seeds: Vec>, +} diff --git a/sdk-libs/compressed-token-sdk/tests/pack_test.rs b/sdk-libs/compressed-token-sdk/tests/pack_test.rs new file mode 100644 index 0000000000..67f7502bc4 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/tests/pack_test.rs @@ -0,0 +1,121 @@ +#[cfg(test)] +mod tests { + + use light_compressed_token_sdk::{ + compat::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + Pack, + }; + use light_sdk::instruction::PackedAccounts; + use solana_pubkey::Pubkey; + + #[test] + fn test_token_data_packing() { + let mut remaining_accounts = PackedAccounts::default(); + + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + + let token_data = TokenData { + owner, + mint, + amount: 1000, + delegate: Some(delegate), + state: Default::default(), + tlv: None, + }; + + // Pack the token data + let packed = token_data.pack(&mut remaining_accounts); + + // Verify the packed data + assert_eq!(packed.owner, 0); // First pubkey gets index 0 + assert_eq!(packed.mint, 1); // Second pubkey gets index 1 + assert_eq!(packed.delegate, 2); // Third pubkey gets index 2 + assert_eq!(packed.amount, 1000); + assert!(packed.has_delegate); + assert_eq!(packed.version, 3); // TokenDataVersion::ShaFlat + + // Verify remaining_accounts contains the pubkeys + let pubkeys = remaining_accounts.packed_pubkeys(); + assert_eq!(pubkeys[0], owner); + assert_eq!(pubkeys[1], mint); + assert_eq!(pubkeys[2], delegate); + } + + #[test] + fn test_token_data_with_variant_packing() { + use anchor_lang::{AnchorDeserialize, AnchorSerialize}; + + #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)] + enum MyVariant { + TypeA = 0, + TypeB = 1, + } + + let mut remaining_accounts = PackedAccounts::default(); + + let token_with_variant = TokenDataWithVariant { + variant: MyVariant::TypeA, + token_data: TokenData { + owner: Pubkey::new_unique(), + mint: Pubkey::new_unique(), + amount: 500, + delegate: None, + state: Default::default(), + tlv: None, + }, + }; + + // Pack the wrapper + let packed: PackedCTokenDataWithVariant = + token_with_variant.pack(&mut remaining_accounts); + + // Verify variant is unchanged + assert!(matches!(packed.variant, MyVariant::TypeA)); + + // Verify token data is packed + assert_eq!(packed.token_data.owner, 0); + assert_eq!(packed.token_data.mint, 1); + assert_eq!(packed.token_data.amount, 500); + assert!(!packed.token_data.has_delegate); + } + + #[test] + fn test_deduplication_in_packing() { + let mut remaining_accounts = PackedAccounts::default(); + + let shared_owner = Pubkey::new_unique(); + let shared_mint = Pubkey::new_unique(); + + let token1 = TokenData { + owner: shared_owner, + mint: shared_mint, + amount: 100, + delegate: None, + state: Default::default(), + tlv: None, + }; + + let token2 = TokenData { + owner: shared_owner, // Same owner + mint: shared_mint, // Same mint + amount: 200, + delegate: None, + state: Default::default(), + tlv: None, + }; + + // Pack both tokens + let packed1 = token1.pack(&mut remaining_accounts); + let packed2 = token2.pack(&mut remaining_accounts); + + // Both should reference the same indices + assert_eq!(packed1.owner, packed2.owner); + assert_eq!(packed1.mint, packed2.mint); + + // Only 2 unique pubkeys should be stored + let pubkeys = remaining_accounts.packed_pubkeys(); + assert_eq!(pubkeys.len(), 2); + } +} diff --git a/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs b/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs index 4716848078..5c0b7347da 100644 --- a/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs +++ b/sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs @@ -1,6 +1,6 @@ use crate::{AnchorDeserialize, AnchorSerialize}; -/// Authority types for compressed mint updates, following SPL Token-2022 pattern +/// Authority types for compressed mint updates #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] pub enum CompressedMintAuthorityType { diff --git a/sdk-libs/compressed-token-types/src/lib.rs b/sdk-libs/compressed-token-types/src/lib.rs index ee7e1d7813..16c3c4a72a 100644 --- a/sdk-libs/compressed-token-types/src/lib.rs +++ b/sdk-libs/compressed-token-types/src/lib.rs @@ -2,7 +2,6 @@ pub mod account_infos; pub mod constants; pub mod error; pub mod instruction; -pub mod token_data; // Conditional anchor re-exports #[cfg(feature = "anchor")] @@ -11,4 +10,3 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use constants::*; pub use instruction::*; -pub use token_data::*; diff --git a/sdk-libs/compressed-token-types/src/token_data.rs b/sdk-libs/compressed-token-types/src/token_data.rs deleted file mode 100644 index b126d6582f..0000000000 --- a/sdk-libs/compressed-token-types/src/token_data.rs +++ /dev/null @@ -1,25 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -#[repr(u8)] -pub enum AccountState { - Initialized, - Frozen, -} - -#[derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Clone)] -pub struct TokenData { - /// The mint associated with this account - pub mint: [u8; 32], - /// The owner of this account. - pub owner: [u8; 32], - /// The amount of tokens this account holds. - pub amount: u64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: Option<[u8; 32]>, - /// The account's state - pub state: AccountState, - /// Placeholder for TokenExtension tlv data (unimplemented) - pub tlv: Option>, -} diff --git a/sdk-libs/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml new file mode 100644 index 0000000000..c368d3db35 --- /dev/null +++ b/sdk-libs/compressible-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "light-compressible-client" +version = "0.13.1" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/lightprotocol/light-protocol" +description = "Client instruction builders for Light Protocol compressible accounts" + +[features] +anchor = ["anchor-lang", "light-sdk/anchor"] + +[dependencies] +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-account = { workspace = true } + +light-client = { workspace = true, features = ["v2"] } +light-sdk = { workspace = true, features = ["v2", "cpi-context"] } + +anchor-lang = { workspace = true, features = ["idl-build"], optional = true } +borsh = { workspace = true } + +thiserror = { workspace = true } \ No newline at end of file diff --git a/sdk-libs/compressible-client/src/get_compressible_account.rs b/sdk-libs/compressible-client/src/get_compressible_account.rs new file mode 100644 index 0000000000..cd056e5bbd --- /dev/null +++ b/sdk-libs/compressible-client/src/get_compressible_account.rs @@ -0,0 +1,162 @@ +use light_client::{ + indexer::{Indexer, TreeInfo}, + rpc::{Rpc, RpcError}, +}; +use light_sdk::address::v1::derive_address; +use solana_account::Account; +use solana_pubkey::Pubkey; +use thiserror::Error; + +#[cfg(not(feature = "anchor"))] +use crate::AnchorDeserialize; + +#[derive(Debug, Error)] +pub enum CompressibleAccountError { + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), + + #[error("Indexer error: {0}")] + Indexer(#[from] light_client::indexer::IndexerError), + + #[error("Compressed account has no data")] + NoData, + + #[cfg(feature = "anchor")] + #[error("Anchor deserialization error: {0}")] + AnchorDeserialization(#[from] anchor_lang::error::Error), + + #[error("Borsh deserialization error: {0}")] + BorshDeserialization(#[from] std::io::Error), +} + +#[derive(Debug, Clone)] +pub struct MerkleContext { + pub tree_info: TreeInfo, + pub hash: [u8; 32], + pub leaf_index: u32, + pub prove_by_index: bool, +} + +#[derive(Debug, Clone)] +pub struct AccountInfoInterface { + pub account_info: Account, + pub is_compressed: bool, + pub merkle_context: Option, +} + +/// Get account info with unified interface. +/// +/// If the account is cold, returns additional metadata for loading it to hot +/// state. +pub async fn get_account_info_interface( + address: &Pubkey, + program_id: &Pubkey, + address_tree_info: &TreeInfo, + rpc: &mut R, +) -> Result, CompressibleAccountError> +where + R: Rpc + Indexer, +{ + let (compressed_address, _) = + derive_address(&[&address.to_bytes()], &address_tree_info.tree, program_id); + + // TODO: concurrency + let onchain_account = rpc.get_account(*address).await?; + let compressed_account = rpc + .get_compressed_account(compressed_address, None) + .await? + .value; + + if let Some(onchain) = onchain_account { + let merkle_context = compressed_account.as_ref().map(|ca| MerkleContext { + tree_info: ca.tree_info, + hash: ca.hash, + leaf_index: ca.leaf_index, + prove_by_index: ca.prove_by_index, + }); + + return Ok(Some(AccountInfoInterface { + account_info: onchain, + is_compressed: false, + merkle_context, + })); + } + + if let Some(ca) = compressed_account { + if let Some(data) = ca.data.as_ref() { + if !data.data.is_empty() { + let mut account_data = + Vec::with_capacity(data.discriminator.len() + data.data.len()); + account_data.extend_from_slice(&data.discriminator); + account_data.extend_from_slice(&data.data); + + let account_info = Account { + lamports: ca.lamports, + data: account_data, + owner: ca.owner, + executable: false, + // TODO: consider 0. + rent_epoch: u64::MAX, + }; + + return Ok(Some(AccountInfoInterface { + account_info, + is_compressed: true, + merkle_context: Some(MerkleContext { + tree_info: ca.tree_info, + hash: ca.hash, + leaf_index: ca.leaf_index, + prove_by_index: ca.prove_by_index, + }), + })); + } + } + } + + Ok(None) +} + +#[cfg(feature = "anchor")] +#[allow(clippy::result_large_err)] +pub fn deserialize_account(account: &AccountInfoInterface) -> Result +where + T: anchor_lang::AccountDeserialize, +{ + let data = &account.account_info.data; + T::try_deserialize(&mut &data[..]).map_err(CompressibleAccountError::AnchorDeserialization) +} + +// TODO: add discriminator check. +#[cfg(not(feature = "anchor"))] +#[allow(clippy::result_large_err)] +pub fn deserialize_account(account: &AccountInfoInterface) -> Result +where + T: AnchorDeserialize, +{ + let data = &account.account_info.data; + if data.len() < 8 { + return Err(CompressibleAccountError::BorshDeserialization( + std::io::Error::new(std::io::ErrorKind::InvalidData, "Account data too short"), + )); + } + T::deserialize(&mut &data[8..]).map_err(CompressibleAccountError::BorshDeserialization) +} + +#[cfg(feature = "anchor")] +#[allow(clippy::result_large_err)] +pub async fn get_anchor_account( + address: &Pubkey, + program_id: &Pubkey, + address_tree_info: &TreeInfo, + rpc: &mut R, +) -> Result +where + T: anchor_lang::AccountDeserialize, + R: Rpc + Indexer, +{ + let account_interface = get_account_info_interface(address, program_id, address_tree_info, rpc) + .await? + .ok_or(CompressibleAccountError::NoData)?; + + deserialize_account::(&account_interface) +} diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs new file mode 100644 index 0000000000..7a679d74b1 --- /dev/null +++ b/sdk-libs/compressible-client/src/lib.rs @@ -0,0 +1,393 @@ +pub mod get_compressible_account; + +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +use light_client::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext}; +pub use light_sdk::compressible::config::CompressibleConfig; +use light_sdk::{ + compressible::{compression_info::CompressedAccountData, Pack}, + constants::C_TOKEN_PROGRAM_ID, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + SystemAccountMetaConfig, ValidityProof, + }, +}; +use solana_account::Account; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeCompressionConfigData { + pub compression_delay: u32, + pub rent_sponsor: Pubkey, + pub address_space: Vec, + pub config_bump: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct UpdateCompressionConfigData { + pub new_compression_delay: Option, + pub new_rent_sponsor: Option, + pub new_address_space: Option>, + pub new_update_authority: Option, +} + +/// T is the packed type from calling .pack() on the original type +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct DecompressMultipleAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec>, + pub system_accounts_offset: u8, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CompressAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec, + pub signer_seeds: Vec>>, + pub system_accounts_offset: u8, +} + +/// Instruction builders for compressible accounts +pub mod compressible_instruction { + use super::*; + + /// SHA256("global:initialize_compression_config")[..8] + pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [133, 228, 12, 169, 56, 76, 222, 61]; + /// SHA256("global:update_compression_config")[..8] + pub const UPDATE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = + [135, 215, 243, 81, 163, 146, 33, 70]; + /// SHA256("global:decompress_accounts_idempotent")[..8] + pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [114, 67, 61, 123, 234, 31, 1, 112]; + /// SHA256("global:compress_token_account_ctoken_signer")[..8] + pub const COMPRESS_TOKEN_ACCOUNT_CTOKEN_SIGNER_DISCRIMINATOR: [u8; 8] = + [243, 154, 172, 243, 44, 214, 139, 73]; + /// SHA256("global:compress_accounts_idempotent")[..8] + pub const COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR: [u8; 8] = + [89, 130, 165, 88, 12, 207, 178, 185]; + + /// Creates an initialize_compression_config instruction + #[allow(clippy::too_many_arguments)] + pub fn initialize_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + payer: &Pubkey, + authority: &Pubkey, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, + config_bump: Option, + ) -> Instruction { + let config_bump = config_bump.unwrap_or(0); + let (config_pda, _) = CompressibleConfig::derive_pda(program_id, config_bump); + + // Get program data account for BPF Loader Upgradeable + let bpf_loader_upgradeable_id = + solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"); + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable_id); + + let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + let accounts = vec![ + AccountMeta::new(*payer, true), // payer + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(program_data_pda, false), // program_data + AccountMeta::new_readonly(*authority, true), // authority + AccountMeta::new_readonly(system_program_id, false), // system_program + ]; + + let instruction_data = InitializeCompressionConfigData { + compression_delay, + rent_sponsor, + address_space, + config_bump, + }; + + // Prepend discriminator to serialized data, following Solana SDK pattern + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Instruction { + program_id: *program_id, + accounts, + data, + } + } + + /// Updates compression config + pub fn update_compression_config( + program_id: &Pubkey, + discriminator: &[u8], + authority: &Pubkey, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Instruction { + let (config_pda, _) = CompressibleConfig::derive_pda(program_id, 0); + + let accounts = vec![ + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(*authority, true), // authority + ]; + + let instruction_data = UpdateCompressionConfigData { + new_compression_delay, + new_rent_sponsor, + new_address_space, + new_update_authority, + }; + + // Prepend discriminator to serialized data, following Solana SDK pattern + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + let mut data = Vec::with_capacity(discriminator.len() + serialized_data.len()); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Instruction { + program_id: *program_id, + accounts, + data, + } + } + + /// Builds decompress_accounts_idempotent instruction + #[allow(clippy::too_many_arguments)] + pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + decompressed_account_addresses: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, + ) -> Result> + where + T: Pack + Clone + std::fmt::Debug, + { + let mut remaining_accounts = PackedAccounts::default(); + + let mut has_tokens = false; + let mut has_pdas = false; + for (compressed_account, _) in compressed_accounts.iter() { + if compressed_account.owner == C_TOKEN_PROGRAM_ID.into() { + has_tokens = true; + } else { + has_pdas = true; + } + if has_tokens && has_pdas { + break; + } + } + if !has_tokens && !has_pdas { + return Err("No tokens or PDAs found in compressed accounts".into()); + }; + if decompressed_account_addresses.len() != compressed_accounts.len() { + return Err("PDA accounts and compressed accounts must have the same length".into()); + } + + // pack cpi_context_account if required. + if has_pdas && has_tokens { + let cpi_context_of_first_input = + compressed_accounts[0].0.tree_info.cpi_context.unwrap(); + let system_config = SystemAccountMetaConfig::new_with_cpi_context( + *program_id, + cpi_context_of_first_input, + ); + remaining_accounts.add_system_accounts_v2(system_config)?; + } else { + let system_config = SystemAccountMetaConfig::new(*program_id); + remaining_accounts.add_system_accounts_v2(system_config)?; + } + + // pack output queue + let output_state_tree_index = + remaining_accounts.insert_or_get(output_state_tree_info.queue); + + // pack tree infos + let packed_tree_infos = + validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + let mut accounts = program_account_metas.to_vec(); + + // pack account data + let typed_compressed_accounts: Vec> = compressed_accounts + .iter() + .map(|(compressed_account, data)| { + let queue_index = + remaining_accounts.insert_or_get(compressed_account.tree_info.queue); + // create compressed_account_meta + let compressed_meta = CompressedAccountMetaNoLamportsNoAddress { + tree_info: packed_tree_infos + .state_trees + .as_ref() + .unwrap() + .packed_tree_infos + .iter() + .find(|pti| { + pti.queue_pubkey_index == queue_index + && pti.leaf_index == compressed_account.leaf_index + }) + .copied() + .ok_or( + "Matching PackedStateTreeInfo (queue_pubkey_index + leaf_index) not found", + )?, + output_state_tree_index, + }; + + let packed_data = data.pack(&mut remaining_accounts); + Ok(CompressedAccountData { + meta: compressed_meta, + data: packed_data, + }) + }) + .collect::, Box>>()?; + + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + for account in decompressed_account_addresses { + accounts.push(AccountMeta::new(*account, false)); + } + + let instruction_data = DecompressMultipleAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: typed_compressed_accounts, + system_accounts_offset: system_accounts_offset as u8, + }; + + let serialized_data = instruction_data.try_to_vec()?; + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } + + /// Builds compress_accounts_idempotent instruction for PDAs and token accounts + #[allow(clippy::too_many_arguments)] + pub fn compress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + account_pubkeys: &[Pubkey], + accounts_to_compress: &[Account], + program_account_metas: &[AccountMeta], + signer_seeds: Vec>>, + validity_proof_with_context: ValidityProofWithContext, + output_state_tree_info: TreeInfo, + ) -> Result> { + if account_pubkeys.len() != accounts_to_compress.len() { + return Err("Accounts pubkeys length must match accounts length".into()); + } + println!( + "compress_accounts_idempotent - account_pubkeys: {:?}", + account_pubkeys + ); + // Sanity checks. + if !signer_seeds.is_empty() && signer_seeds.len() != accounts_to_compress.len() { + return Err("Signer seeds length must match accounts length or be empty".into()); + } + for (i, account) in account_pubkeys.iter().enumerate() { + if !signer_seeds.is_empty() { + let seeds = &signer_seeds[i]; + if !seeds.is_empty() { + let derived = Pubkey::create_program_address( + &seeds.iter().map(|v| v.as_slice()).collect::>(), + program_id, + ); + match derived { + Ok(derived_pubkey) => { + if derived_pubkey != *account { + return Err(format!( + "Derived PDA does not match account_to_compress at index {}: expected {}, got {:?}", + i, + account, + derived_pubkey + ).into()); + } + } + Err(e) => { + return Err(format!( + "Failed to derive PDA for account_to_compress at index {}: {}", + i, e + ) + .into()); + } + } + } + } + } + + let mut remaining_accounts = PackedAccounts::default(); + + let system_config = SystemAccountMetaConfig::new(*program_id); + remaining_accounts.add_system_accounts_v2(system_config)?; + + let output_state_tree_index = + remaining_accounts.insert_or_get(output_state_tree_info.queue); + + let packed_tree_infos = + validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + let mut compressed_account_metas_no_lamports_no_address = Vec::new(); + + for packed_tree_info in packed_tree_infos + .state_trees + .as_ref() + .unwrap() + .packed_tree_infos + .iter() + { + compressed_account_metas_no_lamports_no_address.push( + CompressedAccountMetaNoLamportsNoAddress { + tree_info: *packed_tree_info, + output_state_tree_index, + }, + ); + } + + let mut accounts = program_account_metas.to_vec(); + + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + for account in account_pubkeys { + accounts.push(AccountMeta::new(*account, false)); + } + + let instruction_data = CompressAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: compressed_account_metas_no_lamports_no_address, + signer_seeds, + system_accounts_offset: system_accounts_offset as u8, + }; + + let serialized_data = instruction_data.try_to_vec()?; + let mut data = Vec::new(); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized_data); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) + } +} + +pub use compressible_instruction as CompressibleInstruction; diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 52ff204f02..a4e0a32279 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [features] default = [] -devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-compressed-token-sdk", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] +devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-ctoken-types", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] v2 = ["light-client/v2"] [dependencies] @@ -20,7 +20,7 @@ light-concurrent-merkle-tree = { workspace = true, optional = true } light-hasher = { workspace = true, features = ["poseidon", "sha256", "keccak", "std"] } light-ctoken-types = { workspace = true, optional = true } light-compressible = { workspace = true, optional = true } -light-compressed-token-sdk = { workspace = true, optional = true } +light-compressed-token-sdk = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor", "poseidon"] } light-batched-merkle-tree = { workspace = true, features = ["test-only"], optional = true } light-event = { workspace = true } @@ -30,6 +30,8 @@ light-prover-client = { workspace = true } light-zero-copy = { workspace = true } litesvm = { workspace = true } spl-token-2022 = { workspace = true } +light-compressible-client = { workspace = true, features = ["anchor"] } + light-registry = { workspace = true, features = ["cpi"], optional = true } light-compressed-token = { workspace = true, features = ["cpi"], optional = true } diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 65a6dceaca..64d4e2e1f4 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -166,3 +166,136 @@ pub async fn claim_and_compress( Ok(()) } + +#[cfg(feature = "devenv")] +pub async fn auto_compress_program_pdas( + rpc: &mut LightProgramTest, + program_id: Pubkey, +) -> Result<(), RpcError> { + use solana_instruction::AccountMeta; + use solana_sdk::signature::Signer; + + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let cfg_acc = rpc + .get_account(config_pda) + .await? + .ok_or_else(|| RpcError::CustomError("compressible config not found".into()))?; + let cfg = CompressibleConfig::deserialize(&mut &cfg_acc.data[..]) + .map_err(|e| RpcError::CustomError(format!("config deserialize: {e:?}")))?; + let rent_sponsor = cfg.rent_sponsor; + let address_tree = cfg.address_space[0]; + + let program_accounts = rpc.context.get_program_accounts(&program_id); + if program_accounts.is_empty() { + return Ok(()); + } + + let output_state_tree_info = rpc + .get_random_state_tree_info() + .map_err(|e| RpcError::CustomError(format!("no state tree: {e:?}")))?; + + let program_metas = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(rent_sponsor, false), + ]; + + const BATCH_SIZE: usize = 5; + let mut chunk = Vec::with_capacity(BATCH_SIZE); + for (pubkey, account) in program_accounts + .into_iter() + .filter(|(_, acc)| acc.lamports > 0 && !acc.data.is_empty()) + { + chunk.push((pubkey, account)); + if chunk.len() == BATCH_SIZE { + try_compress_chunk( + rpc, + &program_id, + &chunk, + &program_metas, + &address_tree, + output_state_tree_info, + ) + .await; + chunk.clear(); + } + } + + if !chunk.is_empty() { + try_compress_chunk( + rpc, + &program_id, + &chunk, + &program_metas, + &address_tree, + output_state_tree_info, + ) + .await; + } + + Ok(()) +} + +#[cfg(feature = "devenv")] +async fn try_compress_chunk( + rpc: &mut LightProgramTest, + program_id: &Pubkey, + chunk: &[(Pubkey, solana_sdk::account::Account)], + program_metas: &[solana_instruction::AccountMeta], + address_tree: &Pubkey, + output_state_tree_info: light_client::indexer::TreeInfo, +) { + use light_client::indexer::Indexer; + use light_compressed_account::address::derive_address; + use light_compressible_client::CompressibleInstruction; + use solana_sdk::signature::Signer; + + let mut pdas = Vec::with_capacity(chunk.len()); + let mut accounts_to_compress = Vec::with_capacity(chunk.len()); + let mut hashes = Vec::with_capacity(chunk.len()); + for (pda, acc) in chunk.iter() { + let addr = derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &program_id.to_bytes(), + ); + if let Ok(resp) = rpc.get_compressed_account(addr, None).await { + if let Some(cacc) = resp.value { + pdas.push(*pda); + accounts_to_compress.push(acc.clone()); + hashes.push(cacc.hash); + } + } + } + if pdas.is_empty() { + return; + } + + let proof_with_context = match rpc.get_validity_proof(hashes, vec![], None).await { + Ok(r) => r.value, + Err(_) => return, + }; + + let signer_seeds: Vec>> = (0..pdas.len()).map(|_| Vec::new()).collect(); + + let ix_res = CompressibleInstruction::compress_accounts_idempotent( + program_id, + &CompressibleInstruction::COMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &pdas, + &accounts_to_compress, + program_metas, + signer_seeds, + proof_with_context, + output_state_tree_info, + ) + .map_err(|e| e.to_string()); + if let Ok(ix) = ix_res { + let payer = rpc.get_payer().insecure_clone(); + let payer_pubkey = payer.pubkey(); + let _ = rpc + .create_and_send_transaction(&[ix], &payer_pubkey, &[&payer]) + .await; + } +} diff --git a/sdk-libs/program-test/src/indexer/extensions.rs b/sdk-libs/program-test/src/indexer/extensions.rs index 3e802ec66e..13aa49d04d 100644 --- a/sdk-libs/program-test/src/indexer/extensions.rs +++ b/sdk-libs/program-test/src/indexer/extensions.rs @@ -4,8 +4,8 @@ use light_client::indexer::{ AddressMerkleTreeAccounts, MerkleProof, NewAddressProofWithContext, StateMerkleTreeAccounts, }; use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_event::event::PublicTransactionEvent; -use light_sdk::token::TokenDataWithMerkleContext; use solana_sdk::signature::Keypair; use super::{address_tree::AddressMerkleTreeBundle, state_tree::StateMerkleTreeBundle}; diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index b5ae74e3d0..bc5a07e6e6 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -40,6 +40,7 @@ use light_compressed_account::{ tx_hash::create_tx_hash, TreeType, }; +use light_compressed_token_sdk::compat::{TokenData, TokenDataWithMerkleContext}; use light_event::event::PublicTransactionEvent; use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; use light_merkle_tree_reference::MerkleTree; @@ -66,10 +67,7 @@ use light_prover_client::{ }, }, }; -use light_sdk::{ - light_hasher::Hash, - token::{TokenData, TokenDataWithMerkleContext}, -}; +use light_sdk::light_hasher::Hash; use log::info; use num_bigint::{BigInt, BigUint}; use num_traits::FromBytes; diff --git a/sdk-libs/program-test/src/program_test/compressible_setup.rs b/sdk-libs/program-test/src/program_test/compressible_setup.rs new file mode 100644 index 0000000000..ed187d34a5 --- /dev/null +++ b/sdk-libs/program-test/src/program_test/compressible_setup.rs @@ -0,0 +1,151 @@ +//! Test helpers for compressible account operations +//! +//! This module provides common functionality for testing compressible accounts, +//! including mock program data setup and configuration management. + +use light_client::rpc::{Rpc, RpcError}; +use light_compressible_client::CompressibleInstruction; +use solana_sdk::{ + bpf_loader_upgradeable, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +use crate::program_test::TestRpc; + +/// Create mock program data account for testing +/// +/// This creates a minimal program data account structure that mimics +/// what the BPF loader would create for deployed programs. +pub fn create_mock_program_data(authority: Pubkey) -> Vec { + let mut data = vec![0u8; 1024]; + data[0..4].copy_from_slice(&3u32.to_le_bytes()); // Program data discriminator + data[4..12].copy_from_slice(&0u64.to_le_bytes()); // Slot + data[12] = 1; // Option Some(authority) + data[13..45].copy_from_slice(authority.as_ref()); // Authority pubkey + data +} + +/// Setup mock program data account for testing +/// +/// For testing without ledger, LiteSVM does not create program data accounts, +/// so we need to create them manually. This is required for programs that +/// check their upgrade authority. +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The payer keypair (used as authority) +/// * `program_id` - The program ID to create data account for +/// +/// # Returns +/// The pubkey of the created program data account +pub fn setup_mock_program_data( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, +) -> Pubkey { + let (program_data_pda, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::ID); + let mock_data = create_mock_program_data(payer.pubkey()); + let mock_account = solana_sdk::account::Account { + lamports: 1_000_000, + data: mock_data, + owner: bpf_loader_upgradeable::ID, + executable: false, + rent_epoch: 0, + }; + rpc.set_account(program_data_pda, mock_account); + program_data_pda +} + +/// Initialize compression config for a program +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The transaction fee payer +/// * `program_id` - The program to initialize config for +/// * `authority` - The config authority (can be same as payer) +/// * `compression_delay` - Number of slots to wait before compression +/// * `rent_sponsor` - Where to send rent from compressed accounts +/// * `address_space` - List of address trees for this program +/// +/// # Returns +/// `Result` - The transaction signature +#[allow(clippy::too_many_arguments)] +pub async fn initialize_compression_config( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, + authority: &Keypair, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, + discriminator: &[u8], + config_bump: Option, +) -> Result { + if address_space.is_empty() { + return Err(RpcError::CustomError( + "At least one address space must be provided".to_string(), + )); + } + + let instruction = CompressibleInstruction::initialize_compression_config( + program_id, + discriminator, + &payer.pubkey(), + &authority.pubkey(), + compression_delay, + rent_sponsor, + address_space, + config_bump, + ); + + let signers = if payer.pubkey() == authority.pubkey() { + vec![payer] + } else { + vec![payer, authority] + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) + .await +} + +/// Update compression config for a program +/// +/// # Arguments +/// * `rpc` - The test RPC client +/// * `payer` - The transaction fee payer +/// * `program_id` - The program to update config for +/// * `authority` - The current config authority +/// * `new_compression_delay` - New compression delay (optional) +/// * `new_rent_sponsor` - New rent recipient (optional) +/// * `new_address_space` - New address space list (optional) +/// * `new_update_authority` - New authority (optional) +/// +/// # Returns +/// `Result` - The transaction signature +#[allow(clippy::too_many_arguments)] +pub async fn update_compression_config( + rpc: &mut T, + payer: &Keypair, + program_id: &Pubkey, + authority: &Keypair, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, + discriminator: &[u8], +) -> Result { + let instruction = CompressibleInstruction::update_compression_config( + program_id, + discriminator, + &authority.pubkey(), + new_compression_delay, + new_rent_sponsor, + new_address_space, + new_update_authority, + ); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, authority]) + .await +} diff --git a/sdk-libs/program-test/src/program_test/config.rs b/sdk-libs/program-test/src/program_test/config.rs index 45fa9a8cbc..50eccb7e1a 100644 --- a/sdk-libs/program-test/src/program_test/config.rs +++ b/sdk-libs/program-test/src/program_test/config.rs @@ -21,6 +21,8 @@ pub struct ProgramTestConfig { pub protocol_config: ProtocolConfig, pub with_prover: bool, #[cfg(feature = "devenv")] + pub auto_register_custom_programs_for_pda_compression: bool, + #[cfg(feature = "devenv")] pub skip_register_programs: bool, #[cfg(feature = "devenv")] pub skip_v1_trees: bool, @@ -132,6 +134,8 @@ impl Default for ProgramTestConfig { }, with_prover: true, #[cfg(feature = "devenv")] + auto_register_custom_programs_for_pda_compression: false, + #[cfg(feature = "devenv")] skip_second_v1_tree: false, #[cfg(feature = "devenv")] skip_register_programs: false, diff --git a/sdk-libs/program-test/src/program_test/extensions.rs b/sdk-libs/program-test/src/program_test/extensions.rs index 27c02e2094..b90210db95 100644 --- a/sdk-libs/program-test/src/program_test/extensions.rs +++ b/sdk-libs/program-test/src/program_test/extensions.rs @@ -4,8 +4,8 @@ use light_client::indexer::{ AddressMerkleTreeAccounts, MerkleProof, NewAddressProofWithContext, StateMerkleTreeAccounts, }; use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; use light_event::event::PublicTransactionEvent; -use light_sdk::token::TokenDataWithMerkleContext; use solana_sdk::signature::Keypair; use crate::{ diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 86fe5e824d..67a35fe213 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -33,6 +33,8 @@ pub struct LightProgramTest { pub test_accounts: TestAccounts, pub payer: Keypair, pub transaction_counter: usize, + #[cfg(feature = "devenv")] + pub auto_compress_programs: Vec, } impl LightProgramTest { @@ -77,6 +79,8 @@ impl LightProgramTest { payer, config: config.clone(), transaction_counter: 0, + #[cfg(feature = "devenv")] + auto_compress_programs: Vec::new(), }; let keypairs = TestKeypairs::program_test_default(); @@ -150,6 +154,20 @@ impl LightProgramTest { })?; } } + let (auto_register, additional_programs) = { + let auto = context + .config + .auto_register_custom_programs_for_pda_compression; + let progs = context.config.additional_programs.clone(); + (auto, progs) + }; + if auto_register { + if let Some(programs) = additional_programs { + for (_, pid) in programs.into_iter() { + context.register_auto_compression(pid); + } + } + } // Copy v1 state merkle tree accounts to devnet pubkeys { let tree_account = context @@ -404,6 +422,13 @@ impl LightProgramTest { .ok_or(RpcError::IndexerNotInitialized)?) .clone()) } + + #[cfg(feature = "devenv")] + pub fn register_auto_compression(&mut self, program_id: solana_sdk::pubkey::Pubkey) { + if !self.auto_compress_programs.contains(&program_id) { + self.auto_compress_programs.push(program_id); + } + } } impl MerkleTreeExt for LightProgramTest {} diff --git a/sdk-libs/program-test/src/program_test/mod.rs b/sdk-libs/program-test/src/program_test/mod.rs index c9eee711e3..a6b565f6ae 100644 --- a/sdk-libs/program-test/src/program_test/mod.rs +++ b/sdk-libs/program-test/src/program_test/mod.rs @@ -8,3 +8,6 @@ pub mod test_rpc; pub use light_program_test::LightProgramTest; pub mod indexer; pub use test_rpc::TestRpc; + +pub mod compressible_setup; +pub use compressible_setup::*; diff --git a/sdk-libs/program-test/src/program_test/test_rpc.rs b/sdk-libs/program-test/src/program_test/test_rpc.rs index 49ad31fb02..8660095ab3 100644 --- a/sdk-libs/program-test/src/program_test/test_rpc.rs +++ b/sdk-libs/program-test/src/program_test/test_rpc.rs @@ -157,6 +157,9 @@ impl TestRpc for LightProgramTest { self.context.warp_to_slot(current_slot); let mut store = CompressibleAccountStore::new(); crate::compressible::claim_and_compress(self, &mut store).await?; + for program_id in self.auto_compress_programs.clone() { + crate::compressible::auto_compress_program_pdas(self, program_id).await?; + } Ok(()) } } diff --git a/sdk-libs/program-test/src/utils/mod.rs b/sdk-libs/program-test/src/utils/mod.rs index ccd3e02457..2309bd1055 100644 --- a/sdk-libs/program-test/src/utils/mod.rs +++ b/sdk-libs/program-test/src/utils/mod.rs @@ -6,3 +6,6 @@ pub mod load_accounts; pub mod register_test_forester; pub mod setup_light_programs; pub mod tree_accounts; + +pub mod simulation; +pub use simulation::simulate_cu; diff --git a/sdk-libs/program-test/src/utils/simulation.rs b/sdk-libs/program-test/src/utils/simulation.rs new file mode 100644 index 0000000000..a5af0b331e --- /dev/null +++ b/sdk-libs/program-test/src/utils/simulation.rs @@ -0,0 +1,36 @@ +use solana_sdk::{ + instruction::Instruction, + signature::{Keypair, Signer}, + transaction::{Transaction, VersionedTransaction}, +}; + +use crate::{program_test::LightProgramTest, Rpc}; + +/// Simulate a transaction and return the compute units consumed. +/// +/// This is a test utility function for measuring transaction costs. +pub async fn simulate_cu( + rpc: &mut LightProgramTest, + payer: &Keypair, + instruction: &Instruction, +) -> u64 { + let blockhash = rpc + .get_latest_blockhash() + .await + .expect("Failed to get latest blockhash") + .0; + let tx = Transaction::new_signed_with_payer( + std::slice::from_ref(instruction), + Some(&payer.pubkey()), + &[payer], + blockhash, + ); + let simulate_tx = VersionedTransaction::from(tx); + + let simulate_result = rpc + .context + .simulate_transaction(simulate_tx) + .unwrap_or_else(|err| panic!("Transaction simulation failed: {:?}", err)); + + simulate_result.meta.compute_units_consumed +} diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index 6a53f45446..024adcd982 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -25,7 +25,7 @@ poseidon = ["light-hasher/poseidon", "light-compressed-account/poseidon"] keccak = ["light-hasher/keccak", "light-compressed-account/keccak"] sha256 = ["light-hasher/sha256", "light-compressed-account/sha256"] merkle-tree = ["light-concurrent-merkle-tree/solana"] - +anchor-discriminator = ["light-sdk-macros/anchor-discriminator"] [dependencies] solana-pubkey = { workspace = true, features = ["borsh", "sha2", "curve25519"] } @@ -34,12 +34,17 @@ solana-msg = { workspace = true } solana-cpi = { workspace = true } solana-program-error = { workspace = true } solana-instruction = { workspace = true } +solana-clock = { workspace = true } +solana-sysvar = { workspace = true } +solana-system-interface = { workspace = true } +solana-loader-v3-interface = { workspace = true, features = ["serde"] } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } borsh = { workspace = true } thiserror = { workspace = true } +bincode = "1" light-sdk-macros = { workspace = true } light-sdk-types = { workspace = true, features = ["std"] } @@ -49,6 +54,7 @@ light-hasher = { workspace = true, features = ["std"] } light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } light-concurrent-merkle-tree = { workspace = true, optional = true } +light-ctoken-types = { workspace = true } [dev-dependencies] num-bigint = { workspace = true } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 8c49c28982..fc865d689d 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -302,6 +302,17 @@ pub mod __internal { pub fn owner(&self) -> &Pubkey { &self.owner } + /// Get the byte size of the account type. + pub fn size(&self) -> usize + where + A: Size, + { + self.account.size() + } + + pub fn remove_data(&mut self) { + self.should_remove_data = true; + } pub fn in_account_info(&self) -> &Option { &self.account_info.input diff --git a/sdk-libs/sdk/src/address.rs b/sdk-libs/sdk/src/address.rs index c3d89f1105..d7e7f78707 100644 --- a/sdk-libs/sdk/src/address.rs +++ b/sdk-libs/sdk/src/address.rs @@ -134,6 +134,20 @@ pub mod v2 { ) } + /// Derive address from PDA using Pubkey types. + pub fn derive_compressed_address( + account_address: &Pubkey, + address_tree_pubkey: &Pubkey, + program_id: &Pubkey, + ) -> [u8; 32] { + derive_address( + &[account_address.to_bytes().as_ref()], + address_tree_pubkey, + program_id, + ) + .0 + } + /// Derives an address from provided seeds. Returns that address and a singular /// seed. /// diff --git a/sdk-libs/sdk/src/compressible/close.rs b/sdk-libs/sdk/src/compressible/close.rs new file mode 100644 index 0000000000..95b7e85074 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -0,0 +1,40 @@ +use solana_account_info::AccountInfo; + +use crate::error::{LightSdkError, Result}; + +// close native solana account +pub fn close<'info>( + info: &mut AccountInfo<'info>, + sol_destination: AccountInfo<'info>, +) -> Result<()> { + let lamports_to_transfer = info.lamports(); + + let new_destination_lamports = sol_destination + .lamports() + .checked_add(lamports_to_transfer) + .ok_or(LightSdkError::ConstraintViolation)?; + + if info.lamports() != lamports_to_transfer { + return Err(LightSdkError::ConstraintViolation); + } + + { + let mut destination_lamports = sol_destination + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)?; + **destination_lamports = new_destination_lamports; + } + + { + let mut source_lamports = info + .try_borrow_mut_lamports() + .map_err(|_| LightSdkError::ConstraintViolation)?; + **source_lamports = 0; + } + + let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); + info.assign(&system_program_id); + info.resize(0)?; + + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/compress_account.rs b/sdk-libs/sdk/src/compressible/compress_account.rs new file mode 100644 index 0000000000..d0b8e7dc32 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account.rs @@ -0,0 +1,115 @@ +use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; +use light_hasher::DataHasher; +use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; +use solana_account_info::AccountInfo; +use solana_clock::Clock; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_sysvar::Sysvar; + +use crate::{ + account::sha::LightAccount, + compressible::compression_info::{CompressAs, HasCompressionInfo}, + cpi::v2::CpiAccounts, + error::LightSdkError, + instruction::account_meta::CompressedAccountMeta, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, ProgramError, +}; + +/// Prepare account for compression. +/// +/// # Arguments +/// * `program_id` - The program that owns the account +/// * `account_info` - The account to compress +/// * `account_data` - Mutable reference to the deserialized account data +/// * `compressed_account_meta` - Metadata for the compressed account +/// * `cpi_accounts` - Accounts for CPI to light system program +/// * `compression_delay` - Minimum slots before compression allowed +/// * `address_space` - Address space for validation +#[cfg(feature = "v2")] +pub fn prepare_account_for_compression<'info, A>( + program_id: &Pubkey, + account_info: &AccountInfo<'info>, + account_data: &mut A, + compressed_account_meta: &CompressedAccountMetaNoLamportsNoAddress, + cpi_accounts: &CpiAccounts<'_, 'info>, + compression_delay: &u32, + address_space: &[Pubkey], +) -> std::result::Result +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo + + CompressAs, + A::Output: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + HasCompressionInfo + + Default + + crate::compressible::compression_info::CompressedInitSpace, +{ + use light_compressed_account::address::derive_address; + + let derived_c_pda = derive_address( + &account_info.key.to_bytes(), + &address_space[0].to_bytes(), + &program_id.to_bytes(), + ); + + let meta_with_address = CompressedAccountMeta { + tree_info: compressed_account_meta.tree_info, + address: derived_c_pda, + output_state_tree_index: compressed_account_meta.output_state_tree_index, + }; + + let current_slot = Clock::get()?.slot; + let last_written_slot = account_data.compression_info().last_written_slot(); + + if current_slot < last_written_slot + *compression_delay as u64 { + msg!( + "prepare_account_for_compression failed: Cannot compress yet. {} slots remaining", + (last_written_slot + *compression_delay as u64).saturating_sub(current_slot) + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + account_data.compression_info_mut().set_compressed(); + { + let mut data = account_info + .try_borrow_mut_data() + .map_err(|_| LightSdkError::ConstraintViolation)?; + let writer = &mut &mut data[..]; + account_data.serialize(writer).map_err(|e| { + msg!("Failed to serialize account data: {}", e); + LightSdkError::ConstraintViolation + })?; + } + + let owner_program_id = cpi_accounts.self_program_id(); + let mut compressed_account = + LightAccount::::new_empty(&owner_program_id, &meta_with_address)?; + + let compressed_data = match account_data.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), + std::borrow::Cow::Owned(data) => data, + }; + compressed_account.account = compressed_data; + { + use crate::compressible::compression_info::CompressedInitSpace; + let __lp_size = 8 + ::COMPRESSED_INIT_SPACE; + if __lp_size > 800 { + msg!( + "Compressed account would exceed 800-byte limit ({} bytes)", + __lp_size + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + } + + compressed_account.to_account_info() +} diff --git a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs new file mode 100644 index 0000000000..1bf9c1ad89 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs @@ -0,0 +1,102 @@ +use light_compressed_account::instruction_data::{ + data::NewAddressParamsAssignedPacked, with_account_info::CompressedAccountInfo, +}; +use light_hasher::DataHasher; +use solana_account_info::AccountInfo; +use solana_msg::msg; +use solana_pubkey::Pubkey; + +use crate::{ + account::sha::LightAccount, compressible::compression_info::HasCompressionInfo, + cpi::v2::CpiAccounts, error::LightSdkError, light_account_checks::AccountInfoTrait, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, ProgramError, +}; + +/// Prepare a compressed account on init. +/// +/// Does NOT close the PDA, does NOT invoke CPI. +/// +/// # Arguments +/// * `account_info` - The PDA AccountInfo +/// * `account_data` - Mutable reference to deserialized account data +/// * `address` - The address for the compressed account +/// * `new_address_param` - Address parameters for the compressed account +/// * `output_state_tree_index` - Output state tree index +/// * `cpi_accounts` - Accounts for validation +/// * `address_space` - Address space for validation (can contain multiple tree +/// pubkeys) +/// * `with_data` - If true, copies account data to compressed account, if +/// false, creates empty compressed account +/// +/// # Returns +/// CompressedAccountInfo +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "v2")] +pub fn prepare_compressed_account_on_init<'info, A>( + account_info: &AccountInfo<'info>, + account_data: &mut A, + address: [u8; 32], + new_address_param: NewAddressParamsAssignedPacked, + output_state_tree_index: u8, + cpi_accounts: &CpiAccounts<'_, 'info>, + address_space: &[Pubkey], + with_data: bool, +) -> std::result::Result +where + A: DataHasher + + LightDiscriminator + + AnchorSerialize + + AnchorDeserialize + + Default + + Clone + + HasCompressionInfo, +{ + let tree = cpi_accounts + .get_tree_account_info(new_address_param.address_merkle_tree_account_index as usize) + .map_err(|_| { + msg!( + "Failed to get tree account at index {}", + new_address_param.address_merkle_tree_account_index + ); + LightSdkError::ConstraintViolation + })? + .pubkey(); + if !address_space.iter().any(|a| a == &tree) { + msg!("Address tree {} not in allowed address space", tree); + return Err(LightSdkError::ConstraintViolation.into()); + } + *account_data.compression_info_mut_opt() = + Some(super::compression_info::CompressionInfo::new_decompressed()?); + + if with_data { + account_data.compression_info_mut().set_compressed(); + } else { + account_data + .compression_info_mut() + .bump_last_written_slot()?; + } + { + let mut data = account_info + .try_borrow_mut_data() + .map_err(|_| LightSdkError::ConstraintViolation)?; + account_data.serialize(&mut &mut data[..]).map_err(|e| { + msg!("Failed to serialize account data: {}", e); + LightSdkError::ConstraintViolation + })?; + } + + let owner_program_id = cpi_accounts.self_program_id(); + + let mut compressed_account = + LightAccount::::new_init(&owner_program_id, Some(address), output_state_tree_index); + + if with_data { + let mut compressed_data = account_data.clone(); + compressed_data.set_compression_info_none(); + compressed_account.account = compressed_data; + } else { + compressed_account.remove_data(); + } + + compressed_account.to_account_info() +} diff --git a/sdk-libs/sdk/src/compressible/compression_info.rs b/sdk-libs/sdk/src/compressible/compression_info.rs new file mode 100644 index 0000000000..3027385bd7 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -0,0 +1,121 @@ +use std::borrow::Cow; + +use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; +use solana_account_info::AccountInfo; +use solana_clock::Clock; +use solana_sysvar::Sysvar; + +use crate::{instruction::PackedAccounts, AnchorDeserialize, AnchorSerialize}; + +/// Replace 32-byte Pubkeys with 1-byte indices to save space. +/// If your type has no Pubkeys, just return self. +pub trait Pack { + type Packed: AnchorSerialize + Clone + std::fmt::Debug; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; +} + +pub trait Unpack { + type Unpacked; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> Result; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +#[repr(u8)] +pub enum AccountState { + Initialized, + Frozen, +} + +pub trait HasCompressionInfo { + fn compression_info(&self) -> &CompressionInfo; + fn compression_info_mut(&mut self) -> &mut CompressionInfo; + fn compression_info_mut_opt(&mut self) -> &mut Option; + fn set_compression_info_none(&mut self); +} + +/// Account space when compressed. +pub trait CompressedInitSpace { + const COMPRESSED_INIT_SPACE: usize; +} + +/// Override what gets stored when compressing. Return Self or a different type. +pub trait CompressAs { + type Output: crate::AnchorSerialize + + crate::AnchorDeserialize + + crate::LightDiscriminator + + crate::account::Size + + HasCompressionInfo + + Default + + Clone; + + fn compress_as(&self) -> Cow<'_, Self::Output>; +} + +#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)] +pub struct CompressionInfo { + pub last_written_slot: u64, + pub state: CompressionState, +} + +#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize, PartialEq)] +pub enum CompressionState { + #[default] + Uninitialized, + Decompressed, + Compressed, +} + +impl CompressionInfo { + pub fn new_decompressed() -> Result { + Ok(Self { + last_written_slot: Clock::get()?.slot, + state: CompressionState::Decompressed, + }) + } + + pub fn bump_last_written_slot(&mut self) -> Result<(), crate::ProgramError> { + self.last_written_slot = Clock::get()?.slot; + Ok(()) + } + + pub fn set_last_written_slot(&mut self, slot: u64) { + self.last_written_slot = slot; + } + + pub fn last_written_slot(&self) -> u64 { + self.last_written_slot + } + + pub fn set_compressed(&mut self) { + self.state = CompressionState::Compressed; + } + + pub fn is_compressed(&self) -> bool { + self.state == CompressionState::Compressed + } +} + +pub trait Space { + const INIT_SPACE: usize; +} + +impl Space for CompressionInfo { + const INIT_SPACE: usize = 8 + 1; // u64 + state enum (u8) +} + +#[cfg(feature = "anchor")] +impl anchor_lang::Space for CompressionInfo { + const INIT_SPACE: usize = ::INIT_SPACE; +} + +/// Compressed account data used when decompressing. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CompressedAccountData { + pub meta: CompressedAccountMetaNoLamportsNoAddress, + pub data: T, +} diff --git a/sdk-libs/sdk/src/compressible/config.rs b/sdk-libs/sdk/src/compressible/config.rs new file mode 100644 index 0000000000..c5836a8dff --- /dev/null +++ b/sdk-libs/sdk/src/compressible/config.rs @@ -0,0 +1,476 @@ +use std::collections::HashSet; + +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_loader_v3_interface::state::UpgradeableLoaderState; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_system_interface::instruction as system_instruction; +use solana_sysvar::{rent::Rent, Sysvar}; + +use crate::{error::LightSdkError, AnchorDeserialize, AnchorSerialize}; + +pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config"; +pub const MAX_ADDRESS_TREES_PER_SPACE: usize = 1; +const BPF_LOADER_UPGRADEABLE_ID: Pubkey = + Pubkey::from_str_const("BPFLoaderUpgradeab1e11111111111111111111111"); + +// TODO: add rent_authority + rent_func like in ctoken. +/// Global configuration for compressible accounts +#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)] +pub struct CompressibleConfig { + /// Config version for future upgrades + pub version: u8, + /// Number of slots to wait before compression is allowed + pub compression_delay: u32, + /// Authority that can update the config + pub update_authority: Pubkey, + /// Account that receives rent from compressed PDAs + pub rent_sponsor: Pubkey, + /// Config bump seed (0) + pub config_bump: u8, + /// PDA bump seed + pub bump: u8, + /// Address space for compressed accounts (currently 1 address_tree allowed) + pub address_space: Vec, +} + +impl CompressibleConfig { + pub const LEN: usize = 1 + 4 + 32 + 32 + 1 + 4 + (32 * MAX_ADDRESS_TREES_PER_SPACE) + 1; // 107 bytes max + + /// Calculate the exact size needed for a CompressibleConfig with the given + /// number of address spaces + pub fn size_for_address_space(num_address_trees: usize) -> usize { + 1 + 4 + 32 + 32 + 1 + 4 + (32 * num_address_trees) + 1 + } + + /// Derives the config PDA address with config bump + pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) { + Pubkey::find_program_address(&[COMPRESSIBLE_CONFIG_SEED, &[config_bump]], program_id) + } + + /// Derives the default config PDA address (config_bump = 0) + pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) { + Self::derive_pda(program_id, 0) + } + + /// Checks the config account + pub fn validate(&self) -> Result<(), crate::ProgramError> { + if self.version != 1 { + msg!( + "CompressibleConfig validation failed: Unsupported config version: {}", + self.version + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + if self.address_space.len() != 1 { + msg!( + "CompressibleConfig validation failed: Address space must contain exactly 1 pubkey, found: {}", + self.address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + // For now, only allow config_bump = 0 to keep it simple + if self.config_bump != 0 { + msg!( + "CompressibleConfig validation failed: Config bump must be 0 for now, found: {}", + self.config_bump + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + Ok(()) + } + + /// Loads and validates config from account, checking owner and PDA derivation + #[inline(never)] + pub fn load_checked( + account: &AccountInfo, + program_id: &Pubkey, + ) -> Result { + if account.owner != program_id { + msg!( + "CompressibleConfig::load_checked failed: Config account owner mismatch. Expected: {:?}. Found: {:?}.", + program_id, + account.owner + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + let data = account.try_borrow_data()?; + let config = Self::try_from_slice(&data).map_err(|err| { + msg!( + "CompressibleConfig::load_checked failed: Failed to deserialize config data: {:?}", + err + ); + LightSdkError::Borsh + })?; + config.validate()?; + + // CHECK: PDA derivation + let (expected_pda, _) = Self::derive_pda(program_id, config.config_bump); + if expected_pda != *account.key { + msg!( + "CompressibleConfig::load_checked failed: Config account key mismatch. Expected PDA: {:?}. Found: {:?}.", + expected_pda, + account.key + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + Ok(config) + } +} + +/// Creates a new compressible config PDA +/// +/// # Security - Solana Best Practice +/// This function follows the standard Solana pattern where only the program's +/// upgrade authority can create the initial config. This prevents unauthorized +/// parties from hijacking the config system. +/// +/// # Arguments +/// * `config_account` - The config PDA account to initialize +/// * `update_authority` - Authority that can update the config after creation +/// * `rent_sponsor` - Account that receives rent from compressed PDAs +/// * `address_space` - Address space for compressed accounts (currently 1 address_tree allowed) +/// * `compression_delay` - Number of slots to wait before compression +/// * `config_bump` - Config bump seed (must be 0 for now) +/// * `payer` - Account paying for the PDA creation +/// * `system_program` - System program +/// * `program_id` - The program that owns the config +/// +/// # Required Validation (must be done by caller) +/// The caller MUST validate that the signer is the program's upgrade authority +/// by checking against the program data account. This cannot be done in the SDK +/// due to dependency constraints. +/// +/// # Returns +/// * `Ok(())` if config was created successfully +/// * `Err(ProgramError)` if there was an error +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_compression_config_account_info<'info>( + config_account: &AccountInfo<'info>, + update_authority: &AccountInfo<'info>, + rent_sponsor: &Pubkey, + address_space: Vec, + compression_delay: u32, + config_bump: u8, + payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + // CHECK: only 1 address_space + if config_bump != 0 { + msg!("Config bump must be 0 for now, found: {}", config_bump); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: not already initialized + if config_account.data_len() > 0 { + msg!("Config account already initialized"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: only 1 address_space + if address_space.len() != 1 { + msg!( + "Address space must contain exactly 1 pubkey, found: {}", + address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: unique pubkeys in address_space + validate_address_space_no_duplicates(&address_space)?; + + // CHECK: signer + if !update_authority.is_signer { + msg!("Update authority must be signer for initial config creation"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: pda derivation + let (derived_pda, bump) = CompressibleConfig::derive_pda(program_id, config_bump); + if derived_pda != *config_account.key { + msg!("Invalid config PDA"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + let rent = Rent::get().map_err(LightSdkError::from)?; + let account_size = CompressibleConfig::size_for_address_space(address_space.len()); + let rent_lamports = rent.minimum_balance(account_size); + + let seeds = &[COMPRESSIBLE_CONFIG_SEED, &[config_bump], &[bump]]; + let create_account_ix = system_instruction::create_account( + payer.key, + config_account.key, + rent_lamports, + account_size as u64, + program_id, + ); + + invoke_signed( + &create_account_ix, + &[ + payer.clone(), + config_account.clone(), + system_program.clone(), + ], + &[seeds], + ) + .map_err(LightSdkError::from)?; + + let config = CompressibleConfig { + version: 1, + compression_delay, + update_authority: *update_authority.key, + rent_sponsor: *rent_sponsor, + config_bump, + address_space, + bump, + }; + + let mut data = config_account + .try_borrow_mut_data() + .map_err(LightSdkError::from)?; + config + .serialize(&mut &mut data[..]) + .map_err(|_| LightSdkError::Borsh)?; + + Ok(()) +} + +/// Updates an existing compressible config +/// +/// # Arguments +/// * `config_account` - The config PDA account to update +/// * `authority` - Current update authority (must match config) +/// * `new_update_authority` - Optional new update authority +/// * `new_rent_sponsor` - Optional new rent recipient +/// * `new_address_space` - Optional new address space (currently 1 address_tree allowed) +/// * `new_compression_delay` - Optional new compression delay +/// * `owner_program_id` - The program that owns the config +/// +/// # Returns +/// * `Ok(())` if config was updated successfully +/// * `Err(ProgramError)` if there was an error +pub fn process_update_compression_config<'info>( + config_account: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + new_update_authority: Option<&Pubkey>, + new_rent_sponsor: Option<&Pubkey>, + new_address_space: Option>, + new_compression_delay: Option, + owner_program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + // CHECK: PDA derivation + let mut config = CompressibleConfig::load_checked(config_account, owner_program_id)?; + + // CHECK: signer + if !authority.is_signer { + msg!("Update authority must be signer"); + return Err(LightSdkError::ConstraintViolation.into()); + } + // CHECK: authority + if *authority.key != config.update_authority { + msg!("Invalid update authority"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + if let Some(new_authority) = new_update_authority { + config.update_authority = *new_authority; + } + if let Some(new_recipient) = new_rent_sponsor { + config.rent_sponsor = *new_recipient; + } + if let Some(new_address_space) = new_address_space { + // CHECK: address space length + if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE { + msg!( + "New address space must contain exactly 1 pubkey, found: {}", + new_address_space.len() + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + validate_address_space_no_duplicates(&new_address_space)?; + + validate_address_space_only_adds(&config.address_space, &new_address_space)?; + + config.address_space = new_address_space; + } + if let Some(new_delay) = new_compression_delay { + config.compression_delay = new_delay; + } + + let mut data = config_account.try_borrow_mut_data().map_err(|e| { + msg!("Failed to borrow mut data for config_account: {:?}", e); + LightSdkError::from(e) + })?; + config.serialize(&mut &mut data[..]).map_err(|e| { + msg!("Failed to serialize updated config: {:?}", e); + LightSdkError::Borsh + })?; + + Ok(()) +} + +/// Checks that the signer is the program's upgrade authority +/// +/// # Arguments +/// * `program_id` - The program to check +/// * `program_data_account` - The program's data account (ProgramData) +/// * `authority` - The authority to verify +/// +/// # Returns +/// * `Ok(())` if authority is valid +/// * `Err(LightSdkError)` if authority is invalid or verification fails +pub fn check_program_upgrade_authority( + program_id: &Pubkey, + program_data_account: &AccountInfo, + authority: &AccountInfo, +) -> Result<(), crate::ProgramError> { + // CHECK: program data PDA + let (expected_program_data, _) = + Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID); + if program_data_account.key != &expected_program_data { + msg!("Invalid program data account"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + let data = program_data_account.try_borrow_data()?; + let program_state: UpgradeableLoaderState = bincode::deserialize(&data).map_err(|_| { + msg!("Failed to deserialize program data account"); + LightSdkError::ConstraintViolation + })?; + + // Extract upgrade authority + let upgrade_authority = match program_state { + UpgradeableLoaderState::ProgramData { + slot: _, + upgrade_authority_address, + } => { + match upgrade_authority_address { + Some(auth) => { + // Check for invalid zero authority when authority exists + if auth == Pubkey::default() { + msg!("Invalid state: authority is zero pubkey"); + return Err(LightSdkError::ConstraintViolation.into()); + } + auth + } + None => { + msg!("Program has no upgrade authority"); + return Err(LightSdkError::ConstraintViolation.into()); + } + } + } + _ => { + msg!("Account is not ProgramData, found: {:?}", program_state); + return Err(LightSdkError::ConstraintViolation.into()); + } + }; + + // CHECK: upgrade authority is signer + if !authority.is_signer { + msg!("Authority must be signer"); + return Err(LightSdkError::ConstraintViolation.into()); + } + + // CHECK: upgrade authority is program's upgrade authority + if *authority.key != upgrade_authority { + msg!( + "Signer is not the program's upgrade authority. Signer: {:?}, Expected Authority: {:?}", + authority.key, + upgrade_authority + ); + return Err(LightSdkError::ConstraintViolation.into()); + } + + Ok(()) +} + +/// Creates a new compressible config PDA. +/// +/// # Arguments +/// * `config_account` - The config PDA account to initialize +/// * `update_authority` - Must be the program's upgrade authority +/// * `program_data_account` - The program's data account for validation +/// * `rent_sponsor` - Account that receives rent from compressed PDAs +/// * `address_space` - Address spaces for compressed accounts (exactly 1 +/// allowed) +/// * `compression_delay` - Number of slots to wait before compression +/// * `config_bump` - Config bump seed (must be 0 for now) +/// * `payer` - Account paying for the PDA creation +/// * `system_program` - System program +/// * `program_id` - The program that owns the config +/// +/// # Returns +/// * `Ok(())` if config was created successfully +/// * `Err(ProgramError)` if there was an error or authority validation fails +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_compression_config_checked<'info>( + config_account: &AccountInfo<'info>, + update_authority: &AccountInfo<'info>, + program_data_account: &AccountInfo<'info>, + rent_sponsor: &Pubkey, + address_space: Vec, + compression_delay: u32, + config_bump: u8, + payer: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<(), crate::ProgramError> { + msg!( + "create_compression_config_checked program_data_account: {:?}", + program_data_account.key + ); + msg!( + "create_compression_config_checked program_id: {:?}", + program_id + ); + // Verify the signer is the program's upgrade authority + check_program_upgrade_authority(program_id, program_data_account, update_authority)?; + + // Create the config with validated authority + process_initialize_compression_config_account_info( + config_account, + update_authority, + rent_sponsor, + address_space, + compression_delay, + config_bump, + payer, + system_program, + program_id, + ) +} + +/// Validates that address_space contains no duplicate pubkeys +fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> { + let mut seen = HashSet::new(); + for pubkey in address_space { + if !seen.insert(pubkey) { + msg!("Duplicate pubkey found in address_space: {}", pubkey); + return Err(LightSdkError::ConstraintViolation); + } + } + Ok(()) +} + +/// Validates that new_address_space only adds to existing address_space (no removals) +fn validate_address_space_only_adds( + existing_address_space: &[Pubkey], + new_address_space: &[Pubkey], +) -> Result<(), LightSdkError> { + // Check that all existing pubkeys are still present in new address space + for existing_pubkey in existing_address_space { + if !new_address_space.contains(existing_pubkey) { + msg!( + "Cannot remove existing pubkey from address_space: {}", + existing_pubkey + ); + return Err(LightSdkError::ConstraintViolation); + } + } + Ok(()) +} diff --git a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs new file mode 100644 index 0000000000..42ef34640d --- /dev/null +++ b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs @@ -0,0 +1,141 @@ +#![allow(clippy::all)] // TODO: Remove. + +use light_compressed_account::address::derive_address; +use light_sdk_types::instruction::account_meta::{ + CompressedAccountMeta, CompressedAccountMetaNoLamportsNoAddress, +}; +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; +use solana_msg::msg; +use solana_pubkey::Pubkey; +use solana_system_interface::instruction as system_instruction; +use solana_sysvar::{rent::Rent, Sysvar}; + +use crate::{ + account::sha::LightAccount, compressible::compression_info::HasCompressionInfo, + cpi::v2::CpiAccounts, error::LightSdkError, AnchorDeserialize, AnchorSerialize, + LightDiscriminator, +}; + +/// Convert a `CompressedAccountMetaNoLamportsNoAddress` to a +/// `CompressedAccountMeta` by deriving the compressed address from the solana +/// account's pubkey. +pub fn into_compressed_meta_with_address<'info>( + compressed_meta_no_lamports_no_address: &CompressedAccountMetaNoLamportsNoAddress, + solana_account: &AccountInfo<'info>, + address_space: Pubkey, + program_id: &Pubkey, +) -> CompressedAccountMeta { + let derived_c_pda = derive_address( + &solana_account.key.to_bytes(), + &address_space.to_bytes(), + &program_id.to_bytes(), + ); + + let meta_with_address = CompressedAccountMeta { + tree_info: compressed_meta_no_lamports_no_address.tree_info, + address: derived_c_pda, + output_state_tree_index: compressed_meta_no_lamports_no_address.output_state_tree_index, + }; + + meta_with_address +} + +// TODO: consider folding into main fn. +/// Helper to invoke create_account on heap. +#[inline(never)] +fn invoke_create_account_with_heap<'info>( + rent_payer: &AccountInfo<'info>, + solana_account: &AccountInfo<'info>, + rent_minimum_balance: u64, + space: u64, + program_id: &Pubkey, + seeds: &[&[u8]], + system_program: &AccountInfo<'info>, +) -> Result<(), LightSdkError> { + let create_account_ix = system_instruction::create_account( + rent_payer.key, + solana_account.key, + rent_minimum_balance, + space, + program_id, + ); + + invoke_signed( + &create_account_ix, + &[ + rent_payer.clone(), + solana_account.clone(), + system_program.clone(), + ], + &[seeds], + ) + .map_err(|e| LightSdkError::ProgramError(e)) +} + +/// Helper function to decompress a compressed account into a PDA +/// idempotently with seeds. +#[inline(never)] +#[cfg(feature = "v2")] +pub fn prepare_account_for_decompression_idempotent<'a, 'info, T>( + program_id: &Pubkey, + data: T, + compressed_meta: CompressedAccountMeta, + solana_account: &AccountInfo<'info>, + rent_payer: &AccountInfo<'info>, + cpi_accounts: &CpiAccounts<'a, 'info>, + signer_seeds: &[&[u8]], +) -> Result< + Option, + LightSdkError, +> +where + T: Clone + + crate::account::Size + + LightDiscriminator + + Default + + AnchorSerialize + + AnchorDeserialize + + HasCompressionInfo + + 'info, +{ + if !solana_account.data_is_empty() { + msg!("Account already initialized, skipping"); + return Ok(None); + } + let rent = Rent::get().map_err(|err| { + msg!("Failed to get rent: {:?}", err); + LightSdkError::Borsh + })?; + + let light_account = LightAccount::::new_close(program_id, &compressed_meta, data)?; + + let space = T::size(&light_account.account); + let rent_minimum_balance = rent.minimum_balance(space); + + invoke_create_account_with_heap( + rent_payer, + solana_account, + rent_minimum_balance, + space as u64, + &cpi_accounts.self_program_id(), + signer_seeds, + cpi_accounts.system_program()?, + )?; + + let mut decompressed_pda = light_account.account.clone(); + *decompressed_pda.compression_info_mut_opt() = + Some(super::compression_info::CompressionInfo::new_decompressed()?); + + let mut account_data = solana_account.try_borrow_mut_data()?; + let discriminator_len = T::LIGHT_DISCRIMINATOR.len(); + account_data[..discriminator_len].copy_from_slice(&T::LIGHT_DISCRIMINATOR); + decompressed_pda + .serialize(&mut &mut account_data[discriminator_len..]) + .map_err(|err| { + msg!("Failed to serialize decompressed PDA: {:?}", err); + LightSdkError::Borsh + })?; + + Ok(Some(light_account.to_account_info()?)) +} diff --git a/sdk-libs/sdk/src/compressible/mod.rs b/sdk-libs/sdk/src/compressible/mod.rs new file mode 100644 index 0000000000..7c72acbb9f --- /dev/null +++ b/sdk-libs/sdk/src/compressible/mod.rs @@ -0,0 +1,28 @@ +pub mod close; +pub mod compression_info; +pub mod config; + +#[cfg(feature = "v2")] +pub mod compress_account; +#[cfg(feature = "v2")] +pub mod compress_account_on_init; +#[cfg(feature = "v2")] +pub mod decompress_idempotent; +#[cfg(feature = "v2")] +pub use close::close; +#[cfg(feature = "v2")] +pub use compress_account::prepare_account_for_compression; +#[cfg(feature = "v2")] +pub use compress_account_on_init::prepare_compressed_account_on_init; +pub use compression_info::{ + CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Space, Unpack, +}; +pub use config::{ + process_initialize_compression_config_account_info, + process_initialize_compression_config_checked, process_update_compression_config, + CompressibleConfig, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE, +}; +#[cfg(feature = "v2")] +pub use decompress_idempotent::{ + into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, +}; diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index a8586aba90..87606a1dda 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -147,11 +147,13 @@ pub mod error; /// Utilities to build instructions for programs with compressed accounts. pub mod instruction; pub mod legacy; -pub mod token; +pub mod proof; /// Transfer compressed sol between compressed accounts. pub mod transfer; pub mod utils; +pub use proof::borsh_compat; +pub mod compressible; #[cfg(feature = "merkle-tree")] pub mod merkle_tree; @@ -159,6 +161,12 @@ pub mod merkle_tree; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use compressible::{ + process_initialize_compression_config_account_info, + process_initialize_compression_config_checked, process_update_compression_config, CompressAs, + CompressedInitSpace, CompressibleConfig, CompressionInfo, HasCompressionInfo, Pack, Space, + Unpack, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE, +}; pub use light_account_checks::{self, discriminator::Discriminator as LightDiscriminator}; pub use light_hasher; #[cfg(feature = "poseidon")] diff --git a/sdk-libs/sdk/src/proof.rs b/sdk-libs/sdk/src/proof.rs new file mode 100644 index 0000000000..c64deb7bf0 --- /dev/null +++ b/sdk-libs/sdk/src/proof.rs @@ -0,0 +1,88 @@ +// TODO: try removing in separate PR +pub mod borsh_compat { + use crate::{AnchorDeserialize, AnchorSerialize}; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] + pub struct CompressedProof { + pub a: [u8; 32], + pub b: [u8; 64], + pub c: [u8; 32], + } + + impl Default for CompressedProof { + fn default() -> Self { + Self { + a: [0; 32], + b: [0; 64], + c: [0; 32], + } + } + } + + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] + /// Borsh-compatible ValidityProof. Use this in your anchor program unless + /// you have zero-copy instruction data. + pub struct ValidityProof(pub Option); + + impl ValidityProof { + pub fn new(proof: Option) -> Self { + Self(proof) + } + } + + impl From + for CompressedProof + { + fn from( + proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, + ) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From + for light_compressed_account::instruction_data::compressed_proof::CompressedProof + { + fn from(proof: CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From + for ValidityProof + { + fn from( + proof: light_compressed_account::instruction_data::compressed_proof::ValidityProof, + ) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From + for light_compressed_account::instruction_data::compressed_proof::ValidityProof + { + fn from(proof: ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for ValidityProof { + fn from(proof: CompressedProof) -> Self { + Self(Some(proof)) + } + } + + impl From> for ValidityProof { + fn from(proof: Option) -> Self { + Self(proof) + } + } +} diff --git a/sdk-libs/sdk/src/token.rs b/sdk-libs/sdk/src/token.rs deleted file mode 100644 index 14c0cafb7b..0000000000 --- a/sdk-libs/sdk/src/token.rs +++ /dev/null @@ -1,59 +0,0 @@ -use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; -use light_hasher::{sha256::Sha256BE, HasherError}; - -use crate::{AnchorDeserialize, AnchorSerialize, Pubkey}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] -#[repr(u8)] -pub enum AccountState { - #[default] - Initialized, - Frozen, -} -// TODO: extract token data from program into into a separate crate, import it and remove this file. -#[derive(Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Clone, Default)] -pub struct TokenData { - /// The mint associated with this account - pub mint: Pubkey, - /// The owner of this account. - pub owner: Pubkey, - /// The amount of tokens this account holds. - pub amount: u64, - /// If `delegate` is `Some` then `delegated_amount` represents - /// the amount authorized by the delegate - pub delegate: Option, - /// The account's state - pub state: AccountState, - /// Placeholder for TokenExtension tlv data (unimplemented) - pub tlv: Option>, -} - -impl TokenData { - /// TokenDataVersion 3 - /// CompressedAccount Discriminator [0,0,0,0,0,0,0,4] - #[inline(always)] - pub fn hash_sha_flat(&self) -> Result<[u8; 32], HasherError> { - use light_hasher::Hasher; - let bytes = self.try_to_vec().map_err(|_| HasherError::BorshError)?; - Sha256BE::hash(bytes.as_slice()) - } -} -#[derive(Debug, Clone, PartialEq)] -pub struct TokenDataWithMerkleContext { - pub token_data: TokenData, - pub compressed_account: CompressedAccountWithMerkleContext, -} - -impl TokenDataWithMerkleContext { - /// Only works for sha flat hash - pub fn hash(&self) -> Result<[u8; 32], HasherError> { - if let Some(data) = self.compressed_account.compressed_account.data.as_ref() { - match data.discriminator { - [0, 0, 0, 0, 0, 0, 0, 4] => self.token_data.hash_sha_flat(), - _ => Err(HasherError::EmptyInput), - } - } else { - Err(HasherError::EmptyInput) - } - } -} diff --git a/sdk-libs/token-client/Cargo.toml b/sdk-libs/token-client/Cargo.toml index 7790616f5b..815940f3e0 100644 --- a/sdk-libs/token-client/Cargo.toml +++ b/sdk-libs/token-client/Cargo.toml @@ -14,6 +14,7 @@ light-sdk = { workspace = true } light-client = { workspace = true, features = ["v2"] } light-compressed-token-sdk = { workspace = true } light-zero-copy = { workspace = true } +light-compressible = { workspace = true } # Solana dependencies solana-pubkey = { workspace = true, features = ["sha2", "curve25519"] } diff --git a/sdk-libs/token-client/src/actions/create_compressible_token_account.rs b/sdk-libs/token-client/src/actions/create_compressible_token_account.rs index 9ec93b93d5..54f0ce4598 100644 --- a/sdk-libs/token-client/src/actions/create_compressible_token_account.rs +++ b/sdk-libs/token-client/src/actions/create_compressible_token_account.rs @@ -1,6 +1,7 @@ use light_client::rpc::{Rpc, RpcError}; use light_compressed_token_sdk::instructions::{ - create_compressible_token_account as create_instruction, CreateCompressibleTokenAccount, + create_compressible_token_account_instruction as create_instruction, + CreateCompressibleTokenAccount, }; use light_ctoken_types::state::TokenDataVersion; use solana_keypair::Keypair; diff --git a/sdk-libs/token-client/src/actions/ctoken_transfer.rs b/sdk-libs/token-client/src/actions/ctoken_transfer.rs index 9f4a20cb43..38c9b9be54 100644 --- a/sdk-libs/token-client/src/actions/ctoken_transfer.rs +++ b/sdk-libs/token-client/src/actions/ctoken_transfer.rs @@ -5,8 +5,7 @@ use solana_pubkey::Pubkey; use solana_signature::Signature; use solana_signer::Signer; -/// Transfer SPL tokens between decompressed compressed token accounts (accounts with compressible extensions). -/// This performs a regular SPL token transfer on accounts that were decompressed from compressed tokens. +/// Transfer from one c-token account to another. /// /// # Arguments /// * `rpc` - RPC client @@ -18,7 +17,7 @@ use solana_signer::Signer; /// /// # Returns /// `Result` - The transaction signature -pub async fn ctoken_transfer( +pub async fn transfer_ctoken( rpc: &mut R, source: Pubkey, destination: Pubkey, @@ -27,7 +26,7 @@ pub async fn ctoken_transfer( payer: &Keypair, ) -> Result { let transfer_instruction = - create_ctoken_transfer_instruction(source, destination, amount, authority.pubkey())?; + create_transfer_ctoken_instruction(source, destination, amount, authority.pubkey())?; let mut signers = vec![payer]; if authority.pubkey() != payer.pubkey() { @@ -38,9 +37,8 @@ pub async fn ctoken_transfer( .await } -/// Create a decompressed token transfer instruction. -/// This creates an instruction that uses discriminator 3 (CTokenTransfer) to perform -/// SPL token transfers on decompressed compressed token accounts. +// TODO: consume the variant from compressed-token-sdk instead +/// Create a ctoken transfer instruction. /// /// # Arguments /// * `source` - Source token account @@ -51,7 +49,7 @@ pub async fn ctoken_transfer( /// # Returns /// `Result` #[allow(clippy::result_large_err)] -pub fn create_ctoken_transfer_instruction( +pub fn create_transfer_ctoken_instruction( source: Pubkey, destination: Pubkey, amount: u64, @@ -62,12 +60,13 @@ pub fn create_ctoken_transfer_instruction( accounts: vec![ AccountMeta::new(source, false), // Source token account AccountMeta::new(destination, false), // Destination token account - AccountMeta::new(authority, true), // Owner/Authority (signer, writable for lamport transfers) - AccountMeta::new_readonly(Pubkey::default(), false), // System program for CPI transfers + AccountMeta::new(authority, true), + AccountMeta::new_readonly(Pubkey::default(), false), ], data: { - let mut data = vec![3u8]; // CTokenTransfer discriminator - // Add SPL Token Transfer instruction data exactly like SPL does + // CTokenTransfer discriminator + let mut data = vec![3u8]; + // Add SPL Token Transfer instruction data exactly like SPL does data.extend_from_slice(&amount.to_le_bytes()); // Amount as u64 little-endian data }, diff --git a/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs b/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs index 60c149ff66..a8775e12f1 100644 --- a/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs +++ b/sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs @@ -3,7 +3,8 @@ use light_client::{ rpc::{Rpc, RpcError}, }; use light_compressed_token_sdk::{ - account2::create_ctoken_to_spl_transfer_instruction, token_pool::find_token_pool_pda_with_index, + instructions::create_transfer_ctoken_to_spl_instruction, + token_pool::find_token_pool_pda_with_index, SPL_TOKEN_PROGRAM_ID, }; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -11,7 +12,7 @@ use solana_signature::Signature; use solana_signer::Signer; /// Transfer tokens from a compressed token account to an SPL token account -pub async fn ctoken_to_spl_transfer( +pub async fn transfer_ctoken_to_spl( rpc: &mut R, source_ctoken_account: Pubkey, destination_spl_token_account: Pubkey, @@ -24,7 +25,7 @@ pub async fn ctoken_to_spl_transfer( let (token_pool_pda, token_pool_pda_bump) = find_token_pool_pda_with_index(&mint, 0); // Create the transfer instruction - let transfer_ix = create_ctoken_to_spl_transfer_instruction( + let transfer_ix = create_transfer_ctoken_to_spl_instruction( source_ctoken_account, destination_spl_token_account, amount, @@ -33,6 +34,7 @@ pub async fn ctoken_to_spl_transfer( payer.pubkey(), token_pool_pda, token_pool_pda_bump, + Pubkey::new_from_array(SPL_TOKEN_PROGRAM_ID), // TODO: make dynamic ) .map_err(|e| RpcError::AssertRpcError(format!("Failed to create instruction: {:?}", e)))?; diff --git a/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs b/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs index 99607ed3aa..890e95b793 100644 --- a/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs +++ b/sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs @@ -3,7 +3,8 @@ use light_client::{ rpc::{Rpc, RpcError}, }; use light_compressed_token_sdk::{ - account2::create_spl_to_ctoken_transfer_instruction, token_pool::find_token_pool_pda_with_index, + instructions::create_transfer_spl_to_ctoken_instruction, + token_pool::find_token_pool_pda_with_index, SPL_TOKEN_PROGRAM_ID, }; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -12,21 +13,7 @@ use solana_signer::Signer; use spl_pod::bytemuck::pod_from_bytes; use spl_token_2022::pod::PodAccount; -/// Transfer SPL tokens directly to compressed tokens in a single transaction. -/// -/// This function wraps `create_spl_to_ctoken_transfer_instruction` to provide -/// a convenient action for transferring from SPL token accounts to compressed tokens. -/// -/// # Arguments -/// * `rpc` - RPC client with indexer capabilities -/// * `source_spl_token_account` - The SPL token account to transfer from -/// * `to` - Recipient pubkey for the compressed tokens -/// * `amount` - Amount of tokens to transfer -/// * `authority` - Authority that can spend from the SPL token account -/// * `payer` - Transaction fee payer -/// -/// # Returns -/// `Result` - The transaction signature +/// Transfer SPL tokens to compressed tokens pub async fn spl_to_ctoken_transfer( rpc: &mut R, source_spl_token_account: Pubkey, @@ -35,7 +22,6 @@ pub async fn spl_to_ctoken_transfer( authority: &Keypair, payer: &Keypair, ) -> Result { - // Get mint from SPL token account let token_account_info = rpc .get_account(source_spl_token_account) .await? @@ -46,11 +32,9 @@ pub async fn spl_to_ctoken_transfer( let mint = pod_account.mint; - // Derive token pool PDA let (token_pool_pda, bump) = find_token_pool_pda_with_index(&mint, 0); - // Create the SPL to CToken transfer instruction - let ix = create_spl_to_ctoken_transfer_instruction( + let ix = create_transfer_spl_to_ctoken_instruction( source_spl_token_account, to, amount, @@ -59,16 +43,15 @@ pub async fn spl_to_ctoken_transfer( payer.pubkey(), token_pool_pda, bump, + Pubkey::new_from_array(SPL_TOKEN_PROGRAM_ID), // TODO: make dynamic ) .map_err(|e| RpcError::CustomError(e.to_string()))?; - // Prepare signers let mut signers = vec![payer]; if authority.pubkey() != payer.pubkey() { signers.push(authority); } - // Send transaction rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &signers) .await } diff --git a/sdk-libs/token-client/src/instructions/create_spl_mint.rs b/sdk-libs/token-client/src/instructions/create_spl_mint.rs index 2ad637160d..5ac5a40ce0 100644 --- a/sdk-libs/token-client/src/instructions/create_spl_mint.rs +++ b/sdk-libs/token-client/src/instructions/create_spl_mint.rs @@ -3,9 +3,12 @@ use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; -use light_compressed_token_sdk::instructions::{ - create_spl_mint_instruction as sdk_create_spl_mint_instruction, derive_token_pool, - find_spl_mint_address, CreateSplMintInputs, +use light_compressed_token_sdk::{ + instructions::{ + create_spl_mint_instruction as sdk_create_spl_mint_instruction, find_spl_mint_address, + CreateSplMintInputs, + }, + token_pool::derive_token_pool, }; use light_ctoken_types::{ instructions::mint_action::CompressedMintWithContext, state::CompressedMint, diff --git a/sdk-libs/token-client/src/instructions/mint_action.rs b/sdk-libs/token-client/src/instructions/mint_action.rs index d9b06c2017..6fa6dad77a 100644 --- a/sdk-libs/token-client/src/instructions/mint_action.rs +++ b/sdk-libs/token-client/src/instructions/mint_action.rs @@ -3,9 +3,12 @@ use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; -use light_compressed_token_sdk::instructions::{ - create_mint_action, derive_compressed_mint_address, derive_token_pool, find_spl_mint_address, - mint_action::{MintActionInputs, MintActionType, MintToRecipient}, +use light_compressed_token_sdk::{ + instructions::{ + create_mint_action, derive_compressed_mint_address, find_spl_mint_address, + mint_action::{MintActionInputs, MintActionType, MintToRecipient}, + }, + token_pool::derive_token_pool, }; use light_ctoken_types::{ instructions::{ diff --git a/sdk-libs/token-client/src/instructions/mint_to_compressed.rs b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs index 11fa87ab2d..74b67d73c4 100644 --- a/sdk-libs/token-client/src/instructions/mint_to_compressed.rs +++ b/sdk-libs/token-client/src/instructions/mint_to_compressed.rs @@ -5,10 +5,10 @@ use light_client::{ }; use light_compressed_token_sdk::{ instructions::{ - create_mint_to_compressed_instruction, derive_compressed_mint_from_spl_mint, - derive_token_pool, DecompressedMintConfig, MintToCompressedInputs, + create_mint_to_compressed_instruction, derive_cmint_from_spl_mint, DecompressedMintConfig, + MintToCompressedInputs, }, - token_pool::find_token_pool_pda_with_index, + token_pool::{derive_token_pool, find_token_pool_pda_with_index}, }; use light_ctoken_types::{ instructions::mint_action::{CompressedMintWithContext, Recipient}, @@ -28,8 +28,7 @@ pub async fn mint_to_compressed_instruction( ) -> Result { // Derive compressed mint address from SPL mint PDA let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_mint_address = - derive_compressed_mint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); + let compressed_mint_address = derive_cmint_from_spl_mint(&spl_mint_pda, &address_tree_pubkey); // Get the compressed mint account let compressed_mint_account = rpc diff --git a/sdk-libs/token-client/src/lib.rs b/sdk-libs/token-client/src/lib.rs index 8f5d67d6dc..3e6a942795 100644 --- a/sdk-libs/token-client/src/lib.rs +++ b/sdk-libs/token-client/src/lib.rs @@ -1,2 +1,4 @@ pub mod actions; pub mod instructions; +// re-export +pub use light_compressed_token_sdk::ctoken; diff --git a/sdk-tests/client-test/Cargo.toml b/sdk-tests/client-test/Cargo.toml index 20ab2c3169..86c9cfaa1d 100644 --- a/sdk-tests/client-test/Cargo.toml +++ b/sdk-tests/client-test/Cargo.toml @@ -26,6 +26,7 @@ light-zero-copy = { workspace = true } light-hasher = { workspace = true, features = ["poseidon"] } light-compressed-account = { workspace = true, features = ["std"] } light-compressed-token = { workspace = true } +light-compressed-token-sdk = { workspace = true } light-indexed-array = { workspace = true } light-merkle-tree-reference = { workspace = true } light-macros = { workspace = true } diff --git a/sdk-tests/client-test/tests/light_client.rs b/sdk-tests/client-test/tests/light_client.rs index 7f82532f10..d36fd37e90 100644 --- a/sdk-tests/client-test/tests/light_client.rs +++ b/sdk-tests/client-test/tests/light_client.rs @@ -11,13 +11,11 @@ use light_compressed_account::{hash_to_bn254_field_size_be, TreeType}; use light_compressed_token::mint_sdk::{ create_create_token_pool_instruction, create_mint_to_instruction, }; +use light_compressed_token_sdk::compat::{AccountState, TokenData}; use light_hasher::Poseidon; use light_merkle_tree_reference::{indexed::IndexedMerkleTree, MerkleTree}; use light_program_test::accounts::test_accounts::TestAccounts; -use light_sdk::{ - address::{v1::derive_address, NewAddressParams}, - token::{AccountState, TokenData}, -}; +use light_sdk::address::{v1::derive_address, NewAddressParams}; use light_test_utils::{system_program::create_invoke_instruction, Rpc, RpcError}; use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_keypair::Keypair; diff --git a/sdk-tests/client-test/tests/light_program_test.rs b/sdk-tests/client-test/tests/light_program_test.rs index bf3b49fe6f..c51ee482cf 100644 --- a/sdk-tests/client-test/tests/light_program_test.rs +++ b/sdk-tests/client-test/tests/light_program_test.rs @@ -9,13 +9,11 @@ use light_compressed_account::hash_to_bn254_field_size_be; use light_compressed_token::mint_sdk::{ create_create_token_pool_instruction, create_mint_to_instruction, }; +use light_compressed_token_sdk::compat::{AccountState, TokenData}; use light_program_test::{ accounts::test_accounts::TestAccounts, program_test::LightProgramTest, ProgramTestConfig, }; -use light_sdk::{ - address::{v1::derive_address, NewAddressParams}, - token::{AccountState, TokenData}, -}; +use light_sdk::address::{v1::derive_address, NewAddressParams}; use light_test_utils::{system_program::create_invoke_instruction, RpcError}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, diff --git a/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs new file mode 100644 index 0000000000..2a53f9b1d5 --- /dev/null +++ b/sdk-tests/csdk-anchor-test/tests/user_record_tests.rs @@ -0,0 +1,280 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use sdk_compressible_test::UserRecord; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests +// 1. init compressed, decompress, and compress +// 2. update_record bumps compression info +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(101).unwrap(); + + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; + assert!(result.is_err(), "Compression should fail due to slot delay"); + if let Err(err) = result { + let err_msg = format!("{:?}", err); + assert!( + err_msg.contains("Custom(16001)"), + "Expected error message about slot delay, got: {}", + err_msg + ); + } + rpc.warp_to_slot(200).unwrap(); + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; + assert!(result.is_ok(), "Compression should succeed"); +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(150).unwrap(); + + let accounts = sdk_compressible_test::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = sdk_compressible_test::instruction::UpdateRecord { + name: "Updated User".to_string(), + score: 42, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_ok(), "Update record transaction should succeed"); + + rpc.warp_to_slot(200).unwrap(); + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User record account should exist after update" + ); + + let account_data = user_pda_account.unwrap().data; + let updated_user_record = UserRecord::try_deserialize(&mut &account_data[..]).unwrap(); + + assert_eq!(updated_user_record.name, "Updated User"); + assert_eq!(updated_user_record.score, 42); + assert_eq!(updated_user_record.owner, payer.pubkey()); + + assert_eq!( + updated_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + 150 + ); + assert!(!updated_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +pub async fn compress_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + should_fail: bool, +) -> Result { + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User PDA account should exist before compression" + ); + let account = user_pda_account.unwrap(); + assert!( + account.lamports > 0, + "Account should have lamports before compression" + ); + assert!( + !account.data.is_empty(), + "Account data should not be empty before compression" + ); + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value + .unwrap(); + let compressed_address = compressed_account.address.unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*user_record_pda], + &[account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![sdk_compressible_test::get_userrecord_seeds(&payer.pubkey()).0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + if should_fail { + assert!(result.is_err(), "Compress transaction should fail"); + return result; + } else { + assert!(result.is_ok(), "Compress transaction should succeed"); + } + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); + Ok(result.unwrap()) +} diff --git a/sdk-tests/sdk-compressible-test/Anchor.toml b/sdk-tests/sdk-compressible-test/Anchor.toml new file mode 100644 index 0000000000..7225c40f12 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/Anchor.toml @@ -0,0 +1,19 @@ +[toolchain] + +[features] +resolution = true +skip-lint = false + +[programs.localnet] +sdk_compressible_test = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + diff --git a/sdk-tests/sdk-compressible-test/Cargo.toml b/sdk-tests/sdk-compressible-test/Cargo.toml new file mode 100644 index 0000000000..1cc326d68b --- /dev/null +++ b/sdk-tests/sdk-compressible-test/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "sdk-compressible-test" +version = "0.1.0" +description = "Simple Anchor program template with user records" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "sdk_compressible_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-hasher = { workspace = true, features = ["solana"] } +solana-program = { workspace = true } +light-macros = { workspace = true, features = ["solana"] } +borsh = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } +anchor-lang = { workspace = true, features = ["idl-build"] } +anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } +light-ctoken-types = { workspace = true, features = ["anchor"] } +light-compressed-token-sdk = { workspace = true, features = ["anchor"] } +light-compressed-token-types = { workspace = true, features = ["anchor"] } +light-compressible = { workspace = true, features = ["anchor"] } + +[dev-dependencies] +light-token-client = { workspace = true } +light-program-test = { workspace = true, features = ["v2", "devenv"] } +light-client = { workspace = true, features = ["v2"] } +light-compressible-client = { workspace = true, features = ["anchor"] } +light-test-utils = { workspace = true} +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-logger = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-signature = { workspace = true } +solana-signer = { workspace = true } +solana-keypair = { workspace = true } +solana-account = { workspace = true } +bincode = "1.3" + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] \ No newline at end of file diff --git a/sdk-tests/sdk-compressible-test/Xargo.toml b/sdk-tests/sdk-compressible-test/Xargo.toml new file mode 100644 index 0000000000..1744f098ae --- /dev/null +++ b/sdk-tests/sdk-compressible-test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/sdk-tests/sdk-compressible-test/package.json b/sdk-tests/sdk-compressible-test/package.json new file mode 100644 index 0000000000..3711173d19 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@lightprotocol/sdk-compressible-test", + "version": "0.1.0", + "license": "Apache-2.0", + "scripts": { + "build": "cargo build-sbf", + "test": "cargo test-sbf -p sdk-compressible-test -- --nocapture" + }, + "nx": {} +} + diff --git a/sdk-tests/sdk-compressible-test/src/constants.rs b/sdk-tests/sdk-compressible-test/src/constants.rs new file mode 100644 index 0000000000..2fcae0aa3a --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/constants.rs @@ -0,0 +1,3 @@ +pub const POOL_VAULT_SEED: &str = "pool_vault"; +pub const USER_RECORD_SEED: &str = "user_record"; +pub const CTOKEN_SIGNER_SEED: &str = "ctoken_signer"; diff --git a/sdk-tests/sdk-compressible-test/src/errors.rs b/sdk-tests/sdk-compressible-test/src/errors.rs new file mode 100644 index 0000000000..78da53fab4 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/errors.rs @@ -0,0 +1,91 @@ +use anchor_lang::prelude::*; + +#[repr(u32)] +pub enum ErrorCode { + InvalidAccountCount, + InvalidRentRecipient, + MintCreationFailed, + MissingCompressedTokenProgram, + MissingCompressedTokenProgramAuthorityPDA, + RentRecipientMismatch, + InvalidAccountDiscriminator, + DerivedTokenAccountMismatch, + MissingAuthority, + MissingCpiContext, +} + +#[automatically_derived] +impl ::core::fmt::Debug for ErrorCode { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::write_str( + f, + match self { + ErrorCode::InvalidAccountCount => "InvalidAccountCount", + ErrorCode::InvalidRentRecipient => "InvalidRentRecipient", + ErrorCode::MintCreationFailed => "MintCreationFailed", + ErrorCode::MissingCompressedTokenProgram => "MissingCompressedTokenProgram", + ErrorCode::MissingCompressedTokenProgramAuthorityPDA => { + "MissingCompressedTokenProgramAuthorityPDA" + } + ErrorCode::RentRecipientMismatch => "RentRecipientMismatch", + ErrorCode::InvalidAccountDiscriminator => "InvalidAccountDiscriminator", + ErrorCode::DerivedTokenAccountMismatch => "DerivedTokenAccountMismatch", + ErrorCode::MissingAuthority => "MissingAuthority", + ErrorCode::MissingCpiContext => "MissingCpiContext", + }, + ) + } +} + +impl std::fmt::Display for ErrorCode { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + ErrorCode::InvalidAccountCount => fmt.write_fmt(format_args!( + "Invalid account count: PDAs and compressed accounts must match", + )), + ErrorCode::InvalidRentRecipient => { + fmt.write_fmt(format_args!("Rent recipient does not match config")) + } + ErrorCode::MintCreationFailed => { + fmt.write_fmt(format_args!("Failed to create compressed mint")) + } + ErrorCode::MissingCompressedTokenProgram => fmt.write_fmt(format_args!( + "Compressed token program account not found in remaining accounts", + )), + ErrorCode::MissingCompressedTokenProgramAuthorityPDA => fmt.write_fmt(format_args!( + "Compressed token program authority PDA account not found in remaining accounts", + )), + ErrorCode::RentRecipientMismatch => { + fmt.write_fmt(format_args!("Rent recipient does not match config")) + } + ErrorCode::InvalidAccountDiscriminator => fmt.write_fmt(format_args!( + "Trying to compress account with invalid discriminator" + )), + ErrorCode::DerivedTokenAccountMismatch => fmt.write_fmt(format_args!( + "Derived token account address must match owner_info.key" + )), + ErrorCode::MissingAuthority => fmt.write_fmt(format_args!( + "Authority account is missing from CPI accounts" + )), + ErrorCode::MissingCpiContext => fmt.write_fmt(format_args!( + "CPI context account is missing from CPI accounts" + )), + } + } +} + +impl From for ProgramError { + fn from(e: ErrorCode) -> Self { + ProgramError::Custom(e as u32) + } +} + +#[repr(u32)] +pub enum CompressibleInstructionError { + InvalidRentRecipient, + CTokenDecompressionNotImplemented, + PdaDecompressionNotImplemented, + TokenCompressionNotImplemented, + PdaCompressionNotImplemented, +} diff --git a/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs b/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs new file mode 100644 index 0000000000..560219358d --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs @@ -0,0 +1,222 @@ +use anchor_lang::prelude::*; + +/// CompressAccountsIdempotent, DecompressAccountsIdempotent, +/// InitializeCompressionConfig, UpdateCompressionConfig accounts are all +/// auto-generated by compressible_instructions macro. +use crate::state::*; + +#[derive(Accounts)] +pub struct CreateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(placeholder_id: u64)] +pub struct CreatePlaceholderRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + compression_info + owner + string len + name + placeholder_id + space = 8 + 10 + 32 + 4 + 32 + 8, + seeds = [b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + bump, + )] + pub placeholder_record: Account<'info, PlaceholderRecord>, + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(account_data: AccountCreationData)] +pub struct CreateUserRecordAndGameSession<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + init, + payer = user, + // discriminator + owner + string len + name + score + + // option. Note that in the onchain space + // CompressionInfo is always Some. + space = 8 + 32 + 4 + 32 + 8 + 10, + seeds = [b"user_record", user.key().as_ref()], + bump, + )] + pub user_record: Account<'info, UserRecord>, + #[account( + init, + payer = user, + // discriminator + option + session_id + player + + // string len + game_type + start_time + end_time(Option) + score + space = 8 + 10 + 8 + 32 + 4 + 32 + 8 + 9 + 8, + seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + + // Compressed mint creation accounts - only token-specific ones needed + /// The mint signer used for PDA derivation + pub mint_signer: Signer<'info>, + + /// The mint authority used for PDA derivation + pub mint_authority: Signer<'info>, + + /// Compressed token program + /// CHECK: Program ID validated using C_TOKEN_PROGRAM_ID constant + pub ctoken_program: UncheckedAccount<'info>, + + /// CHECK: CPI authority of the compressed token program + pub compress_token_program_cpi_authority: UncheckedAccount<'info>, + + /// Needs to be here for the init anchor macro to work. + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct CreateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + init, + payer = player, + space = 8 + 9 + 8 + 32 + 4 + 32 + 8 + 9 + 8, // discriminator + compression_info + session_id + player + string len + game_type + start_time + end_time(Option) + score + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + )] + pub game_session: Account<'info, GameSession>, + pub system_program: Program<'info, System>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateRecord<'info> { + #[account(mut)] + pub user: Signer<'info>, + #[account( + mut, + seeds = [b"user_record", user.key().as_ref()], + bump, + constraint = user_record.owner == user.key() + )] + pub user_record: Account<'info, UserRecord>, +} + +#[derive(Accounts)] +#[instruction(session_id: u64)] +pub struct UpdateGameSession<'info> { + #[account(mut)] + pub player: Signer<'info>, + #[account( + mut, + seeds = [b"game_session", session_id.to_le_bytes().as_ref()], + bump, + constraint = game_session.player == player.key() + )] + pub game_session: Account<'info, GameSession>, +} + +#[derive(Accounts)] +pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: Config is validated by the SDK's load_checked method + pub config: AccountInfo<'info>, + /// Rent recipient - must match config + /// CHECK: Rent recipient is validated against the config + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, +} + +// TODO: split into one ix with ctoken and one without. +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// The global config account + /// CHECK: load_checked. + pub config: AccountInfo<'info>, + /// UNCHECKED: Anyone can pay to init PDAs. + #[account(mut)] + pub rent_payer: Signer<'info>, + /// CHECK: Checked in protocol. + #[account(mut)] + pub ctoken_rent_sponsor: UncheckedAccount<'info>, + /// CHECK: Checked in protocol. + pub ctoken_config: UncheckedAccount<'info>, + /// ctoken program (always required in mixed variant) + /// CHECK: Checked by Protocol. + pub ctoken_program: UncheckedAccount<'info>, + /// CPI authority PDA of the compressed token program (always required in mixed variant) + /// CHECK: Checked by Protocol. + pub ctoken_cpi_authority: UncheckedAccount<'info>, + /// CHECK: unchecked. + pub some_mint: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct InitializeCompressionConfig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Config PDA is created and validated by the SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// The program's data account + /// CHECK: Program data account is validated by the SDK + pub program_data: AccountInfo<'info>, + /// The program's upgrade authority (must sign) + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct UpdateCompressionConfig<'info> { + /// CHECK: Config PDA is created and validated by the SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// Must match the update authority stored in config + pub authority: Signer<'info>, +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs new file mode 100644 index 0000000000..1b2a77f7a0 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs @@ -0,0 +1,137 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{compress_account::prepare_account_for_compression, CompressibleConfig}, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, ValidityProof}, + LightDiscriminator, +}; + +/// Auto-generated by compressible_instructions macro. +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, +) -> Result<()> { + let compression_config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + if ctx.accounts.rent_sponsor.key() != compression_config.rent_sponsor { + msg!( + "rent recipient passed: {:?}", + ctx.accounts.rent_sponsor.key() + ); + msg!( + "rent recipient config: {:?}", + compression_config.rent_sponsor + ); + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + let cpi_accounts = CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ); + + let pda_accounts_start = ctx.remaining_accounts.len() - signer_seeds.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + + let mut compressed_pda_infos = Vec::new(); + let mut pda_indices_to_close: Vec = Vec::new(); + + for (i, account_info) in solana_accounts.iter().enumerate() { + if account_info.data_is_empty() { + msg!("No data. Account already compressed or uninitialized. Skipping."); + continue; + } + if account_info.owner == &crate::ID { + let data = account_info.try_borrow_data()?; + let discriminator = &data[0..8]; + let meta = compressed_accounts[i]; + + // TODO: consider CHECKING seeds. + match discriminator { + d if d == UserRecord::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data()?; + let mut account_data = UserRecord::try_deserialize(&mut &data_borrow[..])?; + drop(data_borrow); + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + account_info, + &mut account_data, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + compressed_pda_infos.push(compressed_info); + pda_indices_to_close.push(i); + } + d if d == GameSession::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data()?; + let mut account_data = GameSession::try_deserialize(&mut &data_borrow[..])?; + drop(data_borrow); + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + account_info, + &mut account_data, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + compressed_pda_infos.push(compressed_info); + pda_indices_to_close.push(i); + } + d if d == PlaceholderRecord::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data()?; + let mut account_data = + PlaceholderRecord::try_deserialize(&mut &data_borrow[..])?; + drop(data_borrow); + + let compressed_info = prepare_account_for_compression::( + &crate::ID, + account_info, + &mut account_data, + &meta, + &cpi_accounts, + &compression_config.compression_delay, + &compression_config.address_space, + )?; + + compressed_pda_infos.push(compressed_info); + pda_indices_to_close.push(i); + } + _ => { + return Err(ProgramError::from(ErrorCode::InvalidAccountDiscriminator).into()); + } + } + } + } + let has_pdas = !compressed_pda_infos.is_empty(); + if has_pdas { + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_account_infos(&compressed_pda_infos) + .invoke(cpi_accounts)?; + + // Close + for idx in pda_indices_to_close.into_iter() { + let mut info = solana_accounts[idx].clone(); + light_sdk::compressible::close::close(&mut info, ctx.accounts.rent_sponsor.clone()) + .map_err(anchor_lang::prelude::ProgramError::from)?; + } + } + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs new file mode 100644 index 0000000000..e6fcd68e13 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs @@ -0,0 +1,76 @@ +use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn create_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, + session_id: u64, + game_type: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, +) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + // Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Set your account data. + game_session.session_id = session_id; + game_session.player = ctx.accounts.player.key(); + game_session.game_type = game_type; + game_session.start_time = Clock::get()?.unix_timestamp as u64; + game_session.end_time = None; + game_session.score = 0; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // Create CPI accounts. + let player_account_info = ctx.accounts.player.to_account_info(); + let cpi_accounts = CpiAccounts::new( + &player_account_info, + ctx.remaining_accounts, + LIGHT_CPI_SIGNER, + ); + + // Prepare new address params. The cpda takes the address of the + // compressible pda account as seed. + let new_address_params = address_tree_info + .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(0)); + + let game_session_info = game_session.to_account_info(); + let game_session_data_mut = &mut **game_session; + let compressed_info = prepare_compressed_account_on_init::( + &game_session_info, + game_session_data_mut, + compressed_address, + new_address_params, + output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_new_addresses(&[new_address_params]) + .with_account_infos(&[compressed_info]) + .invoke(cpi_accounts)?; + + game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs new file mode 100644 index 0000000000..b8ee6fbe06 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs @@ -0,0 +1,67 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, +) -> Result<()> { + let placeholder_record = &mut ctx.accounts.placeholder_record; + + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + placeholder_record.owner = ctx.accounts.user.key(); + placeholder_record.name = name; + placeholder_record.placeholder_id = placeholder_id; + + // Verify rent recipient matches config + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); + + let new_address_params = address_tree_info.into_new_address_params_assigned_packed( + placeholder_record.key().to_bytes().into(), + Some(0), + ); + + let placeholder_info = placeholder_record.to_account_info(); + let placeholder_data_mut = &mut **placeholder_record; + let compressed_info = prepare_compressed_account_on_init::( + &placeholder_info, + placeholder_data_mut, + compressed_address, + new_address_params, + output_state_tree_index, + &cpi_accounts, + &config.address_space, + false, // with_data = false for empty compressed account + )?; + + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_new_addresses(&[new_address_params]) + .with_account_infos(&[compressed_info]) + .invoke(cpi_accounts)?; + + // Note: PDA is NOT closed in this example (compression_info is set, account remains) + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs new file mode 100644 index 0000000000..eb50dfc649 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs @@ -0,0 +1,67 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{PackedAddressTreeInfo, ValidityProof}, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; + +pub fn create_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, +) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + // 1. Load config from the config account + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + user_record.owner = ctx.accounts.user.key(); + user_record.name = name; + user_record.score = 11; + + // 2. Verify rent recipient matches config + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // 3. Create CPI accounts + let user_account_info = ctx.accounts.user.to_account_info(); + let cpi_accounts = + CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); + + let new_address_params = address_tree_info + .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); + + let user_record_info = user_record.to_account_info(); + let user_record_data_mut = &mut **user_record; + let compressed_info = prepare_compressed_account_on_init::( + &user_record_info, + user_record_data_mut, + compressed_address, + new_address_params, + output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_new_addresses(&[new_address_params]) + .with_account_infos(&[compressed_info]) + .invoke(cpi_accounts)?; + + // Close the PDA + user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs new file mode 100644 index 0000000000..2d82dc0884 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs @@ -0,0 +1,205 @@ +use anchor_lang::{ + prelude::*, + solana_program::{program::invoke, sysvar::clock::Clock}, +}; +use light_compressed_token_sdk::instructions::{ + create_mint_action_cpi, find_spl_mint_address, MintActionInputs, +}; +use light_sdk::{ + compressible::{ + compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, +}; +use light_sdk_types::{ + cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, +}; + +use crate::{errors::ErrorCode, instruction_accounts::*, seeds::*, state::*, LIGHT_CPI_SIGNER}; +pub fn create_user_record_and_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, + account_data: AccountCreationData, + compression_params: CompressionParams, +) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + let game_session = &mut ctx.accounts.game_session; + + // Load your config checked. + let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; + + // Check that rent recipient matches your config. + if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { + return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); + } + + // Set your account data. + user_record.owner = ctx.accounts.user.key(); + user_record.name = account_data.user_name.clone(); + user_record.score = 11; + + game_session.session_id = account_data.session_id; + game_session.player = ctx.accounts.user.key(); + game_session.game_type = account_data.game_type.clone(); + game_session.start_time = Clock::get()?.unix_timestamp as u64; + game_session.end_time = None; + game_session.score = 0; + + // Create CPI accounts from remaining accounts + let cpi_accounts = CpiAccounts::new_with_config( + ctx.accounts.user.as_ref(), + ctx.remaining_accounts, + CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), + ); + let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); + let cpi_context_account = cpi_accounts.cpi_context().unwrap(); + + // Prepare new address params. One per pda account. + let user_new_address_params = compression_params + .user_address_tree_info + .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); + let game_new_address_params = compression_params + .game_address_tree_info + .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); + + let mut all_compressed_infos = Vec::new(); + + // Prepare user record for compression + let user_record_info = user_record.to_account_info(); + let user_record_data_mut = &mut **user_record; + let user_compressed_info = prepare_compressed_account_on_init::( + &user_record_info, + user_record_data_mut, + compression_params.user_compressed_address, + user_new_address_params, + compression_params.user_output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + + all_compressed_infos.push(user_compressed_info); + + // Prepare game session for compression + let game_session_info = game_session.to_account_info(); + let game_session_data_mut = &mut **game_session; + let game_compressed_info = prepare_compressed_account_on_init::( + &game_session_info, + game_session_data_mut, + compression_params.game_compressed_address, + game_new_address_params, + compression_params.game_output_state_tree_index, + &cpi_accounts, + &config.address_space, + true, // with_data + )?; + all_compressed_infos.push(game_compressed_info); + + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority().unwrap(), + cpi_context: cpi_context_account, + cpi_signer: LIGHT_CPI_SIGNER, + }; + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) + .with_new_addresses(&[user_new_address_params, game_new_address_params]) + .with_account_infos(&all_compressed_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(cpi_context_accounts)?; + + // these are custom seeds of the caller program that are used to derive the program owned onchain tokenb account PDA. + // dual use: as owner of the compressed token account. + let mint = find_spl_mint_address(&ctx.accounts.mint_signer.key()).0; + let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); + + let actions = vec![ + light_compressed_token_sdk::instructions::mint_action::MintActionType::MintTo { + recipients: vec![ + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: token_account_address, // TRY: THE DECOMPRESS TOKEN ACCOUNT ADDRES IS THE OWNER OF ITS COMPRESSIBLED VERSION. + amount: 1000, // Mint the full supply to the user + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer2_seeds(&ctx.accounts.user.key()).1, + amount: 1000, + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer3_seeds(&ctx.accounts.user.key()).1, + amount: 1000, + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer4_seeds( + &ctx.accounts.user.key(), + &ctx.accounts.user.key(), + ) + .1, // user as fee_payer + amount: 1000, + }, + light_compressed_token_sdk::instructions::mint_action::MintToRecipient { + recipient: get_ctoken_signer5_seeds(&ctx.accounts.user.key(), &mint, 42).1, // Fixed index 42 + amount: 1000, + }, + ], + token_account_version: 3, + }, + ]; + + let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA + let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA + + let mint_action_inputs = MintActionInputs { + compressed_mint_inputs: compression_params.mint_with_context.clone(), + mint_seed: ctx.accounts.mint_signer.key(), + mint_bump: Some(compression_params.mint_bump), + create_mint: true, + authority: ctx.accounts.mint_authority.key(), + payer: ctx.accounts.user.key(), + proof: compression_params.proof.into(), + actions, + input_queue: None, // Not needed for create_mint: true + output_queue, + tokens_out_queue: Some(output_queue), // For MintTo actions + address_tree_pubkey, + token_pool: None, // Not needed for simple compressed mint creation + }; + + let mint_action_instruction = create_mint_action_cpi( + mint_action_inputs, + Some(light_ctoken_types::instructions::mint_action::CpiContext { + address_tree_pubkey: address_tree_pubkey.to_bytes(), + set_context: false, + first_set_context: false, + in_tree_index: 1, // address tree + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 2, + read_only_address_trees: [0; 4], + }), + Some(cpi_context_pubkey), + ) + .unwrap(); + + // Get all account infos needed for the mint action + let mut account_infos = cpi_accounts.to_account_infos(); + account_infos.push( + ctx.accounts + .compress_token_program_cpi_authority + .to_account_info(), + ); + account_infos.push(ctx.accounts.ctoken_program.to_account_info()); + account_infos.push(ctx.accounts.mint_authority.to_account_info()); + account_infos.push(ctx.accounts.mint_signer.to_account_info()); + account_infos.push(ctx.accounts.user.to_account_info()); + + // Invoke the mint action instruction directly + invoke(&mint_action_instruction, &account_infos)?; + + // at the end of the instruction we always clean up all onchain pdas that we compressed + user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; + game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs new file mode 100644 index 0000000000..de7bb34f5f --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs @@ -0,0 +1,426 @@ +// Auto-generated by compressible_instructions macro. +use anchor_lang::prelude::*; +use light_compressed_token_sdk::instructions::create_token_account::create_ctoken_account_signed; +use light_sdk::{ + compressible::{ + decompress_idempotent::{ + into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, + }, + Unpack, + }, + cpi::{ + v2::{CpiAccounts, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, +}; +use light_sdk_types::cpi_accounts::CpiAccountsConfig; + +use crate::{constants::*, errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; +pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, +) -> Result<()> { + // Helper functions to handle each account type - kept out of main frame + #[inline(never)] + #[allow(clippy::too_many_arguments)] + fn handle_user_record<'b, 'info>( + data: UserRecord, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + solana_accounts: &[AccountInfo<'info>], + i: usize, + address_space: Pubkey, + cpi_accounts: &CpiAccounts<'b, 'info>, + rent_payer: &Signer<'info>, + out: &mut Vec< + light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, + >, + ) -> Result<()> { + let seeds_vec = { + let seeds: &[&[u8]] = &[USER_RECORD_SEED.as_bytes(), (data.owner).as_ref()]; + let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] + }; + let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); + let infos = prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), + &solana_accounts[i], + rent_payer, + cpi_accounts, + seed_refs.as_slice(), + ) + .map_err(ProgramError::from)?; + out.extend(infos); + Ok(()) + } + + #[inline(never)] + #[allow(clippy::too_many_arguments)] + fn handle_game_session<'b, 'info>( + data: GameSession, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + solana_accounts: &[AccountInfo<'info>], + i: usize, + address_space: Pubkey, + cpi_accounts: &CpiAccounts<'b, 'info>, + rent_payer: &Signer<'info>, + out: &mut Vec< + light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, + >, + ) -> Result<()> { + let seed_binding_1 = data.session_id.to_le_bytes(); + let seeds_vec = { + let seeds: &[&[u8]] = &["game_session".as_bytes(), seed_binding_1.as_ref()]; + let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] + }; + let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); + let infos = prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), + &solana_accounts[i], + rent_payer, + cpi_accounts, + seed_refs.as_slice(), + ) + .map_err(ProgramError::from)?; + out.extend(infos); + Ok(()) + } + + #[inline(never)] + #[allow(clippy::too_many_arguments)] + fn handle_placeholder_record<'b, 'info>( + data: PlaceholderRecord, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + solana_accounts: &[AccountInfo<'info>], + i: usize, + address_space: Pubkey, + cpi_accounts: &CpiAccounts<'b, 'info>, + rent_payer: &Signer<'info>, + out: &mut Vec< + light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, + >, + ) -> Result<()> { + let seed_binding_1 = data.placeholder_id.to_le_bytes(); + let seeds_vec = { + let seeds: &[&[u8]] = &["placeholder_record".as_bytes(), seed_binding_1.as_ref()]; + let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] + }; + let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); + let infos = prepare_account_for_decompression_idempotent::( + &crate::ID, + data, + into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), + &solana_accounts[i], + rent_payer, + cpi_accounts, + seed_refs.as_slice(), + ) + .map_err(ProgramError::from)?; + out.extend(infos); + Ok(()) + } + + #[inline(never)] + fn check_account_types(compressed_accounts: &[CompressedAccountData]) -> (bool, bool) { + let (mut has_tokens, mut has_pdas) = (false, false); + for c in compressed_accounts { + match c.data { + CompressedAccountVariant::PackedCTokenData(_) => { + has_tokens = true; + } + _ => has_pdas = true, + } + if has_tokens && has_pdas { + break; + } + } + (has_tokens, has_pdas) + } + /// Helper function to process token decompression - separated to avoid stack overflow + #[inline(never)] + #[allow(clippy::too_many_arguments, clippy::extra_unused_lifetimes)] + fn process_tokens<'a, 'b, 'info>( + accounts: &DecompressAccountsIdempotent<'info>, + remaining_accounts: &[anchor_lang::prelude::AccountInfo<'info>], + fee_payer: &anchor_lang::prelude::AccountInfo<'info>, + ctoken_program: &anchor_lang::prelude::UncheckedAccount<'info>, + ctoken_rent_sponsor: &anchor_lang::prelude::AccountInfo<'info>, + ctoken_cpi_authority: &anchor_lang::prelude::UncheckedAccount<'info>, + ctoken_config: &anchor_lang::prelude::AccountInfo<'info>, + config: &anchor_lang::prelude::AccountInfo<'info>, + ctoken_accounts: Vec<( + light_compressed_token_sdk::compat::PackedCTokenData, + light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + )>, + proof: light_sdk::instruction::ValidityProof, + cpi_accounts: &CpiAccounts<'b, 'info>, + post_system_accounts: &[anchor_lang::prelude::AccountInfo<'info>], + has_pdas: bool, + ) -> Result<()> { + let mut token_decompress_indices: Box< + Vec, + > = Box::new(Vec::with_capacity(ctoken_accounts.len())); + // Collect per-owner signer seed groups; invoke_signed requires one seed group per PDA signer + let mut token_signers_seed_groups: Vec>> = + Vec::with_capacity(ctoken_accounts.len()); + let packed_accounts = post_system_accounts; + use crate::seeds::ctoken_seed_system::{CTokenSeedContext, CTokenSeedProvider}; + let seed_context = CTokenSeedContext { + accounts, + remaining_accounts, + }; + let authority = cpi_accounts + .authority() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; + let cpi_context = cpi_accounts + .cpi_context() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; + + for (token_data, meta) in ctoken_accounts.into_iter() { + let owner_index: u8 = token_data.token_data.owner; + let mint_index: u8 = token_data.token_data.mint; + let mint_info = packed_accounts[mint_index as usize].to_account_info(); + let owner_info = packed_accounts[owner_index as usize].to_account_info(); + let (ctoken_signer_seeds, derived_token_account_address) = + token_data.variant.get_seeds(&seed_context); + { + if derived_token_account_address != *owner_info.key { + msg!( + "derived_token_account_address: {:?}", + derived_token_account_address + ); + msg!("owner_info.key: {:?}", owner_info.key); + return Err(ProgramError::from(ErrorCode::DerivedTokenAccountMismatch).into()); + } + + // Convert Vec> to &[&[&[u8]]] + let seed_refs: Vec<&[u8]> = + ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect(); + let seeds_slice: &[&[u8]] = &seed_refs; + + create_ctoken_account_signed( + crate::ID, + fee_payer.clone().to_account_info(), + owner_info.clone(), + mint_info.clone(), + *authority.clone().to_account_info().key, + seeds_slice, + ctoken_rent_sponsor.clone().to_account_info(), + ctoken_config.to_account_info(), + Some(2), // TODO: make this configurable + None, // TODO: make this configurable + )?; + } + + // Construct MultiInputTokenDataWithContext from token data and meta + let source = + light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext { + owner: token_data.token_data.owner, + amount: token_data.token_data.amount, + has_delegate: token_data.token_data.has_delegate, + delegate: token_data.token_data.delegate, + mint: token_data.token_data.mint, + version: token_data.token_data.version, + merkle_context: meta.tree_info.into(), + root_index: meta.tree_info.root_index, + }; + let decompress_index = + light_compressed_token_sdk::instructions::DecompressFullIndices { + source, + destination_index: owner_index, + }; + token_decompress_indices.push(decompress_index); + token_signers_seed_groups.push(ctoken_signer_seeds); + } + + let ctoken_ix = + light_compressed_token_sdk::instructions::decompress_full_ctoken_accounts_with_indices( + fee_payer.key(), + proof, + if has_pdas { + Some(cpi_context.key()) + } else { + None + }, + &token_decompress_indices, + packed_accounts, + ) + .map_err(anchor_lang::prelude::ProgramError::from)?; + { + let mut all_account_infos = <[_]>::into_vec(Box::new([fee_payer.to_account_info()])); + all_account_infos.extend(ctoken_cpi_authority.to_account_infos()); + all_account_infos.extend(ctoken_program.to_account_infos()); + all_account_infos.extend(ctoken_rent_sponsor.to_account_infos()); + all_account_infos.extend(config.to_account_infos()); + all_account_infos.extend(cpi_accounts.to_account_infos()); + // Build &[&[&[u8]]] where each inner slice is a distinct PDA seed group + let signer_seed_refs: Vec> = token_signers_seed_groups + .iter() + .map(|group| group.iter().map(|s| s.as_slice()).collect()) + .collect(); + let signer_seed_slices: Vec<&[&[u8]]> = + signer_seed_refs.iter().map(|g| g.as_slice()).collect(); + + anchor_lang::solana_program::program::invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + signer_seed_slices.as_slice(), + )?; + } + Ok(()) + } + + let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( + &ctx.accounts.config, + &crate::ID, + )?; + let address_space = compression_config.address_space[0]; + + let (has_tokens, has_pdas) = check_account_types(&compressed_accounts); + if !has_tokens && !has_pdas { + return Ok(()); + } + + // Pre-count for exact alloc. + let (mut token_count, mut pda_count) = (0usize, 0usize); + for c in &compressed_accounts { + match c.data { + CompressedAccountVariant::PackedCTokenData(_) => token_count += 1, + _ => pda_count += 1, + } + } + + let mut ctoken_accounts: Vec<( + light_compressed_token_sdk::compat::PackedCTokenData, + light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + )> = Vec::with_capacity(token_count); + let mut compressed_pda_infos = Vec::with_capacity(pda_count); + + let cpi_accounts = if has_tokens { + CpiAccounts::new_with_config( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), + ) + } else { + CpiAccounts::new( + ctx.accounts.fee_payer.as_ref(), + &ctx.remaining_accounts[system_accounts_offset as usize..], + LIGHT_CPI_SIGNER, + ) + }; + + let pda_accounts_start = ctx.remaining_accounts.len() - compressed_accounts.len(); + let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; + let post_system_offset = cpi_accounts.system_accounts_end_offset(); + let all_infos = cpi_accounts.account_infos(); + let post_system_accounts = &all_infos[post_system_offset..]; + for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { + let unpacked_data = compressed_data.data.unpack(post_system_accounts)?; + match unpacked_data { + CompressedAccountVariant::UserRecord(data) => { + handle_user_record( + data, + &compressed_data.meta, + solana_accounts, + i, + address_space, + &cpi_accounts, + &ctx.accounts.rent_payer, + &mut compressed_pda_infos, + )?; + } + CompressedAccountVariant::GameSession(data) => { + handle_game_session( + data, + &compressed_data.meta, + solana_accounts, + i, + address_space, + &cpi_accounts, + &ctx.accounts.rent_payer, + &mut compressed_pda_infos, + )?; + } + CompressedAccountVariant::PlaceholderRecord(data) => { + handle_placeholder_record( + data, + &compressed_data.meta, + solana_accounts, + i, + address_space, + &cpi_accounts, + &ctx.accounts.rent_payer, + &mut compressed_pda_infos, + )?; + } + CompressedAccountVariant::PackedCTokenData(data) => { + ctoken_accounts.push((data, compressed_data.meta)); + } + CompressedAccountVariant::PackedUserRecord(_) + | CompressedAccountVariant::PackedGameSession(_) + | CompressedAccountVariant::PackedPlaceholderRecord(_) + | CompressedAccountVariant::CTokenData(_) => { + panic!("internal error: entered unreachable code"); + } + } + } + // return if no uninitialized accounts. + let has_pdas = !compressed_pda_infos.is_empty(); + let has_tokens = !ctoken_accounts.is_empty(); + if !has_pdas && !has_tokens { + return Ok(()); + } + let fee_payer = ctx.accounts.fee_payer.as_ref(); + + // init PDAs. + if has_pdas && has_tokens { + let authority = cpi_accounts + .authority() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; + let cpi_context = cpi_accounts + .cpi_context() + .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; + let system_cpi_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer: LIGHT_CPI_SIGNER, + }; + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .with_account_infos(&compressed_pda_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(system_cpi_accounts)?; + } else if has_pdas { + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_account_infos(&compressed_pda_infos) + .invoke(cpi_accounts.clone())?; + } + + // init tokens. + if has_tokens { + process_tokens( + ctx.accounts, + ctx.remaining_accounts, + fee_payer, + &ctx.accounts.ctoken_program, + &ctx.accounts.ctoken_rent_sponsor, + &ctx.accounts.ctoken_cpi_authority, + &ctx.accounts.ctoken_config, + &ctx.accounts.config, + ctoken_accounts, + proof, + &cpi_accounts, + post_system_accounts, + has_pdas, + )?; + } + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs new file mode 100644 index 0000000000..ec1f8f046e --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs @@ -0,0 +1,28 @@ +// Auto-generated by compressible_instructions macro. + +use anchor_lang::prelude::*; +use light_sdk::compressible::process_initialize_compression_config_checked; + +use crate::instruction_accounts::*; + +pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, +) -> Result<()> { + process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_sponsor, + address_space, + compression_delay, + 0, // one global config for now, so bump is 0. + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + )?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/mod.rs b/sdk-tests/sdk-compressible-test/src/instructions/mod.rs new file mode 100644 index 0000000000..5c7bd74407 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/mod.rs @@ -0,0 +1,10 @@ +pub mod compress_accounts_idempotent; +pub mod create_game_session; +pub mod create_placeholder_record; +pub mod create_record; +pub mod create_user_record_and_game_session; +pub mod decompress_accounts_idempotent; +pub mod initialize_compression_config; +pub mod update_compression_config; +pub mod update_game_session; +pub mod update_record; diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs new file mode 100644 index 0000000000..c0e96f83f2 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs @@ -0,0 +1,25 @@ +// Auto-generated by compressible_instructions macro. +use anchor_lang::prelude::*; +use light_sdk::compressible::process_update_compression_config; + +use crate::instruction_accounts::*; + +pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, +) -> Result<()> { + process_update_compression_config( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + new_update_authority.as_ref(), + new_rent_sponsor.as_ref(), + new_address_space, + new_compression_delay, + &crate::ID, + )?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs new file mode 100644 index 0000000000..25b45b4528 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs @@ -0,0 +1,21 @@ +use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; +use light_sdk::compressible::HasCompressionInfo; + +use crate::instruction_accounts::*; +pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, +) -> Result<()> { + let game_session = &mut ctx.accounts.game_session; + + game_session.score = new_score; + game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); + + // Must manually set compression info + game_session + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs new file mode 100644 index 0000000000..6b8c7cb618 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; +use light_sdk::compressible::HasCompressionInfo; + +use crate::instruction_accounts::*; + +pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + let user_record = &mut ctx.accounts.user_record; + + user_record.name = name; + user_record.score = score; + + // 1. Must manually set compression info + user_record + .compression_info_mut() + .bump_last_written_slot()?; + + Ok(()) +} diff --git a/sdk-tests/sdk-compressible-test/src/lib.rs b/sdk-tests/sdk-compressible-test/src/lib.rs new file mode 100644 index 0000000000..5e160c4931 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/lib.rs @@ -0,0 +1,178 @@ +#![allow(deprecated)] + +use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; +use light_sdk::derive_light_cpi_signer; +use light_sdk_types::CpiSigner; + +pub mod constants; +pub mod errors; +pub mod instruction_accounts; +pub mod instructions; +pub mod seeds; +pub mod state; + +pub use constants::*; +pub use errors::*; +pub use instruction_accounts::*; +// Re-export types needed by Anchor's macro expansion +pub use light_sdk::instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, ValidityProof, +}; +pub use seeds::*; +pub use state::*; + +declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); + +#[program] +pub mod sdk_compressible_test { + use light_sdk::instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, + ValidityProof, + }; + + use super::*; + + pub fn create_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + instructions::create_record::create_record( + ctx, + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + ) + } + + pub fn create_user_record_and_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, + account_data: AccountCreationData, + compression_params: CompressionParams, + ) -> Result<()> { + instructions::create_user_record_and_game_session::create_user_record_and_game_session( + ctx, + account_data, + compression_params, + ) + } + + pub fn update_game_session( + ctx: Context, + _session_id: u64, + new_score: u64, + ) -> Result<()> { + instructions::update_game_session::update_game_session(ctx, _session_id, new_score) + } + + pub fn create_game_session<'info>( + ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, + session_id: u64, + game_type: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + instructions::create_game_session::create_game_session( + ctx, + session_id, + game_type, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + ) + } + + pub fn initialize_compression_config( + ctx: Context, + compression_delay: u32, + rent_sponsor: Pubkey, + address_space: Vec, + ) -> Result<()> { + instructions::initialize_compression_config::initialize_compression_config( + ctx, + compression_delay, + rent_sponsor, + address_space, + ) + } + + pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { + instructions::update_record::update_record(ctx, name, score) + } + + pub fn create_placeholder_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, + placeholder_id: u64, + name: String, + proof: ValidityProof, + compressed_address: [u8; 32], + address_tree_info: PackedAddressTreeInfo, + output_state_tree_index: u8, + ) -> Result<()> { + instructions::create_placeholder_record::create_placeholder_record( + ctx, + placeholder_id, + name, + proof, + compressed_address, + address_tree_info, + output_state_tree_index, + ) + } + + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + instructions::decompress_accounts_idempotent::decompress_accounts_idempotent( + ctx, + proof, + compressed_accounts, + system_accounts_offset, + ) + } + + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + signer_seeds: Vec>>, + system_accounts_offset: u8, + ) -> Result<()> { + instructions::compress_accounts_idempotent::compress_accounts_idempotent( + ctx, + proof, + compressed_accounts, + signer_seeds, + system_accounts_offset, + ) + } + + pub fn update_compression_config( + ctx: Context, + new_compression_delay: Option, + new_rent_sponsor: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + instructions::update_compression_config::update_compression_config( + ctx, + new_compression_delay, + new_rent_sponsor, + new_address_space, + new_update_authority, + ) + } +} diff --git a/sdk-tests/sdk-compressible-test/src/seeds.rs b/sdk-tests/sdk-compressible-test/src/seeds.rs new file mode 100644 index 0000000000..414bbd6d5b --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/seeds.rs @@ -0,0 +1,219 @@ +// Auto-generated by macro. Seed getter implementations. + +use anchor_lang::prelude::Pubkey; + +use crate::constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED, USER_RECORD_SEED}; + +pub fn get_userrecord_seeds(owner: &Pubkey) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(2usize + 1); + seed_values.push((USER_RECORD_SEED.as_bytes()).to_vec()); + seed_values.push((owner.as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_gamesession_seeds(session_id: u64) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(2usize + 1); + seed_values.push(("game_session".as_bytes()).to_vec()); + seed_values.push((session_id.to_le_bytes().as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_placeholderrecord_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(2usize + 1); + seed_values.push(("placeholder_record".as_bytes()).to_vec()); + seed_values.push((placeholder_id.to_le_bytes().as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_ctokensigner_seeds(fee_payer: &Pubkey, some_mint: &Pubkey) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(3usize + 1); + seed_values.push((CTOKEN_SIGNER_SEED.as_bytes()).to_vec()); + seed_values.push((fee_payer.as_ref()).to_vec()); + seed_values.push((some_mint.as_ref()).to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(<[_]>::into_vec(Box::new([bump]))); + (seed_values, pda) +} + +pub fn get_ctoken_signer_seeds<'a>(user: &'a Pubkey, mint: &'a Pubkey) -> (Vec>, Pubkey) { + let mut seeds = vec![ + b"ctoken_signer".to_vec(), + user.to_bytes().to_vec(), + mint.to_bytes().to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctoken_signer2_seeds(user: &Pubkey) -> (Vec>, Pubkey) { + let mut seeds = vec![b"user_vault".to_vec(), user.to_bytes().to_vec()]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctoken_signer3_seeds(user: &Pubkey) -> (Vec>, Pubkey) { + let mut seeds = vec![ + POOL_VAULT_SEED.as_bytes().to_vec(), + user.to_bytes().to_vec(), + b"liquidity".to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctokensigner_authority_seeds() -> (Vec>, Pubkey) { + let mut seeds = vec![b"cpi_authority".to_vec()]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctokensigner2_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub fn get_ctokensigner3_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub fn get_ctoken_signer4_seeds<'a>( + user: &'a Pubkey, + fee_payer: &'a Pubkey, +) -> (Vec>, Pubkey) { + let mut seeds = vec![ + b"multi_account".to_vec(), + user.to_bytes().to_vec(), + fee_payer.to_bytes().to_vec(), + crate::ID.to_bytes().to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctoken_signer5_seeds<'a>( + user: &'a Pubkey, + mint: &'a Pubkey, + index: u64, +) -> (Vec>, Pubkey) { + let mut seeds = vec![ + b"indexed_vault".to_vec(), + user.to_bytes().to_vec(), + mint.to_bytes().to_vec(), + index.to_le_bytes().to_vec(), + b"final".to_vec(), + ]; + let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); + let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); + seeds.push(vec![bump]); + (seeds, pda) +} + +pub fn get_ctokensigner4_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub fn get_ctokensigner5_authority_seeds() -> (Vec>, Pubkey) { + get_ctokensigner_authority_seeds() +} + +pub mod ctoken_seed_system { + use anchor_lang::prelude::{AccountInfo, Pubkey}; + + use super::super::{ + constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED}, + instruction_accounts::DecompressAccountsIdempotent, + state::CTokenAccountVariant, + }; + + pub struct CTokenSeedContext<'a, 'info> { + pub accounts: &'a DecompressAccountsIdempotent<'info>, + pub remaining_accounts: &'a [AccountInfo<'info>], + } + + pub trait CTokenSeedProvider { + fn get_seeds<'a, 'info>( + &self, + ctx: &CTokenSeedContext<'a, 'info>, + ) -> (Vec>, Pubkey); + } + + impl CTokenSeedProvider for CTokenAccountVariant { + fn get_seeds<'a, 'info>( + &self, + ctx: &CTokenSeedContext<'a, 'info>, + ) -> (Vec>, Pubkey) { + match self { + CTokenAccountVariant::CTokenSigner => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seed_2 = ctx.accounts.some_mint.key.to_bytes(); + let seeds: &[&[u8]] = &[CTOKEN_SIGNER_SEED.as_bytes(), &seed_1, &seed_2]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner2 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seeds: &[&[u8]] = &[b"user_vault", &seed_1]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner3 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seeds: &[&[u8]] = &[POOL_VAULT_SEED.as_bytes(), &seed_1, b"liquidity"]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner4 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seed_2 = ctx.accounts.fee_payer.key.to_bytes(); + let program_id_bytes = crate::ID.to_bytes(); + let seeds: &[&[u8]] = &[b"multi_account", &seed_1, &seed_2, &program_id_bytes]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + CTokenAccountVariant::CTokenSigner5 => { + let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); + let seed_2 = ctx.accounts.some_mint.key.to_bytes(); + let index_bytes = 42u64.to_le_bytes(); + let seeds: &[&[u8]] = + &[b"indexed_vault", &seed_1, &seed_2, &index_bytes, b"final"]; + let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); + seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); + (seeds_vec, pda) + } + } + } + } +} diff --git a/sdk-tests/sdk-compressible-test/src/state.rs b/sdk-tests/sdk-compressible-test/src/state.rs new file mode 100644 index 0000000000..dd3446c331 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/src/state.rs @@ -0,0 +1,522 @@ +// Only CompressionParams is custom to the caller program. All other structs are +// auto-generated by macro. Compressed account variant, pack, unpack, +// hasCompressionInfo implementions. + +use anchor_lang::prelude::*; +use light_compressed_token_sdk::Pack as _TokenPack; +use light_ctoken_types::instructions::mint_action::CompressedMintWithContext; +use light_sdk::{ + account::Size, + compressible::{ + CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack as SdkPack, + Unpack as SdkUnpack, + }, + instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + PackedAddressTreeInfo, ValidityProof, + }, + LightDiscriminator, LightHasher, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] +#[repr(u8)] +pub enum CTokenAccountVariant { + CTokenSigner = 0, + CTokenSigner2 = 1, + CTokenSigner3 = 2, + CTokenSigner4 = 3, + CTokenSigner5 = 4, +} + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedAccountVariant { + UserRecord(UserRecord), + PackedUserRecord(PackedUserRecord), + GameSession(GameSession), + PackedGameSession(PackedGameSession), + PlaceholderRecord(PlaceholderRecord), + PackedPlaceholderRecord(PackedPlaceholderRecord), + PackedCTokenData(light_compressed_token_sdk::compat::PackedCTokenData), + CTokenData(light_compressed_token_sdk::compat::CTokenData), +} + +impl Default for CompressedAccountVariant { + fn default() -> Self { + Self::UserRecord(UserRecord::default()) + } +} + +impl LightDiscriminator for CompressedAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; // This won't be used directly + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +impl HasCompressionInfo for CompressedAccountVariant { + fn compression_info(&self) -> &CompressionInfo { + match self { + Self::UserRecord(data) => data.compression_info(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info(), + Self::PlaceholderRecord(data) => data.compression_info(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + match self { + Self::UserRecord(data) => data.compression_info_mut(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info_mut(), + Self::PlaceholderRecord(data) => data.compression_info_mut(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + match self { + Self::UserRecord(data) => data.compression_info_mut_opt(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.compression_info_mut_opt(), + Self::PlaceholderRecord(data) => data.compression_info_mut_opt(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } + + fn set_compression_info_none(&mut self) { + match self { + Self::UserRecord(data) => data.set_compression_info_none(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.set_compression_info_none(), + Self::PlaceholderRecord(data) => data.set_compression_info_none(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } +} + +impl Size for CompressedAccountVariant { + fn size(&self) -> usize { + match self { + Self::UserRecord(data) => data.size(), + Self::PackedUserRecord(_) => unreachable!(), + Self::GameSession(data) => data.size(), + Self::PlaceholderRecord(data) => data.size(), + Self::PackedCTokenData(_) => unreachable!(), + Self::CTokenData(_) => unreachable!(), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } +} + +// Pack implementation for CompressedAccountVariant +// This delegates to the underlying type's Pack implementation +impl SdkPack for CompressedAccountVariant { + type Packed = Self; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + match self { + Self::PackedUserRecord(_) => unreachable!(), + Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), + Self::GameSession(data) => Self::GameSession(data.pack(remaining_accounts)), + Self::PlaceholderRecord(data) => Self::PlaceholderRecord(data.pack(remaining_accounts)), + Self::PackedCTokenData(_) => { + unreachable!() + } + Self::CTokenData(data) => Self::PackedCTokenData(data.pack(remaining_accounts)), + Self::PackedGameSession(_) => unreachable!(), + Self::PackedPlaceholderRecord(_) => unreachable!(), + } + } +} + +// Unpack implementation for CompressedAccountVariant +// This delegates to the underlying type's Unpack implementation +impl SdkUnpack for CompressedAccountVariant { + type Unpacked = Self; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + match self { + Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), + Self::UserRecord(_) => unreachable!(), + Self::GameSession(data) => Ok(Self::GameSession(data.unpack(remaining_accounts)?)), + Self::PlaceholderRecord(data) => { + Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) + } + Self::PackedCTokenData(_data) => Ok(self.clone()), // as-is + Self::CTokenData(_data) => unreachable!(), // as-is + Self::PackedGameSession(_data) => unreachable!(), + Self::PackedPlaceholderRecord(_data) => unreachable!(), + } + } +} + +// Auto-derived via macro. Ix data implemented for Variant. +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct CompressedAccountData { + pub meta: CompressedAccountMetaNoLamportsNoAddress, + pub data: CompressedAccountVariant, +} + +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct UserRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub score: u64, +} + +// Auto-derived via macro. +impl HasCompressionInfo for UserRecord { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl CompressedInitSpace for UserRecord { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} + +impl CompressedInitSpace for GameSession { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} + +impl CompressedInitSpace for PlaceholderRecord { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} + +impl Size for UserRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for UserRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + // Simple case: return owned data with compression_info = None + // We can't return Cow::Borrowed because compression_info must always be None for compressed storage + std::borrow::Cow::Owned(Self { + compression_info: None, // ALWAYS None for compressed storage + owner: self.owner, + name: self.name.clone(), + score: self.score, + }) + } +} + +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub score: u64, +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for UserRecord { + type Packed = PackedUserRecord; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedUserRecord { + compression_info: None, + owner: remaining_accounts.insert_or_get(self.owner), + name: self.name.clone(), + score: self.score, + } + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for UserRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for PackedUserRecord { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for PackedUserRecord { + type Unpacked = UserRecord; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(UserRecord { + compression_info: None, + owner: *remaining_accounts[self.owner as usize].key, + name: self.name.clone(), + score: self.score, + }) + } +} + +// Your existing account structs must be manually extended: +// 1. Add compression_info field to the struct, with type +// Option. +// 2. add a #[skip] field for the compression_info field. +// 3. Add LightHasher, LightDiscriminator. +// 4. Add #[hash] attribute to ALL fields that can be >31 bytes. (eg Pubkeys, +// Strings) +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct GameSession { + #[skip] + pub compression_info: Option, + pub session_id: u64, + #[hash] + pub player: Pubkey, + #[max_len(32)] + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +// Auto-derived via macro. +impl HasCompressionInfo for GameSession { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl Size for GameSession { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for GameSession { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + // Custom compression: return owned data with modified fields + std::borrow::Cow::Owned(Self { + compression_info: None, // ALWAYS None for compressed storage + session_id: self.session_id, // KEEP - identifier + player: self.player, // KEEP - identifier + game_type: self.game_type.clone(), // KEEP - core property + start_time: 0, // RESET - clear timing + end_time: None, // RESET - clear timing + score: 0, // RESET - clear progress + }) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for GameSession { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for GameSession { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// PlaceholderRecord - demonstrates empty compressed account creation +// The PDA remains intact while an empty compressed account is created +#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] +#[account] +pub struct PlaceholderRecord { + #[skip] + pub compression_info: Option, + #[hash] + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub placeholder_id: u64, +} + +impl HasCompressionInfo for PlaceholderRecord { + fn compression_info(&self) -> &CompressionInfo { + self.compression_info + .as_ref() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + self.compression_info + .as_mut() + .expect("CompressionInfo must be Some on-chain") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +impl Size for PlaceholderRecord { + fn size(&self) -> usize { + Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE + } +} + +impl CompressAs for PlaceholderRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + compression_info: None, + owner: self.owner, + name: self.name.clone(), + placeholder_id: self.placeholder_id, + }) + } +} + +// Identity Pack implementation - no custom packing needed for PDA types +impl SdkPack for PlaceholderRecord { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity Unpack implementation - PDA types are sent unpacked +impl SdkUnpack for PlaceholderRecord { + type Unpacked = Self; + + fn unpack( + &self, + _remaining_accounts: &[AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedGameSession { + pub compression_info: Option, + pub session_id: u64, + pub player: u8, + pub game_type: String, + pub start_time: u64, + pub end_time: Option, + pub score: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedPlaceholderRecord { + pub compression_info: Option, + pub owner: u8, + pub name: String, + pub placeholder_id: u64, +} + +// Add these struct definitions before the program module +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct AccountCreationData { + pub user_name: String, + pub session_id: u64, + pub game_type: String, + // TODO: Add mint metadata fields when implementing mint functionality + pub mint_name: String, + pub mint_symbol: String, + pub mint_uri: String, + pub mint_decimals: u8, + pub mint_supply: u64, + pub mint_update_authority: Option, + pub mint_freeze_authority: Option, + pub additional_metadata: Option>, +} + +/// Information about a token account to compress +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct TokenAccountInfo { + pub user: Pubkey, + pub mint: Pubkey, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct CompressionParams { + pub proof: ValidityProof, + pub user_compressed_address: [u8; 32], + pub user_address_tree_info: PackedAddressTreeInfo, + pub user_output_state_tree_index: u8, + pub game_compressed_address: [u8; 32], + pub game_address_tree_info: PackedAddressTreeInfo, + pub game_output_state_tree_index: u8, + pub mint_bump: u8, + pub mint_with_context: CompressedMintWithContext, +} diff --git a/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs b/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs new file mode 100644 index 0000000000..5f19878457 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs @@ -0,0 +1,232 @@ +use anchor_lang::{AccountDeserialize, AnchorDeserialize, Discriminator, ToAccountMetas}; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::compressible::{CompressAs, CompressibleConfig}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_game_session, ADDRESS_SPACE, RENT_SPONSOR}; + +// Test: create, decompress game session, compress with custom data at +// compression +#[tokio::test] +async fn test_custom_compression_game_session() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let session_id = 42424u64; + let (game_session_pda, _game_bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &program_id, + ); + + create_game_session( + &mut rpc, + &payer, + &program_id, + &config_pda, + &game_session_pda, + session_id, + None, + ) + .await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_game_session( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + &_game_bump, + session_id, + "Battle Royale", + 100, + 0, + ) + .await; + + rpc.warp_to_slot(250).unwrap(); + + compress_game_session_with_custom_data( + &mut rpc, + &payer, + &program_id, + &game_session_pda, + session_id, + ) + .await; +} + +#[allow(clippy::too_many_arguments)] +pub async fn decompress_single_game_session( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + game_session_pda: &Pubkey, + _game_bump: &u8, + session_id: u64, + expected_game_type: &str, + expected_slot: u64, + expected_score: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = + sdk_compressible_test::GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_game_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*game_session_pda], + &[( + c_game_pda, + sdk_compressible_test::CompressedAccountVariant::GameSession(c_game_session), + )], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" + ); + + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + sdk_compressible_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, expected_score); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); +} + +pub async fn compress_game_session_with_custom_data( + rpc: &mut LightProgramTest, + _payer: &Keypair, + _program_id: &Pubkey, + game_session_pda: &Pubkey, + _session_id: u64, +) { + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); + let game_pda_data = game_pda_account.data; + let original_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + + let custom_compressed_data = match original_game_session.compress_as() { + std::borrow::Cow::Borrowed(data) => data.clone(), + std::borrow::Cow::Owned(data) => data, + }; + + assert_eq!( + custom_compressed_data.session_id, original_game_session.session_id, + "Session ID should be kept" + ); + assert_eq!( + custom_compressed_data.player, original_game_session.player, + "Player should be kept" + ); + assert_eq!( + custom_compressed_data.game_type, original_game_session.game_type, + "Game type should be kept" + ); + assert_eq!( + custom_compressed_data.start_time, 0, + "Start time should be RESET to 0" + ); + assert_eq!( + custom_compressed_data.end_time, None, + "End time should be RESET to None" + ); + assert_eq!( + custom_compressed_data.score, 0, + "Score should be RESET to 0" + ); +} diff --git a/sdk-tests/sdk-compressible-test/tests/helpers.rs b/sdk-tests/sdk-compressible-test/tests/helpers.rs new file mode 100644 index 0000000000..847da9ddaf --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/helpers.rs @@ -0,0 +1,336 @@ +// Common test helpers and constants for all test files +#![allow(dead_code)] + +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; +use light_compressible_client::CompressibleInstruction; +use light_macros::pubkey; +use light_program_test::{program_test::LightProgramTest, AddressWithTree, Indexer, Rpc}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use sdk_compressible_test::{CompressedAccountVariant, GameSession, UserRecord}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; +pub const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +pub const CTOKEN_RENT_SPONSOR: Pubkey = pubkey!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); +pub const CTOKEN_RENT_AUTHORITY: Pubkey = pubkey!("8r3QmazwoLHYppYWysXPgUxYJ3Khn7vh3e313jYDcCKy"); + +// Helper functions used across multiple test files + +pub async fn create_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + state_tree_queue: Option, +) { + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let accounts = sdk_compressible_test::accounts::CreateRecord { + user: payer.pubkey(), + user_record: *user_record_pda, + system_program: solana_sdk::system_program::ID, + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + }; + + let compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let address_tree_info = packed_tree_infos.address_trees[0]; + + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreateRecord { + name: "Test User".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + + let user_record = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); +} + +pub async fn decompress_single_user_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + _user_record_bump: &u8, + expected_user_name: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_user_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + + let compressed_account = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert!(compressed_account.data.unwrap().data.is_empty()); + + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA account data len must be > 0 after decompression" + ); + + let user_pda_data = user_pda_account.unwrap().data; + assert_eq!( + &user_pda_data[0..8], + UserRecord::DISCRIMINATOR, + "User account anchor discriminator mismatch" + ); + + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + assert_eq!(decompressed_user_record.name, expected_user_name); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); +} + +pub async fn create_game_session( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + state_tree_queue: Option, +) { + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let accounts = sdk_compressible_test::accounts::CreateGameSession { + player: payer.pubkey(), + game_session: *game_session_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_sponsor: RENT_SPONSOR, + }; + + let compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let address_tree_info = packed_tree_infos.address_trees[0]; + + let output_state_tree_index = remaining_accounts.insert_or_get( + state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), + ); + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreateGameSession { + session_id, + game_type: "Battle Royale".to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!(result.is_ok(), "Transaction should succeed"); + + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_game_session = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_game_session.address, Some(compressed_address)); + assert!(compressed_game_session.data.is_some()); + + let buf = compressed_game_session.data.as_ref().unwrap().data.clone(); + + let game_session = GameSession::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(game_session.session_id, session_id); + assert_eq!(game_session.game_type, "Battle Royale"); + assert_eq!(game_session.player, payer.pubkey()); + assert_eq!(game_session.score, 0); + assert!(game_session.compression_info.is_none()); +} diff --git a/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs b/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs new file mode 100644 index 0000000000..6ce75929c1 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs @@ -0,0 +1,141 @@ +use anchor_lang::{AccountDeserialize, AnchorDeserialize, ToAccountMetas}; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::ctoken; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::compressible::CompressibleConfig; +use sdk_compressible_test::{CompressedAccountVariant, UserRecord}; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; + +#[tokio::test] +async fn test_double_decompression_attack() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + let c_user_record = + UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA should be decompressed after first operation" + ); + + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_user_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + &program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[user_record_pda], + &[( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + )], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(&program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + + assert!( + result.is_ok(), + "Second decompression should succeed idempotently" + ); + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + let user_pda_data = user_pda_account.unwrap().data; + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + + assert_eq!(decompressed_user_record.name, "Test User"); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} diff --git a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs new file mode 100644 index 0000000000..291992b3f1 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs @@ -0,0 +1,1389 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use light_client::indexer::CompressedAccount; +use light_compressed_account::address::derive_address; +use light_compressed_token_sdk::{ + ctoken, + instructions::{create_compressed_mint::find_spl_mint_address, derive_compressed_mint_address}, + pack::compat::CTokenDataWithVariant, +}; +use light_compressed_token_types::CPI_AUTHORITY_PDA; +use light_compressible_client::CompressibleInstruction; +use light_ctoken_types::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::CompressedMintMetadata, +}; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + AddressWithTree, Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use light_sdk_types::C_TOKEN_PROGRAM_ID; +use sdk_compressible_test::{ + get_ctoken_signer2_seeds, get_ctoken_signer3_seeds, get_ctoken_signer4_seeds, + get_ctoken_signer5_seeds, get_ctoken_signer_seeds, CTokenAccountVariant, + CompressedAccountVariant, GameSession, UserRecord, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_game_session, create_record, ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests +// 1. create and decompress two accounts and compress token accounts after +// decompression +// 2. create and decompress accounts with different state trees +#[tokio::test] +async fn test_create_and_decompress_two_accounts() { + let program_id = sdk_compressible_test::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![crate::helpers::ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let combined_user = Keypair::new(); + let fund_user_ix = solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &combined_user.pubkey(), + 1e9 as u64, + ); + let fund_result = rpc + .create_and_send_transaction(&[fund_user_ix], &payer.pubkey(), &[&payer]) + .await; + assert!(fund_result.is_ok(), "Funding combined user should succeed"); + let combined_session_id = 99999u64; + let (combined_user_record_pda, _combined_user_record_bump) = Pubkey::find_program_address( + &[b"user_record", combined_user.pubkey().as_ref()], + &program_id, + ); + let (combined_game_session_pda, _combined_game_bump) = Pubkey::find_program_address( + &[b"game_session", combined_session_id.to_le_bytes().as_ref()], + &program_id, + ); + + let ( + ctoken_account, + _mint_signer, + ctoken_account_2, + ctoken_account_3, + ctoken_account_4, + ctoken_account_5, + ) = create_user_record_and_game_session( + &mut rpc, + &combined_user, + &program_id, + &config_pda, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + ) + .await; + + rpc.warp_to_slot(200).unwrap(); + + let (_, ctoken_account_address) = sdk_compressible_test::get_ctoken_signer_seeds( + &combined_user.pubkey(), + &ctoken_account.token.mint, + ); + + let (_, ctoken_account_address_2) = + sdk_compressible_test::get_ctoken_signer2_seeds(&combined_user.pubkey()); + + let (_, ctoken_account_address_3) = + sdk_compressible_test::get_ctoken_signer3_seeds(&combined_user.pubkey()); + + let (_, ctoken_account_address_4) = sdk_compressible_test::get_ctoken_signer4_seeds( + &combined_user.pubkey(), + &combined_user.pubkey(), + ); + + let (_, ctoken_account_address_5) = sdk_compressible_test::get_ctoken_signer5_seeds( + &combined_user.pubkey(), + &ctoken_account.token.mint, + 42, + ); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let compressed_user_record_address = derive_address( + &combined_user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_game_session_address = derive_address( + &combined_game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let user_record_before_decompression: CompressedAccount = rpc + .get_compressed_account(compressed_user_record_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_session_before_decompression: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value + .unwrap(); + + decompress_multiple_pdas_with_ctoken( + &mut rpc, + &combined_user, + &program_id, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + "Combined User", + "Combined Game", + 200, + ctoken_account.clone(), + ctoken_account_address, + ctoken_account_2.clone(), + ctoken_account_address_2, + ctoken_account_3.clone(), + ctoken_account_address_3, + ctoken_account_4.clone(), + ctoken_account_address_4, + ctoken_account_5.clone(), + ctoken_account_address_5, + ) + .await; + + rpc.warp_to_slot(300).unwrap(); + + compress_token_account_after_decompress( + &mut rpc, + &combined_user, + &program_id, + &config_pda, + ctoken_account_address, + ctoken_account_address_2, + ctoken_account_address_3, + ctoken_account_address_4, + ctoken_account_address_5, + ctoken_account.token.mint, + ctoken_account.token.amount, + &combined_user_record_pda, + &combined_game_session_pda, + combined_session_id, + user_record_before_decompression.hash, + game_session_before_decompression.hash, + ) + .await; +} + +#[allow(clippy::too_many_arguments)] +pub async fn create_user_record_and_game_session( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, +) -> ( + light_client::indexer::CompressedTokenAccount, + Pubkey, + light_client::indexer::CompressedTokenAccount, + light_client::indexer::CompressedTokenAccount, + light_client::indexer::CompressedTokenAccount, + light_client::indexer::CompressedTokenAccount, +) { + let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new_with_cpi_context( + *program_id, + state_tree_info.cpi_context.unwrap(), + ); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let decimals = 6u8; + let mint_authority_keypair = Keypair::new(); + let mint_authority = mint_authority_keypair.pubkey(); + let freeze_authority = mint_authority; + let mint_signer = Keypair::new(); + let compressed_mint_address = + derive_compressed_mint_address(&mint_signer.pubkey(), &address_tree_pubkey); + + let (spl_mint, mint_bump) = find_spl_mint_address(&mint_signer.pubkey()); + let accounts = sdk_compressible_test::accounts::CreateUserRecordAndGameSession { + user: user.pubkey(), + user_record: *user_record_pda, + game_session: *game_session_pda, + mint_signer: mint_signer.pubkey(), + ctoken_program: C_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_sponsor: RENT_SPONSOR, + mint_authority, + compress_token_program_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), + }; + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![ + AddressWithTree { + address: user_compressed_address, + tree: address_tree_pubkey, + }, + AddressWithTree { + address: game_compressed_address, + tree: address_tree_pubkey, + }, + AddressWithTree { + address: compressed_mint_address, + tree: address_tree_pubkey, + }, + ], + None, + ) + .await + .unwrap() + .value; + + let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let user_address_tree_info = packed_tree_infos.address_trees[0]; + let game_address_tree_info = packed_tree_infos.address_trees[1]; + let mint_address_tree_info = packed_tree_infos.address_trees[2]; + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreateUserRecordAndGameSession { + account_data: sdk_compressible_test::AccountCreationData { + user_name: "Combined User".to_string(), + session_id, + game_type: "Combined Game".to_string(), + mint_name: "Test Game Token".to_string(), + mint_symbol: "TGT".to_string(), + mint_uri: "https://example.com/token.json".to_string(), + mint_decimals: 9, + mint_supply: 1_000_000_000, + mint_update_authority: Some(mint_authority), + mint_freeze_authority: Some(freeze_authority), + additional_metadata: None, + }, + compression_params: sdk_compressible_test::CompressionParams { + proof: rpc_result.proof, + user_compressed_address, + user_address_tree_info, + user_output_state_tree_index, + game_compressed_address, + game_address_tree_info, + game_output_state_tree_index, + mint_bump, + mint_with_context: CompressedMintWithContext { + leaf_index: 0, + prove_by_index: false, + root_index: mint_address_tree_info.root_index, + address: compressed_mint_address, + mint: CompressedMintInstructionData { + supply: 0, + decimals, + metadata: CompressedMintMetadata { + version: 3, + mint: spl_mint.into(), + spl_mint_initialized: false, + }, + mint_authority: Some(mint_authority.into()), + freeze_authority: Some(freeze_authority.into()), + extensions: None, + }, + }, + }, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction( + &[instruction], + &user.pubkey(), + &[user, &mint_signer, &mint_authority_keypair], + ) + .await; + + assert!( + result.is_ok(), + "Combined creation transaction should succeed" + ); + + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_record_account.is_none(), + "User record account should not exist after compression" + ); + + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_session_account.is_none(), + "Game session account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_user_record.address, + Some(user_compressed_address) + ); + assert!(compressed_user_record.data.is_some()); + + let user_buf = compressed_user_record.data.unwrap().data; + + let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); + + assert_eq!(user_record.name, "Combined User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, user.pubkey()); + + let compressed_game_session = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_game_session.address, + Some(game_compressed_address) + ); + assert!(compressed_game_session.data.is_some()); + + let game_buf = compressed_game_session.data.unwrap().data; + let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); + assert_eq!(game_session.session_id, session_id); + assert_eq!(game_session.game_type, "Combined Game"); + assert_eq!(game_session.player, user.pubkey()); + assert_eq!(game_session.score, 0); + + let token_account_address = get_ctoken_signer_seeds( + &user.pubkey(), + &find_spl_mint_address(&mint_signer.pubkey()).0, + ) + .1; + + let mint = find_spl_mint_address(&mint_signer.pubkey()).0; + let token_account_address_2 = get_ctoken_signer2_seeds(&user.pubkey()).1; + let token_account_address_3 = get_ctoken_signer3_seeds(&user.pubkey()).1; + let token_account_address_4 = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()).1; + let token_account_address_5 = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42).1; + + let ctoken_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_2 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_3 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_4 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_5 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) + .await + .unwrap() + .value; + + assert!( + !ctoken_accounts.items.is_empty(), + "Should have at least one compressed token account" + ); + assert!( + !ctoken_accounts_2.items.is_empty(), + "Should have at least one compressed token account 2" + ); + assert!( + !ctoken_accounts_3.items.is_empty(), + "Should have at least one compressed token account 3" + ); + assert!( + !ctoken_accounts_4.items.is_empty(), + "Should have at least one compressed token account 4" + ); + assert!( + !ctoken_accounts_5.items.is_empty(), + "Should have at least one compressed token account 5" + ); + + let ctoken_account = ctoken_accounts.items[0].clone(); + let ctoken_account_2 = ctoken_accounts_2.items[0].clone(); + let ctoken_account_3 = ctoken_accounts_3.items[0].clone(); + let ctoken_account_4 = ctoken_accounts_4.items[0].clone(); + let ctoken_account_5 = ctoken_accounts_5.items[0].clone(); + + ( + ctoken_account, + mint_signer.pubkey(), + ctoken_account_2, + ctoken_account_3, + ctoken_account_4, + ctoken_account_5, + ) +} + +#[allow(clippy::too_many_arguments)] +pub async fn decompress_multiple_pdas_with_ctoken( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, + ctoken_account: light_client::indexer::CompressedTokenAccount, + native_token_account: Pubkey, + ctoken_account_2: light_client::indexer::CompressedTokenAccount, + native_token_account_2: Pubkey, + ctoken_account_3: light_client::indexer::CompressedTokenAccount, + native_token_account_3: Pubkey, + ctoken_account_4: light_client::indexer::CompressedTokenAccount, + native_token_account_4: Pubkey, + ctoken_account_5: light_client::indexer::CompressedTokenAccount, + native_token_account_5: Pubkey, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_account_data = c_game_pda.data.as_ref().unwrap(); + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof( + vec![ + c_user_pda.hash, + c_game_pda.hash, + ctoken_account.clone().account.hash, + ctoken_account_2.clone().account.hash, + ctoken_account_3.clone().account.hash, + ctoken_account_4.clone().account.hash, + ctoken_account_5.clone().account.hash, + ], + vec![], + None, + ) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let ctoken_config = ctoken::config_pda(); + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[ + *user_record_pda, + *game_session_pda, + native_token_account, + native_token_account_2, + native_token_account_3, + native_token_account_4, + native_token_account_5, + ], + &[ + ( + c_user_pda.clone(), + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda.clone(), + CompressedAccountVariant::GameSession(c_game_session), + ), + ( + { + let acc = ctoken_account.clone().account; + let _token = ctoken_account.clone().token; + acc + }, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner, + token_data: ctoken_account.clone().token, + }), + ), + ( + ctoken_account_2.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner2, + token_data: ctoken_account_2.clone().token, + }), + ), + ( + ctoken_account_3.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner3, + token_data: ctoken_account_3.clone().token, + }), + ), + ( + ctoken_account_4.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner4, + token_data: ctoken_account_4.clone().token, + }), + ), + ( + ctoken_account_5.clone().account, + CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< + CTokenAccountVariant, + > { + variant: CTokenAccountVariant::CTokenSigner5, + token_data: ctoken_account_5.clone().token, + }), + ), + ], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config, + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: ctoken_account.token.mint, + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert_eq!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "Game PDA account data len must be 0 before decompression" + ); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA account data len must be > 0 after decompression" + ); + + let user_pda_data = user_pda_account.unwrap().data; + assert_eq!( + &user_pda_data[0..8], + UserRecord::DISCRIMINATOR, + "User account anchor discriminator mismatch" + ); + + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + assert_eq!(decompressed_user_record.name, expected_user_name); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" + ); + + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + sdk_compressible_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, 0); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let token_account_data = rpc + .get_account(native_token_account) + .await + .unwrap() + .unwrap(); + assert!( + !token_account_data.data.is_empty(), + "Token account should have data" + ); + assert_eq!(token_account_data.owner, C_TOKEN_PROGRAM_ID.into()); + + let compressed_user_record_data = rpc + .get_compressed_account(c_user_pda.clone().address.unwrap(), None) + .await + .unwrap() + .value + .unwrap(); + let compressed_game_session_data = rpc + .get_compressed_account(c_game_pda.clone().address.unwrap(), None) + .await + .unwrap() + .value + .unwrap(); + for ctoken in [ + &ctoken_account, + &ctoken_account_2, + &ctoken_account_3, + &ctoken_account_4, + &ctoken_account_5, + ] { + let response = rpc + .get_compressed_account_by_hash(ctoken.clone().account.hash, None) + .await + .unwrap(); + assert!( + response.value.is_none(), + "Compressed token account should have value == None after being closed" + ); + } + + assert!( + compressed_user_record_data.data.unwrap().data.is_empty(), + "Compressed user record should be closed/empty after decompression" + ); + assert!( + compressed_game_session_data.data.unwrap().data.is_empty(), + "Compressed game session should be closed/empty after decompression" + ); +} + +#[allow(clippy::too_many_arguments)] +pub async fn decompress_multiple_pdas( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + expected_user_name: &str, + expected_game_type: &str, + expected_slot: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let user_compressed_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_user_pda = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_account_data = c_user_pda.data.as_ref().unwrap(); + + let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); + + let game_compressed_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_account_data = c_game_pda.data.as_ref().unwrap(); + + let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::decompress_accounts_idempotent( + program_id, + &CompressibleInstruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &[*user_record_pda, *game_session_pda], + &[ + ( + c_user_pda, + CompressedAccountVariant::UserRecord(c_user_record), + ), + ( + c_game_pda, + CompressedAccountVariant::GameSession(c_game_session), + ), + ], + &sdk_compressible_test::accounts::DecompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_payer: payer.pubkey(), + ctoken_rent_sponsor: ctoken::rent_sponsor_pda(), + ctoken_config: ctoken::config_pda(), + ctoken_program: ctoken::id(), + ctoken_cpi_authority: ctoken::cpi_authority(), + some_mint: payer.pubkey(), + } + .to_account_metas(None), + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert_eq!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "User PDA account data len must be 0 before decompression" + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert_eq!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), + 0, + "Game PDA account data len must be 0 before decompression" + ); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + assert!(result.is_ok(), "Decompress transaction should succeed"); + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "User PDA account data len must be > 0 after decompression" + ); + + let user_pda_data = user_pda_account.unwrap().data; + assert_eq!( + &user_pda_data[0..8], + UserRecord::DISCRIMINATOR, + "User account anchor discriminator mismatch" + ); + + let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); + assert_eq!(decompressed_user_record.name, expected_user_name); + assert_eq!(decompressed_user_record.score, 11); + assert_eq!(decompressed_user_record.owner, payer.pubkey()); + assert!(!decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); + assert!( + game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, + "Game PDA account data len must be > 0 after decompression" + ); + + let game_pda_data = game_pda_account.unwrap().data; + assert_eq!( + &game_pda_data[0..8], + sdk_compressible_test::GameSession::DISCRIMINATOR, + "Game account anchor discriminator mismatch" + ); + + let decompressed_game_session = + sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); + assert_eq!(decompressed_game_session.session_id, session_id); + assert_eq!(decompressed_game_session.game_type, expected_game_type); + assert_eq!(decompressed_game_session.player, payer.pubkey()); + assert_eq!(decompressed_game_session.score, 0); + assert!(!decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .is_compressed()); + assert_eq!( + decompressed_game_session + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + expected_slot + ); + + let c_game_pda = rpc + .get_compressed_account(game_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert!(c_game_pda.data.is_some()); + assert_eq!(c_game_pda.data.unwrap().data.len(), 0); +} + +#[allow(clippy::too_many_arguments)] +pub async fn compress_token_account_after_decompress( + rpc: &mut LightProgramTest, + user: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + token_account_address: Pubkey, + _token_account_address_2: Pubkey, + _token_account_address_3: Pubkey, + _token_account_address_4: Pubkey, + _token_account_address_5: Pubkey, + mint: Pubkey, + amount: u64, + user_record_pda: &Pubkey, + game_session_pda: &Pubkey, + session_id: u64, + user_record_hash_before_decompression: [u8; 32], + game_session_hash_before_decompression: [u8; 32], +) { + let token_account_data = rpc.get_account(token_account_address).await.unwrap(); + assert!( + token_account_data.is_some(), + "Token account should exist before compression" + ); + + let account = token_account_data.unwrap(); + + assert!( + account.lamports > 0, + "Token account should have lamports before compression" + ); + assert!( + !account.data.is_empty(), + "Token account should have data before compression" + ); + + let (user_record_seeds, user_record_pubkey) = + sdk_compressible_test::get_userrecord_seeds(&user.pubkey()); + let (game_session_seeds, game_session_pubkey) = + sdk_compressible_test::get_gamesession_seeds(session_id); + let (_, token_account_address) = get_ctoken_signer_seeds(&user.pubkey(), &mint); + + let (_, token_account_address_2) = get_ctoken_signer2_seeds(&user.pubkey()); + let (_, token_account_address_3) = get_ctoken_signer3_seeds(&user.pubkey()); + let (_, token_account_address_4) = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()); + let (_, token_account_address_5) = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42); + let (_token_signer_seeds, _ctoken_1_authority_pda) = + sdk_compressible_test::get_ctokensigner_authority_seeds(); + + let (_token_signer_seeds_2, _ctoken_2_authority_pda) = + sdk_compressible_test::get_ctokensigner2_authority_seeds(); + + let (_token_signer_seeds_3, _ctoken_3_authority_pda) = + sdk_compressible_test::get_ctokensigner3_authority_seeds(); + + let (_token_signer_seeds_4, _ctoken_4_authority_pda) = + sdk_compressible_test::get_ctokensigner4_authority_seeds(); + + let (_token_signer_seeds_5, _ctoken_5_authority_pda) = + sdk_compressible_test::get_ctokensigner5_authority_seeds(); + + let _cpisigner = Pubkey::new_from_array(sdk_compressible_test::LIGHT_CPI_SIGNER.cpi_signer); + + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap().unwrap(); + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); + let _token_account = rpc + .get_account(token_account_address) + .await + .unwrap() + .unwrap(); + let _token_account_2 = rpc + .get_account(token_account_address_2) + .await + .unwrap() + .unwrap(); + let _token_account_3 = rpc + .get_account(token_account_address_3) + .await + .unwrap() + .unwrap(); + let _token_account_4 = rpc + .get_account(token_account_address_4) + .await + .unwrap() + .unwrap(); + let _token_account_5 = rpc + .get_account(token_account_address_5) + .await + .unwrap() + .unwrap(); + + assert_eq!(*user_record_pda, user_record_pubkey); + assert_eq!(*game_session_pda, game_session_pubkey); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let compressed_user_record_address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let compressed_game_session_address = derive_address( + &game_session_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + let user_record: CompressedAccount = rpc + .get_compressed_account(compressed_user_record_address, None) + .await + .unwrap() + .value + .unwrap(); + let game_session: CompressedAccount = rpc + .get_compressed_account(compressed_game_session_address, None) + .await + .unwrap() + .value + .unwrap(); + + let user_record_hash = user_record.hash; + let game_session_hash = game_session.hash; + + assert_ne!( + user_record_hash, user_record_hash_before_decompression, + "User record hash NOT_EQUAL before and after compression" + ); + assert_ne!( + game_session_hash, game_session_hash_before_decompression, + "Game session hash NOT_EQUAL before and after compression" + ); + + let proof_with_context = rpc + .get_validity_proof(vec![user_record_hash, game_session_hash], vec![], None) + .await + .unwrap() + .value; + + let random_tree_info = rpc.get_random_state_tree_info().unwrap(); + let instruction = + light_compressible_client::CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[user_record_pubkey, game_session_pubkey], + &[user_record_account, game_session_account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: user.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![user_record_seeds, game_session_seeds], + proof_with_context, + random_tree_info, + ) + .unwrap(); + + for _account in instruction.accounts.iter() {} + + let result = rpc + .create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) + .await; + + assert!( + result.is_ok(), + "PDA compression should succeed: {:?}", + result + ); + + rpc.warp_slot_forward(20000).await.unwrap(); + + let token_account_after = rpc.get_account(token_account_address).await.unwrap(); + assert!( + token_account_after.is_none(), + "Token account should not exist after compression" + ); + let token_account_after_2 = rpc.get_account(token_account_address_2).await.unwrap(); + assert!( + token_account_after_2.is_none(), + "Token account 2 should not exist after compression" + ); + let token_account_after_3 = rpc.get_account(token_account_address_3).await.unwrap(); + assert!( + token_account_after_3.is_none(), + "Token account 3 should not exist after compression" + ); + let token_account_after_4 = rpc.get_account(token_account_address_4).await.unwrap(); + assert!( + token_account_after_4.is_none(), + "Token account 4 should not exist after compression" + ); + let token_account_after_5 = rpc.get_account(token_account_address_5).await.unwrap(); + assert!( + token_account_after_5.is_none(), + "Token account 5 should not exist after compression" + ); + + let ctoken_accounts = rpc + .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_2 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_3 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_4 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) + .await + .unwrap() + .value; + let ctoken_accounts_5 = rpc + .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) + .await + .unwrap() + .value; + + assert!( + !ctoken_accounts.items.is_empty(), + "Should have at least one compressed token account after compression" + ); + assert!( + !ctoken_accounts_2.items.is_empty(), + "Should have at least one compressed token account 2 after compression" + ); + assert!( + !ctoken_accounts_3.items.is_empty(), + "Should have at least one compressed token account 3 after compression" + ); + assert!( + !ctoken_accounts_4.items.is_empty(), + "Should have at least one compressed token account 4 after compression" + ); + assert!( + !ctoken_accounts_5.items.is_empty(), + "Should have at least one compressed token account 5 after compression" + ); + + let ctoken = &ctoken_accounts.items[0]; + assert_eq!( + ctoken.token.mint, mint, + "Compressed token should have the same mint" + ); + assert_eq!( + ctoken.token.owner, token_account_address, + "Compressed token owner should be the token account address" + ); + assert_eq!( + ctoken.token.amount, amount, + "Compressed token should have the same amount" + ); + let ctoken2 = &ctoken_accounts_2.items[0]; + assert_eq!( + ctoken2.token.mint, mint, + "Compressed token 2 should have the same mint" + ); + assert_eq!( + ctoken2.token.owner, token_account_address_2, + "Compressed token 2 owner should be the token account address" + ); + assert_eq!( + ctoken2.token.amount, amount, + "Compressed token 2 should have the same amount" + ); + let ctoken3 = &ctoken_accounts_3.items[0]; + assert_eq!( + ctoken3.token.mint, mint, + "Compressed token 3 should have the same mint" + ); + assert_eq!( + ctoken3.token.owner, token_account_address_3, + "Compressed token 3 owner should be the token account address" + ); + assert_eq!( + ctoken3.token.amount, amount, + "Compressed token 3 should have the same amount" + ); + let ctoken4 = &ctoken_accounts_4.items[0]; + assert_eq!( + ctoken4.token.mint, mint, + "Compressed token 4 should have the same mint" + ); + assert_eq!( + ctoken4.token.owner, token_account_address_4, + "Compressed token 4 owner should be the token account address" + ); + assert_eq!( + ctoken4.token.amount, amount, + "Compressed token 4 should have the same amount" + ); + let ctoken5 = &ctoken_accounts_5.items[0]; + assert_eq!( + ctoken5.token.mint, mint, + "Compressed token 5 should have the same mint" + ); + assert_eq!( + ctoken5.token.owner, token_account_address_5, + "Compressed token 5 owner should be the token account address" + ); + assert_eq!( + ctoken5.token.amount, amount, + "Compressed token 5 should have the same amount" + ); + let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); + let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); + let token_account = rpc.get_account(token_account_address).await.unwrap(); + let token_account_3 = rpc.get_account(token_account_address_3).await.unwrap(); + let token_account_4 = rpc.get_account(token_account_address_4).await.unwrap(); + let token_account_5 = rpc.get_account(token_account_address_5).await.unwrap(); + + assert!( + user_record_account.is_none(), + "User record account should be None" + ); + assert!( + game_session_account.is_none(), + "Game session account should be None" + ); + assert!(token_account.is_none(), "Token account should be None"); + assert!( + user_record_account + .map(|a| a.data.is_empty()) + .unwrap_or(true), + "User record account should be empty" + ); + assert!( + game_session_account + .map(|a| a.data.is_empty()) + .unwrap_or(true), + "Game session account should be empty" + ); + assert!( + token_account.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account should be empty" + ); + assert!( + token_account_3.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account 3 should be empty" + ); + assert!( + token_account_4.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account 4 should be empty" + ); + assert!( + token_account_5.map(|a| a.data.is_empty()).unwrap_or(true), + "Token account 5 should be empty" + ); +} + +#[tokio::test] +async fn test_create_and_decompress_accounts_with_different_state_trees() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, _user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + let session_id = 54321u64; + let (game_session_pda, _game_bump) = Pubkey::find_program_address( + &[b"game_session", session_id.to_le_bytes().as_ref()], + &program_id, + ); + + let first_state_tree_info = rpc.get_state_tree_infos()[0]; + let second_state_tree_info = rpc.get_state_tree_infos()[1]; + + create_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + Some(first_state_tree_info.queue), + ) + .await; + + create_game_session( + &mut rpc, + &payer, + &program_id, + &config_pda, + &game_session_pda, + session_id, + Some(second_state_tree_info.queue), + ) + .await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_multiple_pdas( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &game_session_pda, + session_id, + "Test User", + "Battle Royale", + 100, + ) + .await; +} diff --git a/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs b/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs new file mode 100644 index 0000000000..c4c07f8900 --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs @@ -0,0 +1,536 @@ +use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas}; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests for the simplest possible compression flows: +// 1. Create empty compressed account (do not compress at init) +// 2. Idempotent double compression +#[tokio::test] +async fn test_create_empty_compressed_account() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let placeholder_id = 54321u64; + let (placeholder_record_pda, placeholder_record_bump) = Pubkey::find_program_address( + &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + &program_id, + ); + + create_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + placeholder_id, + "Test Placeholder", + ) + .await; + + let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_account.is_some(), + "Placeholder PDA should exist after empty compression" + ); + let account = placeholder_pda_account.unwrap(); + assert!( + account.lamports > 0, + "Placeholder PDA should have lamports (not closed)" + ); + assert!( + !account.data.is_empty(), + "Placeholder PDA should have data (not closed)" + ); + + let placeholder_data = account.data; + let decompressed_placeholder_record = + sdk_compressible_test::PlaceholderRecord::try_deserialize(&mut &placeholder_data[..]) + .unwrap(); + assert_eq!(decompressed_placeholder_record.name, "Test Placeholder"); + assert_eq!( + decompressed_placeholder_record.placeholder_id, + placeholder_id + ); + assert_eq!(decompressed_placeholder_record.owner, payer.pubkey()); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_placeholder.address, + Some(compressed_address), + "Compressed account should exist with correct address" + ); + assert!( + compressed_placeholder.data.is_some(), + "Compressed account should have data field" + ); + + let compressed_data = compressed_placeholder.data.unwrap(); + assert_eq!( + compressed_data.data.len(), + 0, + "Compressed account data should be empty" + ); + + rpc.warp_to_slot(200).unwrap(); + + compress_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + &placeholder_record_bump, + placeholder_id, + ) + .await; +} + +#[tokio::test] +async fn test_double_compression_attack() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let placeholder_id = 99999u64; + let (placeholder_record_pda, _placeholder_record_bump) = Pubkey::find_program_address( + &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], + &program_id, + ); + + create_placeholder_record( + &mut rpc, + &payer, + &program_id, + &config_pda, + &placeholder_record_pda, + placeholder_id, + "Double Compression Test", + ) + .await; + + let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_account.is_some(), + "Placeholder PDA should exist before compression" + ); + let account_before = placeholder_pda_account.unwrap(); + assert!( + account_before.lamports > 0, + "Placeholder PDA should have lamports before compression" + ); + assert!( + !account_before.data.is_empty(), + "Placeholder PDA should have data before compression" + ); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder_before = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_placeholder_before.address, + Some(compressed_address), + "Empty compressed account should exist" + ); + assert_eq!( + compressed_placeholder_before + .data + .as_ref() + .unwrap() + .data + .len(), + 0, + "Compressed account should be empty initially" + ); + + rpc.warp_to_slot(200).unwrap(); + + let first_compression_result = compress_placeholder_record_for_double_test( + &mut rpc, + &payer, + &program_id, + &placeholder_record_pda, + placeholder_id, + Some(account_before.clone()), + ) + .await; + assert!( + first_compression_result.is_ok(), + "First compression should succeed: {:?}", + first_compression_result + ); + + let placeholder_pda_after_first = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_after_first.is_none(), + "PDA should not exist after first compression" + ); + + let compressed_placeholder_after_first = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let first_data_len = compressed_placeholder_after_first + .data + .as_ref() + .unwrap() + .data + .len(); + assert!( + first_data_len > 0, + "Compressed account should contain data after first compression" + ); + + let second_compression_result = compress_placeholder_record_for_double_test( + &mut rpc, + &payer, + &program_id, + &placeholder_record_pda, + placeholder_id, + Some(account_before), + ) + .await; + + assert!( + second_compression_result.is_ok(), + "Second compression should succeed idempotently: {:?}", + second_compression_result + ); + + let placeholder_pda_after_second = rpc.get_account(placeholder_record_pda).await.unwrap(); + assert!( + placeholder_pda_after_second.is_none(), + "PDA should still not exist after second compression" + ); + + let compressed_placeholder_after_second = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!( + compressed_placeholder_after_first.hash, compressed_placeholder_after_second.hash, + "Compressed account hash should be unchanged after second compression" + ); + assert_eq!( + compressed_placeholder_after_first + .data + .as_ref() + .unwrap() + .data, + compressed_placeholder_after_second + .data + .as_ref() + .unwrap() + .data, + "Compressed account data should be unchanged after second compression" + ); +} + +pub async fn create_placeholder_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + config_pda: &Pubkey, + placeholder_record_pda: &Pubkey, + placeholder_id: u64, + name: &str, +) { + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let accounts = sdk_compressible_test::accounts::CreatePlaceholderRecord { + user: payer.pubkey(), + placeholder_record: *placeholder_record_pda, + system_program: solana_sdk::system_program::ID, + config: *config_pda, + rent_sponsor: RENT_SPONSOR, + }; + + let compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_program_test::AddressWithTree { + address: compressed_address, + tree: address_tree_pubkey, + }], + None, + ) + .await + .unwrap() + .value; + + let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let address_tree_info = packed_tree_infos.address_trees[0]; + + let output_state_tree_index = + remaining_accounts.insert_or_get(rpc.get_random_state_tree_info().unwrap().queue); + + let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction_data = sdk_compressible_test::instruction::CreatePlaceholderRecord { + placeholder_id, + name: name.to_string(), + proof: rpc_result.proof, + compressed_address, + address_tree_info, + output_state_tree_index, + }; + + let instruction = Instruction { + program_id: *program_id, + accounts: [accounts.to_account_metas(None), system_accounts].concat(), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CreatePlaceholderRecord transaction should succeed" + ); +} + +pub async fn compress_placeholder_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + _config_pda: &Pubkey, + placeholder_record_pda: &Pubkey, + _placeholder_record_bump: &u8, + placeholder_id: u64, +) { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); + + let account = rpc + .get_account(*placeholder_record_pda) + .await + .unwrap() + .unwrap(); + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = + light_compressible_client::CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &[account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + assert!( + result.is_ok(), + "CompressPlaceholderRecord transaction should succeed: {:?}", + result + ); + + let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); + + let compressed_placeholder_after = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert!( + compressed_placeholder_after.data.is_some(), + "Compressed account should have data after compression" + ); + + let compressed_data_after = compressed_placeholder_after.data.unwrap(); + + assert!( + !compressed_data_after.data.is_empty(), + "Compressed account should contain the PDA data" + ); +} + +pub async fn compress_placeholder_record_for_double_test( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + placeholder_record_pda: &Pubkey, + placeholder_id: u64, + previous_account: Option, +) -> Result { + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let placeholder_compressed_address = derive_address( + &placeholder_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_placeholder = rpc + .get_compressed_account(placeholder_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) + .await + .unwrap() + .value; + + let placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let accounts_to_compress = if let Some(account) = previous_account { + vec![account] + } else { + panic!("Previous account should be provided"); + }; + let instruction = + light_compressible_client::CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*placeholder_record_pda], + &accounts_to_compress, + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![placeholder_seeds.0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} diff --git a/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs b/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs new file mode 100644 index 0000000000..857b77568c --- /dev/null +++ b/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs @@ -0,0 +1,280 @@ +use anchor_lang::{ + AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, +}; +use light_compressed_account::address::derive_address; +use light_compressible_client::CompressibleInstruction; +use light_program_test::{ + program_test::{ + initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, + }, + Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::{ + compressible::CompressibleConfig, + instruction::{PackedAccounts, SystemAccountMetaConfig}, +}; +use sdk_compressible_test::UserRecord; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +mod helpers; +use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; + +// Tests +// 1. init compressed, decompress, and compress +// 2. update_record bumps compression info +#[tokio::test] +async fn test_create_decompress_compress_single_account() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(101).unwrap(); + + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; + assert!(result.is_err(), "Compression should fail due to slot delay"); + if let Err(err) = result { + let err_msg = format!("{:?}", err); + assert!( + err_msg.contains("Custom(16001)"), + "Expected error message about slot delay, got: {}", + err_msg + ); + } + rpc.warp_to_slot(200).unwrap(); + let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; + assert!(result.is_ok(), "Compression should succeed"); +} + +#[tokio::test] +async fn test_update_record_compression_info() { + let program_id = sdk_compressible_test::ID; + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let result = initialize_compression_config( + &mut rpc, + &payer, + &program_id, + &payer, + 100, + RENT_SPONSOR, + vec![ADDRESS_SPACE[0]], + &CompressibleInstruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, + None, + ) + .await; + assert!(result.is_ok(), "Initialize config should succeed"); + + let (user_record_pda, user_record_bump) = + Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); + + create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; + + rpc.warp_to_slot(100).unwrap(); + decompress_single_user_record( + &mut rpc, + &payer, + &program_id, + &user_record_pda, + &user_record_bump, + "Test User", + 100, + ) + .await; + + rpc.warp_to_slot(150).unwrap(); + + let accounts = sdk_compressible_test::accounts::UpdateRecord { + user: payer.pubkey(), + user_record: user_record_pda, + }; + + let instruction_data = sdk_compressible_test::instruction::UpdateRecord { + name: "Updated User".to_string(), + score: 42, + }; + + let instruction = Instruction { + program_id, + accounts: accounts.to_account_metas(None), + data: instruction_data.data(), + }; + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await; + assert!(result.is_ok(), "Update record transaction should succeed"); + + rpc.warp_to_slot(200).unwrap(); + + let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User record account should exist after update" + ); + + let account_data = user_pda_account.unwrap().data; + let updated_user_record = UserRecord::try_deserialize(&mut &account_data[..]).unwrap(); + + assert_eq!(updated_user_record.name, "Updated User"); + assert_eq!(updated_user_record.score, 42); + assert_eq!(updated_user_record.owner, payer.pubkey()); + + assert_eq!( + updated_user_record + .compression_info + .as_ref() + .unwrap() + .last_written_slot(), + 150 + ); + assert!(!updated_user_record + .compression_info + .as_ref() + .unwrap() + .is_compressed()); +} + +pub async fn compress_record( + rpc: &mut LightProgramTest, + payer: &Keypair, + program_id: &Pubkey, + user_record_pda: &Pubkey, + should_fail: bool, +) -> Result { + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_some(), + "User PDA account should exist before compression" + ); + let account = user_pda_account.unwrap(); + assert!( + account.lamports > 0, + "Account should have lamports before compression" + ); + assert!( + !account.data.is_empty(), + "Account data should not be empty before compression" + ); + + let mut remaining_accounts = PackedAccounts::default(); + let system_config = SystemAccountMetaConfig::new(*program_id); + let _ = remaining_accounts.add_system_accounts_v2(system_config); + + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let address = derive_address( + &user_record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value + .unwrap(); + let compressed_address = compressed_account.address.unwrap(); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + + let output_state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + let instruction = CompressibleInstruction::compress_accounts_idempotent( + program_id, + sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, + &[*user_record_pda], + &[account], + &sdk_compressible_test::accounts::CompressAccountsIdempotent { + fee_payer: payer.pubkey(), + config: CompressibleConfig::derive_pda(program_id, 0).0, + rent_sponsor: RENT_SPONSOR, + } + .to_account_metas(None), + vec![sdk_compressible_test::get_userrecord_seeds(&payer.pubkey()).0], + rpc_result, + output_state_tree_info, + ) + .unwrap(); + + let result = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await; + + if should_fail { + assert!(result.is_err(), "Compress transaction should fail"); + return result; + } else { + assert!(result.is_ok(), "Compress transaction should succeed"); + } + + let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); + assert!( + user_pda_account.is_none(), + "Account should not exist after compression" + ); + + let compressed_user_record = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + + assert_eq!(compressed_user_record.address, Some(compressed_address)); + assert!(compressed_user_record.data.is_some()); + + let buf = compressed_user_record.data.unwrap().data; + let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); + + assert_eq!(user_record.name, "Test User"); + assert_eq!(user_record.score, 11); + assert_eq!(user_record.owner, payer.pubkey()); + assert!(user_record.compression_info.is_none()); + Ok(result.unwrap()) +} diff --git a/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs b/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs index ee57ab658e..2eedefe23d 100644 --- a/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs +++ b/sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs @@ -1,6 +1,6 @@ use anchor_lang::{prelude::*, solana_program::program::invoke_signed}; use light_compressed_token_sdk::instructions::create_token_account::{ - create_compressible_token_account, CreateCompressibleTokenAccount, + create_compressible_token_account_instruction, CreateCompressibleTokenAccount, }; use light_ctoken_types::instructions::extensions::compressible::CompressToPubkey; @@ -38,8 +38,8 @@ pub fn process_create_ctoken_with_compress_to_pubkey<'info>( token_account_version: light_ctoken_types::state::TokenDataVersion::ShaFlat, }; - let instruction = - create_compressible_token_account(create_account_inputs).map_err(ProgramError::from)?; + let instruction = create_compressible_token_account_instruction(create_account_inputs) + .map_err(ProgramError::from)?; let seeds = [seeds[0], seeds[1], &[bump]]; diff --git a/sdk-tests/sdk-token-test/src/process_four_transfer2.rs b/sdk-tests/sdk-token-test/src/process_four_transfer2.rs index 58171bdd82..703ace5298 100644 --- a/sdk-tests/sdk-token-test/src/process_four_transfer2.rs +++ b/sdk-tests/sdk-token-test/src/process_four_transfer2.rs @@ -207,13 +207,11 @@ pub fn process_four_transfer2<'info>( ) .map_err(ProgramError::from)?; - msg!("tree_pubkeys {:?}", cpi_accounts.tree_pubkeys()); let tree_accounts = cpi_accounts.tree_accounts().unwrap(); let mut packed_accounts = Vec::with_capacity(tree_accounts.len()); for account_info in tree_accounts { packed_accounts.push(account_meta_from_account_info(account_info)); } - msg!("packed_accounts {:?}", packed_accounts); let inputs = Transfer2Inputs { validity_proof: proof,